arraybridge 0.3.6__tar.gz → 0.3.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arraybridge-0.3.6 → arraybridge-0.3.7}/PKG-INFO +15 -1
- {arraybridge-0.3.6 → arraybridge-0.3.7}/README.md +14 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/index.rst +2 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/release_035_validation.rst +1 -1
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/release_036_validation.rst +1 -1
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/__init__.py +1 -1
- arraybridge-0.3.7/src/arraybridge/array_operations.py +450 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/types.py +38 -7
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_callable_dtype_default.py +3 -1
- arraybridge-0.3.7/tests/test_native_geometry.py +65 -0
- arraybridge-0.3.6/src/arraybridge/array_operations.py +0 -396
- {arraybridge-0.3.6 → arraybridge-0.3.7}/.github/workflows/ci.yml +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/.github/workflows/coverage-pages.yml +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/.github/workflows/gpu-tests.yml +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/.github/workflows/publish.yml +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/.gitignore +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/.readthedocs.yml +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/ADDING_NEW_FRAMEWORKS.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/CONTRIBUTING.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/LICENSE +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/TESTING.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/Makefile +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/CI_ARTIFACT_UPDATE.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/CI_CUPY_FIX.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/CI_IMPROVEMENTS.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/CONFLICT_VISUALIZATION.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/COVERAGE_AUDIT_PLAN.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/DEPLOYMENT_SUMMARY.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/GPU_TESTING_SETUP.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/MERGE_CONFLICT_RESOLUTION.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/MIGRATION_SUMMARY.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/QUICK_FIX_GUIDE.txt +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/README.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/archive/RESOLVED_FILES.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/ci-cd.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/.gitignore +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/Makefile +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/README.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/advanced_topics.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/api_reference.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/ci-cd.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/conf.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/contributing.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/converters.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/decorators.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/examples/basic_conversion.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/examples/decorators.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/examples/index.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/examples/multi_framework.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/gpu_features.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/installation.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/quickstart.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/stack_utils.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/docs/source/user_guide.rst +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/paper.bib +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/paper.md +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/pyproject.toml +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/scripts/release.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/scripts/update_version.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/scripts/verify_release_ready.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/array_geometry.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/array_payload.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/converters.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/converters_registry.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/decorators.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/dtype_scaling.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/exceptions.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/framework_config.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/framework_ops.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/gpu_cleanup.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/oom_recovery.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/slice_processing.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/stack_utils.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/src/arraybridge/utils.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/__init__.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/conftest.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_array_geometry.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_converters.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_converters_registry.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_decorators.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_dtype_scaling.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_durable_decorator_context.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_exceptions.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_framework_config.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_gpu_cleanup.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_integration.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_oom_recovery.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_registry_integration.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_slice_processing.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_stack_utils.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_types.py +0 -0
- {arraybridge-0.3.6 → arraybridge-0.3.7}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraybridge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto with automatic memory type conversion
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenHCSDev/arraybridge
|
|
6
6
|
Project-URL: Documentation, https://arraybridge.readthedocs.io
|
|
@@ -143,3 +143,17 @@ pip install "arraybridge[cupy]"
|
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
Documentation: <https://arraybridge.readthedocs.io>
|
|
146
|
+
|
|
147
|
+
### Native array geometry
|
|
148
|
+
|
|
149
|
+
`MemoryType.reshape(array, shape)` and `MemoryType.broadcast_to(array, shape)`
|
|
150
|
+
operate in the array's framework and device. `MemoryType.ones_like(reference,
|
|
151
|
+
shape=..., dtype=...)` allocates on the reference's device; omitting `shape`
|
|
152
|
+
retains its shape. Reshape and broadcast preserve views where the backend can
|
|
153
|
+
represent them. Broadcasting is not a promise of a writable, independent buffer.
|
|
154
|
+
|
|
155
|
+
These operations never silently project pixels to NumPy. Pyclesperanto cannot
|
|
156
|
+
provide native changed-shape reshape or broadcasting, so those requests raise
|
|
157
|
+
`NotImplementedError`. Its native ones allocation supports one to three
|
|
158
|
+
dimensions and its supported integer/float32 dtypes; boolean allocation is
|
|
159
|
+
explicitly unsupported. Existing conversion and disk boundaries remain separate.
|
|
@@ -71,3 +71,17 @@ pip install "arraybridge[cupy]"
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Documentation: <https://arraybridge.readthedocs.io>
|
|
74
|
+
|
|
75
|
+
### Native array geometry
|
|
76
|
+
|
|
77
|
+
`MemoryType.reshape(array, shape)` and `MemoryType.broadcast_to(array, shape)`
|
|
78
|
+
operate in the array's framework and device. `MemoryType.ones_like(reference,
|
|
79
|
+
shape=..., dtype=...)` allocates on the reference's device; omitting `shape`
|
|
80
|
+
retains its shape. Reshape and broadcast preserve views where the backend can
|
|
81
|
+
represent them. Broadcasting is not a promise of a writable, independent buffer.
|
|
82
|
+
|
|
83
|
+
These operations never silently project pixels to NumPy. Pyclesperanto cannot
|
|
84
|
+
provide native changed-shape reshape or broadcasting, so those requests raise
|
|
85
|
+
`NotImplementedError`. Its native ones allocation supports one to three
|
|
86
|
+
dimensions and its supported integer/float32 dtypes; boolean allocation is
|
|
87
|
+
explicitly unsupported. Existing conversion and disk boundaries remain separate.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
ArrayBridge typed thread-local release correction
|
|
2
|
-
|
|
2
|
+
=================================================
|
|
3
3
|
|
|
4
4
|
Parent integration owner. Actual failed original 0.3.5 publisher:
|
|
5
5
|
https://github.com/OpenHCSDev/ArrayBridge/actions/runs/36946585391.
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Native array operations carried by MemoryType declarations."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
_SCALING_RANGES: dict[str, float | tuple[float, float]] = {
|
|
10
|
+
"uint8": 255.0,
|
|
11
|
+
"uint16": 65535.0,
|
|
12
|
+
"uint32": 4294967295.0,
|
|
13
|
+
"int16": (65535.0, 32768.0),
|
|
14
|
+
"int32": (4294967295.0, 2147483648.0),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _dtype_name(dtype: Any) -> str:
|
|
19
|
+
declared_name = getattr(dtype, "name", None)
|
|
20
|
+
if declared_name is not None:
|
|
21
|
+
return str(declared_name)
|
|
22
|
+
return getattr(dtype, "__name__", str(dtype).rsplit(".", maxsplit=1)[-1])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _numpy_dtype_name(dtype: Any) -> str:
|
|
26
|
+
return str(np.dtype(dtype).name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _torch_dtype_name(dtype: Any) -> str:
|
|
30
|
+
return str(dtype).rsplit(".", maxsplit=1)[-1]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _tensorflow_dtype_name(dtype: Any) -> str:
|
|
34
|
+
numpy_dtype = getattr(dtype, "as_numpy_dtype", dtype)
|
|
35
|
+
return str(np.dtype(numpy_dtype).name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _scaled_values(result: Any, result_min: Any, result_max: Any, target_dtype: Any) -> Any:
|
|
39
|
+
normalized = (result - result_min) / (result_max - result_min)
|
|
40
|
+
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
|
|
41
|
+
if range_info is None:
|
|
42
|
+
return normalized
|
|
43
|
+
if isinstance(range_info, tuple):
|
|
44
|
+
scale, offset = range_info
|
|
45
|
+
return normalized * scale - offset
|
|
46
|
+
return normalized * range_info
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _clamp_bounds(target_dtype: Any) -> tuple[float, float] | None:
|
|
50
|
+
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
|
|
51
|
+
if range_info is None:
|
|
52
|
+
return None
|
|
53
|
+
if isinstance(range_info, tuple):
|
|
54
|
+
scale, offset = range_info
|
|
55
|
+
return -offset, scale - offset - 128
|
|
56
|
+
return 0, range_info
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _mapped_dtype(target_dtype: Any, module: Any) -> Any:
|
|
60
|
+
try:
|
|
61
|
+
dtype_name = np.dtype(target_dtype).name
|
|
62
|
+
except TypeError as error:
|
|
63
|
+
raise TypeError(f"Unsupported target dtype {target_dtype!r}") from error
|
|
64
|
+
mapped = getattr(module, dtype_name, None)
|
|
65
|
+
if mapped is None:
|
|
66
|
+
module_name = getattr(module, "__name__", type(module).__name__)
|
|
67
|
+
raise TypeError(f"{module_name} does not expose dtype {dtype_name}")
|
|
68
|
+
return mapped
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ArrayOperations(ABC):
|
|
72
|
+
"""Native array semantics selected by the existing MemoryType declaration."""
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
77
|
+
"""Project pixels to the explicit host boundary."""
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
82
|
+
"""Admit host pixels on the selected framework device."""
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
87
|
+
"""Apply the framework's existing intensity conversion law."""
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def dtype_name(dtype: Any) -> str:
|
|
91
|
+
return _numpy_dtype_name(dtype)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
95
|
+
return module.stack(tuple(values), axis=0)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
99
|
+
return data.astype(dtype, copy=False)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def logical_and(left: Any, right: Any, module: Any) -> Any:
|
|
103
|
+
return module.logical_and(left, right)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
107
|
+
return data.reshape(shape)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
111
|
+
return module.broadcast_to(data, shape)
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
115
|
+
return module.ones(shape, dtype=dtype)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class NumpyArrayOperations(ArrayOperations):
|
|
119
|
+
"""Numpy native operation leaves."""
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
123
|
+
del module
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
128
|
+
del module, device_id
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
133
|
+
if not hasattr(result, "dtype"):
|
|
134
|
+
return result
|
|
135
|
+
if not (
|
|
136
|
+
module.issubdtype(result.dtype, module.floating)
|
|
137
|
+
and module.issubdtype(target_dtype, module.integer)
|
|
138
|
+
):
|
|
139
|
+
return result.astype(target_dtype)
|
|
140
|
+
result_min = result.min()
|
|
141
|
+
result_max = result.max()
|
|
142
|
+
if result_max <= result_min:
|
|
143
|
+
return result.astype(target_dtype)
|
|
144
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
145
|
+
bounds = _clamp_bounds(target_dtype)
|
|
146
|
+
if bounds is not None:
|
|
147
|
+
scaled = module.clip(scaled, *bounds)
|
|
148
|
+
return scaled.astype(target_dtype)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class CupyArrayOperations(ArrayOperations):
|
|
152
|
+
"""Cupy native operation leaves."""
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
156
|
+
del module
|
|
157
|
+
return data.get()
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
161
|
+
del device_id
|
|
162
|
+
return module.array(data)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
166
|
+
if not hasattr(result, "dtype"):
|
|
167
|
+
return result
|
|
168
|
+
if not (
|
|
169
|
+
module.issubdtype(result.dtype, module.floating)
|
|
170
|
+
and not module.issubdtype(target_dtype, module.floating)
|
|
171
|
+
):
|
|
172
|
+
return result.astype(target_dtype)
|
|
173
|
+
result_min = module.min(result)
|
|
174
|
+
result_max = module.max(result)
|
|
175
|
+
if result_max <= result_min:
|
|
176
|
+
return result.astype(target_dtype)
|
|
177
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
178
|
+
bounds = _clamp_bounds(target_dtype)
|
|
179
|
+
if bounds is not None:
|
|
180
|
+
scaled = module.clip(scaled, *bounds)
|
|
181
|
+
return scaled.astype(target_dtype)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TorchArrayOperations(ArrayOperations):
|
|
185
|
+
"""Torch native operation leaves."""
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
189
|
+
del module
|
|
190
|
+
return data.cpu().numpy()
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
194
|
+
host_data = (
|
|
195
|
+
np.ascontiguousarray(data)
|
|
196
|
+
if any(stride < 0 for stride in getattr(data, "strides", ()))
|
|
197
|
+
else data
|
|
198
|
+
)
|
|
199
|
+
return module.from_numpy(host_data).to(f"cuda:{device_id}")
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
203
|
+
if not hasattr(result, "dtype"):
|
|
204
|
+
return result
|
|
205
|
+
mapped = _mapped_dtype(target_dtype, module)
|
|
206
|
+
floats = (module.float16, module.float32, module.float64)
|
|
207
|
+
if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
|
|
208
|
+
return result.to(mapped)
|
|
209
|
+
result_min = result.min()
|
|
210
|
+
result_max = result.max()
|
|
211
|
+
if result_max <= result_min:
|
|
212
|
+
return result.to(mapped)
|
|
213
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
214
|
+
bounds = _clamp_bounds(target_dtype)
|
|
215
|
+
if bounds is not None:
|
|
216
|
+
scaled = module.clamp(scaled, min=bounds[0], max=bounds[1])
|
|
217
|
+
return scaled.to(mapped)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
221
|
+
return module.stack(tuple(values), dim=0)
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
225
|
+
return data.to(dtype=_mapped_dtype(dtype, module))
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def dtype_name(dtype: Any) -> str:
|
|
229
|
+
return str(dtype).rsplit(".", maxsplit=1)[-1]
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
233
|
+
return data.expand(shape)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
237
|
+
return module.ones(shape, dtype=_mapped_dtype(dtype, module), device=reference.device)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TensorflowArrayOperations(ArrayOperations):
|
|
241
|
+
"""Tensorflow native operation leaves."""
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
245
|
+
del module
|
|
246
|
+
return data.numpy()
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
250
|
+
del device_id
|
|
251
|
+
return module.convert_to_tensor(data)
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
255
|
+
if not hasattr(result, "dtype"):
|
|
256
|
+
return result
|
|
257
|
+
mapped = _mapped_dtype(target_dtype, module)
|
|
258
|
+
floats = (module.float16, module.float32, module.float64)
|
|
259
|
+
if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
|
|
260
|
+
return module.cast(result, mapped)
|
|
261
|
+
result_min = module.reduce_min(result)
|
|
262
|
+
result_max = module.reduce_max(result)
|
|
263
|
+
if result_max <= result_min:
|
|
264
|
+
return module.cast(result, mapped)
|
|
265
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
266
|
+
bounds = _clamp_bounds(target_dtype)
|
|
267
|
+
if bounds is not None:
|
|
268
|
+
scaled = module.clip_by_value(scaled, *bounds)
|
|
269
|
+
return module.cast(scaled, mapped)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
273
|
+
return module.cast(data, _mapped_dtype(dtype, module))
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
277
|
+
return module.reshape(data, shape)
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
281
|
+
with module.device(reference.device):
|
|
282
|
+
return module.ones(shape, dtype=_mapped_dtype(dtype, module))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class JaxArrayOperations(ArrayOperations):
|
|
286
|
+
"""Jax native operation leaves."""
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
290
|
+
del module
|
|
291
|
+
return np.asarray(data)
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
295
|
+
devices = tuple(device for device in module.devices() if device.platform == "gpu")
|
|
296
|
+
return module.device_put(data, devices[device_id])
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
300
|
+
if not hasattr(result, "dtype"):
|
|
301
|
+
return result
|
|
302
|
+
if np.dtype(target_dtype) == np.dtype(np.float64):
|
|
303
|
+
x64_enabled = getattr(module.config, "x64_enabled", None)
|
|
304
|
+
if x64_enabled is None:
|
|
305
|
+
x64_enabled = module.config.read("jax_enable_x64")
|
|
306
|
+
if not x64_enabled:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
"JAX float64 output requires x64 mode; set JAX_ENABLE_X64=true before import"
|
|
309
|
+
)
|
|
310
|
+
jnp = module.numpy
|
|
311
|
+
mapped = _mapped_dtype(target_dtype, jnp)
|
|
312
|
+
floats = (jnp.float16, jnp.float32, jnp.float64)
|
|
313
|
+
if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
|
|
314
|
+
return result.astype(mapped)
|
|
315
|
+
result_min = jnp.min(result)
|
|
316
|
+
result_max = jnp.max(result)
|
|
317
|
+
if result_max <= result_min:
|
|
318
|
+
return result.astype(mapped)
|
|
319
|
+
scaled = _scaled_values(result, result_min, result_max, target_dtype)
|
|
320
|
+
bounds = _clamp_bounds(target_dtype)
|
|
321
|
+
if bounds is not None:
|
|
322
|
+
scaled = jnp.clip(scaled, *bounds)
|
|
323
|
+
return scaled.astype(mapped)
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
327
|
+
return module.numpy.stack(tuple(values), axis=0)
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
331
|
+
return data.astype(_mapped_dtype(dtype, module.numpy))
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def logical_and(left: Any, right: Any, module: Any) -> Any:
|
|
335
|
+
return module.numpy.logical_and(left, right)
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
339
|
+
return module.numpy.broadcast_to(data, shape)
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
343
|
+
return module.numpy.ones(shape, dtype=dtype, device=reference.device)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class PyclesperantoArrayOperations(ArrayOperations):
|
|
347
|
+
"""Pyclesperanto native operation leaves."""
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def to_numpy(data: Any, module: Any) -> Any:
|
|
351
|
+
return module.pull(data)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def from_numpy(data: Any, module: Any, device_id: int) -> Any:
|
|
355
|
+
del device_id
|
|
356
|
+
return module.push(data)
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
|
|
360
|
+
if not hasattr(result, "dtype"):
|
|
361
|
+
return result
|
|
362
|
+
target_is_int = np.issubdtype(np.dtype(target_dtype), np.integer)
|
|
363
|
+
if not (np.issubdtype(result.dtype, np.floating) and target_is_int):
|
|
364
|
+
return module.push(module.pull(result).astype(target_dtype))
|
|
365
|
+
result_min = float(module.minimum_of_all_pixels(result))
|
|
366
|
+
result_max = float(module.maximum_of_all_pixels(result))
|
|
367
|
+
if result_max <= result_min:
|
|
368
|
+
return module.push(module.pull(result).astype(target_dtype))
|
|
369
|
+
normalized = module.subtract_image_from_scalar(result, scalar=result_min)
|
|
370
|
+
normalized = module.multiply_image_and_scalar(
|
|
371
|
+
normalized,
|
|
372
|
+
scalar=1.0 / (result_max - result_min),
|
|
373
|
+
)
|
|
374
|
+
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
|
|
375
|
+
if isinstance(range_info, tuple):
|
|
376
|
+
scale, offset = range_info
|
|
377
|
+
scaled = module.multiply_image_and_scalar(normalized, scalar=scale)
|
|
378
|
+
scaled = module.subtract_image_from_scalar(scaled, scalar=offset)
|
|
379
|
+
elif range_info is not None:
|
|
380
|
+
scaled = module.multiply_image_and_scalar(normalized, scalar=range_info)
|
|
381
|
+
else:
|
|
382
|
+
scaled = normalized
|
|
383
|
+
host_values = module.pull(scaled)
|
|
384
|
+
bounds = _clamp_bounds(target_dtype)
|
|
385
|
+
if bounds is not None:
|
|
386
|
+
host_values = np.clip(host_values, *bounds)
|
|
387
|
+
return module.push(host_values.astype(target_dtype))
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def stack(values: Sequence[Any], module: Any) -> Any:
|
|
391
|
+
if not values:
|
|
392
|
+
raise ValueError("Cannot stack an empty pyclesperanto sequence")
|
|
393
|
+
if len(values) == 1:
|
|
394
|
+
source = values[0]
|
|
395
|
+
result = module.create((1, *source.shape), dtype=source.dtype)
|
|
396
|
+
return module.copy_slice(source, result, 0)
|
|
397
|
+
result = values[0]
|
|
398
|
+
for value in values[1:]:
|
|
399
|
+
result = module.concatenate_along_z(result, value)
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def cast(data: Any, dtype: Any, module: Any) -> Any:
|
|
404
|
+
return module.push(module.pull(data).astype(dtype, copy=False))
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def logical_and(left: Any, right: Any, module: Any) -> Any:
|
|
408
|
+
return module.push(np.logical_and(module.pull(left), module.pull(right)))
|
|
409
|
+
|
|
410
|
+
@staticmethod
|
|
411
|
+
def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
412
|
+
if tuple(data.shape) == shape:
|
|
413
|
+
return data
|
|
414
|
+
raise NotImplementedError(
|
|
415
|
+
"pyclesperanto does not provide native array reshape; its reshape downloads pixels"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
|
|
420
|
+
if tuple(data.shape) == shape:
|
|
421
|
+
return data
|
|
422
|
+
raise NotImplementedError("pyclesperanto does not provide native array broadcasting")
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
|
|
426
|
+
if np.dtype(dtype).name not in {
|
|
427
|
+
"float32",
|
|
428
|
+
"int8",
|
|
429
|
+
"int16",
|
|
430
|
+
"int32",
|
|
431
|
+
"uint8",
|
|
432
|
+
"uint16",
|
|
433
|
+
"uint32",
|
|
434
|
+
}:
|
|
435
|
+
raise NotImplementedError(f"pyclesperanto cannot allocate native dtype {dtype!r}")
|
|
436
|
+
if not 1 <= len(shape) <= 3:
|
|
437
|
+
raise NotImplementedError(
|
|
438
|
+
"pyclesperanto native allocation requires one to three dimensions"
|
|
439
|
+
)
|
|
440
|
+
result = module.create(shape, dtype=dtype, device=reference.device)
|
|
441
|
+
module.set(result, scalar=1, device=reference.device)
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
NUMPY_OPERATIONS = NumpyArrayOperations()
|
|
446
|
+
CUPY_OPERATIONS = CupyArrayOperations()
|
|
447
|
+
TORCH_OPERATIONS = TorchArrayOperations()
|
|
448
|
+
TENSORFLOW_OPERATIONS = TensorflowArrayOperations()
|
|
449
|
+
JAX_OPERATIONS = JaxArrayOperations()
|
|
450
|
+
PYCLESPERANTO_OPERATIONS = PyclesperantoArrayOperations()
|
|
@@ -12,7 +12,7 @@ import importlib.util
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
-
from collections.abc import Callable, Iterator, Mapping, MutableMapping
|
|
15
|
+
from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence
|
|
16
16
|
from contextlib import AbstractContextManager, contextmanager, nullcontext
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from enum import Enum
|
|
@@ -834,11 +834,44 @@ class MemoryType(_MemoryTypeFields, Enum):
|
|
|
834
834
|
"""Cast one array through this framework member's operation leaf."""
|
|
835
835
|
|
|
836
836
|
framework = module if module is not None else self.import_module()
|
|
837
|
-
|
|
838
|
-
scope = nullcontext() if device_id is None else self.device_scope(device_id, framework)
|
|
839
|
-
with scope:
|
|
837
|
+
with self._array_device_scope(data, framework):
|
|
840
838
|
return self._operations.cast(data, dtype, framework)
|
|
841
839
|
|
|
840
|
+
def _array_device_scope(self, reference: Any, module: Any) -> AbstractContextManager[None]:
|
|
841
|
+
"""Derive every native operation scope from its actual input array."""
|
|
842
|
+
device_id = self.device_id_of(reference, module)
|
|
843
|
+
return nullcontext() if device_id is None else self.device_scope(device_id, module)
|
|
844
|
+
|
|
845
|
+
def reshape(self, data: Any, shape: Sequence[int], module: Any | None = None) -> Any:
|
|
846
|
+
"""Reshape on the input device without an implicit host projection."""
|
|
847
|
+
framework = module if module is not None else self.import_module()
|
|
848
|
+
with self._array_device_scope(data, framework):
|
|
849
|
+
return self._operations.reshape(data, tuple(shape), framework)
|
|
850
|
+
|
|
851
|
+
def broadcast_to(self, data: Any, shape: Sequence[int], module: Any | None = None) -> Any:
|
|
852
|
+
"""Broadcast on the input device without an implicit host projection."""
|
|
853
|
+
framework = module if module is not None else self.import_module()
|
|
854
|
+
with self._array_device_scope(data, framework):
|
|
855
|
+
return self._operations.broadcast_to(data, tuple(shape), framework)
|
|
856
|
+
|
|
857
|
+
def ones_like(
|
|
858
|
+
self,
|
|
859
|
+
reference: Any,
|
|
860
|
+
*,
|
|
861
|
+
shape: Sequence[int] | None = None,
|
|
862
|
+
dtype: Any = bool,
|
|
863
|
+
module: Any | None = None,
|
|
864
|
+
) -> Any:
|
|
865
|
+
"""Allocate ones on the supplied reference's framework-local device."""
|
|
866
|
+
framework = module if module is not None else self.import_module()
|
|
867
|
+
with self._array_device_scope(reference, framework):
|
|
868
|
+
return self._operations.ones_like(
|
|
869
|
+
reference,
|
|
870
|
+
tuple(reference.shape if shape is None else shape),
|
|
871
|
+
dtype,
|
|
872
|
+
framework,
|
|
873
|
+
)
|
|
874
|
+
|
|
842
875
|
def logical_and(
|
|
843
876
|
self,
|
|
844
877
|
left: Any,
|
|
@@ -848,9 +881,7 @@ class MemoryType(_MemoryTypeFields, Enum):
|
|
|
848
881
|
"""Intersect two arrays through this framework member's operation leaf."""
|
|
849
882
|
|
|
850
883
|
framework = module if module is not None else self.import_module()
|
|
851
|
-
|
|
852
|
-
scope = nullcontext() if device_id is None else self.device_scope(device_id, framework)
|
|
853
|
-
with scope:
|
|
884
|
+
with self._array_device_scope(left, framework):
|
|
854
885
|
return self._operations.logical_and(left, right, framework)
|
|
855
886
|
|
|
856
887
|
def available_device_ids(self, module: Any | None = None) -> tuple[int, ...]:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Callable defaults use the existing typed dtype policy and real conversion."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
import inspect
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pytest
|
|
@@ -10,6 +10,8 @@ from arraybridge.decorators import (
|
|
|
10
10
|
DtypeConversion,
|
|
11
11
|
DtypeConversionConfig,
|
|
12
12
|
PreserveInputDtypeConfig,
|
|
13
|
+
)
|
|
14
|
+
from arraybridge.decorators import (
|
|
13
15
|
numpy as numpy_func,
|
|
14
16
|
)
|
|
15
17
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Native geometry must preserve pixels, sharing and the reference device."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from arraybridge.array_operations import PYCLESPERANTO_OPERATIONS
|
|
7
|
+
from arraybridge.types import MemoryType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_numpy_views_and_independent_reference_allocation():
|
|
11
|
+
source = np.arange(12, dtype=np.float32).reshape(3, 4)[:, ::-1]
|
|
12
|
+
shaped = MemoryType.NUMPY.reshape(source, (3, 1, 4))
|
|
13
|
+
broadcast = MemoryType.NUMPY.broadcast_to(shaped, (3, 2, 4))
|
|
14
|
+
assert np.shares_memory(shaped, source)
|
|
15
|
+
assert np.shares_memory(broadcast, source)
|
|
16
|
+
np.testing.assert_array_equal(broadcast, np.broadcast_to(source[:, None], (3, 2, 4)))
|
|
17
|
+
ones = MemoryType.NUMPY.ones_like(source, shape=(2, 4), dtype=bool)
|
|
18
|
+
assert ones.dtype == np.bool_
|
|
19
|
+
assert not np.shares_memory(ones, source)
|
|
20
|
+
np.testing.assert_array_equal(ones, np.ones((2, 4), dtype=bool))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_numpy_geometry_rejects_invalid_shapes():
|
|
24
|
+
source = np.arange(6).reshape(2, 3)
|
|
25
|
+
with pytest.raises(ValueError):
|
|
26
|
+
MemoryType.NUMPY.reshape(source, (7,))
|
|
27
|
+
with pytest.raises(ValueError):
|
|
28
|
+
MemoryType.NUMPY.broadcast_to(source, (2, 4))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_cupy_geometry_and_allocation_stay_native():
|
|
32
|
+
cp = pytest.importorskip("cupy")
|
|
33
|
+
if not MemoryType.CUPY.available_device_ids(cp):
|
|
34
|
+
pytest.skip("No CUDA device")
|
|
35
|
+
source = cp.asarray(np.arange(12, dtype=np.float32).reshape(3, 4))[:, ::-1]
|
|
36
|
+
shaped = MemoryType.CUPY.reshape(source, (3, 1, 4))
|
|
37
|
+
broadcast = MemoryType.CUPY.broadcast_to(shaped, (3, 2, 4))
|
|
38
|
+
assert shaped.data.ptr == source.data.ptr
|
|
39
|
+
assert broadcast.data.ptr == source.data.ptr
|
|
40
|
+
assert broadcast.device.id == source.device.id
|
|
41
|
+
np.testing.assert_array_equal(
|
|
42
|
+
broadcast.get(), np.broadcast_to(source.get()[:, None], (3, 2, 4))
|
|
43
|
+
)
|
|
44
|
+
ones = MemoryType.CUPY.ones_like(source, shape=(2, 4), dtype=bool)
|
|
45
|
+
assert ones.device.id == source.device.id
|
|
46
|
+
assert ones.dtype == cp.bool_
|
|
47
|
+
np.testing.assert_array_equal(ones.get(), np.ones((2, 4), dtype=bool))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_pycles_geometry_rejects_host_fallback():
|
|
51
|
+
class DeviceOnlyArray:
|
|
52
|
+
shape = (2, 3)
|
|
53
|
+
|
|
54
|
+
def __array__(self):
|
|
55
|
+
pytest.fail("Native geometry must never download pixels")
|
|
56
|
+
|
|
57
|
+
source = DeviceOnlyArray()
|
|
58
|
+
assert PYCLESPERANTO_OPERATIONS.reshape(source, (2, 3), None) is source
|
|
59
|
+
assert PYCLESPERANTO_OPERATIONS.broadcast_to(source, (2, 3), None) is source
|
|
60
|
+
with pytest.raises(NotImplementedError, match="reshape"):
|
|
61
|
+
PYCLESPERANTO_OPERATIONS.reshape(source, (1, 2, 3), None)
|
|
62
|
+
with pytest.raises(NotImplementedError, match="broadcasting"):
|
|
63
|
+
PYCLESPERANTO_OPERATIONS.broadcast_to(source, (4, 2, 3), None)
|
|
64
|
+
with pytest.raises(NotImplementedError, match="dtype"):
|
|
65
|
+
PYCLESPERANTO_OPERATIONS.ones_like(source, (2, 3), bool, None)
|