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
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2Microscope interfaces for openhcs.
4This module provides abstract base classes for microscope-specific functionality,
5including filename parsing and metadata handling.
6"""
8from abc import ABC, abstractmethod
9from pathlib import Path
10from typing import Any, Dict, Optional, Tuple, Union
12from openhcs.constants.constants import DEFAULT_PIXEL_SIZE
15class FilenameParser(ABC):
16 """
17 Abstract base class for parsing microscopy image filenames.
18 """
20 # Constants
21 FILENAME_COMPONENTS = ['well', 'site', 'channel', 'z_index', 'extension']
22 PLACEHOLDER_PATTERN = '{iii}'
24 @classmethod
25 @abstractmethod
26 def can_parse(cls, filename: str) -> bool:
27 """
28 Check if this parser can parse the given filename.
30 Args:
31 filename (str): Filename to check
33 Returns:
34 bool: True if this parser can parse the filename, False otherwise
35 """
36 pass
38 @abstractmethod
39 def parse_filename(self, filename: str) -> Optional[Dict[str, Any]]:
40 """
41 Parse a microscopy image filename to extract all components.
43 Args:
44 filename (str): Filename to parse
46 Returns:
47 dict or None: Dictionary with extracted components or None if parsing fails
48 """
49 pass
51 @abstractmethod
52 def extract_row_column(self, well: str) -> Tuple[str, str]:
53 """
54 Extract row and column from a well identifier.
56 Args:
57 well (str): Well identifier (e.g., 'A01', 'R03C04', 'C04')
59 Returns:
60 Tuple[str, str]: (row, column) where row is like 'A', 'B' and column is like '01', '04'
62 Raises:
63 ValueError: If well format is invalid for this parser
64 """
65 pass
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.
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)
85 Returns:
86 str: Constructed filename
87 """
88 pass
91class MetadataHandler(ABC):
92 """
93 Abstract base class for handling microscope metadata.
95 All metadata methods require str or Path objects for file paths.
97 Subclasses can define FALLBACK_VALUES for explicit fallbacks:
98 FALLBACK_VALUES = {'pixel_size': 1.0, 'grid_dimensions': (3, 3)}
99 """
101 FALLBACK_VALUES = {
102 'pixel_size': DEFAULT_PIXEL_SIZE, # Default pixel size in micrometers
103 'grid_dimensions': None, # No grid dimensions by default
104 }
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]
113 @abstractmethod
114 def find_metadata_file(self, plate_path: Union[str, Path]) -> Path:
115 """
116 Find the metadata file for a plate.
118 Args:
119 plate_path: Path to the plate folder (str or Path)
121 Returns:
122 Path to the metadata file
124 Raises:
125 TypeError: If plate_path is not a valid path type
126 FileNotFoundError: If no metadata file is found
127 """
128 pass
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.
135 Args:
136 plate_path: Path to the plate folder (str or Path)
138 Returns:
139 Tuple of (grid_size_x, grid_size_y)
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
148 @abstractmethod
149 def get_pixel_size(self, plate_path: Union[str, Path]) -> float:
150 """
151 Get the pixel size from metadata.
153 Args:
154 plate_path: Path to the plate folder (str or Path)
156 Returns:
157 Pixel size in micrometers
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
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.
171 Args:
172 plate_path: Path to the plate folder (str or Path)
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
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.
185 Args:
186 plate_path: Path to the plate folder (str or Path)
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
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.
199 Args:
200 plate_path: Path to the plate folder (str or Path)
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
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.
213 Args:
214 plate_path: Path to the plate folder (str or Path)
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
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.
226 This method iterates through GroupBy components and calls the corresponding
227 abstract methods to collect all available metadata.
229 Args:
230 plate_path: Path to the plate folder (str or Path)
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
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 }
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