Coverage for openhcs/utils/display_config_factory.py: 63.8%
78 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
1"""
2Display configuration factory for creating viewer-specific config dataclasses.
4Provides generic infrastructure for creating display configuration dataclasses
5with component-specific dimension modes, supporting both Napari and Fiji viewers.
6"""
7from dataclasses import dataclass
8from enum import Enum
9from typing import Dict, Any, Callable, Optional, Type
12def create_display_config(
13 name: str,
14 base_fields: Dict[str, tuple[Type, Any]],
15 component_mode_enum: Type[Enum],
16 component_defaults: Optional[Dict[str, Any]] = None,
17 virtual_components: Optional[Type[Enum]] = None,
18 component_order: Optional[list[str]] = None,
19 default_mode: Optional[Any] = None,
20 methods: Optional[Dict[str, Callable]] = None,
21 docstring: Optional[str] = None
22) -> Type:
23 """
24 Generic factory for creating display configuration dataclasses.
26 Creates a frozen dataclass with:
27 - Base fields (e.g., colormap, variable_size_handling)
28 - Component-specific mode fields (e.g., channel_mode, z_index_mode, well_mode)
29 - Virtual component mode fields (e.g., step_name_mode, source_mode)
30 - Custom methods (e.g., get_dimension_mode, get_colormap_name)
31 - COMPONENT_ORDER class attribute for canonical layer naming order
33 Args:
34 name: Name of the dataclass to create
35 base_fields: Dict mapping field names to (type, default_value) tuples
36 component_mode_enum: Enum class for component dimension modes
37 component_defaults: Optional dict mapping component names to default modes
38 virtual_components: Optional enum of virtual components (step_name, source, etc.)
39 component_order: Canonical order for layer naming (e.g., ['step_name', 'source', 'well', ...])
40 default_mode: Default mode for components not specified in component_defaults (required)
41 methods: Optional dict mapping method names to method implementations
42 docstring: Optional docstring for the created class
44 Returns:
45 Dynamically created frozen dataclass
47 Example:
48 >>> NapariDisplayConfig = create_display_config(
49 ... name='NapariDisplayConfig',
50 ... base_fields={
51 ... 'colormap': (NapariColormap, NapariColormap.GRAY),
52 ... 'variable_size_handling': (NapariVariableSizeHandling, NapariVariableSizeHandling.SEPARATE_LAYERS)
53 ... },
54 ... component_mode_enum=NapariDimensionMode,
55 ... component_defaults={'channel': NapariDimensionMode.SLICE},
56 ... virtual_components=VirtualComponents,
57 ... component_order=['step_name', 'source', 'well', 'channel'],
58 ... methods={'get_colormap_name': lambda self: self.colormap.value}
59 ... )
60 """
61 from openhcs.constants import AllComponents
63 # Use AllComponents instead of VariableComponents so display configs include ALL dimensions
64 # (including the multiprocessing axis). Display configuration should be independent of
65 # multiprocessing axis choice - users should be able to control how wells/any dimension
66 # are displayed regardless of which dimension is used for parallelization.
67 all_components = list(AllComponents)
68 component_defaults = component_defaults or {}
70 # Require explicit default_mode - no magic fallbacks
71 if default_mode is None: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 raise ValueError("default_mode is required - specify the default mode for unspecified components")
74 annotations = {}
75 defaults = {}
77 for field_name, (field_type, default_value) in base_fields.items():
78 annotations[field_name] = field_type
79 defaults[field_name] = default_value
81 # Generate mode fields for filename components
82 for component in all_components:
83 field_name = f"{component.value}_mode"
84 annotations[field_name] = component_mode_enum
85 defaults[field_name] = component_defaults.get(component.value, default_mode)
87 # Generate mode fields for virtual components
88 if virtual_components: 88 ↛ 94line 88 didn't jump to line 94 because the condition on line 88 was always true
89 for component in virtual_components:
90 field_name = f"{component.value}_mode"
91 annotations[field_name] = component_mode_enum
92 defaults[field_name] = component_defaults.get(component.value, default_mode)
94 def __init__(self, **kwargs):
95 for field_name, default_value in defaults.items():
96 if field_name not in kwargs:
97 kwargs[field_name] = default_value
98 for field_name, value in kwargs.items():
99 object.__setattr__(self, field_name, value)
101 class_attrs = {
102 '__annotations__': annotations,
103 '__init__': __init__,
104 '__doc__': docstring or f"Display configuration for {name}",
105 }
107 for field_name, default_value in defaults.items():
108 class_attrs[field_name] = default_value
110 # Add component order as class attribute
111 if component_order: 111 ↛ 114line 111 didn't jump to line 114 because the condition on line 111 was always true
112 class_attrs['COMPONENT_ORDER'] = component_order
114 if methods: 114 ↛ 117line 114 didn't jump to line 117 because the condition on line 114 was always true
115 class_attrs.update(methods)
117 DisplayConfig = type(name, (), class_attrs)
118 DisplayConfig = dataclass(frozen=True)(DisplayConfig)
120 # Set proper module and qualname for pickling support
121 DisplayConfig.__module__ = 'openhcs.core.config'
122 DisplayConfig.__qualname__ = name
124 return DisplayConfig
127def create_napari_display_config(
128 colormap_enum: Type[Enum],
129 dimension_mode_enum: Type[Enum],
130 variable_size_handling_enum: Type[Enum],
131 virtual_components: Optional[Type[Enum]] = None,
132 component_order: Optional[list[str]] = None,
133 virtual_component_defaults: Optional[Dict[str, Any]] = None
134) -> Type:
135 """
136 Create NapariDisplayConfig with component-specific fields.
138 Args:
139 colormap_enum: Enum for colormap options
140 dimension_mode_enum: Enum for dimension modes (SLICE/STACK)
141 variable_size_handling_enum: Enum for variable size handling
142 virtual_components: Optional enum of virtual components (step_name, source, etc.)
143 component_order: Canonical order for layer naming
144 virtual_component_defaults: Optional dict mapping virtual component names to default modes
146 Returns:
147 NapariDisplayConfig dataclass
148 """
149 def get_dimension_mode(self, component):
150 if hasattr(component, 'value'):
151 component_value = component.value
152 elif hasattr(component, 'name'):
153 component_value = component.name.lower()
154 else:
155 component_value = str(component).lower()
157 field_name = f"{component_value}_mode"
158 mode = getattr(self, field_name, None)
160 if mode is None:
161 # Default: all components are STACK (well, channel, site, z_index, timepoint)
162 return dimension_mode_enum.STACK
164 return mode
166 def get_colormap_name(self):
167 return self.colormap.value
169 # Merge component defaults - all components default to STACK
170 component_defaults = {
171 'well': dimension_mode_enum.STACK,
172 'channel': dimension_mode_enum.STACK,
173 'site': dimension_mode_enum.STACK,
174 'z_index': dimension_mode_enum.STACK,
175 'timepoint': dimension_mode_enum.STACK
176 }
177 if virtual_component_defaults: 177 ↛ 180line 177 didn't jump to line 180 because the condition on line 177 was always true
178 component_defaults.update(virtual_component_defaults)
180 return create_display_config(
181 name='NapariDisplayConfig',
182 base_fields={
183 'colormap': (colormap_enum, colormap_enum.GRAY),
184 'variable_size_handling': (variable_size_handling_enum, variable_size_handling_enum.PAD_TO_MAX),
185 },
186 component_mode_enum=dimension_mode_enum,
187 component_defaults=component_defaults,
188 virtual_components=virtual_components,
189 component_order=component_order,
190 default_mode=dimension_mode_enum.STACK,
191 methods={
192 'get_dimension_mode': get_dimension_mode,
193 'get_colormap_name': get_colormap_name,
194 },
195 docstring="""Configuration for napari display behavior for all OpenHCS components.
197 This class is dynamically generated with individual fields for each component dimension.
198 Each component has a corresponding {component}_mode field that controls whether
199 it's displayed as a slice or stack in napari.
201 Includes ALL dimensions (site, channel, z_index, timepoint, well) regardless of
202 which dimension is used as the multiprocessing axis.
204 Also includes virtual components (step_name, step_index, source) for streaming contexts.
205 """
206 )
209def create_fiji_display_config(
210 lut_enum: Type[Enum],
211 dimension_mode_enum: Type[Enum],
212 virtual_components: Optional[Type[Enum]] = None,
213 component_order: Optional[list[str]] = None,
214 virtual_component_defaults: Optional[Dict[str, Any]] = None
215) -> Type:
216 """
217 Create FijiDisplayConfig with component-specific fields.
219 Maps OpenHCS dimensions to ImageJ hyperstack dimensions (C, Z, T).
220 Default mapping:
221 - well → FRAME (wells become frames)
222 - site → FRAME (sites become frames)
223 - channel → CHANNEL (channels become channels)
224 - z_index → SLICE (z-planes become slices)
225 - timepoint → FRAME (timepoints become frames)
227 Args:
228 lut_enum: Enum for Fiji LUT options
229 dimension_mode_enum: Enum for dimension modes (WINDOW/CHANNEL/SLICE/FRAME)
230 virtual_components: Optional enum of virtual components (step_name, source, etc.)
231 component_order: Canonical order for layer naming
232 virtual_component_defaults: Optional dict mapping virtual component names to default modes
234 Returns:
235 FijiDisplayConfig dataclass
236 """
237 def get_dimension_mode(self, component):
238 if hasattr(component, 'value'):
239 component_value = component.value
240 elif hasattr(component, 'name'):
241 component_value = component.name.lower()
242 else:
243 component_value = str(component).lower()
245 field_name = f"{component_value}_mode"
246 mode = getattr(self, field_name, None)
248 if mode is None:
249 # Default mapping for Fiji hyperstacks
250 defaults = {
251 'well': dimension_mode_enum.FRAME,
252 'site': dimension_mode_enum.FRAME,
253 'channel': dimension_mode_enum.CHANNEL,
254 'z_index': dimension_mode_enum.SLICE,
255 'timepoint': dimension_mode_enum.FRAME
256 }
257 return defaults.get(component_value, dimension_mode_enum.CHANNEL)
259 return mode
261 def get_lut_name(self):
262 return self.lut.value
264 # Merge component defaults
265 component_defaults = {
266 'well': dimension_mode_enum.FRAME,
267 'site': dimension_mode_enum.FRAME,
268 'channel': dimension_mode_enum.CHANNEL,
269 'z_index': dimension_mode_enum.SLICE,
270 'timepoint': dimension_mode_enum.FRAME
271 }
272 if virtual_component_defaults: 272 ↛ 275line 272 didn't jump to line 275 because the condition on line 272 was always true
273 component_defaults.update(virtual_component_defaults)
275 return create_display_config(
276 name='FijiDisplayConfig',
277 base_fields={
278 'lut': (lut_enum, lut_enum.GRAYS),
279 'auto_contrast': (bool, True),
280 },
281 component_mode_enum=dimension_mode_enum,
282 component_defaults=component_defaults,
283 virtual_components=virtual_components,
284 component_order=component_order,
285 default_mode=dimension_mode_enum.CHANNEL,
286 methods={
287 'get_dimension_mode': get_dimension_mode,
288 'get_lut_name': get_lut_name,
289 },
290 docstring="""Configuration for Fiji display behavior for all OpenHCS components.
292 This class is dynamically generated with individual fields for each component dimension.
293 Each component has a corresponding {component}_mode field that controls how it maps
294 to ImageJ hyperstack dimensions (WINDOW/CHANNEL/SLICE/FRAME).
296 Includes ALL dimensions (site, channel, z_index, timepoint, well) regardless of
297 which dimension is used as the multiprocessing axis.
299 Also includes virtual components (step_name, step_index, source) for streaming contexts.
301 ImageJ hyperstacks have 3 dimensions:
302 - Channels (C): Color channels or sites
303 - Slices (Z): Z-planes or depth
304 - Frames (T): Time points or temporal dimension
306 WINDOW mode creates separate windows instead of combining into hyperstack.
307 """
308 )