Source code for sequences.sequenced_surface_group

"""SequencedSurfaceGroup: the ``SurfaceGroup`` interface over a sequence of views.

Presents a list of :class:`~optiland.sequences.surface_view.SurfaceView`
objects — resolved from a base surface list and a raw step list — through
the read/trace subset of the :class:`~optiland.surfaces.surface_group.SurfaceGroup`
interface that analyses and the tracing pipeline rely on. A sequence is
static once resolved: unlike ``SurfaceGroup``, there is no ``add``/``remove``;
to change a sequence's traversal, resolve a new one.

Kramer Harrison, 2026
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import optiland.backend as be
from optiland.coatings import BaseCoatingPolarized
from optiland.sequences.resolver import resolve_sequence

if TYPE_CHECKING:
    from optiland.sequences.steps import RawStep
    from optiland.sequences.surface_view import SurfaceView
    from optiland.surfaces.standard_surface import Surface


[docs] class SequencedSurfaceGroup: """A traversal-ordered group of ``SurfaceView`` over shared base surfaces. Args: base_surfaces: The optic's base surfaces, indexed as in ``raw_steps``. raw_steps: The raw sequence definition. See :func:`optiland.sequences.steps.parse_steps`. Raises: ValueError: If ``raw_steps`` is empty, malformed, or references an out-of-range surface index. SequenceValidationError: If adjacent steps are not physically consistent. """ def __init__(self, base_surfaces: list[Surface], raw_steps: list[RawStep]): self.base_surfaces = base_surfaces self.raw_steps = raw_steps self._views: list[SurfaceView] = resolve_sequence(base_surfaces, raw_steps) @property def surfaces(self) -> tuple[SurfaceView, ...]: return tuple(self._views) def __getitem__(self, index): return self._views[index] def __iter__(self): return iter(self._views) def __len__(self): return len(self._views) @property def num_surfaces(self) -> int: """int: the number of steps in the sequence.""" return len(self._views) @property def stop_index(self) -> int: """int: the index (within this sequence) of the aperture stop step.""" for index, view in enumerate(self._views): if view.is_stop: return index raise ValueError("No stop surface found in this sequence.") @property def x(self): """np.array: x intersection points at each step.""" return be.stack([v.x for v in self._views if be.size(v.x) > 0]) @property def y(self): """np.array: y intersection points at each step.""" return be.stack([v.y for v in self._views if be.size(v.y) > 0]) @property def z(self): """np.array: z intersection points at each step.""" return be.stack([v.z for v in self._views if be.size(v.z) > 0]) @property def L(self): """np.array: x direction cosines at each step.""" return be.stack([v.L for v in self._views if be.size(v.L) > 0]) @property def M(self): """np.array: y direction cosines at each step.""" return be.stack([v.M for v in self._views if be.size(v.M) > 0]) @property def N(self): """np.array: z direction cosines at each step.""" return be.stack([v.N for v in self._views if be.size(v.N) > 0]) @property def opd(self): """np.array: optical path difference recorded at each step.""" return be.stack([v.opd for v in self._views if be.size(v.opd) > 0]) @property def u(self): """np.array: paraxial ray angles at each step.""" return be.stack([v.u for v in self._views if be.size(v.u) > 0]) @property def intensity(self): """np.array: ray intensities at each step.""" return be.stack([v.intensity for v in self._views if be.size(v.intensity) > 0]) @property def positions(self): """np.array: z positions of each step's surface vertex, in traversal order.""" positions = be.array([v.geometry.cs.position_in_gcs[2] for v in self._views]) return positions.reshape(-1, 1) @property def radii(self): """np.array: radii of curvature at each step.""" return be.array([v.geometry.radius for v in self._views]) @property def uses_polarization(self) -> bool: """bool: True if any step's interaction uses polarization.""" return any( isinstance(v.interaction_model.coating, BaseCoatingPolarized) for v in self._views )
[docs] def n(self, wavelength): """Get the exit-medium refractive index at each step. Args: wavelength (float or str): The wavelength for which to calculate the refractive indices. Returns: numpy.ndarray: The refractive indices at each step. """ n = [be.atleast_1d(v.material_post.n(wavelength)) for v in self._views] return be.ravel(be.array(n))
[docs] def get_thickness(self, step_number: int): """Calculate the (signed) axial distance between two consecutive steps. Args: step_number (int): The index (within this sequence) of the first step. Returns: float: The distance between the two steps' surface vertices. Negative if the sequence traverses backward at this point. """ t = self.positions return t[step_number + 1] - t[step_number]
[docs] def trace(self, rays, skip: int = 0): """Trace the given rays through the sequence. Args: rays (BaseRays): The rays to be traced. skip (int, optional): Number of steps to skip before tracing. Defaults to 0. Returns: BaseRays: The traced rays. """ self.reset() for view in self._views[skip:]: view.trace(rays) return rays
[docs] def reset(self) -> None: """Resets the recorded information owned by every view in the sequence.""" for view in self._views: view.reset()