Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: move c++ wrapping from pybind11 to nanobind #621

Merged
merged 26 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
122 changes: 40 additions & 82 deletions .github/workflows/ci-pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,93 +11,51 @@ on:

env:
# Updates or changes to this, or the runner OS or arch will invalidate the cache
python_version: '3.10' # Python version to use for testing - update when needed
python_version: "3.10" # Python version to use for testing - update when needed

jobs:
test:
runs-on: ubuntu-20.04
# Default shell needs to be bash for conda
# https://github.com/conda-incubator/setup-miniconda?tab=readme-ov-file#important
defaults:
run:
shell: bash -el {0}
run:
shell: bash -el {0}
steps:
- name: Setup Miniconda
uses: conda-incubator/setup-miniconda@030178870c779d9e5e1b4e563269f3aa69b04081 # v3.0.3, devs recommend using hash
with:
miniconda-version: "latest"
python-version: ${{ env.python_version }}
auto-update-conda: true
activate-environment: "test-env"

- name: Configure Conda to use only .tar.bz2
run: |
conda config --set use_only_tar_bz2 true

- name: Checkout code
uses: actions/checkout@v4 # Checkout PR code to 'antspy-pr'
with:
path: antspy-pr

- name: Load conda environment from cache if available
id: cache-env
uses: actions/cache@v4
with:
path: ~/conda-env.tar.bz2
key: >-
${{ runner.os }}-conda-${{ env.python_version }}-${{ hashFiles('antspy-pr/ants/environment.yml',
'antspy-pr/ants/requirements.txt', 'antspy-pr/ants/setup.py', 'antspy-pr/scripts/configure_ITK.sh',
'antspy-pr/scripts/configure_ANTsPy.sh', 'antspy-pr/ants/lib/*') }}

- name: Unpack cached environment
if: steps.cache-env.outputs.cache-hit == 'true'
run: |
mkdir -p ${CONDA}/envs/antspy-env
tar -xjf ~/conda-env.tar.bz2 -C ${CONDA}/envs/antspy-env
conda activate antspy-env
conda-unpack

- name: Install dependencies and ANTsPy from PR
if: steps.cache-env.outputs.cache-hit != 'true'
run: |
conda create -n antspy-env python=${{ env.python_version }} -y
conda activate antspy-env
conda install -c conda-forge conda-pack coverage
conda info
pip install ./antspy-pr
conda list
antspy-pr/tests/run_tests.sh -c
conda pack -n antspy-env -o ~/conda-env.tar.bz2

- name: Cache Conda environment
if: steps.cache-env.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: ~/conda-env.tar.bz2
key: >-
${{ runner.os }}-conda-${{ env.python_version }}-${{ hashFiles('antspy-pr/ants/environment.yml',
'antspy-pr/ants/requirements.txt', 'antspy-pr/ants/setup.py', 'antspy-pr/scripts/configure_ITK.sh',
'antspy-pr/scripts/configure_ANTsPy.sh', 'antspy-pr/ants/lib/*') }}) }}

- name: Replace installed ANTsPy with PR code
if: steps.cache-env.outputs.cache-hit == 'true'
run: |
conda activate antspy-env
ANTS_SITE_PACKAGES="${CONDA}/envs/antspy-env/lib/python${{ env.python_version }}/site-packages/ants"
for d in contrib core learn registration segmentation utils viz; do
rm -rf $ANTS_SITE_PACKAGES/$d;
cp -r antspy-pr/ants/$d $ANTS_SITE_PACKAGES/$d;
done
find $ANTS_SITE_PACKAGES -name '__pycache__' -exec rm -rf {} +

- name: Run tests
if: steps.cache-env.outputs.cache-hit == 'true'
run: |
conda activate antspy-env
bash antspy-pr/tests/run_tests.sh -c

- name: Coveralls
uses: coverallsapp/github-action@v2
with:
files: antspy-pr/tests/coverage.xml

- name: Setup Miniconda
uses: conda-incubator/setup-miniconda@030178870c779d9e5e1b4e563269f3aa69b04081 # v3.0.3, devs recommend using hash
with:
miniconda-version: "latest"
python-version: ${{ env.python_version }}
auto-update-conda: true
activate-environment: "test-env"

- name: Configure Conda to use only .tar.bz2
run: |
conda config --set use_only_tar_bz2 true

- name: Checkout code
uses: actions/checkout@v4 # Checkout PR code to 'antspy-pr'
with:
path: antspy-pr

- name: Install dependencies and ANTsPy from PR
run: |
conda create -n antspy-env python=${{ env.python_version }} -y
conda activate antspy-env
conda install -c conda-forge conda-pack coverage
conda info
pip install ./antspy-pr
conda list
antspy-pr/tests/run_tests.sh -c
conda pack -n antspy-env -o ~/conda-env.tar.bz2

- name: Run tests
run: |
conda activate antspy-env
bash antspy-pr/tests/run_tests.sh -c

- name: Coveralls
uses: coverallsapp/github-action@v2
with:
files: antspy-pr/tests/coverage.xml
90 changes: 90 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"files.associations": {
"iosfwd": "cpp",
"__bit_reference": "cpp",
"__bits": "cpp",
"__config": "cpp",
"__debug": "cpp",
"__errc": "cpp",
"__functional_base": "cpp",
"__hash_table": "cpp",
"__locale": "cpp",
"__mutex_base": "cpp",
"__node_handle": "cpp",
"__nullptr": "cpp",
"__split_buffer": "cpp",
"__string": "cpp",
"__threading_support": "cpp",
"__tuple": "cpp",
"algorithm": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"complex": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"exception": "cpp",
"fstream": "cpp",
"functional": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"ios": "cpp",
"iostream": "cpp",
"istream": "cpp",
"iterator": "cpp",
"limits": "cpp",
"locale": "cpp",
"memory": "cpp",
"mutex": "cpp",
"new": "cpp",
"optional": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"sstream": "cpp",
"stack": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"string": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"utility": "cpp",
"vector": "cpp",
"random": "cpp",
"__tree": "cpp",
"any": "cpp",
"cfenv": "cpp",
"condition_variable": "cpp",
"csetjmp": "cpp",
"csignal": "cpp",
"future": "cpp",
"list": "cpp",
"map": "cpp",
"numeric": "cpp",
"queue": "cpp",
"regex": "cpp",
"set": "cpp",
"unordered_set": "cpp",
"valarray": "cpp",
"variant": "cpp",
"*.in": "cpp",
"*.inc": "cpp",
"*.tmpl": "cpp"
}
}
69 changes: 69 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
cmake_minimum_required(VERSION 3.16.3...3.26)

project(ants LANGUAGES CXX)

set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Try to import all Python components potentially needed by nanobind
find_package(Python 3.8
REQUIRED COMPONENTS Interpreter Development.Module
OPTIONAL_COMPONENTS Development.SABIModule)

# Import nanobind through CMake's find_package mechanism
find_package(nanobind CONFIG REQUIRED)

# TODO: make this run only if ITK + ANTs are not already built (for now: comment these 2 lines out to quickly rebuild antspy)
# TODO: handle different OS either here or within the configure script
# TODO: move this outside of CMakeLists.txt like in the old antspy (issue: how to run scripts from pyproject.toml?)
execute_process(COMMAND bash ${PROJECT_SOURCE_DIR}/scripts/configure_ITK.sh)
execute_process(COMMAND bash ${PROJECT_SOURCE_DIR}/scripts/configure_ANTs.sh)

# ITK
set(ITK_DIR "./itkbuild")
find_package(ITK REQUIRED)
include(${ITK_USE_FILE})

# ANTS
add_library(antsUtilities STATIC src/antscore/antsUtilities.cxx src/antscore/antsCommandLineOption.cxx src/antscore/antsCommandLineParser.cxx src/antscore/ReadWriteData.cxx src/antscore/ANTsVersion.cxx)
add_library(registrationUtilities STATIC src/antscore/antsRegistrationTemplateHeader.cxx
src/antscore/antsRegistration2DDouble.cxx src/antscore/antsRegistration2DFloat.cxx
src/antscore/antsRegistration3DDouble.cxx src/antscore/antsRegistration3DFloat.cxx
src/antscore/antsRegistration4DDouble.cxx src/antscore/antsRegistration4DFloat.cxx)


add_library(imageMathUtilities STATIC src/antscore/ImageMathHelper2D.cxx src/antscore/ImageMathHelper3D.cxx src/antscore/ImageMathHelper4D.cxx)

# this may not be needed
target_link_libraries(antsUtilities ${ITK_LIBRARIES})
target_link_libraries(registrationUtilities ${ITK_LIBRARIES})
target_link_libraries(imageMathUtilities ${ITK_LIBRARIES})

nanobind_add_module(
lib
STABLE_ABI
NB_STATIC
src/main.cpp
src/antscore/antsAffineInitializer.cxx
src/antscore/antsApplyTransforms.cxx
src/antscore/antsApplyTransformsToPoints.cxx
src/antscore/antsJointFusion.cxx
src/antscore/antsRegistration.cxx
src/antscore/Atropos.cxx
src/antscore/AverageAffineTransform.cxx
src/antscore/AverageAffineTransformNoRigid.cxx
src/antscore/CreateJacobianDeterminantImage.cxx
src/antscore/DenoiseImage.cxx
src/antscore/iMath.cxx
src/antscore/KellyKapowski.cxx
src/antscore/LabelClustersUniquely.cxx
src/antscore/LabelGeometryMeasures.cxx
src/antscore/N3BiasFieldCorrection.cxx
src/antscore/N4BiasFieldCorrection.cxx
src/antscore/ResampleImage.cxx
src/antscore/ThresholdImage.cxx
src/antscore/TileImages.cxx
)
target_link_libraries(lib PRIVATE ${ITK_LIBRARIES} antsUtilities registrationUtilities imageMathUtilities)

# Install directive for scikit-build-core
install(TARGETS lib LIBRARY DESTINATION ants)
2 changes: 1 addition & 1 deletion ants/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

__version__ = '0.5.2'
__version__ = '0.6.0'

from .core import *
from .utils import *
Expand Down
26 changes: 13 additions & 13 deletions ants/core/ants_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(self, pixeltype='float', dimension=3, components=1, pointer=None, i

self._libsuffix = '%s%s%i' % (self._shortpclass, utils.short_ptype(self.pixeltype), self.dimension)

self.shape = utils.get_lib_fn('getShape%s'%self._libsuffix)(self.pointer)
self.shape = tuple(utils.get_lib_fn('getShape')(self.pointer))
self.physical_shape = tuple([round(sh*sp,3) for sh,sp in zip(self.shape, self.spacing)])

self._array = None
Expand All @@ -94,8 +94,8 @@ def spacing(self):
-------
tuple
"""
libfn = utils.get_lib_fn('getSpacing%s'%self._libsuffix)
return libfn(self.pointer)
libfn = utils.get_lib_fn('getSpacing')
return tuple(libfn(self.pointer))

def set_spacing(self, new_spacing):
"""
Expand All @@ -116,7 +116,7 @@ def set_spacing(self, new_spacing):
if len(new_spacing) != self.dimension:
raise ValueError('must give a spacing value for each dimension (%i)' % self.dimension)

libfn = utils.get_lib_fn('setSpacing%s'%self._libsuffix)
libfn = utils.get_lib_fn('setSpacing')
libfn(self.pointer, new_spacing)

@property
Expand All @@ -128,8 +128,8 @@ def origin(self):
-------
tuple
"""
libfn = utils.get_lib_fn('getOrigin%s'%self._libsuffix)
return libfn(self.pointer)
libfn = utils.get_lib_fn('getOrigin')
return tuple(libfn(self.pointer))

def set_origin(self, new_origin):
"""
Expand All @@ -150,7 +150,7 @@ def set_origin(self, new_origin):
if len(new_origin) != self.dimension:
raise ValueError('must give a origin value for each dimension (%i)' % self.dimension)

libfn = utils.get_lib_fn('setOrigin%s'%self._libsuffix)
libfn = utils.get_lib_fn('setOrigin')
libfn(self.pointer, new_origin)

@property
Expand All @@ -162,8 +162,8 @@ def direction(self):
-------
tuple
"""
libfn = utils.get_lib_fn('getDirection%s'%self._libsuffix)
return libfn(self.pointer)
libfn = utils.get_lib_fn('getDirection')
return np.array(libfn(self.pointer)).reshape(self.dimension,self.dimension)

def set_direction(self, new_direction):
"""
Expand All @@ -187,7 +187,7 @@ def set_direction(self, new_direction):
if len(new_direction) != self.dimension:
raise ValueError('must give a origin value for each dimension (%i)' % self.dimension)

libfn = utils.get_lib_fn('setDirection%s'%self._libsuffix)
libfn = utils.get_lib_fn('setDirection')
libfn(self.pointer, new_direction)

@property
Expand Down Expand Up @@ -221,7 +221,7 @@ def view(self, single_components=False):
shape = img.shape[::-1]
if img.has_components or (single_components == True):
shape = list(shape) + [img.components]
libfn = utils.get_lib_fn('toNumpy%s'%img._libsuffix)
libfn = utils.get_lib_fn('toNumpy')
memview = libfn(img.pointer)
return np.asarray(memview).view(dtype = dtype).reshape(shape).view(np.ndarray).T

Expand Down Expand Up @@ -281,7 +281,7 @@ def clone(self, pixeltype=None):
p1_short = utils.short_ptype(self.pixeltype)
p2_short = utils.short_ptype(pixeltype)
ndim = self.dimension
fn_suffix = '%s%i%s%i' % (p1_short,ndim,p2_short,ndim)
fn_suffix = '%s%i' % (p2_short,ndim)
libfn = utils.get_lib_fn('antsImageClone%s'%fn_suffix)
pointer_cloned = libfn(self.pointer)
return ANTsImage(pixeltype=pixeltype,
Expand Down Expand Up @@ -348,7 +348,7 @@ def to_file(self, filename):
filepath to which the image will be written
"""
filename = os.path.expanduser(filename)
libfn = utils.get_lib_fn('toFile%s'%self._libsuffix)
libfn = utils.get_lib_fn('toFile')
libfn(self.pointer, filename)
to_filename = to_file

Expand Down
Loading