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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +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
11from openhcs.constants.constants import VariableComponents, AllComponents
12from openhcs.core.components.parser_metaprogramming import GenericFilenameParser
14from openhcs.constants.constants import DEFAULT_PIXEL_SIZE
17class FilenameParser(GenericFilenameParser):
18 """
19 Abstract base class for parsing microscopy image filenames.
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 """
26 def __init__(self):
27 """Initialize the parser with AllComponents enum."""
28 from openhcs.constants.constants import AllComponents
29 super().__init__(AllComponents)
31 @classmethod
32 @abstractmethod
33 def can_parse(cls, filename: str) -> bool:
34 """
35 Check if this parser can parse the given filename.
37 Args:
38 filename (str): Filename to check
40 Returns:
41 bool: True if this parser can parse the filename, False otherwise
42 """
43 pass
45 @abstractmethod
46 def parse_filename(self, filename: str) -> Optional[Dict[str, Any]]:
47 """
48 Parse a microscopy image filename to extract all components.
50 Args:
51 filename (str): Filename to parse
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
59 @abstractmethod
60 def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]:
61 """
62 Extract coordinates from component identifier (typically well).
64 Args:
65 component_value (str): Component identifier (e.g., 'A01', 'R03C04', 'C04')
67 Returns:
68 Tuple[str, str]: (row, column) where row is like 'A', 'B' and column is like '01', '04'
70 Raises:
71 ValueError: If component format is invalid for this parser
72 """
73 pass
75 @abstractmethod
76 def construct_filename(self, extension: str = '.tif', **component_values) -> str:
77 """
78 Construct a filename from component values.
80 This method now uses **kwargs to accept any component values dynamically,
81 making it truly generic and adaptable to any component configuration.
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
89 Returns:
90 str: Constructed filename
92 Example:
93 construct_filename(well='A01', site=1, channel=2, z_index=1, extension='.tif')
94 """
95 pass
98class MetadataHandler(ABC):
99 """
100 Abstract base class for handling microscope metadata.
102 All metadata methods require str or Path objects for file paths.
104 Subclasses can define FALLBACK_VALUES for explicit fallbacks:
105 FALLBACK_VALUES = {'pixel_size': 1.0, 'grid_dimensions': (3, 3)}
106 """
108 FALLBACK_VALUES = {
109 'pixel_size': DEFAULT_PIXEL_SIZE, # Default pixel size in micrometers
110 'grid_dimensions': None, # No grid dimensions by default
111 }
113 def __init__(self):
114 """Initialize metadata handler with VariableComponents enum."""
115 self.component_enum = VariableComponents
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]
124 @abstractmethod
125 def find_metadata_file(self, plate_path: Union[str, Path]) -> Path:
126 """
127 Find the metadata file for a plate.
129 Args:
130 plate_path: Path to the plate folder (str or Path)
132 Returns:
133 Path to the metadata file
135 Raises:
136 TypeError: If plate_path is not a valid path type
137 FileNotFoundError: If no metadata file is found
138 """
139 pass
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.
146 Args:
147 plate_path: Path to the plate folder (str or Path)
149 Returns:
150 Tuple of (grid_size_x, grid_size_y)
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
159 @abstractmethod
160 def get_pixel_size(self, plate_path: Union[str, Path]) -> float:
161 """
162 Get the pixel size from metadata.
164 Args:
165 plate_path: Path to the plate folder (str or Path)
167 Returns:
168 Pixel size in micrometers
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
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.
182 Args:
183 plate_path: Path to the plate folder (str or Path)
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
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.
196 Args:
197 plate_path: Path to the plate folder (str or Path)
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
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.
210 Args:
211 plate_path: Path to the plate folder (str or Path)
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
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.
224 Args:
225 plate_path: Path to the plate folder (str or Path)
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
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.
237 This method iterates through VariableComponents and calls the corresponding
238 abstract methods to collect all available metadata.
240 Args:
241 plate_path: Path to the plate folder (str or Path)
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