# scikit-build-core builds the extension through CMake and its debug sources # are not tracked into debugsourcefiles.list (empty -> FTBFS), as with # python-pymeshfix; disable the debug package. %global debug_package %{nil} %global pypi_name antspyx # ANTsRCore commit holding the r64/r85 test slices ANTsPy downloads from figshare # (byte-identical to the figshare copies; ANTsRCore is Apache-2.0 like ANTsPy). %global antsrcore_commit 9a875df253d48e700d0cdf6be14bd60eba62fb6f Name: python-%{pypi_name} Version: 0.6.3 Release: 9%{?dist} Summary: ANTsPy: Advanced Normalization Tools in Python License: Apache-2.0 URL: https://github.com/ANTsX/ANTsPy Source0: %{url}/archive/v%{version}/ANTsPy-%{version}.tar.gz Source1: https://raw.githubusercontent.com/ANTsX/ANTsRCore/%{antsrcore_commit}/inst/extdata/r64slice.jpg Source2: https://raw.githubusercontent.com/ANTsX/ANTsRCore/%{antsrcore_commit}/inst/extdata/r85slice.jpg # Build against system ITK and ANTs. Upstream's CMakeLists runs # scripts/configure_ITK.sh and configure_ANTs.sh, which git-clone ITK 5.4.5 and # ANTs b53aad3, build ITK statically and compile a private copy of 19 ANTs # programs into the module. We link the same programs from ANTs' libl_* # libraries instead (ANTs-devel >= 2.6.5-20 carries the b53aad3 backport). Patch0: python-antspyx-system-itk-ants.patch # ants.get_data(): use the slices shipped in %%{_datadir}/antspyx before # downloading them to ~/.antspy. Patch1: python-antspyx-system-data.patch # numpy 2.4 (Fedora's; upstream caps numpy<2.4): np.reshape(newshape=) is gone, and # assigning a 1-element array into a scalar slot is an error. Found by upstream's # own tests (test_deeplearn x2, test_utils label_image_centroids) in %%check. Patch2: python-antspyx-numpy-2.4.patch BuildRequires: python3-devel BuildRequires: gcc-c++ BuildRequires: cmake BuildRequires: ninja-build BuildRequires: ANTs-devel >= 2.6.5-20 BuildRequires: InsightToolkit5-devel >= 5.4.7-24 # Re-found by ITK's module cmake files through ITK_USE_FILE (as in ANTs.spec). BuildRequires: hdf5-devel BuildRequires: libminc-devel BuildRequires: python3-pytest %global _description %{expand: ANTsPy is the Python interface to Advanced Normalization Tools (ANTs): image registration (rigid, affine, SyN and the antsRegistrationSyN presets), N3/N4 bias field correction, Atropos segmentation, cortical thickness (KellyKapowski), joint label fusion, denoising and resampling, with images held as NumPy-interoperable ANTsImage objects. This build links the system ANTs and ITK libraries instead of the private copies upstream's wheels compile in.} %description %_description %package -n python3-%{pypi_name} Summary: %{summary} # The module shares ITK objects (images, transforms) with libITK* and libl_*: # pin the ITK layout state, as ANTs does (see InsightToolkit5.spec). Requires: itk(abi) = %{?itk_abi}%{!?itk_abi:UNSET} %description -n python3-%{pypi_name} %_description %prep %autosetup -n ANTsPy-%{version} -p1 # Upstream caps numpy <2.4 and scipy <1.16 for its wheel matrix; Fedora ships # numpy 2.4 and scipy 1.16, and the test suite below runs against them. sed -i -e 's/"numpy<2.4.0"/"numpy"/' -e 's/"scipy<1.16"/"scipy"/' pyproject.toml grep -q '^ "numpy",$' pyproject.toml grep -q '^ "scipy"$' pyproject.toml # The configure scripts must not run (they download); Patch0 removed the calls. ! grep -q configure_ CMakeLists.txt # nanobind: upstream pins <2.6; the module builds with Fedora's 2.11. sed -i 's/"nanobind >=1.3.2,<2.6"/"nanobind >=1.3.2"/' pyproject.toml %generate_buildrequires %pyproject_buildrequires %build test -n "%{?itk_abi}" || { echo "ERROR: itk_abi undefined - needs InsightToolkit5-devel >= 5.4.7-24 (macros.itk)" >&2; exit 1; } export CMAKE_ARGS="-DITK_DIR:PATH=%{_prefix}/lib/cmake/ITK-5.4 -DCMAKE_BUILD_TYPE=RelWithDebInfo" export CMAKE_BUILD_PARALLEL_LEVEL=%{_smp_build_ncpus} %pyproject_wheel %install %pyproject_install %pyproject_save_files -L ants install -d %{buildroot}%{_datadir}/antspyx install -p -m 0644 data/r16slice.jpg data/r27slice.jpg data/r30slice.jpg data/r62slice.jpg \ %{SOURCE1} %{SOURCE2} %{buildroot}%{_datadir}/antspyx/ %check # ants.contrib.sklearn_interface does `from ...registration import interface`, # a module that does not exist in ANTsPy 0.6.3: broken upstream, not by us. %pyproject_check_import -e 'ants.contrib.sklearn_interface*' export PYTHONPATH=%{buildroot}%{python3_sitearch} # Upstream's configure_ANTs.sh copies data/ to ~/.antspy; the tests read the # MNI and ch2 volumes from there. The slices resolve to the installed # %%{_datadir}/antspyx copies, so point the patched lookup at the buildroot. export HOME=$PWD/test-home mkdir -p $HOME/.antspy cp -p data/*.nii.gz $HOME/.antspy/ sed -i "s|^SYSTEM_DATA_DIRECTORY = .*|SYSTEM_DATA_DIRECTORY = '%{buildroot}%{_datadir}/antspyx'|" \ %{buildroot}%{python3_sitearch}/ants/utils/get_ants_data.py # 1. Real operations through the system libl_* libraries, with numbers checked. cd / %{python3} -X faulthandler - <<'PYEOF' import numpy as np, ants fi = ants.image_read(ants.get_data('r16')) mi = ants.image_read(ants.get_data('r64')) assert fi.shape == (256, 256) and mi.shape == (256, 256), (fi.shape, mi.shape) def mi_metric(a, b): return ants.image_mutual_information(a, b) before = mi_metric(fi, mi) reg = ants.registration(fi, mi, type_of_transform='SyN', random_seed=42) after = mi_metric(fi, reg['warpedmovout']) print(f"SyN r64->r16: MI {before:.4f} -> {after:.4f}") # ANTs' MI is negative; more negative is better. assert after < before - 0.1, (before, after) assert len(reg['fwdtransforms']) == 2 and len(reg['invtransforms']) == 2 # A second run reaches the same quality. Not bit-identical: threaded SyN is # not deterministic with a seed alone (upstream has the antsRegistrationSyNRepro # presets for that), so compare the metric, not the pixels. reg2 = ants.registration(fi, mi, type_of_transform='SyN', random_seed=42) after2 = mi_metric(fi, reg2['warpedmovout']) print(f"repeat: MI {after2:.4f}") assert abs(after2 - after) < 0.05 and after2 < before - 0.1, (after, after2) # apply_transforms (libl_antsApplyTransforms) reproduces warpedmovout. w = ants.apply_transforms(fi, mi, transformlist=reg['fwdtransforms']) d = np.abs(w.numpy() - reg['warpedmovout'].numpy()).max() assert d < 0.05, d # pixel range 0-255: float rounding only # Vector image through apply_transforms: the in-memory itk::VectorImage path # from ANTs b53aad3 (the -devel backport). imagetype=4 is multi-channel; the default # scalar path on a vector image dereferences the wrong pixel type and crashes. # Each channel must match a scalar warp. vec = ants.merge_channels([mi, mi * 2]) wvec = ants.apply_transforms(fi, vec, transformlist=reg['fwdtransforms'], imagetype=4) ch = ants.split_channels(wvec) assert len(ch) == 2 assert np.abs(ch[0].numpy() - w.numpy()).max() < 0.05 assert np.abs(ch[1].numpy() - 2 * w.numpy()).max() < 0.1 # N4 (libl_N4BiasFieldCorrection): remove a synthetic multiplicative bias. yy, xx = np.mgrid[0:256, 0:256] / 255.0 bias = ants.from_numpy((1.0 + 0.8 * xx).astype('float32'), spacing=fi.spacing, origin=fi.origin, direction=fi.direction) biased = fi * bias mask = ants.get_mask(fi) n4 = ants.n4_bias_field_correction(biased, mask=mask) def cv_of_ratio(img): r = (img.numpy() / np.maximum(fi.numpy(), 1e-3))[mask.numpy() > 0] return r.std() / r.mean() print(f"N4: ratio CV {cv_of_ratio(biased):.3f} -> {cv_of_ratio(n4):.3f}") # Measured 0.113 -> 0.082 on all six chroots with the default N4 settings. assert cv_of_ratio(n4) < 0.85 * cv_of_ratio(biased) # Resampling (libl_ResampleImage) and Atropos segmentation (libl_Atropos). rs = ants.resample_image(fi, (2, 2), use_voxels=False, interp_type=0) assert rs.shape == (128, 128), rs.shape seg = ants.atropos(a=fi, m='[0.2,1x1]', c='[2,0]', i='kmeans[3]', x=mask) labels = np.unique(seg['segmentation'].numpy()) assert set(labels.astype(int)) == {0, 1, 2, 3}, labels print("ANTsPy checks passed") PYEOF cd - # 2. Upstream's unit tests (offline: all data is local, see above). cd tests %pytest -v -p no:cacheprovider cd - %files -n python3-%{pypi_name} -f %{pyproject_files} %license LICENSE %doc README.md %{_datadir}/antspyx/ %changelog * Fri Oct 16 2026 Morgan Hough - 0.6.3-9 - Patch2: three numpy 2.4 breakages found by upstream's tests (251 passed, 3 failed at -8): reshape(newshape=) in randomly_transform_image_data, array-to-scalar assignment in label_image_centroids. * Fri Oct 16 2026 Morgan Hough - 0.6.3-8 - %%check: N4 threshold set from the measurement (residual bias CV 0.113 -> 0.082). * Fri Oct 16 2026 Morgan Hough - 0.6.3-7 - %%check: warp the multi-channel image with imagetype=4. -5/-6 segfaulted because the test passed it down the scalar (imagetype=0) path. * Fri Oct 16 2026 Morgan Hough - 0.6.3-6 - %%check: run with -X faulthandler (segfault between SyN and N4 in -5). * Fri Oct 16 2026 Morgan Hough - 0.6.3-5 - %%check: tolerances on the 0-255 pixel scale (apply_transforms differs from warpedmovout by 0.003, float rounding). * Fri Oct 16 2026 Morgan Hough - 0.6.3-4 - %%check: compare the MI of two SyN runs, not their pixels (threaded SyN is not bit-reproducible with a seed alone; measured max pixel difference 50-146). * Fri Oct 16 2026 Morgan Hough - 0.6.3-3 - %%check: skip the import of ants.contrib.sklearn_interface (imports a non-existent ants.registration.interface; broken upstream in 0.6.3). * Fri Oct 16 2026 Morgan Hough - 0.6.3-2 - No PEP 639 License-File in the metadata: save_files -L, LICENSE listed by hand. * Fri Oct 16 2026 Morgan Hough - 0.6.3-1 - Initial package, linked against system ANTs (ANTs-devel) and ITK.