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
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +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 super().__init__(AllComponents)
30 @classmethod
31 @abstractmethod
32 def can_parse(cls, filename: str) -> bool:
33 """
34 Check if this parser can parse the given filename.
36 Args:
37 filename (str): Filename to check
39 Returns:
40 bool: True if this parser can parse the filename, False otherwise
41 """
42 pass
44 @abstractmethod
45 def parse_filename(self, filename: str) -> Optional[Dict[str, Any]]:
46 """
47 Parse a microscopy image filename to extract all components.
49 Args:
50 filename (str): Filename to parse
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
58 @abstractmethod
59 def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]:
60 """
61 Extract coordinates from component identifier (typically well).
63 Args:
64 component_value (str): Component identifier (e.g., 'A01', 'R03C04', 'C04')
66 Returns:
67 Tuple[str, str]: (row, column) where row is like 'A', 'B' and column is like '01', '04'
69 Raises:
70 ValueError: If component format is invalid for this parser
71 """
72 pass
74 @abstractmethod
75 def construct_filename(self, extension: str = '.tif', **component_values) -> str:
76 """
77 Construct a filename from component values.
79 This method now uses **kwargs to accept any component values dynamically,
80 making it truly generic and adaptable to any component configuration.
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
88 Returns:
89 str: Constructed filename
91 Example:
92 construct_filename(well='A01', site=1, channel=2, z_index=1, extension='.tif')
93 """
94 pass
97class MetadataHandler(ABC):
98 """
99 Abstract base class for handling microscope metadata.
101 All metadata methods require str or Path objects for file paths.
103 Subclasses can define FALLBACK_VALUES for explicit fallbacks:
104 FALLBACK_VALUES = {'pixel_size': 1.0, 'grid_dimensions': (3, 3)}
105 """
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 }
112 def __init__(self):
113 """Initialize metadata handler with VariableComponents enum."""
114 self.component_enum = VariableComponents
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]
123 @abstractmethod
124 def find_metadata_file(self, plate_path: Union[str, Path]) -> Path:
125 """
126 Find the metadata file for a plate.
128 Args:
129 plate_path: Path to the plate folder (str or Path)
131 Returns:
132 Path to the metadata file
134 Raises:
135 TypeError: If plate_path is not a valid path type
136 FileNotFoundError: If no metadata file is found
137 """
138 pass
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.
145 Args:
146 plate_path: Path to the plate folder (str or Path)
148 Returns:
149 Tuple of (grid_size_x, grid_size_y)
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
158 @abstractmethod
159 def get_pixel_size(self, plate_path: Union[str, Path]) -> float:
160 """
161 Get the pixel size from metadata.
163 Args:
164 plate_path: Path to the plate folder (str or Path)
166 Returns:
167 Pixel size in micrometers
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
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.
181 Args:
182 plate_path: Path to the plate folder (str or Path)
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
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.
195 Args:
196 plate_path: Path to the plate folder (str or Path)
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
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.
209 Args:
210 plate_path: Path to the plate folder (str or Path)
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
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.
223 Args:
224 plate_path: Path to the plate folder (str or Path)
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
232 def get_image_files(self, plate_path: Union[str, Path]) -> list[str]:
233 """
234 Get list of image files from OpenHCS metadata.
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.
239 Subclasses can override if they need different behavior (e.g., OpenHCS reads directly from metadata).
241 Args:
242 plate_path: Path to the plate folder (str or Path)
244 Returns:
245 List of image filenames with subdirectory prefix (e.g., "Images/file.tif" or "file.tif")
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
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__}")
259 # Ensure the path exists
260 if not plate_path.exists():
261 raise FileNotFoundError(f"Plate path does not exist: {plate_path}")
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__)
268 openhcs_handler = OpenHCSMetadataHandler(self.filemanager)
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")
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()))
280 logger.info(f"get_image_files: Using main subdirectory '{main_subdir_key}'")
281 subdir_data = subdirs[main_subdir_key]
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())
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
293 except Exception:
294 # Fallback: no metadata yet, return empty list
295 return []
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.
301 This method iterates through VariableComponents and calls the corresponding
302 abstract methods to collect all available metadata.
304 Args:
305 plate_path: Path to the plate folder (str or Path)
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