Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ jobs:
{version: '3.11', glob: cp311*},
{version: '3.12', glob: cp312*},
{version: '3.13', glob: cp313*},
{version: '3.14', glob: cp314*}
{version: '3.14', glob: cp314-*},
{version: '3.14t', glob: cp314t*}
]
name: Python ${{ matrix.python-versions.version }} wheel on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down
15 changes: 9 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,15 @@ and call out anything CI-relevant (wheel matrix, pybind11 ABI, ITK).

## CI specifics

GitHub Actions builds wheels for Python 3.11–3.14 on `ubuntu-latest`,
GitHub Actions builds wheels for Python 3.11–3.14 (both standard and free-threaded) on `ubuntu-latest`,
`ubuntu-24.04-arm`, and `macos-latest` via cibuildwheel.
`pyproject.toml`'s `[tool.cibuildwheel]` skips `*musllinux*` and the
`cp314t-*` free-threaded build; re-enabling free-threaded support requires
auditing the pybind11 + ITK code paths for the no-GIL ABI. The sdist job
also runs `uv run coverage run` then `coverage report -m` — keep coverage
healthy when adding code (the `[tool.coverage.report]` config in
`pyproject.toml`'s `[tool.cibuildwheel]` skips only `*musllinux*` builds.
Free-threaded support is enabled via `py::mod_gil_not_used()` in
`src/warpkit.cpp`. `PyErr_CheckSignals()` calls in `include/warps.h` are
wrapped in a `WARPKIT_CHECK_SIGNALS()` macro that compiles to a no-op under
`Py_GIL_DISABLED`, so GIL builds keep Ctrl+C interruptibility during long ITK
operations and the free-threaded build skips the GIL-requiring check. The
sdist job also runs `uv run coverage run` then `coverage report -m` — keep
coverage healthy when adding code (the `[tool.coverage.report]` config in
`pyproject.toml` omits the test files). PyPI publish and the GHCR Docker
image only run on a published GitHub release.
36 changes: 24 additions & 12 deletions include/warps.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@

namespace py = pybind11;

// PyErr_CheckSignals() needs the GIL; free-threaded builds (Py_GIL_DISABLED)
// declare `py::mod_gil_not_used()` and skip the check, trading Ctrl+C
// interruptibility during long ITK operations for no-GIL execution.
#ifdef Py_GIL_DISABLED
#define WARPKIT_CHECK_SIGNALS() ((void)0)
#else
#define WARPKIT_CHECK_SIGNALS() \
do { \
if (PyErr_CheckSignals() != 0) throw py::error_already_set(); \
} while (0)
#endif

/**
* @brief Invert a displacement map
*
Expand Down Expand Up @@ -139,9 +151,9 @@ py::array_t<T, py::array::f_style> invert_displacement_map(py::array_t<T, py::ar

// Get output
typename DisplacementMapType::Pointer inv_map = identity_filter->GetOutput();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
inv_map->Update();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
itk::ImageRegionConstIteratorWithIndex<DisplacementMapType> inv_map_it(inv_map,
inv_map->GetLargestPossibleRegion());
py::array_t<T, py::array::f_style> inverted_displacement_map(displacement_map);
Expand Down Expand Up @@ -170,7 +182,7 @@ py::array_t<T, py::array::f_style> invert_displacement_field(py::array_t<T, py::
py::array_t<T, py::array::f_style> direction,
py::array_t<T, py::array::f_style> spacing,
ssize_t iterations, bool verbose) {
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();

// Get the displacement field shape
const ssize_t* shape = displacement_field.shape();
Expand Down Expand Up @@ -227,9 +239,9 @@ py::array_t<T, py::array::f_style> invert_displacement_field(py::array_t<T, py::

// Get output
typename DisplacementFieldType::Pointer inv_field = invert_displacement_filter->GetOutput();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
inv_field->Update();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
itk::ImageRegionConstIteratorWithIndex<DisplacementFieldType> inv_field_it(inv_field,
inv_field->GetLargestPossibleRegion());
py::array_t<T, py::array::f_style> inverted_displacement_field(displacement_field);
Expand Down Expand Up @@ -261,7 +273,7 @@ py::array_t<T, py::array::f_style> compute_jacobian_determinant(py::array_t<T, p
py::array_t<T, py::array::f_style> origin,
py::array_t<T, py::array::f_style> direction,
py::array_t<T, py::array::f_style> spacing) {
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();

// Get the displacement field shape
const ssize_t* shape = displacement_field.shape();
Expand Down Expand Up @@ -315,9 +327,9 @@ py::array_t<T, py::array::f_style> compute_jacobian_determinant(py::array_t<T, p
// Get the jacobian determinant fields
typename DisplacementFieldJacobianDeterminantFilterType::OutputImagePointer jacobian_determinant =
jacobian_filter->GetOutput();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
jacobian_determinant->Update();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();

// Convert to numpy array
py::array_t<T, py::array::f_style> jacobian_determinant_array({shape[0], shape[1], shape[2]});
Expand Down Expand Up @@ -461,9 +473,9 @@ py::array_t<T, py::array::f_style> resample(

// Get the output
typename OutputImageType::Pointer output_image = warp_filter->GetOutput();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
output_image->Update();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();
itk::ImageRegionConstIteratorWithIndex<OutputImageType> output_iterator(output_image,
output_image->GetLargestPossibleRegion());
py::array_t<T, py::array::f_style> output_array({output_shape.at(0), output_shape.at(1), output_shape.at(2)});
Expand All @@ -481,7 +493,7 @@ T compute_hausdorff_distance(
py::array_t<T, py::array::f_style> image1_direction, py::array_t<T, py::array::f_style> image1_spacing,
py::array_t<T, py::array::f_style> image2, py::array_t<T, py::array::f_style> image2_origin,
py::array_t<T, py::array::f_style> image2_direction, py::array_t<T, py::array::f_style> image2_spacing) {
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();

// Setup types
using ImageType = typename itk::Image<T, 3>;
Expand Down Expand Up @@ -545,7 +557,7 @@ T compute_hausdorff_distance(
// Get the hausdorff distance
hausdorff_filter->Update();
T hausdorff_distance = hausdorff_filter->GetAverageHausdorffDistance();
if (PyErr_CheckSignals() != 0) throw py::error_already_set();
WARPKIT_CHECK_SIGNALS();

// Return the hausdorff distance
return hausdorff_distance;
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ command_line = "-m pytest"
omit = ["tests/*"]

[tool.cibuildwheel]
# Skip musllinux (we ship manylinux only) and free-threaded CPython builds
# (cp31Xt-*) — our pybind11 bindings + ITK haven't been audited for the no-GIL ABI.
skip = "*musllinux* cp314t-*"
# Skip musllinux (we ship manylinux only). Free-threaded (cp31Xt-*) builds
# are kept: warps.h guards PyErr_CheckSignals() behind WARPKIT_CHECK_SIGNALS,
# which is a no-op under Py_GIL_DISABLED.
skip = "*musllinux*"
build-frontend = "build"
build-verbosity = 3

Expand Down
2 changes: 1 addition & 1 deletion src/warpkit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace py = pybind11;

PYBIND11_MODULE(warpkit_cpp, m) {
PYBIND11_MODULE(warpkit_cpp, m, py::mod_gil_not_used()) {
Comment thread
vanandrew marked this conversation as resolved.
m.def("calculate_weights", &romeo::calculate_weights<float>,
"ROMEO edge-weight map (3, nx, ny, nz) uint8. Exposed for port validation; not used by warpkit.",
py::arg("phase"),
Expand Down
Loading