Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ Critical knowledge to avoid repeating mistakes.

## Python packaging / bindings

- **A wheel repair tool does not protect local source/editable installs.**
HiGHS defaults to a shared library on Unix, leaving the installed Python
extension with `@rpath/libhighs.1.dylib` but no `LC_RPATH`; cibuildwheel can
repair release wheels, while `uv pip install -e .` cannot. Build HiGHS and
its extras as scoped PIC static libraries for Python packaging, and assert
the resulting CMake target type during configuration.
- **Never infer Python import provenance from the checkout layout.** The
2026-07-23 probe found a mixed environment: `dtwcpp` and `_api.py` resolve to
`python/dtwcpp/` in the repository, while `_dtwcpp_core` resolves to the venv
Expand Down Expand Up @@ -327,6 +333,11 @@ Critical knowledge to avoid repeating mistakes.

## ARC SLURM Hardware

- **macOS still ships Bash 3.2, where an empty array expansion fails under
`set -u`.** A Slurm script using `"${optional_args[@]}"` worked on newer
cluster Bash versions but exited with `optional_args[@]: unbound variable`
in local macOS tests. Build one always-nonempty command argument array and
conditionally append optional flags before expanding it.
- **htc GPU compute capabilities (corrected from docs).** The ARC docs list CUDA toolkit version, not compute capability. Actual values: P100=6.0, V100=7.0, RTX8000/TitanRTX=7.5, A100=8.0, RTXA6000=8.6, L40S=8.9, H100/GH200=9.0.
- **Rome (htc-g019) and Broadwell (htc-g045-049) lack AVX-512.** Use
`DTWC_ARCH_LEVEL=v3` for a portable x86 htc build; there is no one x86-64-v4
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ This changelog contains a non-exhaustive list of new features and notable bug-fi
<br/><br/>
# Unreleased

- Fixed macOS Python source and editable installs by statically bundling HiGHS
into the native extension, eliminating the unresolved
`@rpath/libhighs.1.dylib` import dependency. Made the generic Slurm job's
optional seed arguments compatible with macOS Bash 3.2, restored
platform-independent CLI discovery in Python tests, and included Matplotlib
in the Python test dependencies.
- **Fix (build, GCC + LTO):** `run_openmp`'s exception-capture region is an
unnamed `#pragma omp critical` again. The named form made GCC emit a COMMON
`.gomp_critical_user_dtwc_run_openmp_exception` symbol into every TU that
Expand Down
58 changes: 42 additions & 16 deletions cmake/Dependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,48 @@ function(dtwc_setup_dependencies)
else()
set(CUPDLP_GPU OFF CACHE BOOL "Enable HiGHS cuPDLP GPU support" FORCE)
endif()
CPMAddPackage(
NAME highs
URL "https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.15.1.tar.gz"
# SHA256 pinned (Task 0.12). Computed 2026-07-08 from the GitHub release
# tarball for the immutable tag v1.15.1 (`curl -sL … | sha256sum`).
URL_HASH SHA256=a840d269dff2fafb371dd247df13ad5e026d7ce3b35ad3dc1eedd59bf0c2fb16
SYSTEM
EXCLUDE_FROM_ALL
OPTIONS
"CI OFF"
"ZLIB OFF"
"BUILD_CXX_EXE OFF"
"BUILD_EXAMPLES OFF"
"BUILD_TESTING OFF"
"FAST_BUILD ON"
)
# A Python extension cannot rely on HiGHS' Unix default of BUILD_SHARED_LIBS=ON:
# the resulting @rpath/libhighs dependency is outside site-packages in local
# source/editable installs. Keep these normal variables scoped to the HiGHS
# subproject so native DTWC++ builds retain their requested shared-library
# policy. HiGHS sets PIC on its static target, making it safe to fold into the
# extension. GPU HiGHS is exempt because its Windows CUDA build requires DLLs.
block(SCOPE_FOR VARIABLES)
if(DTWC_BUILD_PYTHON AND NOT DTWC_HIGHS_GPU)
set(BUILD_SHARED_LIBS OFF)
set(BUILD_SHARED_EXTRAS_LIB OFF)
endif()

CPMAddPackage(
NAME highs
URL "https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.15.1.tar.gz"
# SHA256 pinned (Task 0.12). Computed 2026-07-08 from the GitHub release
# tarball for the immutable tag v1.15.1 (`curl -sL … | sha256sum`).
URL_HASH SHA256=a840d269dff2fafb371dd247df13ad5e026d7ce3b35ad3dc1eedd59bf0c2fb16
SYSTEM
EXCLUDE_FROM_ALL
OPTIONS
"CI OFF"
"ZLIB OFF"
"BUILD_CXX_EXE OFF"
"BUILD_EXAMPLES OFF"
"BUILD_TESTING OFF"
"FAST_BUILD ON"
)
endblock()

# Fail during configuration, rather than shipping another extension with an
# unresolved libhighs dependency, if HiGHS changes how it honours
# BUILD_SHARED_LIBS.
if(DTWC_BUILD_PYTHON AND NOT DTWC_HIGHS_GPU AND TARGET highs)
get_target_property(_dtwc_highs_library_type highs TYPE)
if(NOT _dtwc_highs_library_type STREQUAL "STATIC_LIBRARY")
message(FATAL_ERROR
"Python packages require statically bundled HiGHS; got "
"${_dtwc_highs_library_type}")
endif()
endif()

# Historically HiGHS <=1.14.0 had a debug assertion (ub_consistent) that
# fired on valid warm-start MIP solves (primal-dual integral bookkeeping
# tolerance 1e-12 too tight after a presolve reset). Retained defensively:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Issues = "https://github.com/Battery-Intelligence-Lab/dtw-cpp/issues"
Changelog = "https://github.com/Battery-Intelligence-Lab/dtw-cpp/blob/main/CHANGELOG.md"

[project.optional-dependencies]
test = ["pytest>=6.0"]
test = ["pytest>=6.0", "matplotlib>=3.7"]
sklearn = ["scikit-learn>=1.0"]
hdf5 = ["h5py>=3.0"]
parquet = ["pyarrow>=12.0"]
Expand Down
53 changes: 27 additions & 26 deletions scripts/slurm/jobs/cluster_generic.slurm
Original file line number Diff line number Diff line change
Expand Up @@ -97,34 +97,35 @@ mkdir -p "${RESULTS_DIR}"

T_START=$(date +%s.%N)
DTWC_EXIT=0
SEED_ARGS=()
DTWC_ARGS=(
--input "${DTWC_INPUT}"
-k "${DTWC_K}"
--skip-cols "${DTWC_SKIP_COLS}"
--dtype "${DTWC_DTYPE}"
--method "${DTWC_METHOD}"
--band "${DTWC_BAND}"
--device "${DTWC_DEVICE}"
--max-iter "${DTWC_MAX_ITER}"
--variant "${DTWC_VARIANT}"
--wdtw-g "${DTWC_WDTW_G}"
--adtw-penalty "${DTWC_ADTW_PENALTY}"
--msm-c "${DTWC_MSM_C}"
--twe-nu "${DTWC_TWE_NU}"
--twe-lambda "${DTWC_TWE_LAMBDA}"
--mv-mode "${DTWC_MV_MODE}"
--missing-strategy "${DTWC_MISSING_STRATEGY}"
--metric "${DTWC_METRIC}"
--n-init "${DTWC_N_INIT}"
)
if [[ -n "${DTWC_SEED}" ]]; then
SEED_ARGS=(--seed "${DTWC_SEED}")
DTWC_ARGS+=(--seed "${DTWC_SEED}")
fi
"${DTWC_BIN}" \
--input "${DTWC_INPUT}" \
-k "${DTWC_K}" \
--skip-cols "${DTWC_SKIP_COLS}" \
--dtype "${DTWC_DTYPE}" \
--method "${DTWC_METHOD}" \
--band "${DTWC_BAND}" \
--device "${DTWC_DEVICE}" \
--max-iter "${DTWC_MAX_ITER}" \
--variant "${DTWC_VARIANT}" \
--wdtw-g "${DTWC_WDTW_G}" \
--adtw-penalty "${DTWC_ADTW_PENALTY}" \
--msm-c "${DTWC_MSM_C}" \
--twe-nu "${DTWC_TWE_NU}" \
--twe-lambda "${DTWC_TWE_LAMBDA}" \
--mv-mode "${DTWC_MV_MODE}" \
--missing-strategy "${DTWC_MISSING_STRATEGY}" \
--metric "${DTWC_METRIC}" \
--n-init "${DTWC_N_INIT}" \
"${SEED_ARGS[@]}" \
--name "${DTWC_NAME}" \
--output "${RESULTS_DIR}" \
--verbose \
|| DTWC_EXIT=$?
DTWC_ARGS+=(
--name "${DTWC_NAME}"
--output "${RESULTS_DIR}"
--verbose
)
"${DTWC_BIN}" "${DTWC_ARGS[@]}" || DTWC_EXIT=$?
T_END=$(date +%s.%N)
ELAPSED=$(echo "${T_END} - ${T_START}" | bc 2>/dev/null || echo "?")

Expand Down
19 changes: 18 additions & 1 deletion tests/python/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,24 @@ def test_no_python_side_copy_of_the_local_device(self):
)
@pytest.mark.parametrize(
("requested", "canonical"),
[("cuda", "gpu"), ("cuda:0", "gpu"), ("gpu:0", "gpu"), ("GPU", "gpu")],
[
pytest.param(
"cuda", "gpu",
marks=pytest.mark.skipif(
not (dtwcpp.CUDA_AVAILABLE and dtwcpp.cuda_available()),
reason="explicit CUDA unavailable",
),
),
pytest.param(
"cuda:0", "gpu",
marks=pytest.mark.skipif(
not (dtwcpp.CUDA_AVAILABLE and dtwcpp.cuda_available()),
reason="explicit CUDA unavailable",
),
),
("gpu:0", "gpu"),
("GPU", "gpu"),
],
)
def test_gpu_aliases_canonicalise(self, requested, canonical):
try:
Expand Down
3 changes: 2 additions & 1 deletion tests/python/test_hpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,8 @@ def test_restart_schedule_reaches_dtwc_cl(self):
assert "DTWC_SEED=${SEED}" in wrapper
assert 'DTWC_N_INIT="${DTWC_N_INIT:-1}"' in job
assert '--n-init "${DTWC_N_INIT}"' in job
assert 'SEED_ARGS=(--seed "${DTWC_SEED}")' in job
assert 'DTWC_ARGS+=(--seed "${DTWC_SEED}")' in job
assert '"${DTWC_BIN}" "${DTWC_ARGS[@]}"' in job
for name in (
"MAX_ITER", "VARIANT", "WDTW_G", "ADTW_PENALTY", "MSM_C",
"TWE_NU", "TWE_LAMBDA", "MV_MODE", "MISSING_STRATEGY", "METRIC",
Expand Down
15 changes: 7 additions & 8 deletions tests/python/test_version_ssot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@
import subprocess

import dtwcpp
from dtwcpp._hpc import find_dtwc_binary


ROOT = Path(__file__).resolve().parents[2]


def _cli_path() -> Path:
override = os.environ.get("DTWC_CL_PATH")
candidates = [
Path(override) if override else None,
ROOT / "build" / "cfg-gate-normal" / "dtwc_cl.exe",
ROOT / "build" / "cfg-gate-normal" / "bin" / "dtwc_cl.exe",
]
for candidate in candidates:
if candidate is not None and candidate.exists():
return candidate
if override and Path(override).exists():
return Path(override)

candidate = find_dtwc_binary(str(ROOT))
if candidate is not None:
return Path(candidate)
raise AssertionError("dtwc_cl executable not found; set DTWC_CL_PATH")


Expand Down