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,37 @@
1
+ """
2
+ arraybridge: Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto.
3
+
4
+ This package provides automatic memory type conversion, declarative decorators,
5
+ and unified utilities for working with multiple array/tensor frameworks.
6
+ """
7
+
8
+ __version__ = "0.2.0"
9
+
10
+ from .converters import convert_memory, detect_memory_type
11
+ from .decorators import cupy, jax, memory_types, numpy, tensorflow, torch
12
+ from .exceptions import MemoryConversionError
13
+ from .stack_utils import stack_slices, unstack_slices
14
+ from .types import CPU_MEMORY_TYPES, GPU_MEMORY_TYPES, SUPPORTED_MEMORY_TYPES, MemoryType
15
+
16
+ __all__ = [
17
+ # Types
18
+ "MemoryType",
19
+ "CPU_MEMORY_TYPES",
20
+ "GPU_MEMORY_TYPES",
21
+ "SUPPORTED_MEMORY_TYPES",
22
+ # Converters
23
+ "convert_memory",
24
+ "detect_memory_type",
25
+ # Decorators
26
+ "memory_types",
27
+ "numpy",
28
+ "cupy",
29
+ "torch",
30
+ "tensorflow",
31
+ "jax",
32
+ # Stack utilities
33
+ "stack_slices",
34
+ "unstack_slices",
35
+ # Exceptions
36
+ "MemoryConversionError",
37
+ ]
@@ -0,0 +1,150 @@
1
+ """
2
+ Memory conversion helpers for OpenHCS.
3
+
4
+ This module provides the ABC and metaprogramming infrastructure for memory type conversions.
5
+ Uses enum-driven polymorphism to eliminate 1,567 lines of duplication.
6
+ """
7
+
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+
11
+ from arraybridge.framework_config import _FRAMEWORK_CONFIG
12
+ from arraybridge.types import MemoryType
13
+ from arraybridge.utils import _supports_dlpack
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MemoryTypeConverter(ABC):
19
+ """Abstract base class for memory type converters.
20
+
21
+ Each memory type (numpy, cupy, torch, etc.) has a concrete converter
22
+ that implements these four core operations. All to_X() methods are
23
+ auto-generated using polymorphism.
24
+ """
25
+
26
+ @abstractmethod
27
+ def to_numpy(self, data, gpu_id):
28
+ """Extract to NumPy (type-specific implementation)."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def from_numpy(self, data, gpu_id):
33
+ """Create from NumPy (type-specific implementation)."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def from_dlpack(self, data, gpu_id):
38
+ """Create from DLPack capsule (type-specific implementation)."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ def move_to_device(self, data, gpu_id):
43
+ """Move data to specified GPU device if needed (type-specific implementation)."""
44
+ pass
45
+
46
+
47
+ def _add_converter_methods():
48
+ """Add to_X() methods to MemoryTypeConverter ABC.
49
+
50
+ NOTE: This must be called AFTER _CONVERTERS is defined (see below).
51
+
52
+ For each target memory type, generates a method like to_cupy(), to_torch(), etc.
53
+ that tries GPU-to-GPU conversion via DLPack first, then falls back to CPU roundtrip.
54
+ """
55
+ for target_type in MemoryType:
56
+ method_name = f"to_{target_type.value}"
57
+
58
+ def make_method(tgt):
59
+ def method(self, data, gpu_id):
60
+ # Try GPU-to-GPU first (DLPack)
61
+ if _supports_dlpack(data):
62
+ try:
63
+ target_converter = _CONVERTERS[tgt]
64
+ result = target_converter.from_dlpack(data, gpu_id)
65
+ return target_converter.move_to_device(result, gpu_id)
66
+ except Exception as e:
67
+ logger.warning(f"DLPack conversion failed: {e}. Using CPU roundtrip.")
68
+
69
+ # CPU roundtrip using polymorphism
70
+ numpy_data = self.to_numpy(data, gpu_id)
71
+ target_converter = _CONVERTERS[tgt]
72
+ return target_converter.from_numpy(numpy_data, gpu_id)
73
+ return method
74
+
75
+ setattr(MemoryTypeConverter, method_name, make_method(target_type))
76
+
77
+
78
+ # NOTE: Conversion operations now defined in framework_config.py under 'conversion_ops'
79
+ # This eliminates the scattered _OPS dict
80
+ _OPS = {mem_type: config['conversion_ops'] for mem_type, config in _FRAMEWORK_CONFIG.items()}
81
+
82
+ # Auto-generate lambdas from strings
83
+ def _make_not_implemented(mem_type_value, method_name):
84
+ """Create a lambda that raises NotImplementedError with the correct signature."""
85
+ def not_impl(self, data, gpu_id):
86
+ raise NotImplementedError(f"DLPack not supported for {mem_type_value}")
87
+ # Add proper names for better debugging
88
+ not_impl.__name__ = method_name
89
+ not_impl.__qualname__ = f'{mem_type_value.capitalize()}Converter.{method_name}'
90
+ return not_impl
91
+
92
+ def _make_lambda_with_name(expr_str, mem_type, method_name):
93
+ """Create a lambda from expression string and add proper __name__ for debugging."""
94
+ # Pre-compute the module string to avoid nested f-strings
95
+ # with backslashes (Python 3.11 limitation)
96
+ module_str = f'_ensure_module("{mem_type.value}")'
97
+ lambda_expr = f'lambda self, data, gpu_id: {expr_str.format(mod=module_str)}'
98
+ lambda_func = eval(lambda_expr)
99
+ lambda_func.__name__ = method_name
100
+ lambda_func.__qualname__ = f'{mem_type.value.capitalize()}Converter.{method_name}'
101
+ return lambda_func
102
+
103
+ _TYPE_OPERATIONS = {
104
+ mem_type: {
105
+ method_name: (
106
+ _make_lambda_with_name(expr, mem_type, method_name)
107
+ if expr is not None
108
+ else _make_not_implemented(mem_type.value, method_name)
109
+ )
110
+ for method_name, expr in ops.items() # Iterate over dict items - self-documenting!
111
+ }
112
+ for mem_type, ops in _OPS.items()
113
+ }
114
+
115
+ # Auto-generate all 6 converter classes
116
+ _CONVERTERS = {
117
+ mem_type: type(
118
+ f"{mem_type.value.capitalize()}Converter",
119
+ (MemoryTypeConverter,),
120
+ _TYPE_OPERATIONS[mem_type]
121
+ )()
122
+ for mem_type in MemoryType
123
+ }
124
+
125
+ # NOW call _add_converter_methods() after _CONVERTERS exists
126
+ _add_converter_methods()
127
+
128
+
129
+ # Runtime validation: ensure all converters have required methods
130
+ def _validate_converters():
131
+ """Validate that all generated converters have the required methods."""
132
+ required_methods = ['to_numpy', 'from_numpy', 'from_dlpack', 'move_to_device']
133
+
134
+ for mem_type, converter in _CONVERTERS.items():
135
+ # Check ABC methods
136
+ for method in required_methods:
137
+ if not hasattr(converter, method):
138
+ raise RuntimeError(f"{mem_type.value} converter missing method: {method}")
139
+
140
+ # Check to_X() methods for all memory types
141
+ for target_type in MemoryType:
142
+ method_name = f'to_{target_type.value}'
143
+ if not hasattr(converter, method_name):
144
+ raise RuntimeError(f"{mem_type.value} converter missing method: {method_name}")
145
+
146
+ logger.debug(f"✅ Validated {len(_CONVERTERS)} memory type converters")
147
+
148
+ # Run validation at module load time
149
+ _validate_converters()
150
+
@@ -0,0 +1,61 @@
1
+ """Memory conversion public API for OpenHCS."""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+ from arraybridge.conversion_helpers import _CONVERTERS
8
+ from arraybridge.framework_config import _FRAMEWORK_CONFIG
9
+ from arraybridge.types import MemoryType
10
+
11
+
12
+ def convert_memory(data: Any, source_type: str, target_type: str, gpu_id: int) -> Any:
13
+ """
14
+ Convert data between memory types using the unified converter infrastructure.
15
+
16
+ Args:
17
+ data: The data to convert
18
+ source_type: The source memory type (e.g., "numpy", "torch")
19
+ target_type: The target memory type (e.g., "cupy", "jax")
20
+ gpu_id: The target GPU device ID
21
+
22
+ Returns:
23
+ The converted data in the target memory type
24
+
25
+ Raises:
26
+ ValueError: If source_type or target_type is invalid
27
+ MemoryConversionError: If conversion fails
28
+ """
29
+ source_enum = MemoryType(source_type)
30
+ converter = _CONVERTERS[source_enum]
31
+ method = getattr(converter, f"to_{target_type}")
32
+ return method(data, gpu_id)
33
+
34
+
35
+ def detect_memory_type(data: Any) -> str:
36
+ """
37
+ Detect the memory type of data using framework config.
38
+
39
+ Args:
40
+ data: The data to detect
41
+
42
+ Returns:
43
+ The detected memory type string (e.g., "numpy", "torch")
44
+
45
+ Raises:
46
+ ValueError: If memory type cannot be detected
47
+ """
48
+ # NumPy special case (most common, check first)
49
+ if isinstance(data, np.ndarray):
50
+ return MemoryType.NUMPY.value
51
+
52
+ # Check all frameworks using their module names from config
53
+ module_name = type(data).__module__
54
+
55
+ for mem_type, config in _FRAMEWORK_CONFIG.items():
56
+ import_name = config['import_name']
57
+ # Check if module name starts with or contains the import name
58
+ if module_name.startswith(import_name) or import_name in module_name:
59
+ return mem_type.value
60
+
61
+ raise ValueError(f"Unknown memory type for {type(data)} (module: {module_name})")
@@ -0,0 +1,396 @@
1
+ """
2
+ Memory type declaration decorators for OpenHCS.
3
+
4
+ This module provides decorators for explicitly declaring the memory interface
5
+ of pure functions, enforcing Clause 106-A (Declared Memory Types) and supporting
6
+ memory-type-aware dispatching and orchestration.
7
+
8
+ These decorators annotate functions with input_memory_type and output_memory_type
9
+ attributes and provide automatic thread-local CUDA stream management for GPU
10
+ frameworks to enable true parallelization across multiple threads.
11
+
12
+ REFACTORED: Uses enum-driven metaprogramming to eliminate 79% of code duplication.
13
+ """
14
+
15
+ import functools
16
+ import inspect
17
+ import logging
18
+ import threading
19
+ from enum import Enum
20
+ from typing import Any, Callable, Optional, TypeVar
21
+
22
+ import numpy as np
23
+
24
+ from arraybridge.dtype_scaling import SCALING_FUNCTIONS
25
+ from arraybridge.framework_ops import _FRAMEWORK_OPS
26
+ from arraybridge.oom_recovery import _execute_with_oom_recovery
27
+ from arraybridge.slice_processing import process_slices
28
+ from arraybridge.types import MemoryType
29
+ from arraybridge.utils import optional_import
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ F = TypeVar('F', bound=Callable[..., Any])
34
+
35
+
36
+ class DtypeConversion(Enum):
37
+ """Data type conversion modes for all memory type functions."""
38
+
39
+ PRESERVE_INPUT = "preserve" # Keep input dtype (default)
40
+ NATIVE_OUTPUT = "native" # Use framework's native output
41
+ UINT8 = "uint8" # Force uint8 (0-255 range)
42
+ UINT16 = "uint16" # Force uint16 (microscopy standard)
43
+ INT16 = "int16" # Force int16 (signed microscopy data)
44
+ INT32 = "int32" # Force int32 (large integer values)
45
+ FLOAT32 = "float32" # Force float32 (GPU performance)
46
+ FLOAT64 = "float64" # Force float64 (maximum precision)
47
+
48
+ @property
49
+ def numpy_dtype(self):
50
+ """Get the corresponding numpy dtype."""
51
+ dtype_map = {
52
+ self.UINT8: np.uint8,
53
+ self.UINT16: np.uint16,
54
+ self.INT16: np.int16,
55
+ self.INT32: np.int32,
56
+ self.FLOAT32: np.float32,
57
+ self.FLOAT64: np.float64,
58
+ }
59
+ return dtype_map.get(self, None)
60
+
61
+
62
+ # Thread-local cache for lazy-loaded GPU frameworks
63
+ _gpu_frameworks_cache = {}
64
+
65
+
66
+ def _create_lazy_getter(framework_name: str):
67
+ """Factory function that creates a lazy import getter for a framework."""
68
+ def getter():
69
+ if framework_name not in _gpu_frameworks_cache:
70
+ _gpu_frameworks_cache[framework_name] = optional_import(framework_name)
71
+ if _gpu_frameworks_cache[framework_name] is not None:
72
+ logger.debug(
73
+ f"🔧 Lazy imported {framework_name} in thread "
74
+ f"{threading.current_thread().name}"
75
+ )
76
+ return _gpu_frameworks_cache[framework_name]
77
+ return getter
78
+
79
+
80
+ # Auto-generate lazy getters for all GPU frameworks
81
+ for mem_type in MemoryType:
82
+ ops = _FRAMEWORK_OPS[mem_type]
83
+ if ops['lazy_getter'] is not None:
84
+ getter_func = _create_lazy_getter(ops['import_name'])
85
+ globals()[f"_get_{ops['import_name']}"] = getter_func
86
+
87
+
88
+ # Thread-local storage for GPU streams and contexts
89
+ _thread_gpu_contexts = threading.local()
90
+
91
+ class ThreadGPUContext:
92
+ """Thread-local GPU context manager for CUDA streams."""
93
+
94
+ def __init__(self):
95
+ self.cupy_stream = None
96
+ self.torch_stream = None
97
+ self.tensorflow_device = None
98
+ self.jax_device = None
99
+
100
+ def get_cupy_stream(self):
101
+ """Get or create thread-local CuPy stream."""
102
+ if self.cupy_stream is None:
103
+ cupy = globals().get('_get_cupy', lambda: None)() # noqa: F821
104
+ if cupy is not None and hasattr(cupy, 'cuda'):
105
+ self.cupy_stream = cupy.cuda.Stream()
106
+ logger.debug(f"🔧 Created CuPy stream for thread {threading.current_thread().name}")
107
+ return self.cupy_stream
108
+
109
+ def get_torch_stream(self):
110
+ """Get or create thread-local PyTorch stream."""
111
+ if self.torch_stream is None:
112
+ torch = globals().get('_get_torch', lambda: None)() # noqa: F821
113
+ if torch is not None and hasattr(torch, 'cuda') and torch.cuda.is_available():
114
+ self.torch_stream = torch.cuda.Stream()
115
+ logger.debug(
116
+ f"🔧 Created PyTorch stream for thread "
117
+ f"{threading.current_thread().name}"
118
+ )
119
+ return self.torch_stream
120
+
121
+
122
+ def _get_thread_gpu_context():
123
+ """Get or create thread-local GPU context."""
124
+ if not hasattr(_thread_gpu_contexts, 'context'):
125
+ _thread_gpu_contexts.context = ThreadGPUContext()
126
+ return _thread_gpu_contexts.context
127
+
128
+
129
+ def memory_types(
130
+ input_type: str,
131
+ output_type: str,
132
+ contract: Optional[Callable[[Any], bool]] = None
133
+ ) -> Callable[[F], F]:
134
+ """
135
+ Base decorator for declaring memory types of a function.
136
+
137
+ This is the foundation decorator that all memory-type-specific decorators build upon.
138
+ """
139
+ def decorator(func: F) -> F:
140
+ @functools.wraps(func)
141
+ def wrapper(*args, **kwargs):
142
+ result = func(*args, **kwargs)
143
+
144
+ # Apply contract validation if provided
145
+ if contract is not None and not contract(result):
146
+ raise ValueError(f"Function {func.__name__} violated its output contract")
147
+
148
+ return result
149
+
150
+ # Attach memory type metadata
151
+ wrapper.input_memory_type = input_type
152
+ wrapper.output_memory_type = output_type
153
+
154
+ return wrapper
155
+
156
+ return decorator
157
+
158
+
159
+ def _create_dtype_wrapper(func, mem_type: MemoryType, func_name: str):
160
+ """
161
+ Auto-generate dtype preservation wrapper for any memory type.
162
+
163
+ This single function replaces 6 nearly-identical dtype wrapper functions.
164
+ """
165
+ _FRAMEWORK_OPS[mem_type]
166
+ scale_func = SCALING_FUNCTIONS[mem_type.value]
167
+
168
+ @functools.wraps(func)
169
+ def dtype_wrapper(image, *args, dtype_conversion=None, slice_by_slice: bool = False, **kwargs):
170
+ # Set default dtype_conversion if not provided
171
+ if dtype_conversion is None:
172
+ dtype_conversion = DtypeConversion.PRESERVE_INPUT
173
+
174
+ try:
175
+ # Store original dtype
176
+ original_dtype = image.dtype
177
+
178
+ # Handle slice_by_slice processing for 3D arrays
179
+ if slice_by_slice and hasattr(image, 'ndim') and image.ndim == 3:
180
+ result = process_slices(image, func, args, kwargs)
181
+ else:
182
+ # Call the original function normally
183
+ result = func(image, *args, **kwargs)
184
+
185
+ # Apply dtype conversion based on enum value
186
+ if hasattr(result, 'dtype') and dtype_conversion is not None:
187
+ if dtype_conversion == DtypeConversion.PRESERVE_INPUT:
188
+ # Preserve input dtype
189
+ if result.dtype != original_dtype:
190
+ result = scale_func(result, original_dtype)
191
+ elif dtype_conversion == DtypeConversion.NATIVE_OUTPUT:
192
+ # Return framework's native output dtype
193
+ pass # No conversion needed
194
+ else:
195
+ # Force specific dtype
196
+ target_dtype = dtype_conversion.numpy_dtype
197
+ if target_dtype is not None:
198
+ result = scale_func(result, target_dtype)
199
+
200
+ return result
201
+ except Exception as e:
202
+ logger.error(
203
+ f"Error in {mem_type.value} dtype/slice preserving wrapper "
204
+ f"for {func_name}: {e}"
205
+ )
206
+ # Return original result on error
207
+ return func(image, *args, **kwargs)
208
+
209
+ # Update function signature to include new parameters
210
+ try:
211
+ original_sig = inspect.signature(func)
212
+ new_params = list(original_sig.parameters.values())
213
+
214
+ # Check if parameters already exist
215
+ param_names = [p.name for p in new_params]
216
+
217
+ # Add dtype_conversion parameter first (before slice_by_slice)
218
+ if 'dtype_conversion' not in param_names:
219
+ dtype_param = inspect.Parameter(
220
+ 'dtype_conversion',
221
+ inspect.Parameter.KEYWORD_ONLY,
222
+ default=DtypeConversion.PRESERVE_INPUT,
223
+ annotation=Optional[DtypeConversion]
224
+ )
225
+ new_params.append(dtype_param)
226
+
227
+ # Add slice_by_slice parameter
228
+ if 'slice_by_slice' not in param_names:
229
+ slice_param = inspect.Parameter(
230
+ 'slice_by_slice',
231
+ inspect.Parameter.KEYWORD_ONLY,
232
+ default=False,
233
+ annotation=bool
234
+ )
235
+ new_params.append(slice_param)
236
+
237
+ # Create new signature
238
+ new_sig = original_sig.replace(parameters=new_params)
239
+ dtype_wrapper.__signature__ = new_sig
240
+
241
+ # Update docstring
242
+ if dtype_wrapper.__doc__:
243
+ dtype_wrapper.__doc__ += (
244
+ f"\n\n Additional Parameters "
245
+ f"(added by {mem_type.value} decorator):\n"
246
+ )
247
+ dtype_wrapper.__doc__ += (
248
+ " dtype_conversion (DtypeConversion, optional): "
249
+ "How to handle output dtype.\n"
250
+ )
251
+ dtype_wrapper.__doc__ += (
252
+ " Defaults to PRESERVE_INPUT (match input dtype).\n"
253
+ )
254
+ dtype_wrapper.__doc__ += (
255
+ " slice_by_slice (bool, optional): "
256
+ "Process 3D arrays slice-by-slice.\n"
257
+ )
258
+ dtype_wrapper.__doc__ += (
259
+ " Defaults to False. "
260
+ "Prevents cross-slice contamination.\n"
261
+ )
262
+
263
+ except Exception as e:
264
+ logger.warning(f"Could not update signature for {func_name}: {e}")
265
+
266
+ return dtype_wrapper
267
+
268
+
269
+ def _create_gpu_wrapper(func, mem_type: MemoryType, oom_recovery: bool):
270
+ """
271
+ Auto-generate GPU stream/device wrapper for any GPU memory type.
272
+
273
+ This function creates the GPU-specific wrapper with stream management and OOM recovery.
274
+ """
275
+ ops = _FRAMEWORK_OPS[mem_type]
276
+ framework_name = ops['import_name']
277
+ lazy_getter = globals().get(ops['lazy_getter'])
278
+
279
+ @functools.wraps(func)
280
+ def gpu_wrapper(*args, **kwargs):
281
+ framework = lazy_getter()
282
+
283
+ # Check if GPU is available for this framework
284
+ if framework is not None:
285
+ gpu_check_expr = ops['gpu_check'].format(mod=framework_name)
286
+ try:
287
+ gpu_available = eval(gpu_check_expr, {framework_name: framework})
288
+ except Exception:
289
+ gpu_available = False
290
+
291
+ if gpu_available:
292
+ # Get thread-local context
293
+ ctx = _get_thread_gpu_context()
294
+
295
+ # Get stream if framework supports it
296
+ stream = None
297
+ if mem_type == MemoryType.CUPY:
298
+ stream = ctx.get_cupy_stream()
299
+ elif mem_type == MemoryType.TORCH:
300
+ stream = ctx.get_torch_stream()
301
+
302
+ # Define execution function that captures args/kwargs
303
+ def execute_with_stream():
304
+ if stream is not None:
305
+ with stream:
306
+ return func(*args, **kwargs)
307
+ else:
308
+ return func(*args, **kwargs)
309
+
310
+ # Execute with OOM recovery if enabled
311
+ if oom_recovery and ops['has_oom_recovery']:
312
+ return _execute_with_oom_recovery(execute_with_stream, mem_type.value)
313
+ else:
314
+ return execute_with_stream()
315
+
316
+ # CPU fallback or framework not available
317
+ return func(*args, **kwargs)
318
+
319
+ # Preserve memory type attributes
320
+ gpu_wrapper.input_memory_type = func.input_memory_type
321
+ gpu_wrapper.output_memory_type = func.output_memory_type
322
+
323
+ return gpu_wrapper
324
+
325
+
326
+ def _create_memory_decorator(mem_type: MemoryType):
327
+ """
328
+ Factory function that creates a decorator for a specific memory type.
329
+
330
+ This single factory replaces 6 nearly-identical decorator functions.
331
+ """
332
+ ops = _FRAMEWORK_OPS[mem_type]
333
+
334
+ def decorator(func=None, *, input_type=mem_type.value, output_type=mem_type.value,
335
+ oom_recovery=True, contract=None):
336
+ """
337
+ Decorator for {mem_type} memory type functions.
338
+
339
+ Args:
340
+ func: Function to decorate (when used as @decorator)
341
+ input_type: Expected input memory type (default: {mem_type})
342
+ output_type: Expected output memory type (default: {mem_type})
343
+ oom_recovery: Enable automatic OOM recovery (default: True)
344
+ contract: Optional validation function for outputs
345
+
346
+ Returns:
347
+ Decorated function with memory type metadata and dtype preservation
348
+ """
349
+ def inner_decorator(func):
350
+ # Apply base memory_types decorator
351
+ memory_decorator = memory_types(
352
+ input_type=input_type,
353
+ output_type=output_type,
354
+ contract=contract
355
+ )
356
+ func = memory_decorator(func)
357
+
358
+ # Apply dtype preservation wrapper
359
+ func = _create_dtype_wrapper(func, mem_type, func.__name__)
360
+
361
+ # Apply GPU wrapper if this is a GPU memory type
362
+ if ops['gpu_check'] is not None:
363
+ func = _create_gpu_wrapper(func, mem_type, oom_recovery)
364
+
365
+ return func
366
+
367
+ # Handle both @decorator and @decorator() forms
368
+ if func is None:
369
+ return inner_decorator
370
+ return inner_decorator(func)
371
+
372
+ # Set proper function name and docstring
373
+ decorator.__name__ = mem_type.value
374
+ decorator.__doc__ = decorator.__doc__.format(mem_type=ops['display_name'])
375
+
376
+ return decorator
377
+
378
+
379
+ # Auto-generate all 6 memory type decorators
380
+ for mem_type in MemoryType:
381
+ decorator_func = _create_memory_decorator(mem_type)
382
+ globals()[mem_type.value] = decorator_func
383
+
384
+
385
+ # Export all decorators
386
+ __all__ = [
387
+ 'memory_types',
388
+ 'DtypeConversion',
389
+ 'numpy', # noqa: F822
390
+ 'cupy', # noqa: F822
391
+ 'torch', # noqa: F822
392
+ 'tensorflow', # noqa: F822
393
+ 'jax', # noqa: F822
394
+ 'pyclesperanto', # noqa: F822
395
+ ]
396
+