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.
- arraybridge/__init__.py +37 -0
- arraybridge/conversion_helpers.py +150 -0
- arraybridge/converters.py +61 -0
- arraybridge/decorators.py +396 -0
- arraybridge/dtype_scaling.py +157 -0
- arraybridge/exceptions.py +26 -0
- arraybridge/framework_config.py +459 -0
- arraybridge/framework_ops.py +15 -0
- arraybridge/gpu_cleanup.py +149 -0
- arraybridge/oom_recovery.py +148 -0
- arraybridge/slice_processing.py +73 -0
- arraybridge/stack_utils.py +317 -0
- arraybridge/types.py +70 -0
- arraybridge/utils.py +352 -0
- arraybridge-0.2.0.dist-info/METADATA +228 -0
- arraybridge-0.2.0.dist-info/RECORD +18 -0
- arraybridge-0.2.0.dist-info/WHEEL +4 -0
- arraybridge-0.2.0.dist-info/licenses/LICENSE +21 -0
arraybridge/__init__.py
ADDED
|
@@ -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
|
+
|