arraybridge 0.3.6__tar.gz → 0.3.8__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.
Files changed (92) hide show
  1. {arraybridge-0.3.6 → arraybridge-0.3.8}/PKG-INFO +15 -1
  2. {arraybridge-0.3.6 → arraybridge-0.3.8}/README.md +14 -0
  3. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/index.rst +2 -0
  4. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/release_035_validation.rst +1 -1
  5. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/release_036_validation.rst +1 -1
  6. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/__init__.py +1 -1
  7. arraybridge-0.3.8/src/arraybridge/array_operations.py +492 -0
  8. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/types.py +52 -7
  9. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_callable_dtype_default.py +3 -1
  10. arraybridge-0.3.8/tests/test_native_geometry.py +80 -0
  11. arraybridge-0.3.6/src/arraybridge/array_operations.py +0 -396
  12. {arraybridge-0.3.6 → arraybridge-0.3.8}/.github/workflows/ci.yml +0 -0
  13. {arraybridge-0.3.6 → arraybridge-0.3.8}/.github/workflows/coverage-pages.yml +0 -0
  14. {arraybridge-0.3.6 → arraybridge-0.3.8}/.github/workflows/gpu-tests.yml +0 -0
  15. {arraybridge-0.3.6 → arraybridge-0.3.8}/.github/workflows/publish.yml +0 -0
  16. {arraybridge-0.3.6 → arraybridge-0.3.8}/.gitignore +0 -0
  17. {arraybridge-0.3.6 → arraybridge-0.3.8}/.readthedocs.yml +0 -0
  18. {arraybridge-0.3.6 → arraybridge-0.3.8}/ADDING_NEW_FRAMEWORKS.md +0 -0
  19. {arraybridge-0.3.6 → arraybridge-0.3.8}/CONTRIBUTING.md +0 -0
  20. {arraybridge-0.3.6 → arraybridge-0.3.8}/LICENSE +0 -0
  21. {arraybridge-0.3.6 → arraybridge-0.3.8}/TESTING.md +0 -0
  22. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/Makefile +0 -0
  23. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/CI_ARTIFACT_UPDATE.md +0 -0
  24. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/CI_CUPY_FIX.md +0 -0
  25. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/CI_IMPROVEMENTS.md +0 -0
  26. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/CONFLICT_VISUALIZATION.md +0 -0
  27. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/COVERAGE_AUDIT_PLAN.md +0 -0
  28. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/DEPLOYMENT_SUMMARY.md +0 -0
  29. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/GPU_TESTING_SETUP.md +0 -0
  30. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/MERGE_CONFLICT_RESOLUTION.md +0 -0
  31. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/MIGRATION_SUMMARY.md +0 -0
  32. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/QUICK_FIX_GUIDE.txt +0 -0
  33. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/README.md +0 -0
  34. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/archive/RESOLVED_FILES.md +0 -0
  35. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/ci-cd.md +0 -0
  36. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/.gitignore +0 -0
  37. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/Makefile +0 -0
  38. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/README.md +0 -0
  39. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/advanced_topics.rst +0 -0
  40. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/api_reference.rst +0 -0
  41. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/ci-cd.rst +0 -0
  42. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/conf.py +0 -0
  43. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/contributing.rst +0 -0
  44. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/converters.rst +0 -0
  45. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/decorators.rst +0 -0
  46. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/examples/basic_conversion.rst +0 -0
  47. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/examples/decorators.rst +0 -0
  48. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/examples/index.rst +0 -0
  49. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/examples/multi_framework.rst +0 -0
  50. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/gpu_features.rst +0 -0
  51. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/installation.rst +0 -0
  52. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/quickstart.rst +0 -0
  53. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/stack_utils.rst +0 -0
  54. {arraybridge-0.3.6 → arraybridge-0.3.8}/docs/source/user_guide.rst +0 -0
  55. {arraybridge-0.3.6 → arraybridge-0.3.8}/paper.bib +0 -0
  56. {arraybridge-0.3.6 → arraybridge-0.3.8}/paper.md +0 -0
  57. {arraybridge-0.3.6 → arraybridge-0.3.8}/pyproject.toml +0 -0
  58. {arraybridge-0.3.6 → arraybridge-0.3.8}/scripts/release.py +0 -0
  59. {arraybridge-0.3.6 → arraybridge-0.3.8}/scripts/update_version.py +0 -0
  60. {arraybridge-0.3.6 → arraybridge-0.3.8}/scripts/verify_release_ready.py +0 -0
  61. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/array_geometry.py +0 -0
  62. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/array_payload.py +0 -0
  63. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/converters.py +0 -0
  64. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/converters_registry.py +0 -0
  65. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/decorators.py +0 -0
  66. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/dtype_scaling.py +0 -0
  67. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/exceptions.py +0 -0
  68. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/framework_config.py +0 -0
  69. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/framework_ops.py +0 -0
  70. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/gpu_cleanup.py +0 -0
  71. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/oom_recovery.py +0 -0
  72. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/slice_processing.py +0 -0
  73. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/stack_utils.py +0 -0
  74. {arraybridge-0.3.6 → arraybridge-0.3.8}/src/arraybridge/utils.py +0 -0
  75. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/__init__.py +0 -0
  76. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/conftest.py +0 -0
  77. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_array_geometry.py +0 -0
  78. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_converters.py +0 -0
  79. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_converters_registry.py +0 -0
  80. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_decorators.py +0 -0
  81. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_dtype_scaling.py +0 -0
  82. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_durable_decorator_context.py +0 -0
  83. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_exceptions.py +0 -0
  84. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_framework_config.py +0 -0
  85. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_gpu_cleanup.py +0 -0
  86. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_integration.py +0 -0
  87. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_oom_recovery.py +0 -0
  88. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_registry_integration.py +0 -0
  89. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_slice_processing.py +0 -0
  90. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_stack_utils.py +0 -0
  91. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_types.py +0 -0
  92. {arraybridge-0.3.6 → arraybridge-0.3.8}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraybridge
3
- Version: 0.3.6
3
+ Version: 0.3.8
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.
@@ -20,6 +20,8 @@ applications.
20
20
  examples/index
21
21
  contributing
22
22
  ci-cd
23
+ release_035_validation
24
+ release_036_validation
23
25
 
24
26
  Boundary rule
25
27
  -------------
@@ -1,5 +1,5 @@
1
1
  ArrayBridge 0.3.5 release preparation
2
- ===================================
2
+ =====================================
3
3
 
4
4
  Parent integration owner; base ea3f2a4cc91c4810d12343f58f85c1195e1a41e6.
5
5
  The reviewed callable-native dtype default and durable decorator-context
@@ -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.
@@ -10,7 +10,7 @@ attribute access instead of at import time. Declaration-only consumers
10
10
  NumPy/numcodecs import cost.
11
11
  """
12
12
 
13
- __version__ = "0.3.6"
13
+ __version__ = "0.3.8"
14
14
 
15
15
  _LAZY_EXPORTS: dict[str, str] = {
16
16
  "MemoryType": ".types",
@@ -0,0 +1,492 @@
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
+ @classmethod
118
+ def normalize_planes(
119
+ cls,
120
+ data: Any,
121
+ dtype: Any,
122
+ scales: Sequence[float | None],
123
+ module: Any,
124
+ ) -> Any:
125
+ """Cast and scale each plane before assembling its owned output."""
126
+ return cls.stack(
127
+ tuple(
128
+ (
129
+ cls.cast(plane, dtype, module)
130
+ if scale is None
131
+ else cls.cast(plane, dtype, module) / float(scale)
132
+ )
133
+ for plane, scale in zip(data, scales, strict=True)
134
+ ),
135
+ module,
136
+ )
137
+
138
+
139
+ class MutableArrayOperations(ArrayOperations):
140
+ """Arrays whose allocated output supports native in-place arithmetic."""
141
+
142
+ @classmethod
143
+ def normalize_planes(
144
+ cls,
145
+ data: Any,
146
+ dtype: Any,
147
+ scales: Sequence[float | None],
148
+ module: Any,
149
+ ) -> Any:
150
+ # Integer division promotes the output dtype in the original recipe.
151
+ if not np.issubdtype(np.dtype(dtype), np.inexact) or len(data) == 0:
152
+ return super().normalize_planes(data, dtype, scales, module)
153
+ normalized = module.array(data, dtype=dtype, copy=True)
154
+ for index, scale in enumerate(scales):
155
+ if scale is not None:
156
+ normalized[index] /= float(scale)
157
+ return normalized
158
+
159
+
160
+ class NumpyArrayOperations(MutableArrayOperations):
161
+ """Numpy native operation leaves."""
162
+
163
+ @staticmethod
164
+ def to_numpy(data: Any, module: Any) -> Any:
165
+ del module
166
+ return data
167
+
168
+ @staticmethod
169
+ def from_numpy(data: Any, module: Any, device_id: int) -> Any:
170
+ del module, device_id
171
+ return data
172
+
173
+ @staticmethod
174
+ def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
175
+ if not hasattr(result, "dtype"):
176
+ return result
177
+ if not (
178
+ module.issubdtype(result.dtype, module.floating)
179
+ and module.issubdtype(target_dtype, module.integer)
180
+ ):
181
+ return result.astype(target_dtype)
182
+ result_min = result.min()
183
+ result_max = result.max()
184
+ if result_max <= result_min:
185
+ return result.astype(target_dtype)
186
+ scaled = _scaled_values(result, result_min, result_max, target_dtype)
187
+ bounds = _clamp_bounds(target_dtype)
188
+ if bounds is not None:
189
+ scaled = module.clip(scaled, *bounds)
190
+ return scaled.astype(target_dtype)
191
+
192
+
193
+ class CupyArrayOperations(MutableArrayOperations):
194
+ """Cupy native operation leaves."""
195
+
196
+ @staticmethod
197
+ def to_numpy(data: Any, module: Any) -> Any:
198
+ del module
199
+ return data.get()
200
+
201
+ @staticmethod
202
+ def from_numpy(data: Any, module: Any, device_id: int) -> Any:
203
+ del device_id
204
+ return module.array(data)
205
+
206
+ @staticmethod
207
+ def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
208
+ if not hasattr(result, "dtype"):
209
+ return result
210
+ if not (
211
+ module.issubdtype(result.dtype, module.floating)
212
+ and not module.issubdtype(target_dtype, module.floating)
213
+ ):
214
+ return result.astype(target_dtype)
215
+ result_min = module.min(result)
216
+ result_max = module.max(result)
217
+ if result_max <= result_min:
218
+ return result.astype(target_dtype)
219
+ scaled = _scaled_values(result, result_min, result_max, target_dtype)
220
+ bounds = _clamp_bounds(target_dtype)
221
+ if bounds is not None:
222
+ scaled = module.clip(scaled, *bounds)
223
+ return scaled.astype(target_dtype)
224
+
225
+
226
+ class TorchArrayOperations(ArrayOperations):
227
+ """Torch native operation leaves."""
228
+
229
+ @staticmethod
230
+ def to_numpy(data: Any, module: Any) -> Any:
231
+ del module
232
+ return data.cpu().numpy()
233
+
234
+ @staticmethod
235
+ def from_numpy(data: Any, module: Any, device_id: int) -> Any:
236
+ host_data = (
237
+ np.ascontiguousarray(data)
238
+ if any(stride < 0 for stride in getattr(data, "strides", ()))
239
+ else data
240
+ )
241
+ return module.from_numpy(host_data).to(f"cuda:{device_id}")
242
+
243
+ @staticmethod
244
+ def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
245
+ if not hasattr(result, "dtype"):
246
+ return result
247
+ mapped = _mapped_dtype(target_dtype, module)
248
+ floats = (module.float16, module.float32, module.float64)
249
+ if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
250
+ return result.to(mapped)
251
+ result_min = result.min()
252
+ result_max = result.max()
253
+ if result_max <= result_min:
254
+ return result.to(mapped)
255
+ scaled = _scaled_values(result, result_min, result_max, target_dtype)
256
+ bounds = _clamp_bounds(target_dtype)
257
+ if bounds is not None:
258
+ scaled = module.clamp(scaled, min=bounds[0], max=bounds[1])
259
+ return scaled.to(mapped)
260
+
261
+ @staticmethod
262
+ def stack(values: Sequence[Any], module: Any) -> Any:
263
+ return module.stack(tuple(values), dim=0)
264
+
265
+ @staticmethod
266
+ def cast(data: Any, dtype: Any, module: Any) -> Any:
267
+ return data.to(dtype=_mapped_dtype(dtype, module))
268
+
269
+ @staticmethod
270
+ def dtype_name(dtype: Any) -> str:
271
+ return str(dtype).rsplit(".", maxsplit=1)[-1]
272
+
273
+ @staticmethod
274
+ def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
275
+ return data.expand(shape)
276
+
277
+ @staticmethod
278
+ def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
279
+ return module.ones(shape, dtype=_mapped_dtype(dtype, module), device=reference.device)
280
+
281
+
282
+ class TensorflowArrayOperations(ArrayOperations):
283
+ """Tensorflow native operation leaves."""
284
+
285
+ @staticmethod
286
+ def to_numpy(data: Any, module: Any) -> Any:
287
+ del module
288
+ return data.numpy()
289
+
290
+ @staticmethod
291
+ def from_numpy(data: Any, module: Any, device_id: int) -> Any:
292
+ del device_id
293
+ return module.convert_to_tensor(data)
294
+
295
+ @staticmethod
296
+ def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
297
+ if not hasattr(result, "dtype"):
298
+ return result
299
+ mapped = _mapped_dtype(target_dtype, module)
300
+ floats = (module.float16, module.float32, module.float64)
301
+ if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
302
+ return module.cast(result, mapped)
303
+ result_min = module.reduce_min(result)
304
+ result_max = module.reduce_max(result)
305
+ if result_max <= result_min:
306
+ return module.cast(result, mapped)
307
+ scaled = _scaled_values(result, result_min, result_max, target_dtype)
308
+ bounds = _clamp_bounds(target_dtype)
309
+ if bounds is not None:
310
+ scaled = module.clip_by_value(scaled, *bounds)
311
+ return module.cast(scaled, mapped)
312
+
313
+ @staticmethod
314
+ def cast(data: Any, dtype: Any, module: Any) -> Any:
315
+ return module.cast(data, _mapped_dtype(dtype, module))
316
+
317
+ @staticmethod
318
+ def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
319
+ return module.reshape(data, shape)
320
+
321
+ @staticmethod
322
+ def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
323
+ with module.device(reference.device):
324
+ return module.ones(shape, dtype=_mapped_dtype(dtype, module))
325
+
326
+
327
+ class JaxArrayOperations(ArrayOperations):
328
+ """Jax native operation leaves."""
329
+
330
+ @staticmethod
331
+ def to_numpy(data: Any, module: Any) -> Any:
332
+ del module
333
+ return np.asarray(data)
334
+
335
+ @staticmethod
336
+ def from_numpy(data: Any, module: Any, device_id: int) -> Any:
337
+ devices = tuple(device for device in module.devices() if device.platform == "gpu")
338
+ return module.device_put(data, devices[device_id])
339
+
340
+ @staticmethod
341
+ def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
342
+ if not hasattr(result, "dtype"):
343
+ return result
344
+ if np.dtype(target_dtype) == np.dtype(np.float64):
345
+ x64_enabled = getattr(module.config, "x64_enabled", None)
346
+ if x64_enabled is None:
347
+ x64_enabled = module.config.read("jax_enable_x64")
348
+ if not x64_enabled:
349
+ raise ValueError(
350
+ "JAX float64 output requires x64 mode; set JAX_ENABLE_X64=true before import"
351
+ )
352
+ jnp = module.numpy
353
+ mapped = _mapped_dtype(target_dtype, jnp)
354
+ floats = (jnp.float16, jnp.float32, jnp.float64)
355
+ if not (result.dtype in floats and np.issubdtype(np.dtype(target_dtype), np.integer)):
356
+ return result.astype(mapped)
357
+ result_min = jnp.min(result)
358
+ result_max = jnp.max(result)
359
+ if result_max <= result_min:
360
+ return result.astype(mapped)
361
+ scaled = _scaled_values(result, result_min, result_max, target_dtype)
362
+ bounds = _clamp_bounds(target_dtype)
363
+ if bounds is not None:
364
+ scaled = jnp.clip(scaled, *bounds)
365
+ return scaled.astype(mapped)
366
+
367
+ @staticmethod
368
+ def stack(values: Sequence[Any], module: Any) -> Any:
369
+ return module.numpy.stack(tuple(values), axis=0)
370
+
371
+ @staticmethod
372
+ def cast(data: Any, dtype: Any, module: Any) -> Any:
373
+ return data.astype(_mapped_dtype(dtype, module.numpy))
374
+
375
+ @staticmethod
376
+ def logical_and(left: Any, right: Any, module: Any) -> Any:
377
+ return module.numpy.logical_and(left, right)
378
+
379
+ @staticmethod
380
+ def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
381
+ return module.numpy.broadcast_to(data, shape)
382
+
383
+ @staticmethod
384
+ def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
385
+ return module.numpy.ones(shape, dtype=dtype, device=reference.device)
386
+
387
+
388
+ class PyclesperantoArrayOperations(ArrayOperations):
389
+ """Pyclesperanto native operation leaves."""
390
+
391
+ @staticmethod
392
+ def to_numpy(data: Any, module: Any) -> Any:
393
+ return module.pull(data)
394
+
395
+ @staticmethod
396
+ def from_numpy(data: Any, module: Any, device_id: int) -> Any:
397
+ del device_id
398
+ return module.push(data)
399
+
400
+ @staticmethod
401
+ def scale_dtype(result: Any, target_dtype: Any, module: Any) -> Any:
402
+ if not hasattr(result, "dtype"):
403
+ return result
404
+ target_is_int = np.issubdtype(np.dtype(target_dtype), np.integer)
405
+ if not (np.issubdtype(result.dtype, np.floating) and target_is_int):
406
+ return module.push(module.pull(result).astype(target_dtype))
407
+ result_min = float(module.minimum_of_all_pixels(result))
408
+ result_max = float(module.maximum_of_all_pixels(result))
409
+ if result_max <= result_min:
410
+ return module.push(module.pull(result).astype(target_dtype))
411
+ normalized = module.subtract_image_from_scalar(result, scalar=result_min)
412
+ normalized = module.multiply_image_and_scalar(
413
+ normalized,
414
+ scalar=1.0 / (result_max - result_min),
415
+ )
416
+ range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
417
+ if isinstance(range_info, tuple):
418
+ scale, offset = range_info
419
+ scaled = module.multiply_image_and_scalar(normalized, scalar=scale)
420
+ scaled = module.subtract_image_from_scalar(scaled, scalar=offset)
421
+ elif range_info is not None:
422
+ scaled = module.multiply_image_and_scalar(normalized, scalar=range_info)
423
+ else:
424
+ scaled = normalized
425
+ host_values = module.pull(scaled)
426
+ bounds = _clamp_bounds(target_dtype)
427
+ if bounds is not None:
428
+ host_values = np.clip(host_values, *bounds)
429
+ return module.push(host_values.astype(target_dtype))
430
+
431
+ @staticmethod
432
+ def stack(values: Sequence[Any], module: Any) -> Any:
433
+ if not values:
434
+ raise ValueError("Cannot stack an empty pyclesperanto sequence")
435
+ if len(values) == 1:
436
+ source = values[0]
437
+ result = module.create((1, *source.shape), dtype=source.dtype)
438
+ return module.copy_slice(source, result, 0)
439
+ result = values[0]
440
+ for value in values[1:]:
441
+ result = module.concatenate_along_z(result, value)
442
+ return result
443
+
444
+ @staticmethod
445
+ def cast(data: Any, dtype: Any, module: Any) -> Any:
446
+ return module.push(module.pull(data).astype(dtype, copy=False))
447
+
448
+ @staticmethod
449
+ def logical_and(left: Any, right: Any, module: Any) -> Any:
450
+ return module.push(np.logical_and(module.pull(left), module.pull(right)))
451
+
452
+ @staticmethod
453
+ def reshape(data: Any, shape: tuple[int, ...], module: Any) -> Any:
454
+ if tuple(data.shape) == shape:
455
+ return data
456
+ raise NotImplementedError(
457
+ "pyclesperanto does not provide native array reshape; its reshape downloads pixels"
458
+ )
459
+
460
+ @staticmethod
461
+ def broadcast_to(data: Any, shape: tuple[int, ...], module: Any) -> Any:
462
+ if tuple(data.shape) == shape:
463
+ return data
464
+ raise NotImplementedError("pyclesperanto does not provide native array broadcasting")
465
+
466
+ @staticmethod
467
+ def ones_like(reference: Any, shape: tuple[int, ...], dtype: Any, module: Any) -> Any:
468
+ if np.dtype(dtype).name not in {
469
+ "float32",
470
+ "int8",
471
+ "int16",
472
+ "int32",
473
+ "uint8",
474
+ "uint16",
475
+ "uint32",
476
+ }:
477
+ raise NotImplementedError(f"pyclesperanto cannot allocate native dtype {dtype!r}")
478
+ if not 1 <= len(shape) <= 3:
479
+ raise NotImplementedError(
480
+ "pyclesperanto native allocation requires one to three dimensions"
481
+ )
482
+ result = module.create(shape, dtype=dtype, device=reference.device)
483
+ module.set(result, scalar=1, device=reference.device)
484
+ return result
485
+
486
+
487
+ NUMPY_OPERATIONS = NumpyArrayOperations()
488
+ CUPY_OPERATIONS = CupyArrayOperations()
489
+ TORCH_OPERATIONS = TorchArrayOperations()
490
+ TENSORFLOW_OPERATIONS = TensorflowArrayOperations()
491
+ JAX_OPERATIONS = JaxArrayOperations()
492
+ 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
@@ -804,6 +804,20 @@ class MemoryType(_MemoryTypeFields, Enum):
804
804
  with self.device_scope(device_id, framework):
805
805
  return self._operations.stack(arrays, framework)
806
806
 
807
+ def normalize_planes(
808
+ self,
809
+ data: Any,
810
+ dtype: Any,
811
+ scales: Sequence[float | None],
812
+ module: Any | None = None,
813
+ ) -> Any:
814
+ """Normalize one owned plane stack through its native operation owner."""
815
+ if len(scales) != len(data):
816
+ raise ValueError("Normalization scales must match the leading plane axis.")
817
+ framework = module if module is not None else self.import_module()
818
+ with self._array_device_scope(data, framework):
819
+ return self._operations.normalize_planes(data, dtype, scales, framework)
820
+
807
821
  def scale_dtype(
808
822
  self,
809
823
  data: Any,
@@ -834,11 +848,44 @@ class MemoryType(_MemoryTypeFields, Enum):
834
848
  """Cast one array through this framework member's operation leaf."""
835
849
 
836
850
  framework = module if module is not None else self.import_module()
837
- device_id = self.device_id_of(data, framework)
838
- scope = nullcontext() if device_id is None else self.device_scope(device_id, framework)
839
- with scope:
851
+ with self._array_device_scope(data, framework):
840
852
  return self._operations.cast(data, dtype, framework)
841
853
 
854
+ def _array_device_scope(self, reference: Any, module: Any) -> AbstractContextManager[None]:
855
+ """Derive every native operation scope from its actual input array."""
856
+ device_id = self.device_id_of(reference, module)
857
+ return nullcontext() if device_id is None else self.device_scope(device_id, module)
858
+
859
+ def reshape(self, data: Any, shape: Sequence[int], module: Any | None = None) -> Any:
860
+ """Reshape on the input device without an implicit host projection."""
861
+ framework = module if module is not None else self.import_module()
862
+ with self._array_device_scope(data, framework):
863
+ return self._operations.reshape(data, tuple(shape), framework)
864
+
865
+ def broadcast_to(self, data: Any, shape: Sequence[int], module: Any | None = None) -> Any:
866
+ """Broadcast on the input device without an implicit host projection."""
867
+ framework = module if module is not None else self.import_module()
868
+ with self._array_device_scope(data, framework):
869
+ return self._operations.broadcast_to(data, tuple(shape), framework)
870
+
871
+ def ones_like(
872
+ self,
873
+ reference: Any,
874
+ *,
875
+ shape: Sequence[int] | None = None,
876
+ dtype: Any = bool,
877
+ module: Any | None = None,
878
+ ) -> Any:
879
+ """Allocate ones on the supplied reference's framework-local device."""
880
+ framework = module if module is not None else self.import_module()
881
+ with self._array_device_scope(reference, framework):
882
+ return self._operations.ones_like(
883
+ reference,
884
+ tuple(reference.shape if shape is None else shape),
885
+ dtype,
886
+ framework,
887
+ )
888
+
842
889
  def logical_and(
843
890
  self,
844
891
  left: Any,
@@ -848,9 +895,7 @@ class MemoryType(_MemoryTypeFields, Enum):
848
895
  """Intersect two arrays through this framework member's operation leaf."""
849
896
 
850
897
  framework = module if module is not None else self.import_module()
851
- device_id = self.device_id_of(left, framework)
852
- scope = nullcontext() if device_id is None else self.device_scope(device_id, framework)
853
- with scope:
898
+ with self._array_device_scope(left, framework):
854
899
  return self._operations.logical_and(left, right, framework)
855
900
 
856
901
  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