Source code for optiland.analysis.through_focus

"""Through-focus analysis base class

This module provides a base class for performing through-focus analysis.

Kramer Harrison, 2025
"""

from __future__ import annotations

from abc import ABC, abstractmethod

import optiland.backend as be


[docs] class ThroughFocusAnalysis(ABC): """Base class for through-focus analysis. This class provides the basic framework for analyses that need to be performed over a range of focal planes. It handles the iteration through different focal positions and delegates the specific analysis at each position to subclasses. Args: optic (optiland.optic.Optic): The optical system to analyze. delta_focus (float, optional): The increment of focal shift in mm. Defaults to 0.1. num_steps (int, optional): The number of focal planes to analyze before and after the nominal focus. Defaults to 5. fields (list[tuple[float, float]] | str, optional): Fields for analysis. If "all", uses all fields from `optic.fields`. Otherwise, expects a list of field coordinates. Defaults to "all". wavelengths (list[float] | str, optional): Wavelengths for analysis. If "all", uses all wavelengths from `optic.wavelengths`. Otherwise, expects a list of wavelength values. Defaults to "all". Attributes: results (list): A list to store the results from the analysis performed at each focal plane. The structure of each item in the list is determined by the `_perform_analysis_at_focus` method of the subclass. """ MAX_STEPS = 7 MIN_STEPS = 1 def __init__( self, optic, delta_focus: float = 0.1, num_steps: int = 5, fields="all", wavelengths: str | list[float] = "all", ): self.optic = optic self.delta_focus = delta_focus self._validate_num_steps(num_steps) self.num_steps = num_steps self.fields = self._resolve_fields(fields) self.wavelengths = self._resolve_wavelengths(wavelengths) self.positions = self._generate_focus_positions() self.nominal_focus = be.copy(self.optic.image_surface.geometry.cs.z) self.results = [] self._calculate_through_focus() def _validate_num_steps(self, num_steps): if not isinstance(num_steps, int) or num_steps < self.MIN_STEPS: raise ValueError("'num_steps' must be a positive integer.") if num_steps % 2 == 0: raise ValueError("'num_steps' must be an odd integer.") if num_steps > self.MAX_STEPS: raise ValueError( "'num_steps' must be less than or equal to 7 for performance reasons." ) def _resolve_fields(self, fields): from optiland.utils import resolve_fields as _resolve_fields return _resolve_fields(self.optic, fields) def _resolve_wavelengths(self, wavelengths): from optiland.utils import resolve_wavelengths as _resolve_wavelengths return _resolve_wavelengths(self.optic, wavelengths) def _generate_focus_positions(self): """Generates a list of focal positions based on the delta focus and number of steps.""" nominal_focus = self.optic.image_surface.geometry.cs.z positions = [ nominal_focus + (i - self.num_steps // 2) * self.delta_focus for i in range(self.num_steps) ] return positions def _defocus_image_plane(self, z_position): """Applies defocus to the image plane of the optical system. Args: z_position (float): The new z-coordinate for the image surface """ self.optic.image_surface.geometry.cs.z = z_position def _reset_focus(self): """Resets the image surface to its nominal focus position.""" self._defocus_image_plane(self.nominal_focus)
[docs] @abstractmethod def view(self): """Visualizes or prints the results of the through-focus analysis. This abstract method must be implemented by subclasses. It defines how the collected `self.results` (containing analysis data from all focal planes) should be presented to the user, for example, by plotting graphs or printing a formatted table. """ pass # pragma: no cover
@abstractmethod def _perform_analysis_at_focus(self): """Performs the specific analysis at the current focal position. This abstract method must be implemented by subclasses. It defines the analysis to be performed at each focal position after applying the defocus. The results of this analysis should be returned and will be stored in `self.results`. Returns: Any: The result of the analysis at the current focal position. """ pass # pragma: no cover def _calculate_through_focus(self): """Performs the through-focus analysis by iterating over the focal positions. This method iterates through each focal position, applies the defocus to the optical system, and performs the specific analysis defined in `_perform_analysis_at_focus`. The results are stored in `self.results`. """ for position in self.positions: self._defocus_image_plane(position) result = self._perform_analysis_at_focus() self.results.append(result) self._reset_focus()