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

1""" 

2Display configuration factory for creating viewer-specific config dataclasses. 

3 

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 

10 

11 

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. 

25 

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 

32 

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 

43 

44 Returns: 

45 Dynamically created frozen dataclass 

46 

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 

62 

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 {} 

69 

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") 

73 

74 annotations = {} 

75 defaults = {} 

76 

77 for field_name, (field_type, default_value) in base_fields.items(): 

78 annotations[field_name] = field_type 

79 defaults[field_name] = default_value 

80 

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) 

86 

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) 

93 

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) 

100 

101 class_attrs = { 

102 '__annotations__': annotations, 

103 '__init__': __init__, 

104 '__doc__': docstring or f"Display configuration for {name}", 

105 } 

106 

107 for field_name, default_value in defaults.items(): 

108 class_attrs[field_name] = default_value 

109 

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 

113 

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) 

116 

117 DisplayConfig = type(name, (), class_attrs) 

118 DisplayConfig = dataclass(frozen=True)(DisplayConfig) 

119 

120 # Set proper module and qualname for pickling support 

121 DisplayConfig.__module__ = 'openhcs.core.config' 

122 DisplayConfig.__qualname__ = name 

123 

124 return DisplayConfig 

125 

126 

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. 

137 

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 

145 

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() 

156 

157 field_name = f"{component_value}_mode" 

158 mode = getattr(self, field_name, None) 

159 

160 if mode is None: 

161 # Default: all components are STACK (well, channel, site, z_index, timepoint) 

162 return dimension_mode_enum.STACK 

163 

164 return mode 

165 

166 def get_colormap_name(self): 

167 return self.colormap.value 

168 

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) 

179 

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. 

196 

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. 

200 

201 Includes ALL dimensions (site, channel, z_index, timepoint, well) regardless of 

202 which dimension is used as the multiprocessing axis. 

203 

204 Also includes virtual components (step_name, step_index, source) for streaming contexts. 

205 """ 

206 ) 

207 

208 

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. 

218 

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) 

226 

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 

233 

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() 

244 

245 field_name = f"{component_value}_mode" 

246 mode = getattr(self, field_name, None) 

247 

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) 

258 

259 return mode 

260 

261 def get_lut_name(self): 

262 return self.lut.value 

263 

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) 

274 

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. 

291 

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). 

295 

296 Includes ALL dimensions (site, channel, z_index, timepoint, well) regardless of 

297 which dimension is used as the multiprocessing axis. 

298 

299 Also includes virtual components (step_name, step_index, source) for streaming contexts. 

300 

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 

305 

306 WINDOW mode creates separate windows instead of combining into hyperstack. 

307 """ 

308 ) 

309