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

30 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +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 from openhcs.constants.constants import AllComponents 

29 super().__init__(AllComponents) 

30 

31 @classmethod 

32 @abstractmethod 

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

34 """ 

35 Check if this parser can parse the given filename. 

36 

37 Args: 

38 filename (str): Filename to check 

39 

40 Returns: 

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

42 """ 

43 pass 

44 

45 @abstractmethod 

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

47 """ 

48 Parse a microscopy image filename to extract all components. 

49 

50 Args: 

51 filename (str): Filename to parse 

52 

53 Returns: 

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

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

56 """ 

57 pass 

58 

59 @abstractmethod 

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

61 """ 

62 Extract coordinates from component identifier (typically well). 

63 

64 Args: 

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

66 

67 Returns: 

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

69 

70 Raises: 

71 ValueError: If component format is invalid for this parser 

72 """ 

73 pass 

74 

75 @abstractmethod 

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

77 """ 

78 Construct a filename from component values. 

79 

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

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

82 

83 Args: 

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

85 **component_values: Component values as keyword arguments. 

86 Keys should match VariableComponents enum values. 

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

88 

89 Returns: 

90 str: Constructed filename 

91 

92 Example: 

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

94 """ 

95 pass 

96 

97 

98class MetadataHandler(ABC): 

99 """ 

100 Abstract base class for handling microscope metadata. 

101 

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

103 

104 Subclasses can define FALLBACK_VALUES for explicit fallbacks: 

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

106 """ 

107 

108 FALLBACK_VALUES = { 

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

110 'grid_dimensions': None, # No grid dimensions by default 

111 } 

112 

113 def __init__(self): 

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

115 self.component_enum = VariableComponents 

116 

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

118 try: 

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

120 except Exception: 

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

122 return self.FALLBACK_VALUES[key] 

123 

124 @abstractmethod 

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

126 """ 

127 Find the metadata file for a plate. 

128 

129 Args: 

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

131 

132 Returns: 

133 Path to the metadata file 

134 

135 Raises: 

136 TypeError: If plate_path is not a valid path type 

137 FileNotFoundError: If no metadata file is found 

138 """ 

139 pass 

140 

141 @abstractmethod 

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

143 """ 

144 Get grid dimensions for stitching from metadata. 

145 

146 Args: 

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

148 

149 Returns: 

150 Tuple of (grid_size_x, grid_size_y) 

151 

152 Raises: 

153 TypeError: If plate_path is not a valid path type 

154 FileNotFoundError: If no metadata file is found 

155 ValueError: If grid dimensions cannot be determined 

156 """ 

157 pass 

158 

159 @abstractmethod 

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

161 """ 

162 Get the pixel size from metadata. 

163 

164 Args: 

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

166 

167 Returns: 

168 Pixel size in micrometers 

169 

170 Raises: 

171 TypeError: If plate_path is not a valid path type 

172 FileNotFoundError: If no metadata file is found 

173 ValueError: If pixel size cannot be determined 

174 """ 

175 pass 

176 

177 @abstractmethod 

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

179 """ 

180 Get channel key→name mapping from metadata. 

181 

182 Args: 

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

184 

185 Returns: 

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

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

188 """ 

189 pass 

190 

191 @abstractmethod 

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

193 """ 

194 Get well key→name mapping from metadata. 

195 

196 Args: 

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

198 

199 Returns: 

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

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

202 """ 

203 pass 

204 

205 @abstractmethod 

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

207 """ 

208 Get site key→name mapping from metadata. 

209 

210 Args: 

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

212 

213 Returns: 

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

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

216 """ 

217 pass 

218 

219 @abstractmethod 

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

221 """ 

222 Get z_index key→name mapping from metadata. 

223 

224 Args: 

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

226 

227 Returns: 

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

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

230 """ 

231 pass 

232 

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

234 """ 

235 Parse all metadata using dynamic method resolution. 

236 

237 This method iterates through VariableComponents and calls the corresponding 

238 abstract methods to collect all available metadata. 

239 

240 Args: 

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

242 

243 Returns: 

244 Dict mapping component names to their key→name mappings 

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

246 """ 

247 result = {} 

248 for component in self.component_enum: 

249 component_name = component.value 

250 method_name = f"get_{component_name}_values" 

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

252 values = method(plate_path) 

253 if values: 

254 result[component_name] = values 

255 return result