# 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: 2%{?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 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 %pyproject_check_import 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} - <<'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 # Same seed, same answer (antsRegistration through libl_antsRegistration). reg2 = ants.registration(fi, mi, type_of_transform='SyN', random_seed=42) d = np.abs(reg['warpedmovout'].numpy() - reg2['warpedmovout'].numpy()).max() print("repeat max abs diff:", d) assert d < 1e-3, d # 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 < 1e-3, d # Vector image through apply_transforms: the in-memory itk::VectorImage path # from ANTs b53aad3 (the -devel backport). Each channel must match a scalar warp. vec = ants.merge_channels([mi, mi * 2]) wvec = ants.apply_transforms(fi, vec, transformlist=reg['fwdtransforms']) ch = ants.split_channels(wvec) assert len(ch) == 2 assert np.abs(ch[0].numpy() - w.numpy()).max() < 1e-3 assert np.abs(ch[1].numpy() - 2 * w.numpy()).max() < 1e-2 # 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}") assert cv_of_ratio(n4) < 0.5 * 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-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.