arraybridge 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,157 @@
1
+ """
2
+ Dtype scaling and conversion functions for different memory types.
3
+
4
+ This module provides framework-specific scaling functions that handle conversion
5
+ between floating point and integer dtypes with proper range scaling.
6
+
7
+ Uses enum-driven metaprogramming to eliminate 276 lines of duplication (82% reduction).
8
+ Pattern follows PR #38: pure data → eval() → single generic function.
9
+ """
10
+
11
+ from functools import partial
12
+
13
+ import numpy as np
14
+
15
+ from arraybridge.framework_config import _FRAMEWORK_CONFIG
16
+ from arraybridge.types import MemoryType
17
+ from arraybridge.utils import optional_import
18
+
19
+ # Scaling ranges for integer dtypes (shared across all memory types)
20
+ _SCALING_RANGES = {
21
+ 'uint8': 255.0,
22
+ 'uint16': 65535.0,
23
+ 'uint32': 4294967295.0,
24
+ 'int16': (65535.0, 32768.0), # (scale, offset)
25
+ 'int32': (4294967295.0, 2147483648.0),
26
+ }
27
+
28
+
29
+ # NOTE: Framework-specific scaling operations now defined in framework_config.py
30
+ # This eliminates the scattered _FRAMEWORK_OPS dict
31
+
32
+
33
+ def _scale_generic(result, target_dtype, mem_type: MemoryType):
34
+ """
35
+ Generic scaling function that works for all memory types using framework config.
36
+
37
+ This single function replaces 6 nearly-identical scaling functions.
38
+ """
39
+ # Special case: pyclesperanto
40
+ if mem_type == MemoryType.PYCLESPERANTO:
41
+ return _scale_pyclesperanto(result, target_dtype)
42
+
43
+ config = _FRAMEWORK_CONFIG[mem_type]
44
+ ops = config['scaling_ops']
45
+ mod = optional_import(mem_type.value) # noqa: F841 (used in eval)
46
+ if mod is None:
47
+ return result
48
+
49
+ if not hasattr(result, 'dtype'):
50
+ return result
51
+
52
+ # Handle dtype mapping for frameworks that need it
53
+ target_dtype_mapped = target_dtype # noqa: F841 (used in eval)
54
+ if ops.get('needs_dtype_map'):
55
+ dtype_map = {
56
+ np.uint8: mod.uint8, np.int8: mod.int8, np.int16: mod.int16,
57
+ np.int32: mod.int32, np.int64: mod.int64, np.float16: mod.float16,
58
+ np.float32: mod.float32, np.float64: mod.float64,
59
+ }
60
+ target_dtype_mapped = dtype_map.get(target_dtype, mod.float32) # noqa: F841
61
+
62
+ # Extra imports (e.g., jax.numpy)
63
+ if 'extra_import' in ops:
64
+ jnp = optional_import(ops['extra_import']) # noqa: F841 (used in eval)
65
+
66
+ # Check if conversion needed (float → int)
67
+ result_is_float = eval(ops['check_float'])
68
+ target_is_int = eval(ops['check_int'])
69
+
70
+ if not (result_is_float and target_is_int):
71
+ # Direct conversion
72
+ return eval(ops['astype'])
73
+
74
+ # Get min/max
75
+ result_min = eval(ops['min']) # noqa: F841 (used in eval)
76
+ result_max = eval(ops['max']) # noqa: F841 (used in eval)
77
+
78
+ if result_max <= result_min:
79
+ # Constant image
80
+ return eval(ops['astype'])
81
+
82
+ # Normalize to [0, 1]
83
+ normalized = (result - result_min) / (result_max - result_min) # noqa: F841 (used in eval)
84
+
85
+ # Scale to target range
86
+ if hasattr(target_dtype, '__name__'):
87
+ dtype_name = target_dtype.__name__
88
+ else:
89
+ dtype_name = str(target_dtype).split('.')[-1]
90
+
91
+ if dtype_name in _SCALING_RANGES:
92
+ range_info = _SCALING_RANGES[dtype_name]
93
+ if isinstance(range_info, tuple):
94
+ scale_val, offset_val = range_info
95
+ result = normalized * scale_val - offset_val # noqa: F841 (used in eval)
96
+ else:
97
+ result = normalized * range_info # noqa: F841 (used in eval)
98
+ else:
99
+ result = normalized # noqa: F841 (used in eval)
100
+
101
+ # Convert dtype
102
+ return eval(ops['astype'])
103
+
104
+
105
+ def _scale_pyclesperanto(result, target_dtype):
106
+ """Scale pyclesperanto results (GPU operations require special handling)."""
107
+ cle = optional_import("pyclesperanto")
108
+ if cle is None or not hasattr(result, 'dtype'):
109
+ return result
110
+
111
+ # Check if result is floating point and target is integer
112
+ result_is_float = np.issubdtype(result.dtype, np.floating)
113
+ target_is_int = target_dtype in [np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32]
114
+
115
+ if not (result_is_float and target_is_int):
116
+ # Direct conversion
117
+ return cle.push(cle.pull(result).astype(target_dtype))
118
+
119
+ # Get min/max
120
+ result_min = float(cle.minimum_of_all_pixels(result))
121
+ result_max = float(cle.maximum_of_all_pixels(result))
122
+
123
+ if result_max <= result_min:
124
+ # Constant image
125
+ return cle.push(cle.pull(result).astype(target_dtype))
126
+
127
+ # Normalize to [0, 1] using GPU operations
128
+ normalized = cle.subtract_image_from_scalar(result, scalar=result_min)
129
+ range_val = result_max - result_min
130
+ normalized = cle.multiply_image_and_scalar(normalized, scalar=1.0/range_val)
131
+
132
+ # Scale to target range
133
+ dtype_name = target_dtype.__name__
134
+ if dtype_name in _SCALING_RANGES:
135
+ range_info = _SCALING_RANGES[dtype_name]
136
+ if isinstance(range_info, tuple):
137
+ scale_val, offset_val = range_info
138
+ scaled = cle.multiply_image_and_scalar(normalized, scalar=scale_val)
139
+ scaled = cle.subtract_image_from_scalar(scaled, scalar=offset_val)
140
+ else:
141
+ scaled = cle.multiply_image_and_scalar(normalized, scalar=range_info)
142
+ else:
143
+ scaled = normalized
144
+
145
+ # Convert dtype
146
+ return cle.push(cle.pull(scaled).astype(target_dtype))
147
+
148
+
149
+ # Auto-generate all scaling functions using partial application
150
+ _SCALING_FUNCTIONS_GENERATED = {
151
+ mem_type.value: partial(_scale_generic, mem_type=mem_type)
152
+ for mem_type in MemoryType
153
+ }
154
+
155
+ # Registry mapping memory type names to scaling functions (backward compatibility)
156
+ SCALING_FUNCTIONS = _SCALING_FUNCTIONS_GENERATED
157
+
@@ -0,0 +1,26 @@
1
+ """Exceptions for arraybridge."""
2
+
3
+
4
+ class MemoryConversionError(Exception):
5
+ """
6
+ Exception raised when memory conversion fails.
7
+
8
+ Attributes:
9
+ source_type: The source memory type
10
+ target_type: The target memory type
11
+ method: The conversion method that was attempted
12
+ reason: The reason for the failure
13
+ """
14
+
15
+ def __init__(self, source_type: str, target_type: str, method: str, reason: str):
16
+ self.source_type = source_type
17
+ self.target_type = target_type
18
+ self.method = method
19
+ self.reason = reason
20
+
21
+ message = (
22
+ f"Cannot convert from {source_type} to {target_type} using {method}. "
23
+ f"Reason: {reason}"
24
+ )
25
+
26
+ super().__init__(message)
@@ -0,0 +1,459 @@
1
+ """
2
+ Single source of truth for ALL framework-specific behavior.
3
+
4
+ This module consolidates all framework-specific logic that was previously
5
+ scattered across utils.py, stack_utils.py, gpu_cleanup.py, dtype_scaling.py,
6
+ and framework_ops.py.
7
+
8
+ Architecture:
9
+ - Framework handlers: Custom logic for special cases (pyclesperanto, JAX, TensorFlow)
10
+ - Unified config: Single _FRAMEWORK_CONFIG dict with all framework metadata
11
+ - Polymorphic dispatch: Handlers can be callables or eval expressions
12
+ """
13
+
14
+ import logging
15
+ from typing import Any, Callable
16
+
17
+ from arraybridge.types import MemoryType
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ # ============================================================================
23
+ # FRAMEWORK HANDLERS - All special-case logic lives here
24
+ # ============================================================================
25
+
26
+ def _pyclesperanto_get_device_id(data: Any, mod: Any) -> int:
27
+ """Get device ID for pyclesperanto array."""
28
+ try:
29
+ current_device = mod.get_device()
30
+ if hasattr(current_device, 'id'):
31
+ return current_device.id
32
+ devices = mod.list_available_devices()
33
+ for i, device in enumerate(devices):
34
+ if str(device) == str(current_device):
35
+ return i
36
+ return 0
37
+ except Exception as e:
38
+ logger.warning(f"Failed to get device ID for pyclesperanto: {e}")
39
+ return 0
40
+
41
+
42
+ def _pyclesperanto_set_device(device_id: int, mod: Any) -> None:
43
+ """Set device for pyclesperanto."""
44
+ devices = mod.list_available_devices()
45
+ if device_id >= len(devices):
46
+ raise ValueError(f"Device {device_id} not available. Available: {len(devices)}")
47
+ mod.select_device(device_id)
48
+
49
+
50
+ def _pyclesperanto_move_to_device(data: Any, device_id: int, mod: Any, memory_type: str) -> Any:
51
+ """Move pyclesperanto array to device."""
52
+ # Import here to avoid circular dependency
53
+ from arraybridge.utils import _get_device_id
54
+
55
+ current_device_id = _get_device_id(data, memory_type)
56
+
57
+ if current_device_id != device_id:
58
+ mod.select_device(device_id)
59
+ result = mod.create_like(data)
60
+ mod.copy(data, result)
61
+ return result
62
+ return data
63
+
64
+
65
+ def _pyclesperanto_stack_slices(slices: list, memory_type: str, gpu_id: int, mod: Any) -> Any:
66
+ """Stack slices using pyclesperanto's concatenate_along_z."""
67
+ from arraybridge.converters import convert_memory, detect_memory_type
68
+
69
+ converted_slices = []
70
+ conversion_count = 0
71
+
72
+ for slice_data in slices:
73
+ source_type = detect_memory_type(slice_data)
74
+
75
+ if source_type != memory_type:
76
+ conversion_count += 1
77
+
78
+ if source_type == memory_type:
79
+ converted_slices.append(slice_data)
80
+ else:
81
+ converted = convert_memory(slice_data, source_type, memory_type, gpu_id)
82
+ converted_slices.append(converted)
83
+
84
+ # Log batch conversion
85
+ if conversion_count > 0:
86
+ logger.debug(
87
+ f"🔄 MEMORY CONVERSION: Converted {conversion_count}/{len(slices)} slices "
88
+ f"to {memory_type} for pyclesperanto stacking"
89
+ )
90
+
91
+ return mod.concatenate_along_z(converted_slices)
92
+
93
+
94
+ def _jax_assign_slice(result: Any, index: int, slice_data: Any) -> Any:
95
+ """Assign slice to JAX array (immutable)."""
96
+ return result.at[index].set(slice_data)
97
+
98
+
99
+ def _tensorflow_validate_dlpack(obj: Any, mod: Any) -> bool:
100
+ """Validate TensorFlow DLPack support."""
101
+ # Check version
102
+ major, minor = map(int, mod.__version__.split('.')[:2])
103
+ if major < 2 or (major == 2 and minor < 12):
104
+ raise RuntimeError(
105
+ f"TensorFlow {mod.__version__} does not support stable DLPack. "
106
+ f"Version 2.12.0+ required. "
107
+ f"Clause 88 violation: Cannot infer DLPack capability."
108
+ )
109
+
110
+ # Check GPU
111
+ device_str = obj.device.lower()
112
+ if "gpu" not in device_str:
113
+ raise RuntimeError(
114
+ "TensorFlow tensor on CPU cannot use DLPack operations reliably. "
115
+ "Only GPU tensors are supported for DLPack operations. "
116
+ "Clause 88 violation: Cannot infer GPU capability."
117
+ )
118
+
119
+ # Check module
120
+ if not hasattr(mod.experimental, "dlpack"):
121
+ raise RuntimeError(
122
+ "TensorFlow installation missing experimental.dlpack module. "
123
+ "Clause 88 violation: Cannot infer DLPack capability."
124
+ )
125
+
126
+ return True
127
+
128
+
129
+ def _numpy_dtype_conversion_needed(first_slice: Any, detect_memory_type_func: Callable) -> bool:
130
+ """Check if NumPy needs dtype conversion (only for torch sources)."""
131
+ source_type = detect_memory_type_func(first_slice)
132
+ return source_type == MemoryType.TORCH.value
133
+
134
+
135
+ def _torch_dtype_conversion_needed(first_slice: Any, detect_memory_type_func: Callable) -> bool:
136
+ """Torch always needs dtype conversion to get correct torch dtype."""
137
+ return True
138
+
139
+
140
+ # ============================================================================
141
+ # UNIFIED FRAMEWORK CONFIGURATION
142
+ # ============================================================================
143
+
144
+ _FRAMEWORK_CONFIG = {
145
+ MemoryType.NUMPY: {
146
+ # Metadata
147
+ 'import_name': 'numpy',
148
+ 'display_name': 'NumPy',
149
+ 'is_gpu': False,
150
+
151
+ # Device operations
152
+ 'get_device_id': None, # CPU
153
+ 'set_device': None, # CPU
154
+ 'move_to_device': None, # CPU
155
+
156
+ # Stack operations
157
+ 'allocate_stack': 'np.empty(stack_shape, dtype=dtype)',
158
+ 'allocate_context': None,
159
+ 'needs_dtype_conversion': _numpy_dtype_conversion_needed, # Callable
160
+ 'assign_slice': None, # Standard: result[i] = slice
161
+ 'stack_handler': None, # Standard stacking
162
+
163
+ # Dtype scaling
164
+ 'scaling_ops': {
165
+ 'min': 'result.min()',
166
+ 'max': 'result.max()',
167
+ 'astype': 'result.astype(target_dtype)',
168
+ 'check_float': 'np.issubdtype(result.dtype, np.floating)',
169
+ 'check_int': 'target_dtype in [np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32]', # noqa: E501
170
+ },
171
+
172
+ # Conversion operations
173
+ 'conversion_ops': {
174
+ 'to_numpy': 'data',
175
+ 'from_numpy': 'data',
176
+ 'from_dlpack': None,
177
+ 'move_to_device': 'data',
178
+ },
179
+
180
+ # DLPack
181
+ 'supports_dlpack': False,
182
+ 'validate_dlpack': None,
183
+
184
+ # GPU/Cleanup
185
+ 'lazy_getter': None,
186
+ 'gpu_check': None,
187
+ 'stream_context': None,
188
+ 'device_context': None,
189
+ 'cleanup_ops': None,
190
+ 'has_oom_recovery': False,
191
+ 'oom_exception_types': [],
192
+ 'oom_string_patterns': ['cannot allocate memory', 'memory exhausted'],
193
+ 'oom_clear_cache': 'import gc; gc.collect()',
194
+ },
195
+
196
+ MemoryType.CUPY: {
197
+ # Metadata
198
+ 'import_name': 'cupy',
199
+ 'display_name': 'CuPy',
200
+ 'is_gpu': True,
201
+
202
+ # Device operations (eval expressions)
203
+ 'get_device_id': 'data.device.id',
204
+ 'get_device_id_fallback': '0',
205
+ 'set_device': '{mod}.cuda.Device(device_id).use()',
206
+ 'move_to_device': 'data.copy() if data.device.id != device_id else data',
207
+ 'move_context': '{mod}.cuda.Device(device_id)',
208
+
209
+ # Stack operations
210
+ 'allocate_stack': 'cupy.empty(stack_shape, dtype=first_slice.dtype)',
211
+ 'allocate_context': 'cupy.cuda.Device(gpu_id)',
212
+ 'needs_dtype_conversion': False,
213
+ 'assign_slice': None, # Standard
214
+ 'stack_handler': None, # Standard
215
+
216
+ # Dtype scaling
217
+ 'scaling_ops': {
218
+ 'min': 'mod.min(result)',
219
+ 'max': 'mod.max(result)',
220
+ 'astype': 'result.astype(target_dtype)',
221
+ 'check_float': 'mod.issubdtype(result.dtype, mod.floating)',
222
+ 'check_int': 'not mod.issubdtype(target_dtype, mod.floating)',
223
+ },
224
+
225
+ # Conversion operations
226
+ 'conversion_ops': {
227
+ 'to_numpy': 'data.get()',
228
+ 'from_numpy': '({mod}.cuda.Device(gpu_id), {mod}.array(data))[1]',
229
+ 'from_dlpack': '{mod}.from_dlpack(data)',
230
+ 'move_to_device': 'data if data.device.id == gpu_id else ({mod}.cuda.Device(gpu_id), {mod}.array(data))[1]', # noqa: E501
231
+ },
232
+
233
+ # DLPack
234
+ 'supports_dlpack': True,
235
+ 'validate_dlpack': None,
236
+
237
+ # GPU/Cleanup
238
+ 'lazy_getter': '_get_cupy',
239
+ 'gpu_check': '{mod} is not None and hasattr({mod}, "cuda")',
240
+ 'stream_context': '{mod}.cuda.Stream()',
241
+ 'device_context': '{mod}.cuda.Device({device_id})',
242
+ 'cleanup_ops': '{mod}.get_default_memory_pool().free_all_blocks(); {mod}.get_default_pinned_memory_pool().free_all_blocks(); {mod}.cuda.runtime.deviceSynchronize()', # noqa: E501
243
+ 'has_oom_recovery': True,
244
+ 'oom_exception_types': ['{mod}.cuda.memory.OutOfMemoryError', '{mod}.cuda.runtime.CUDARuntimeError'], # noqa: E501
245
+ 'oom_string_patterns': ['out of memory', 'cuda_error_out_of_memory'],
246
+ 'oom_clear_cache': '{mod}.get_default_memory_pool().free_all_blocks(); {mod}.get_default_pinned_memory_pool().free_all_blocks(); {mod}.cuda.runtime.deviceSynchronize()', # noqa: E501
247
+ },
248
+
249
+ MemoryType.TORCH: {
250
+ # Metadata
251
+ 'import_name': 'torch',
252
+ 'display_name': 'PyTorch',
253
+ 'is_gpu': True,
254
+
255
+ # Device operations
256
+ 'get_device_id': 'data.device.index if data.is_cuda else None',
257
+ 'get_device_id_fallback': 'None',
258
+ 'set_device': None, # PyTorch handles device at tensor creation
259
+ 'move_to_device': 'data.to(f"cuda:{device_id}") if (not data.is_cuda or data.device.index != device_id) else data', # noqa: E501
260
+
261
+ # Stack operations
262
+ 'allocate_stack': 'torch.empty(stack_shape, dtype=sample_converted.dtype, device=sample_converted.device)', # noqa: E501
263
+ 'allocate_context': None,
264
+ 'needs_dtype_conversion': _torch_dtype_conversion_needed, # Callable
265
+ 'assign_slice': None, # Standard
266
+ 'stack_handler': None, # Standard
267
+
268
+ # Dtype scaling
269
+ 'scaling_ops': {
270
+ 'min': 'result.min()',
271
+ 'max': 'result.max()',
272
+ 'astype': 'result.to(target_dtype_mapped)',
273
+ 'check_float': 'result.dtype in [mod.float16, mod.float32, mod.float64]',
274
+ 'check_int': 'target_dtype_mapped in [mod.uint8, mod.int8, mod.int16, mod.int32, mod.int64]', # noqa: E501
275
+ 'needs_dtype_map': True,
276
+ },
277
+
278
+ # Conversion operations
279
+ 'conversion_ops': {
280
+ 'to_numpy': 'data.cpu().numpy()',
281
+ 'from_numpy': '{mod}.from_numpy(data).cuda(gpu_id)',
282
+ 'from_dlpack': '{mod}.from_dlpack(data)',
283
+ 'move_to_device': 'data if data.device.index == gpu_id else data.cuda(gpu_id)',
284
+ },
285
+
286
+ # DLPack
287
+ 'supports_dlpack': True,
288
+ 'validate_dlpack': None,
289
+
290
+ # GPU/Cleanup
291
+ 'lazy_getter': '_get_torch',
292
+ 'gpu_check': '{mod} is not None and hasattr({mod}, "cuda") and {mod}.cuda.is_available()',
293
+ 'stream_context': '{mod}.cuda.Stream()',
294
+ 'device_context': '{mod}.cuda.device({device_id})',
295
+ 'cleanup_ops': '{mod}.cuda.empty_cache(); {mod}.cuda.synchronize()',
296
+ 'has_oom_recovery': True,
297
+ 'oom_exception_types': ['{mod}.cuda.OutOfMemoryError'],
298
+ 'oom_string_patterns': ['out of memory', 'cuda_error_out_of_memory'],
299
+ 'oom_clear_cache': '{mod}.cuda.empty_cache(); {mod}.cuda.synchronize()',
300
+ },
301
+
302
+ MemoryType.TENSORFLOW: {
303
+ # Metadata
304
+ 'import_name': 'tensorflow',
305
+ 'display_name': 'TensorFlow',
306
+ 'is_gpu': True,
307
+
308
+ # Device operations
309
+ 'get_device_id': 'int(data.device.lower().split(":")[-1]) if "gpu" in data.device.lower() else None', # noqa: E501
310
+ 'get_device_id_fallback': 'None',
311
+ 'set_device': None, # TensorFlow handles device at tensor creation
312
+ 'move_to_device': '{mod}.identity(data)',
313
+ 'move_context': '{mod}.device(f"/device:GPU:{device_id}")',
314
+
315
+ # Stack operations
316
+ 'allocate_stack': 'tf.zeros(stack_shape, dtype=first_slice.dtype)', # TF doesn't have empty() # noqa: E501
317
+ 'allocate_context': 'tf.device(f"/device:GPU:{gpu_id}")',
318
+ 'needs_dtype_conversion': False,
319
+ 'assign_slice': None, # Standard
320
+ 'stack_handler': None, # Standard
321
+
322
+ # Dtype scaling
323
+ 'scaling_ops': {
324
+ 'min': 'mod.reduce_min(result)',
325
+ 'max': 'mod.reduce_max(result)',
326
+ 'astype': 'mod.cast(result, target_dtype_mapped)',
327
+ 'check_float': 'result.dtype in [mod.float16, mod.float32, mod.float64]',
328
+ 'check_int': 'target_dtype_mapped in [mod.uint8, mod.int8, mod.int16, mod.int32, mod.int64]', # noqa: E501
329
+ 'needs_dtype_map': True,
330
+ },
331
+
332
+ # Conversion operations
333
+ 'conversion_ops': {
334
+ 'to_numpy': 'data.numpy()',
335
+ 'from_numpy': '{mod}.convert_to_tensor(data)',
336
+ 'from_dlpack': '{mod}.experimental.dlpack.from_dlpack(data)',
337
+ 'move_to_device': 'data',
338
+ },
339
+
340
+ # DLPack
341
+ 'supports_dlpack': True,
342
+ 'validate_dlpack': _tensorflow_validate_dlpack, # Custom validation
343
+
344
+ # GPU/Cleanup
345
+ 'lazy_getter': '_get_tensorflow',
346
+ 'gpu_check': '{mod} is not None and {mod}.config.list_physical_devices("GPU")',
347
+ 'stream_context': None, # TensorFlow manages streams internally
348
+ 'device_context': '{mod}.device("/GPU:0")',
349
+ 'cleanup_ops': None, # TensorFlow has no explicit cache clearing API
350
+ 'has_oom_recovery': True,
351
+ 'oom_exception_types': [
352
+ '{mod}.errors.ResourceExhaustedError',
353
+ '{mod}.errors.InvalidArgumentError',
354
+ ],
355
+ 'oom_string_patterns': ['out of memory', 'resource_exhausted'],
356
+ 'oom_clear_cache': None, # TensorFlow has no explicit cache clearing API
357
+ },
358
+
359
+ MemoryType.JAX: {
360
+ # Metadata
361
+ 'import_name': 'jax',
362
+ 'display_name': 'JAX',
363
+ 'is_gpu': True,
364
+
365
+ # Device operations
366
+ 'get_device_id': 'int(str(data.device).lower().split(":")[-1]) if "gpu" in str(data.device).lower() else None', # noqa: E501
367
+ 'get_device_id_fallback': 'None',
368
+ 'set_device': None, # JAX handles device at array creation
369
+ 'move_to_device': '{mod}.device_put(data, {mod}.devices("gpu")[device_id])',
370
+
371
+ # Stack operations
372
+ 'allocate_stack': 'jnp.empty(stack_shape, dtype=first_slice.dtype)',
373
+ 'allocate_context': None,
374
+ 'needs_dtype_conversion': False,
375
+ 'assign_slice': _jax_assign_slice, # Custom handler for immutability
376
+ 'stack_handler': None, # Standard
377
+
378
+ # Dtype scaling
379
+ 'scaling_ops': {
380
+ 'min': 'jnp.min(result)',
381
+ 'max': 'jnp.max(result)',
382
+ 'astype': 'result.astype(target_dtype_mapped)',
383
+ 'check_float': 'result.dtype in [jnp.float16, jnp.float32, jnp.float64]',
384
+ 'check_int': 'target_dtype_mapped in [jnp.uint8, jnp.int8, jnp.int16, jnp.int32, jnp.int64]', # noqa: E501
385
+ 'needs_dtype_map': True,
386
+ 'extra_import': 'jax.numpy',
387
+ },
388
+
389
+ # Conversion operations
390
+ 'conversion_ops': {
391
+ 'to_numpy': 'np.asarray(data)',
392
+ 'from_numpy': '{mod}.device_put(data, {mod}.devices()[gpu_id])',
393
+ 'from_dlpack': '{mod}.dlpack.from_dlpack(data)',
394
+ 'move_to_device': 'data',
395
+ },
396
+
397
+ # DLPack
398
+ 'supports_dlpack': True,
399
+ 'validate_dlpack': None,
400
+
401
+ # GPU/Cleanup
402
+ 'lazy_getter': '_get_jax',
403
+ 'gpu_check': '{mod} is not None and any(d.platform == "gpu" for d in {mod}.devices())',
404
+ 'stream_context': None, # JAX/XLA manages streams internally
405
+ 'device_context': '{mod}.default_device([d for d in {mod}.devices() if d.platform == "gpu"][0])', # noqa: E501
406
+ 'cleanup_ops': '{mod}.clear_caches()',
407
+ 'has_oom_recovery': True,
408
+ 'oom_exception_types': [],
409
+ 'oom_string_patterns': ['out of memory', 'oom when allocating', 'allocation failure'],
410
+ 'oom_clear_cache': '{mod}.clear_caches()',
411
+ },
412
+
413
+ MemoryType.PYCLESPERANTO: {
414
+ # Metadata
415
+ 'import_name': 'pyclesperanto',
416
+ 'display_name': 'pyclesperanto',
417
+ 'is_gpu': True,
418
+
419
+ # Device operations (custom handlers)
420
+ 'get_device_id': _pyclesperanto_get_device_id, # Callable
421
+ 'get_device_id_fallback': '0',
422
+ 'set_device': _pyclesperanto_set_device, # Callable
423
+ 'move_to_device': _pyclesperanto_move_to_device, # Callable
424
+
425
+ # Stack operations (custom handler)
426
+ 'allocate_stack': None, # Uses concatenate_along_z
427
+ 'allocate_context': None,
428
+ 'needs_dtype_conversion': False,
429
+ 'assign_slice': None, # Not used (custom stacking)
430
+ 'stack_handler': _pyclesperanto_stack_slices, # Custom stacking
431
+
432
+ # Conversion operations
433
+ 'conversion_ops': {
434
+ 'to_numpy': '{mod}.pull(data)',
435
+ 'from_numpy': '{mod}.push(data)',
436
+ 'from_dlpack': None,
437
+ 'move_to_device': 'data',
438
+ },
439
+
440
+ # Dtype scaling (custom implementation in dtype_scaling.py)
441
+ 'scaling_ops': None, # Custom _scale_pyclesperanto function
442
+
443
+ # DLPack
444
+ 'supports_dlpack': False,
445
+ 'validate_dlpack': None,
446
+
447
+ # GPU/Cleanup
448
+ 'lazy_getter': None,
449
+ 'gpu_check': None, # pyclesperanto always uses GPU if available
450
+ 'stream_context': None, # OpenCL manages streams internally
451
+ 'device_context': None, # OpenCL device selection is global
452
+ 'cleanup_ops': None, # pyclesperanto/OpenCL has no explicit cache clearing API
453
+ 'has_oom_recovery': True,
454
+ 'oom_exception_types': [],
455
+ 'oom_string_patterns': ['cl_mem_object_allocation_failure', 'cl_out_of_resources', 'out of memory'], # noqa: E501
456
+ 'oom_clear_cache': None, # pyclesperanto/OpenCL has no explicit cache clearing API
457
+ },
458
+ }
459
+
@@ -0,0 +1,15 @@
1
+ """
2
+ Framework operations data for memory type system.
3
+
4
+ This module now imports from the unified framework_config.py.
5
+ All framework-specific operations are consolidated in a single source of truth.
6
+
7
+ DEPRECATED: This module is maintained for backward compatibility.
8
+ New code should import directly from framework_config.py.
9
+ """
10
+
11
+ from arraybridge.framework_config import _FRAMEWORK_CONFIG
12
+
13
+ # Re-export for backward compatibility
14
+ _FRAMEWORK_OPS = _FRAMEWORK_CONFIG
15
+