Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
41196f6
fix backend switching
StRigaud Mar 18, 2026
a5765e0
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 18, 2026
0e45413
0.21 is rotten, prep bumping version to 0.22.0 for clarity
StRigaud Mar 18, 2026
4d08dd8
fix F824
StRigaud Mar 18, 2026
7c45424
rc1
StRigaud Mar 18, 2026
9bdba03
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 18, 2026
b7bc068
fix local build order
StRigaud Mar 18, 2026
939ce9e
build backend first
StRigaud Mar 18, 2026
9370df2
rc2
StRigaud Mar 18, 2026
a4add78
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 18, 2026
9cd5771
correct nvrtv-buildins linkage
StRigaud Mar 19, 2026
73b97f1
bump rc3
StRigaud Mar 19, 2026
93b7916
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 19, 2026
6377161
fix repair script
StRigaud Mar 19, 2026
1b981e5
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 19, 2026
595ae3a
fix script
StRigaud Mar 19, 2026
db654db
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 19, 2026
a1ddab8
try again
StRigaud Mar 19, 2026
8ccdf3a
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 19, 2026
8594cbb
test with python3
StRigaud Mar 19, 2026
b3a00d1
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 19, 2026
6b9642a
rc4
StRigaud Mar 19, 2026
b1cf0ba
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 19, 2026
5e15b5d
fix loss of ptr when reselecting backend
StRigaud Mar 23, 2026
d7f9c1a
clean up array operators
StRigaud Mar 23, 2026
d33b297
clean up
StRigaud Mar 23, 2026
0a39231
add reshape method
StRigaud Mar 23, 2026
2080e49
improve get/set item
StRigaud Mar 23, 2026
6c2f2b5
bump rc5
StRigaud Mar 23, 2026
60b3f7b
style(pre-commit.ci): auto fixes
pre-commit-ci[bot] Mar 23, 2026
af83e0c
clean up
StRigaud Mar 23, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: Deploy
on:
pull_request:
branches: [main]
types: [labeled]
# types: [labeled]
release:
types: [created]

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ __pycache__/
# IDE
.vscode

# pixi
.pixi/

# Distribution / packaging
_skbuild
.Python
Expand Down
3 changes: 2 additions & 1 deletion backends/cuda/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)
## CLIc dependency
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/../../pyclesperanto/_version.py" VERSION_FILE_CONTENT)
string(REGEX MATCH "CLIC_VERSION = \"([^\"]+)\"" _ ${VERSION_FILE_CONTENT})
set(CLIC_REPO_TAG ${CMAKE_MATCH_1})
# set(CLIC_REPO_TAG ${CMAKE_MATCH_1})
set(CLIC_REPO_TAG "master")
set(CLIC_REPO_URL "https://github.com/clEsperanto/CLIc.git")
# if not set, set the default build type to Release
set(CLE_BACKEND "CUDA" CACHE STRING "Backend to use (CUDA or OCL)")
Expand Down
15 changes: 5 additions & 10 deletions backends/cuda/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,27 +66,22 @@ test-command = "python -c \"from importlib.metadata import distribution; files =
[tool.cibuildwheel.linux]
before-all = "bash {package}/../../scripts/build-cuda-linux.sh"
manylinux-x86_64-image = "manylinux_2_28"
# Build-time environment (stubs needed for linking without GPU)
environment = { CUDA_HOME = "/usr/local/cuda", PATH = "/usr/local/cuda/bin:$PATH", LD_LIBRARY_PATH = "/usr/local/cuda/lib64:/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH", SKBUILD_CMAKE_ARGS = "-DCLE_BACKEND=CUDA"}
# Repair-time: override LD_LIBRARY_PATH to REMOVE stubs, exclude driver libs
environment = { CUDA_HOME = "/usr/local/cuda", PATH = "/usr/local/cuda/bin:$PATH", LD_LIBRARY_PATH = "/usr/local/cuda/lib64:/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH", SKBUILD_CMAKE_ARGS = "-DCLE_BACKEND=CUDA" }
repair-wheel-command = """
LD_LIBRARY_PATH=/usr/local/cuda/lib64 \
auditwheel show {wheel} && \
LD_LIBRARY_PATH=/usr/local/cuda/lib64 \
auditwheel repair \
--lib-sdir .libs \
--exclude libcuda.so.1 \
--exclude libnvidia-ml.so.1 \
--exclude libnvidia-ptxjitcompiler.so.1 \
-w {dest_dir} {wheel}
-w {dest_dir} {wheel} && \
bash {package}/../../scripts/repair-nvrtc-linux.sh {dest_dir}
"""

## IMPORTANT: cuda version must match the one used to build the wheel
## Current version is 12.6
[tool.cibuildwheel.windows]
before-build = "pip install delvewheel"
environment = { SKBUILD_CMAKE_ARGS = "-DCLE_BACKEND=CUDA", CUDA_HOME = "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.6", CUDA_PATH = "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.6", PATH = "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.6\\bin;$PATH"}
repair-wheel-command = "delvewheel show {wheel} && delvewheel repair --add-path \"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6/bin\" --no-dll \"nvcuda.dll;msvcp140.dll;vcruntime140_1.dll;vcruntime140.dll\" -w {dest_dir} {wheel}"
environment = { SKBUILD_CMAKE_ARGS = "-DCLE_BACKEND=CUDA", CUDA_HOME = "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.6", CUDA_PATH = "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.6", PATH = "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.6\\bin;$PATH" }
repair-wheel-command = "delvewheel show {wheel} && delvewheel repair --add-path \"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6/bin\" --add-dll \"nvrtc-builtins64_126.dll\" --no-dll \"nvcuda.dll;msvcp140.dll;vcruntime140_1.dll;vcruntime140.dll\" -w {dest_dir} {wheel}"

[tool.cibuildwheel.macos]
# CUDA is not supported on macOS — builds are skipped via the skip pattern above
3 changes: 2 additions & 1 deletion backends/opencl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)
## CLIc dependency
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/../../pyclesperanto/_version.py" VERSION_FILE_CONTENT)
string(REGEX MATCH "CLIC_VERSION = \"([^\"]+)\"" _ ${VERSION_FILE_CONTENT})
set(CLIC_REPO_TAG ${CMAKE_MATCH_1})
# set(CLIC_REPO_TAG ${CMAKE_MATCH_1})
set(CLIC_REPO_TAG "master")
set(CLIC_REPO_URL "https://github.com/clEsperanto/CLIc.git")
# if not set, set the default build type to Release
set(CLE_BACKEND "OPENCL" CACHE STRING "Backend to use (CUDA or OCL)")
Expand Down
400 changes: 199 additions & 201 deletions pixi.lock

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@ channels = ["conda-forge"]
platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"]

[dependencies]
python = ">=3.8,<3.14"
pip = "*"
wheel = "*"
hatchling = "*"
scikit-build-core = ">=0.10,<1"
pybind11 = ">=3,<4"
cmake = ">=3.20,<4"
ninja = ">=0.13.2,<2"
pre-commit = ">=4.4.0,<5"
python = ">=3.13.12,<3.14"
pip = ">=26.0.1,<27"
wheel = ">=0.46.3,<0.47"
hatchling = ">=1.29.0,<2"
scikit-build-core = ">=0.12.1,<0.13"
pybind11 = ">=3.0.2,<4"
cmake = ">=4.3.0,<5"
ninja = ">=1.13.2,<2"
pre-commit = ">=4.5.1,<5"

[feature.dev.dependencies]
pytest = "*"
pytest-cov = "*"
pytest-benchmark = "*"
scikit-image = "*"
isort = "*"
jupyter = "*"
pandas = "*"
pytest = ">=9.0.2,<10"
pytest-cov = ">=7.0.0,<8"
pytest-benchmark = ">=5.2.3,<6"
scikit-image = ">=0.26.0,<0.27"
isort = ">=8.0.1,<9"
jupyter = ">=1.1.1,<2"
pandas = ">=3.0.1,<4"

[feature.dev.pypi-dependencies]
napari = { extras = ["all"], version = "*" }
napari = { version = ">=0.6.5, <0.7", extras = ["all"] }

[tasks]
build-ocl = "pip install -e ./backends/opencl -v && pip install -e ."
Expand Down
194 changes: 104 additions & 90 deletions pyclesperanto/_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ def _get_array_class():
return _get_backend()._Array


# We need Array to be importable at module level for type hints,
# but it must resolve lazily from the backend.
class _ArrayMeta(type):
"""Metaclass that makes isinstance/issubclass work with the backend _Array."""
"""Metaclass that makes isinstance/issubclass always check the *current* backend."""

def __instancecheck__(cls, instance):
try:
Expand All @@ -30,21 +28,24 @@ def __subclasscheck__(cls, subclass):
except RuntimeError:
return False

# ------------------------------------------------------------------ #
# Forward every attribute access to the *current* backend _Array. #
# This makes cle.Array.create(...) always use the right backend. #
# ------------------------------------------------------------------ #
def __getattr__(cls, name):
return getattr(_get_array_class(), name)


class Array(metaclass=_ArrayMeta):
"""Lazy proxy for the backend Array class.

The actual class methods are patched onto the real backend _Array class
at the end of this module via _patch_array_class().
Attribute access is forwarded to the *currently active* backend _Array,
so switching backends with select_backend() is fully transparent.
"""

pass


# Flag to track if we already patched
_array_patched = False


def _prepare_array(arr) -> np.ndarray:
"""Converts a given array to a numpy array with C memory layout.

Expand Down Expand Up @@ -307,92 +308,105 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
return NotImplemented


def reshape(self, shape):
"""Reshape the Array."""
return self.get().reshape(shape)
def _reset_array_patch():
"""Reset the array patch flag and immediately re-patch with the new backend.

Called when the active backend changes. This ensures that:
1. The Array class is re-bound to the new backend's _Array class
2. Methods are re-patched onto the new class
3. Module-level bindings are updated so cle.Array points to the new backend
"""
global _array_patched
_array_patched = False

# Re-patch the Array class with the new backend
_patch_array_class()

# Update module-level bindings in pyclesperanto/__init__.py
# so that cle.Array always refers to the current backend's Array class
import sys

init_module = sys.modules.get("pyclesperanto")
if init_module is not None:
try:
setattr(init_module, "Array", Array)
setattr(init_module, "Image", Image)
except Exception:
pass # Ignore any errors updating bindings


def _patch_array_class():
"""Patch the backend _Array class with Python methods. Called once on first use."""
global Array, _array_patched, Image
if _array_patched:
return
_array_patched = True

# Replace the proxy Array with the real backend class
Array = _get_array_class()

# Add class methods, properties and magic methods
setattr(Array, "T", property(T))
setattr(Array, "set", set)
setattr(Array, "get", get)
setattr(Array, "__array_ufunc__", __array_ufunc__)
setattr(Array, "__str__", __str__)
setattr(Array, "__repr__", __repr__)
setattr(Array, "__array__", __array__)
setattr(Array, "from_array", classmethod(from_array))
setattr(Array, "empty", classmethod(empty))
setattr(Array, "empty_like", classmethod(empty_like))
setattr(Array, "zeros", classmethod(zeros))
setattr(Array, "zeros_like", classmethod(zeros_like))
setattr(Array, "to_device", classmethod(to_device))
setattr(Array, "reshape", reshape)

# Add operations and class methods from _operators module
setattr(Array, "astype", _operators._astype)
setattr(Array, "max", _operators._max)
setattr(Array, "min", _operators._min)
setattr(Array, "sum", _operators._sum)
setattr(Array, "std", _operators._std)
setattr(Array, "__pos__", _operators.__pos__)
setattr(Array, "__neg__", _operators.__neg__)
setattr(Array, "__add__", _operators.__add__)
setattr(Array, "__iadd__", _operators.__iadd__)
setattr(Array, "__sub__", _operators.__sub__)
setattr(Array, "__div__", _operators.__div__)
setattr(Array, "__truediv__", _operators.__truediv__)
setattr(Array, "__idiv__", _operators.__idiv__)
setattr(Array, "__itruediv__", _operators.__itruediv__)
setattr(Array, "__mul__", _operators.__mul__)
setattr(Array, "__imul__", _operators.__imul__)
setattr(Array, "__gt__", _operators.__gt__)
setattr(Array, "__ge__", _operators.__ge__)
setattr(Array, "__lt__", _operators.__lt__)
setattr(Array, "__le__", _operators.__le__)
setattr(Array, "__eq__", _operators.__eq__)
setattr(Array, "__ne__", _operators.__ne__)
setattr(Array, "__pow__", _operators.__pow__)
setattr(Array, "__ipow__", _operators.__ipow__)
setattr(Array, "_plt_to_png", _operators.__plt_to_png__)
setattr(Array, "_png_to_html", _operators.__png_to_html__)
setattr(Array, "_repr_html_", _operators.__repr_html__)
setattr(Array, "__iter__", _operators.__iter__)
setattr(Array, "__setitem__", _operators.__setitem__)
setattr(Array, "__getitem__", _operators.__getitem__)

# Update module-level Image type
Image = Union[np.ndarray, Array]


# Create Image type (uses proxy initially, updated after patching)
"""Patch the *current* backend _Array class with Python methods.

Safe to call multiple times — re-patches whenever the backend changes.
"""
# NOTE: We intentionally do NOT replace the module-level `Array` name.
# The _ArrayMeta proxy always delegates to _get_array_class() at runtime.
BackendArray = _get_array_class()

setattr(BackendArray, "T", property(T))
setattr(BackendArray, "set", set)
setattr(BackendArray, "get", get)
setattr(BackendArray, "__array_ufunc__", __array_ufunc__)
setattr(BackendArray, "__str__", __str__)
setattr(BackendArray, "__repr__", __repr__)
setattr(BackendArray, "__array__", __array__)
setattr(BackendArray, "from_array", classmethod(from_array))
setattr(BackendArray, "empty", classmethod(empty))
setattr(BackendArray, "empty_like", classmethod(empty_like))
setattr(BackendArray, "zeros", classmethod(zeros))
setattr(BackendArray, "zeros_like", classmethod(zeros_like))
setattr(BackendArray, "to_device", classmethod(to_device))
# setattr(BackendArray, "reshape", reshape)

setattr(BackendArray, "astype", _operators._astype)
setattr(BackendArray, "max", _operators._max)
setattr(BackendArray, "min", _operators._min)
setattr(BackendArray, "sum", _operators._sum)
setattr(BackendArray, "std", _operators._std)
setattr(BackendArray, "__pos__", _operators.__pos__)
setattr(BackendArray, "__neg__", _operators.__neg__)
setattr(BackendArray, "__add__", _operators.__add__)
setattr(BackendArray, "__iadd__", _operators.__iadd__)
setattr(BackendArray, "__sub__", _operators.__sub__)
setattr(BackendArray, "__div__", _operators.__div__)
setattr(BackendArray, "__truediv__", _operators.__truediv__)
setattr(BackendArray, "__idiv__", _operators.__idiv__)
setattr(BackendArray, "__itruediv__", _operators.__itruediv__)
setattr(BackendArray, "__mul__", _operators.__mul__)
setattr(BackendArray, "__imul__", _operators.__imul__)
setattr(BackendArray, "__gt__", _operators.__gt__)
setattr(BackendArray, "__ge__", _operators.__ge__)
setattr(BackendArray, "__lt__", _operators.__lt__)
setattr(BackendArray, "__le__", _operators.__le__)
setattr(BackendArray, "__eq__", _operators.__eq__)
setattr(BackendArray, "__ne__", _operators.__ne__)
setattr(BackendArray, "__pow__", _operators.__pow__)
setattr(BackendArray, "__ipow__", _operators.__ipow__)
# setattr(BackendArray, "_figure_to_png", _operators.__figure_to_png__)
# setattr(BackendArray, "_png_to_html", _operators.__png_to_html__)
setattr(BackendArray, "_repr_html_", _operators.__repr_html__)
setattr(BackendArray, "__iter__", _operators.__iter__)
setattr(BackendArray, "__setitem__", _operators.__setitem__)
setattr(BackendArray, "__getitem__", _operators.__getitem__)


Image = Union[np.ndarray, Array]


def is_image(object):
"""Returns True if the given object is an image."""
return (
isinstance(object, np.ndarray)
or isinstance(object, tuple)
or isinstance(object, list)
or isinstance(object, Array)
or str(type(object))
in [
"<class 'cupy._core.core.ndarray'>",
"<class 'dask.array.core.Array'>",
"<class 'xarray.core.dataarray.DataArray'>",
"<class 'resource_backed_dask_array.ResourceBackedDaskArray'>",
"<class 'torch.Tensor'>",
"<class 'pyclesperanto_prototype._tier0._pycl.OCLArray'>",
"<class 'napari.layers._multiscale_data.MultiScaleData'>",
]
)
if isinstance(object, (np.ndarray, tuple, list, Array)):
return True

type_str = type(object).__module__ + "." + type(object).__qualname__

return type_str in [
"cupy._core.core.ndarray",
"dask.array.core.Array",
"xarray.core.dataarray.DataArray",
"resource_backed_dask_array.ResourceBackedDaskArray",
"torch.Tensor",
"pyclesperanto_prototype._tier0._pycl.OCLArray",
"napari.layers._multiscale_data.MultiScaleData",
]
Loading
Loading