Source code for sequences.steps

"""Sequence step parsing.

A sequence is a list of *raw steps*, each either a bare surface index
(forward traversal, nominal interaction) or a ``(index, interaction_override)``
pair, e.g.::

    steps = [0, 1, 2, (3, "reflect"), (2, "reflect"), 3, 4]

The propagation direction (forward/reverse) is not specified explicitly by
the user. It starts forward and flips every time a step reflects, since a
reflection is what reverses the physical direction of travel. This module
turns the raw, ergonomic step list into fully-resolved :class:`SequenceStep`
objects that carry that inferred direction explicitly.

Kramer Harrison, 2026
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

VALID_OVERRIDES = frozenset({"reflect", "refract"})

RawStep = int | tuple[int, str] | list[Any]


[docs] @dataclass(frozen=True) class SequenceStep: """A single, fully-resolved step in a surface sequence. Args: index: Index of the base surface in the optic's nominal surface list. reverse: Whether this step is traversed in the reverse physical direction (light travelling from the base surface's nominal "post" side toward its "pre" side). interaction_override: ``"reflect"`` or ``"refract"`` to force the interaction type at this step, or ``None`` to use the base surface's nominal interaction. """ index: int reverse: bool = False interaction_override: str | None = None
def _parse_raw_step(raw: RawStep) -> tuple[int, str | None]: """Split a raw step into ``(index, interaction_override)``.""" if isinstance(raw, tuple | list): if len(raw) != 2: raise ValueError( f"Step pair must be (index, interaction_override), got {raw!r}." ) index, override = raw if not isinstance(index, int): raise ValueError( f"Surface index must be an int, got {index!r} of type " f"{type(index).__name__}." ) if not isinstance(override, str) or override not in VALID_OVERRIDES: raise ValueError( f"Unknown interaction override {override!r} at step for surface " f"{index}; expected one of {sorted(VALID_OVERRIDES)}." ) return index, override if not isinstance(raw, int): raise ValueError( f"Invalid raw step {raw!r}. Expected an int surface index or a " "(index, interaction_override) pair." ) return raw, None
[docs] def parse_steps(raw_steps: list[RawStep]) -> list[SequenceStep]: """Parse a raw step list into resolved :class:`SequenceStep` objects. Direction is inferred: it starts forward (``reverse=False``) and flips after every step whose resolved interaction is a reflection, since that is the point at which the physical direction of propagation reverses. Args: raw_steps: The raw sequence, as bare surface indices and/or ``(index, interaction_override)`` pairs. Returns: The resolved sequence steps, in order. Raises: ValueError: If ``raw_steps`` is empty or contains an invalid step. """ if not raw_steps: raise ValueError("A sequence must contain at least one step.") steps = [] reverse = False for raw in raw_steps: index, override = _parse_raw_step(raw) steps.append( SequenceStep(index=index, reverse=reverse, interaction_override=override) ) if override == "reflect": reverse = not reverse return steps