Source code for sequences.resolver
"""Sequence resolution and consistency validation.
Turns a list of base surfaces plus raw steps into a validated list of
:class:`~optiland.sequences.surface_view.SurfaceView` objects. Construction
fails loudly (``SequenceValidationError``) if adjacent steps are not
physically consistent, rather than tracing plausible nonsense.
Kramer Harrison, 2026
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from optiland.sequences.steps import SequenceStep, parse_steps
from optiland.sequences.surface_view import SurfaceView, resolve_view_materials
if TYPE_CHECKING:
from optiland.sequences.steps import RawStep
from optiland.surfaces.standard_surface import Surface
[docs]
class SequenceValidationError(ValueError):
"""Raised when a sequence's steps are not physically consistent."""
def _is_step_reflective(step: SequenceStep, base_surface: Surface) -> bool:
"""Whether a step reflects, considering both overrides and nominal mirrors."""
if step.interaction_override == "reflect":
return True
if step.interaction_override == "refract":
return False
return getattr(base_surface.interaction_model, "is_reflective", False)
def _effective_exit_material(step: SequenceStep, base_surfaces: list[Surface]):
"""The material a ray is actually in when it leaves this step.
Equal to ``material_post`` for refractive/nominal steps, and to
``material_pre`` for reflective steps, since a reflection never crosses
into the far medium.
"""
base_surface = base_surfaces[step.index]
pre, post = resolve_view_materials(
base_surface, step.reverse, step.interaction_override
)
is_reflective = _is_step_reflective(step, base_surface)
return pre if is_reflective else post
[docs]
def validate_sequence(steps: list[SequenceStep], base_surfaces: list[Surface]) -> None:
"""Validate that adjacent steps share a consistent medium at their join.
Args:
steps: The resolved sequence steps.
base_surfaces: The optic's base surfaces, indexed as in ``steps``.
Raises:
SequenceValidationError: If the exit medium of some step does not
equal the incident medium of the following step, naming the
offending step index.
"""
for i in range(len(steps) - 1):
current, following = steps[i], steps[i + 1]
exit_material = _effective_exit_material(current, base_surfaces)
incident_material, _ = resolve_view_materials(
base_surfaces[following.index],
following.reverse,
following.interaction_override,
)
if exit_material != incident_material:
raise SequenceValidationError(
f"Sequence step {i + 1} (surface {following.index}) expects "
f"incident medium {incident_material!r}, but step {i} "
f"(surface {current.index}) exits into {exit_material!r}."
)
[docs]
def resolve_sequence(
base_surfaces: list[Surface], raw_steps: list[RawStep]
) -> list[SurfaceView]:
"""Resolve a raw step list into a validated list of ``SurfaceView``.
Args:
base_surfaces: The optic's base surfaces, indexed by the step
indices used in ``raw_steps``.
raw_steps: The raw sequence, as bare surface indices and/or
``(index, interaction_override)`` pairs. See
:func:`optiland.sequences.steps.parse_steps`.
Returns:
The resolved, validated sequence of views, in traversal order.
Raises:
ValueError: If ``raw_steps`` is empty, malformed, or references an
out-of-range surface index.
SequenceValidationError: If adjacent steps are not physically
consistent.
"""
steps = parse_steps(raw_steps)
for step in steps:
if not (0 <= step.index < len(base_surfaces)):
raise ValueError(
f"Sequence step references surface index {step.index}, but "
f"the optic only has {len(base_surfaces)} surfaces."
)
# Refine reverse direction inference taking nominal mirrors into account
refined_steps: list[SequenceStep] = []
reverse = False
for step in steps:
base_surface = base_surfaces[step.index]
refined_steps.append(
SequenceStep(
index=step.index,
reverse=reverse,
interaction_override=step.interaction_override,
)
)
if _is_step_reflective(step, base_surface):
reverse = not reverse
steps = refined_steps
validate_sequence(steps, base_surfaces)
views: list[SurfaceView] = []
previous_view: SurfaceView | None = None
for step in steps:
view = SurfaceView(
base_surface=base_surfaces[step.index],
reverse=step.reverse,
interaction_override=step.interaction_override,
previous_view=previous_view,
)
views.append(view)
previous_view = view
return views