Coverage for src/arraybridge/stack_utils.py: 81%
62 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 11:15 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 11:15 +0000
1"""
2Stack utilities module for OpenHCS.
4This module provides functions for stacking 2D slices into a 3D array
5and unstacking a 3D array into 2D slices, with explicit memory type handling.
7This module enforces Clause 278 — Mandatory 3D Output Enforcement:
8All functions must return a 3D array of shape [Z, Y, X], even when operating
9on a single 2D slice. No logic may check, coerce, or infer rank at unstack time.
10"""
12import logging
13from typing import Any
15from arraybridge.converters import detect_memory_type
16from arraybridge.types import MemoryType
18logger = logging.getLogger(__name__)
20# 🔍 MEMORY CONVERSION LOGGING: Test log to verify logger is working
21logger.debug("🔄 STACK_UTILS: Module loaded - memory conversion logging enabled")
24def _is_2d(data: Any) -> bool:
25 """
26 Check if data is a 2D array.
28 Args:
29 data: Data to check
31 Returns:
32 True if data is 2D, False otherwise
33 """
34 # Check if data has a shape attribute
35 if not hasattr(data, "shape"):
36 return False
38 # Check if shape has length 2
39 return len(data.shape) == 2
42def _is_3d(data: Any) -> bool:
43 """
44 Check if data is a 3D array.
46 Args:
47 data: Data to check
49 Returns:
50 True if data is 3D, False otherwise
51 """
52 # Check if data has a shape attribute
53 if not hasattr(data, "shape"):
54 return False
56 # Check if shape has length 3
57 return len(data.shape) == 3
60def _enforce_gpu_device_requirements(memory_type: str, gpu_id: int) -> None:
61 """
62 Enforce GPU device requirements.
64 Args:
65 memory_type: The memory type
66 gpu_id: The GPU device ID
68 Raises:
69 ValueError: If gpu_id is negative
70 """
71 mem_type = MemoryType(memory_type)
72 if mem_type.is_gpu:
73 mem_type.require_device(gpu_id)
76def stack_slices(slices: list[Any], memory_type: str, gpu_id: int) -> Any:
77 """
78 Stack 2D slices into a 3D array with the specified memory type.
80 STRICT VALIDATION: Assumes all slices are 2D arrays.
81 No automatic handling of improper inputs.
83 Args:
84 slices: List of 2D slices (numpy arrays, cupy arrays, torch tensors, etc.)
85 memory_type: The memory type to use for the stacked array (REQUIRED)
86 gpu_id: The target GPU device ID (REQUIRED)
88 Returns:
89 A 3D array with the specified memory type of shape [Z, Y, X]
91 Raises:
92 ValueError: If memory_type is not supported or slices is empty
93 ValueError: If gpu_id is negative for GPU memory types
94 ValueError: If slices are not 2D arrays
95 MemoryConversionError: If conversion fails
96 """
97 if not slices:
98 raise ValueError("Cannot stack empty list of slices")
100 # Verify all slices are 2D
101 for i, slice_data in enumerate(slices):
102 if not _is_2d(slice_data):
103 raise ValueError(f"Slice at index {i} is not a 2D array. All slices must be 2D.")
105 # Check GPU requirements
106 _enforce_gpu_device_requirements(memory_type, gpu_id)
108 # Convert each slice and enforce the requested framework-local device.
109 conversion_count = 0
110 mem_type = MemoryType(memory_type)
111 converted_slices = []
112 for slice_data in slices:
113 source_type = detect_memory_type(slice_data)
114 if source_type == memory_type:
115 converted_data = mem_type.move_to_device(slice_data, gpu_id)
116 else:
117 from arraybridge.converters import convert_memory
119 converted_data = convert_memory(
120 data=slice_data,
121 source_type=source_type,
122 target_type=memory_type,
123 gpu_id=gpu_id,
124 )
125 conversion_count += 1
126 converted_slices.append(converted_data)
128 result = mem_type.stack_arrays(converted_slices, gpu_id)
130 # 🔍 MEMORY CONVERSION LOGGING: Only log when conversions happen or issues occur
131 if conversion_count > 0:
132 logger.debug(
133 f"🔄 STACK_SLICES: Converted {conversion_count}/{len(slices)} "
134 f"slices to {memory_type}"
135 )
136 # Silent success for no-conversion cases to reduce log pollution
138 return result
141def unstack_slices(
142 array: Any, memory_type: str, gpu_id: int, validate_slices: bool = True
143) -> list[Any]:
144 """
145 Split a 3D array into 2D slices along axis 0 and convert to the specified memory type.
147 STRICT VALIDATION: Input must be a 3D array. No automatic handling of improper inputs.
149 Args:
150 array: 3D array to split - MUST BE 3D
151 memory_type: The memory type to use for the output slices (REQUIRED)
152 gpu_id: The target GPU device ID (REQUIRED)
153 validate_slices: If True, validates that each extracted slice is 2D
155 Returns:
156 List of 2D slices in the specified memory type
158 Raises:
159 ValueError: If array is not 3D
160 ValueError: If validate_slices is True and any extracted slice is not 2D
161 ValueError: If gpu_id is negative for GPU memory types
162 ValueError: If memory_type is not supported
163 MemoryConversionError: If conversion fails
164 """
165 # Detect input type and check if conversion is needed
166 input_type = detect_memory_type(array)
167 getattr(array, "shape", "unknown")
169 # Verify the array is 3D - fail loudly if not
170 if not _is_3d(array):
171 raise ValueError(f"Array must be 3D, got shape {getattr(array, 'shape', 'unknown')}")
173 # Check GPU requirements
174 _enforce_gpu_device_requirements(memory_type, gpu_id)
176 # Convert to target memory type
177 source_type = input_type # Reuse already detected type
179 # Direct conversion
180 if source_type == memory_type:
181 array = MemoryType(memory_type).move_to_device(array, gpu_id)
182 else:
183 # Convert and log the conversion
184 from arraybridge.converters import convert_memory
186 logger.debug(f"🔄 UNSTACK_SLICES: Converting array - {source_type} → {memory_type}")
187 array = convert_memory(
188 data=array, source_type=source_type, target_type=memory_type, gpu_id=gpu_id
189 )
191 # Extract slices along axis 0 (already in the target memory type)
192 slices = [array[i] for i in range(array.shape[0])]
194 # Validate that all extracted slices are 2D if requested
195 if validate_slices:
196 for i, slice_data in enumerate(slices):
197 if not _is_2d(slice_data):
198 raise ValueError(
199 f"Extracted slice at index {i} is not 2D. "
200 f"This indicates a malformed 3D array."
201 )
203 # 🔍 MEMORY CONVERSION LOGGING: Only log conversions or issues
204 if source_type != memory_type:
205 logger.debug(f"🔄 UNSTACK_SLICES: Converted and extracted {len(slices)} slices")
206 elif len(slices) == 0:
207 logger.warning("🔄 UNSTACK_SLICES: No slices extracted (empty array)")
208 # Silent success for no-conversion cases to reduce log pollution
210 return slices