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

24 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +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 

11 

12from openhcs.constants.constants import DEFAULT_PIXEL_SIZE 

13 

14 

15class FilenameParser(ABC): 

16 """ 

17 Abstract base class for parsing microscopy image filenames. 

18 """ 

19 

20 # Constants 

21 FILENAME_COMPONENTS = ['well', 'site', 'channel', 'z_index', 'extension'] 

22 PLACEHOLDER_PATTERN = '{iii}' 

23 

24 @classmethod 

25 @abstractmethod 

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

27 """ 

28 Check if this parser can parse the given filename. 

29 

30 Args: 

31 filename (str): Filename to check 

32 

33 Returns: 

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

35 """ 

36 pass 

37 

38 @abstractmethod 

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

40 """ 

41 Parse a microscopy image filename to extract all components. 

42 

43 Args: 

44 filename (str): Filename to parse 

45 

46 Returns: 

47 dict or None: Dictionary with extracted components or None if parsing fails 

48 """ 

49 pass 

50 

51 @abstractmethod 

52 def extract_row_column(self, well: str) -> Tuple[str, str]: 

53 """ 

54 Extract row and column from a well identifier. 

55 

56 Args: 

57 well (str): Well identifier (e.g., 'A01', 'R03C04', 'C04') 

58 

59 Returns: 

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

61 

62 Raises: 

63 ValueError: If well format is invalid for this parser 

64 """ 

65 pass 

66 

67 @abstractmethod 

68 def construct_filename(self, well: str, site: Optional[Union[int, str]] = None, 

69 channel: Optional[int] = None, 

70 z_index: Optional[Union[int, str]] = None, 

71 extension: str = '.tif', 

72 site_padding: int = 3, z_padding: int = 3) -> str: 

73 """ 

74 Construct a filename from components. 

75 

76 Args: 

77 well (str): Well ID (e.g., 'A01') 

78 site (int or str, optional): Site number or placeholder string (e.g., '{iii}') 

79 channel (int, optional): Channel/wavelength number 

80 z_index (int or str, optional): Z-index or placeholder string (e.g., '{zzz}') 

81 extension (str, optional): File extension 

82 site_padding (int, optional): Width to pad site numbers to (default: 3) 

83 z_padding (int, optional): Width to pad Z-index numbers to (default: 3) 

84 

85 Returns: 

86 str: Constructed filename 

87 """ 

88 pass 

89 

90 

91class MetadataHandler(ABC): 

92 """ 

93 Abstract base class for handling microscope metadata. 

94 

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

96 

97 Subclasses can define FALLBACK_VALUES for explicit fallbacks: 

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

99 """ 

100 

101 FALLBACK_VALUES = { 

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

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

104 } 

105 

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

107 try: 

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

109 except Exception: 

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

111 return self.FALLBACK_VALUES[key] 

112 

113 @abstractmethod 

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

115 """ 

116 Find the metadata file for a plate. 

117 

118 Args: 

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

120 

121 Returns: 

122 Path to the metadata file 

123 

124 Raises: 

125 TypeError: If plate_path is not a valid path type 

126 FileNotFoundError: If no metadata file is found 

127 """ 

128 pass 

129 

130 @abstractmethod 

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

132 """ 

133 Get grid dimensions for stitching from metadata. 

134 

135 Args: 

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

137 

138 Returns: 

139 Tuple of (grid_size_x, grid_size_y) 

140 

141 Raises: 

142 TypeError: If plate_path is not a valid path type 

143 FileNotFoundError: If no metadata file is found 

144 ValueError: If grid dimensions cannot be determined 

145 """ 

146 pass 

147 

148 @abstractmethod 

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

150 """ 

151 Get the pixel size from metadata. 

152 

153 Args: 

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

155 

156 Returns: 

157 Pixel size in micrometers 

158 

159 Raises: 

160 TypeError: If plate_path is not a valid path type 

161 FileNotFoundError: If no metadata file is found 

162 ValueError: If pixel size cannot be determined 

163 """ 

164 pass 

165 

166 @abstractmethod 

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

168 """ 

169 Get channel key→name mapping from metadata. 

170 

171 Args: 

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

173 

174 Returns: 

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

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

177 """ 

178 pass 

179 

180 @abstractmethod 

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

182 """ 

183 Get well key→name mapping from metadata. 

184 

185 Args: 

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

187 

188 Returns: 

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

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

191 """ 

192 pass 

193 

194 @abstractmethod 

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

196 """ 

197 Get site key→name mapping from metadata. 

198 

199 Args: 

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

201 

202 Returns: 

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

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

205 """ 

206 pass 

207 

208 @abstractmethod 

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

210 """ 

211 Get z_index key→name mapping from metadata. 

212 

213 Args: 

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

215 

216 Returns: 

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

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

219 """ 

220 pass 

221 

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

223 """ 

224 Parse all metadata using enum→method mapping. 

225 

226 This method iterates through GroupBy components and calls the corresponding 

227 abstract methods to collect all available metadata. 

228 

229 Args: 

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

231 

232 Returns: 

233 Dict mapping component names to their key→name mappings 

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

235 """ 

236 # Import here to avoid circular imports 

237 from openhcs.constants.constants import GroupBy 

238 

239 method_map = { 

240 GroupBy.CHANNEL: self.get_channel_values, 

241 GroupBy.WELL: self.get_well_values, 

242 GroupBy.SITE: self.get_site_values, 

243 GroupBy.Z_INDEX: self.get_z_index_values 

244 } 

245 

246 result = {} 

247 for group_by, method in method_map.items(): 

248 values = method(plate_path) 

249 if values: 

250 result[group_by.value] = values 

251 return result