Coverage for openhcs/microscopes/microscope_interfaces.py: 47.2%

58 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +0000

1""" 

2Microscope interfaces for openhcs. 

3 

4This module provides abstract base classes for microscope-specific functionality, 

5including filename parsing and metadata handling. 

6""" 

7 

8from abc import ABC, abstractmethod 

9from pathlib import Path 

10from typing import Any, Dict, Optional, Tuple, Union 

11from openhcs.constants.constants import VariableComponents, AllComponents 

12from openhcs.core.components.parser_metaprogramming import GenericFilenameParser 

13 

14from openhcs.constants.constants import DEFAULT_PIXEL_SIZE 

15 

16 

17class FilenameParser(GenericFilenameParser): 

18 """ 

19 Abstract base class for parsing microscopy image filenames. 

20 

21 This class now uses the metaprogramming system to generate component-specific 

22 methods dynamically based on the VariableComponents enum, eliminating hardcoded 

23 component assumptions. 

24 """ 

25 

26 def __init__(self): 

27 """Initialize the parser with AllComponents enum.""" 

28 super().__init__(AllComponents) 

29 

30 @classmethod 

31 @abstractmethod 

32 def can_parse(cls, filename: str) -> bool: 

33 """ 

34 Check if this parser can parse the given filename. 

35 

36 Args: 

37 filename (str): Filename to check 

38 

39 Returns: 

40 bool: True if this parser can parse the filename, False otherwise 

41 """ 

42 pass 

43 

44 @abstractmethod 

45 def parse_filename(self, filename: str) -> Optional[Dict[str, Any]]: 

46 """ 

47 Parse a microscopy image filename to extract all components. 

48 

49 Args: 

50 filename (str): Filename to parse 

51 

52 Returns: 

53 dict or None: Dictionary with extracted components or None if parsing fails. 

54 The dictionary should contain keys matching VariableComponents enum values plus 'extension'. 

55 """ 

56 pass 

57 

58 @abstractmethod 

59 def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]: 

60 """ 

61 Extract coordinates from component identifier (typically well). 

62 

63 Args: 

64 component_value (str): Component identifier (e.g., 'A01', 'R03C04', 'C04') 

65 

66 Returns: 

67 Tuple[str, str]: (row, column) where row is like 'A', 'B' and column is like '01', '04' 

68 

69 Raises: 

70 ValueError: If component format is invalid for this parser 

71 """ 

72 pass 

73 

74 @abstractmethod 

75 def construct_filename(self, extension: str = '.tif', **component_values) -> str: 

76 """ 

77 Construct a filename from component values. 

78 

79 This method now uses **kwargs to accept any component values dynamically, 

80 making it truly generic and adaptable to any component configuration. 

81 

82 Args: 

83 extension (str, optional): File extension (default: '.tif') 

84 **component_values: Component values as keyword arguments. 

85 Keys should match VariableComponents enum values. 

86 Example: well='A01', site=1, channel=2, z_index=1 

87 

88 Returns: 

89 str: Constructed filename 

90 

91 Example: 

92 construct_filename(well='A01', site=1, channel=2, z_index=1, extension='.tif') 

93 """ 

94 pass 

95 

96 

97class MetadataHandler(ABC): 

98 """ 

99 Abstract base class for handling microscope metadata. 

100 

101 All metadata methods require str or Path objects for file paths. 

102 

103 Subclasses can define FALLBACK_VALUES for explicit fallbacks: 

104 FALLBACK_VALUES = {'pixel_size': 1.0, 'grid_dimensions': (3, 3)} 

105 """ 

106 

107 FALLBACK_VALUES = { 

108 'pixel_size': DEFAULT_PIXEL_SIZE, # Default pixel size in micrometers 

109 'grid_dimensions': (1, 1), # Default grid dimensions (1x1) when not available 

110 } 

111 

112 def __init__(self): 

113 """Initialize metadata handler with VariableComponents enum.""" 

114 self.component_enum = VariableComponents 

115 

116 def _get_with_fallback(self, method_name: str, *args, **kwargs): 

117 try: 

118 return getattr(self, method_name)(*args, **kwargs) 

119 except Exception: 

120 key = method_name.replace('get_', '') 

121 return self.FALLBACK_VALUES[key] 

122 

123 @abstractmethod 

124 def find_metadata_file(self, plate_path: Union[str, Path]) -> Path: 

125 """ 

126 Find the metadata file for a plate. 

127 

128 Args: 

129 plate_path: Path to the plate folder (str or Path) 

130 

131 Returns: 

132 Path to the metadata file 

133 

134 Raises: 

135 TypeError: If plate_path is not a valid path type 

136 FileNotFoundError: If no metadata file is found 

137 """ 

138 pass 

139 

140 @abstractmethod 

141 def get_grid_dimensions(self, plate_path: Union[str, Path]) -> Tuple[int, int]: 

142 """ 

143 Get grid dimensions for stitching from metadata. 

144 

145 Args: 

146 plate_path: Path to the plate folder (str or Path) 

147 

148 Returns: 

149 Tuple of (grid_size_x, grid_size_y) 

150 

151 Raises: 

152 TypeError: If plate_path is not a valid path type 

153 FileNotFoundError: If no metadata file is found 

154 ValueError: If grid dimensions cannot be determined 

155 """ 

156 pass 

157 

158 @abstractmethod 

159 def get_pixel_size(self, plate_path: Union[str, Path]) -> float: 

160 """ 

161 Get the pixel size from metadata. 

162 

163 Args: 

164 plate_path: Path to the plate folder (str or Path) 

165 

166 Returns: 

167 Pixel size in micrometers 

168 

169 Raises: 

170 TypeError: If plate_path is not a valid path type 

171 FileNotFoundError: If no metadata file is found 

172 ValueError: If pixel size cannot be determined 

173 """ 

174 pass 

175 

176 @abstractmethod 

177 def get_channel_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: 

178 """ 

179 Get channel key→name mapping from metadata. 

180 

181 Args: 

182 plate_path: Path to the plate folder (str or Path) 

183 

184 Returns: 

185 Dict mapping channel keys to display names, or None if not available 

186 Example: {"1": "HOECHST 33342", "2": "Calcein", "3": "Alexa 647"} 

187 """ 

188 pass 

189 

190 @abstractmethod 

191 def get_well_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: 

192 """ 

193 Get well key→name mapping from metadata. 

194 

195 Args: 

196 plate_path: Path to the plate folder (str or Path) 

197 

198 Returns: 

199 Dict mapping well keys to display names, or None if not available 

200 Example: {"A01": "Control", "A02": "Treatment"} or None 

201 """ 

202 pass 

203 

204 @abstractmethod 

205 def get_site_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: 

206 """ 

207 Get site key→name mapping from metadata. 

208 

209 Args: 

210 plate_path: Path to the plate folder (str or Path) 

211 

212 Returns: 

213 Dict mapping site keys to display names, or None if not available 

214 Example: {"1": "Center", "2": "Edge"} or None 

215 """ 

216 pass 

217 

218 @abstractmethod 

219 def get_z_index_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: 

220 """ 

221 Get z_index key→name mapping from metadata. 

222 

223 Args: 

224 plate_path: Path to the plate folder (str or Path) 

225 

226 Returns: 

227 Dict mapping z_index keys to display names, or None if not available 

228 Example: {"1": "Bottom", "2": "Middle", "3": "Top"} or None 

229 """ 

230 pass 

231 

232 def get_image_files(self, plate_path: Union[str, Path]) -> list[str]: 

233 """ 

234 Get list of image files from OpenHCS metadata. 

235 

236 Default implementation reads from openhcs_metadata.json after virtual workspace preparation. 

237 Derives image list from workspace_mapping keys if available, otherwise from image_files list. 

238 

239 Subclasses can override if they need different behavior (e.g., OpenHCS reads directly from metadata). 

240 

241 Args: 

242 plate_path: Path to the plate folder (str or Path) 

243 

244 Returns: 

245 List of image filenames with subdirectory prefix (e.g., "Images/file.tif" or "file.tif") 

246 

247 Raises: 

248 TypeError: If plate_path is not a valid path type 

249 FileNotFoundError: If plate path does not exist or no metadata found 

250 """ 

251 from pathlib import Path 

252 

253 # Ensure plate_path is a Path object 

254 if isinstance(plate_path, str): 

255 plate_path = Path(plate_path) 

256 elif not isinstance(plate_path, Path): 

257 raise TypeError(f"Expected str or Path, got {type(plate_path).__name__}") 

258 

259 # Ensure the path exists 

260 if not plate_path.exists(): 

261 raise FileNotFoundError(f"Plate path does not exist: {plate_path}") 

262 

263 # Read from OpenHCS metadata (unified approach for all microscopes) 

264 from openhcs.microscopes.openhcs import OpenHCSMetadataHandler 

265 import logging 

266 logger = logging.getLogger(__name__) 

267 

268 openhcs_handler = OpenHCSMetadataHandler(self.filemanager) 

269 

270 try: 

271 metadata = openhcs_handler._load_metadata_dict(plate_path) 

272 subdirs = metadata.get("subdirectories", {}) 

273 logger.info(f"get_image_files: Found {len(subdirs)} subdirectories") 

274 

275 # Find main subdirectory 

276 main_subdir_key = next((key for key, data in subdirs.items() if data.get("main")), None) 

277 if not main_subdir_key: 

278 main_subdir_key = next(iter(subdirs.keys())) 

279 

280 logger.info(f"get_image_files: Using main subdirectory '{main_subdir_key}'") 

281 subdir_data = subdirs[main_subdir_key] 

282 

283 # Prefer workspace_mapping keys (virtual paths) if available 

284 if workspace_mapping := subdir_data.get("workspace_mapping"): 

285 logger.info(f"get_image_files: Returning {len(workspace_mapping)} files from workspace_mapping") 

286 return list(workspace_mapping.keys()) 

287 

288 # Otherwise use image_files list 

289 image_files = subdir_data.get("image_files", []) 

290 logger.info(f"get_image_files: Returning {len(image_files)} files from image_files list") 

291 return image_files 

292 

293 except Exception: 

294 # Fallback: no metadata yet, return empty list 

295 return [] 

296 

297 def parse_metadata(self, plate_path: Union[str, Path]) -> Dict[str, Dict[str, Optional[str]]]: 

298 """ 

299 Parse all metadata using dynamic method resolution. 

300 

301 This method iterates through VariableComponents and calls the corresponding 

302 abstract methods to collect all available metadata. 

303 

304 Args: 

305 plate_path: Path to the plate folder (str or Path) 

306 

307 Returns: 

308 Dict mapping component names to their key→name mappings 

309 Example: {"channel": {"1": "HOECHST 33342", "2": "Calcein"}} 

310 """ 

311 result = {} 

312 for component in self.component_enum: 

313 component_name = component.value 

314 method_name = f"get_{component_name}_values" 

315 method = getattr(self, method_name) # Let AttributeError bubble up 

316 values = method(plate_path) 

317 if values: 

318 result[component_name] = values 

319 return result