Source code for optiland.rays.ray_aiming.initialization

"""Ray Aiming Initialization Module

This module implements the initialization logic for determining the physical
aperture stop size (Stop Radius) before the main Ray Aiming iteration begins.
It uses a Strategy Pattern to handle different ways of calculating this radius.

Kramer Harrison, 2025
"""

from __future__ import annotations

import abc
import warnings
from typing import TYPE_CHECKING

import optiland.backend as be
from optiland.aperture import FloatByStopAperture
from optiland.rays import RealRays
from optiland.rays.ray_aiming.pupil_map import to_float

if TYPE_CHECKING:
    from optiland.optic import Optic


[docs] class StopSizeStrategy(abc.ABC): """Abstract base class for stop size determination strategies.""" def __init__(self, optic: Optic): self.optic = optic
[docs] @abc.abstractmethod def calculate_stop_radius(self) -> float: """Calculate the radius of the stop surface.""" pass
[docs] class FloatByStopStrategy(StopSizeStrategy): """Strategy for 'Float By Stop Size' aperture type. Simply returns the user-defined semi-diameter of the Stop Surface. """
[docs] def calculate_stop_radius(self) -> float: stop_index = self.optic.surfaces.stop_index surface = self.optic.surfaces[stop_index] # Check for explicit aperture object first if surface.aperture and hasattr(surface.aperture, "r_max"): return surface.aperture.r_max elif surface.aperture and hasattr(surface.aperture, "x_max"): return (surface.aperture.x_max + surface.aperture.y_max) / 2.0 # Fallback to semi_aperture attribute return float(surface.semi_aperture)
[docs] class ParaxialReferenceStrategy(StopSizeStrategy): """Strategy using paraxial ray trace to determine stop radius. Traces a Paraxial Marginal Ray from the center of the object to the Stop Surface. """
[docs] def calculate_stop_radius(self) -> float: stop_index = self.optic.surfaces.stop_index para = self.optic.paraxial # Determine marginal ray height at the stop surface y_marginal, _ = para.marginal_ray() return float(be.abs(y_marginal[stop_index].item()))
[docs] class RealReferenceStrategy(StopSizeStrategy): """Strategy using real ray trace to determine stop radius. Traces a Real Ray from the center of the object toward the edge of the Entrance Pupil. Fallbacks to ParaxialReferenceStrategy on failure. """
[docs] def calculate_stop_radius(self) -> float: try: return self._trace_real_marginal_ray() except Exception as e: warnings.warn( f"RealReferenceStrategy failed: {e}. " "Falling back to ParaxialReferenceStrategy.", stacklevel=2, ) fallback = ParaxialReferenceStrategy(self.optic) return fallback.calculate_stop_radius()
def _trace_real_marginal_ray(self) -> float: wavelength = self.optic.primary_wavelength EPL = float(self.optic.paraxial.entrance_pupil_axial_position()) EPD = float(self.optic.paraxial.EPD()) stop_index = self.optic.surfaces.stop_index obj_surf = self.optic.object_surface is_inf = bool(obj_surf and obj_surf.is_infinite) frame = self._entry_frame_floats() # The entrance pupil is the image of the stop, so a decentered stop has # a decentered entrance pupil. Offsetting by EPD/2 from the axis would # then launch a ray that never goes near the pupil, and the height it # lands at is a distance from the stop center that has nothing to do # with the stop radius (issue #654). Locate the pupil center first and # offset from there. cx, cy, cz, cL, cM, cN = self._solve_pupil_center(stop_index, is_inf, frame) if frame is None: if is_inf: # Object-space rays run parallel to the axis, so the launch # height is the entrance-pupil coordinate and can be offset # directly. origin = (cx, cy + EPD / 2.0, cz) direction = (cL, cM, cN) else: # Offset the chief ray's pupil crossing, then re-aim at it # from the same object point. t = (EPL - cz) / cN target_x = cx + cL * t target_y = cy + cM * t + EPD / 2.0 dx = target_x - cx dy = target_y - cy dz = EPL - cz mag = float(be.sqrt(be.array(dx**2 + dy**2 + dz**2))) origin = (cx, cy, cz) direction = (dx / mag, dy / mag, dz / mag) else: # Frame-aware branch: the marginal probe is displaced from the # pupil-center reference along the entry frame's meridional # transverse axis v -- the generalization of the historical +y # offset -- so no global +z assumption remains. _anchor, axial, d0, _u0, v0 = frame if is_inf: origin = ( cx + EPD / 2.0 * v0[0], cy + EPD / 2.0 * v0[1], cz + EPD / 2.0 * v0[2], ) direction = (cL, cM, cN) else: # Push the chief launch to the pupil plane (the plane through # the apparent pupil point, perpendicular to the entry # direction), offset there by EPD/2 along v, and re-aim. r_ep = tuple(_anchor[i] + (EPL - axial) * d0[i] for i in range(3)) c_pos = (cx, cy, cz) c_dir = (cL, cM, cN) denom = sum(c_dir[i] * d0[i] for i in range(3)) t = sum((r_ep[i] - c_pos[i]) * d0[i] for i in range(3)) / denom target = tuple( c_pos[i] + t * c_dir[i] + EPD / 2.0 * v0[i] for i in range(3) ) delta = tuple(target[i] - c_pos[i] for i in range(3)) mag = float(be.sqrt(be.array(sum(d**2 for d in delta)))) origin = c_pos direction = tuple(d / mag for d in delta) rays = RealRays( x=be.array([origin[0]]), y=be.array([origin[1]]), z=be.array([origin[2]]), L=be.array([direction[0]]), M=be.array([direction[1]]), N=be.array([direction[2]]), wavelength=be.array([wavelength]), intensity=be.array([1.0]), ) # Trace from the first surface up to the stop surface for i in range(1, stop_index + 1): self.optic.surfaces[i].trace(rays) if be.any(be.isnan(rays.x)): raise ValueError("Ray trace resulted in NaNs (TIR or missed surface).") # Localize rays to the stop surface's local frame so the radial height # is measured from the stop center, not the global origin. The pupil # center solve puts the chief ray on that origin, so the marginal ray's # local radius is its separation from the chief ray. stop_cs = self.optic.surfaces[stop_index].geometry.cs local_rays = RealRays( be.copy(rays.x), be.copy(rays.y), be.copy(rays.z), be.copy(rays.L), be.copy(rays.M), be.copy(rays.N), intensity=be.copy(rays.i), wavelength=rays.w, ) stop_cs.localize(local_rays) # Return intersection radial height at Stop in local coords return float(be.sqrt(local_rays.x[0] ** 2 + local_rays.y[0] ** 2)) def _entry_frame_floats(self) -> tuple | None: """The optic's entry frame with components as plain floats. ``None`` on the canonical global-z path, keeping the historical code branch (and its exact arithmetic) for legacy systems. """ frame = self.optic.surfaces._entry_frame() if frame is None: return None anchor, axial, d0, u0, v0 = frame as_floats = tuple( tuple(to_float(component) for component in vector) for vector in (anchor, d0, u0, v0) ) return as_floats[0], to_float(axial), as_floats[1], as_floats[2], as_floats[3] def _solve_pupil_center( self, stop_index: int, is_inf: bool, frame: tuple | None = None ) -> tuple: """Solve the launch state of the axial ray landing on the stop center. For a centered system this is the on-axis launch and the solve is a no-op, but for a decentered or tilted stop it is what locates the entrance pupil, which is the reference the marginal probe is offset from. Args: stop_index: Index of the stop surface. is_inf: Whether the object is at infinity. frame: Entry frame as plain floats (see :meth:`_entry_frame_floats`), or ``None`` on the canonical global-z path. Returns: tuple: ``(x, y, z, L, M, N)`` launch state of the axial ray that lands on the stop surface's local origin. Raises: ValueError: If the solve does not converge. """ # Imported here: iterative.py pulls this module in at call time, so a # module-level import would be circular. from optiland.rays.ray_aiming.iterative import IterativeRayAimer wavelength = self.optic.primary_wavelength EPL = float(self.optic.paraxial.entrance_pupil_axial_position()) if frame is None: # First-order seed: the pupil is the image of the stop, so the # stop's decenter maps to the pupil scaled by the pupil # magnification. Exact for a centered system ahead of the stop; # elsewhere the Newton polish below carries it the rest of the # way. m = self._pupil_magnification() stop_cs = self.optic.surfaces[stop_index].geometry.cs gx, gy, _gz = stop_cs.position_in_gcs ex = to_float(gx) * m ey = to_float(gy) * m if is_inf: # Launch well ahead of surface 1 for robust intersection. z_start = to_float(self.optic.surfaces[1].geometry.cs.z) - 100.0 x0, y0, z0 = be.array([ex]), be.array([ey]), be.array([z_start]) L0, M0, N0 = be.array([0.0]), be.array([0.0]), be.array([1.0]) else: obj_z = to_float(self.optic.object_surface.geometry.cs.z) dz = EPL - obj_z mag = float(be.sqrt(be.array(ex**2 + ey**2 + dz**2))) x0, y0, z0 = be.array([0.0]), be.array([0.0]), be.array([obj_z]) L0 = be.array([ex / mag]) M0 = be.array([ey / mag]) N0 = be.array([dz / mag]) else: # Frame-aware seeds: probe origins sit on the entry line, a # chosen axial distance before the first surface along -d0, and # launch along d0 (infinite) or from the object vertex toward # the apparent entrance-pupil point (finite). The supported # folded domain is piecewise-centered, so no decenter seed is # needed; the Newton polish absorbs any residual. anchor, axial, d0, _u0, _v0 = frame r_ep = tuple(anchor[i] + (EPL - axial) * d0[i] for i in range(3)) if is_inf: origin = tuple(r_ep[i] - 100.0 * d0[i] for i in range(3)) x0 = be.array([origin[0]]) y0 = be.array([origin[1]]) z0 = be.array([origin[2]]) L0 = be.array([d0[0]]) M0 = be.array([d0[1]]) N0 = be.array([d0[2]]) else: obj_cs = self.optic.object_surface.geometry.cs obj_pos = tuple(to_float(c) for c in obj_cs.position_in_gcs) delta = tuple(r_ep[i] - obj_pos[i] for i in range(3)) mag = float(be.sqrt(be.array(sum(d**2 for d in delta)))) x0 = be.array([obj_pos[0]]) y0 = be.array([obj_pos[1]]) z0 = be.array([obj_pos[2]]) L0 = be.array([delta[0] / mag]) M0 = be.array([delta[1] / mag]) N0 = be.array([delta[2] / mag]) zero = be.array([0.0]) aimer = IterativeRayAimer(self.optic) x, y, z, L, M, N, converged, _, _report = aimer._solve_core( x0, y0, z0, L0, M0, N0, be.array([wavelength]), stop_index, is_inf, zero, zero, ) if not bool(be.all(converged)): raise ValueError("Could not solve for the entrance pupil center.") return ( to_float(x), to_float(y), to_float(z), to_float(L), to_float(M), to_float(N), ) def _pupil_magnification(self) -> float: """Signed paraxial magnification from the stop plane to the pupil plane. Returns: float: ``(EPD / 2) / y_stop``, where ``y_stop`` is the paraxial marginal ray height at the stop. Raises: ValueError: If the paraxial marginal ray height at the stop is zero. """ para = self.optic.paraxial y_marginal, _ = para.marginal_ray() y_stop = to_float(y_marginal[self.optic.surfaces.stop_index]) if abs(y_stop) < 1e-12: raise ValueError("Paraxial marginal ray height at the stop is zero.") return float(para.EPD()) / 2.0 / y_stop
[docs] def get_stop_radius_strategy(optic: Optic, aiming_mode: str) -> StopSizeStrategy: """Factory function to select the appropriate stop size strategy. Args: optic: The optical system. aiming_mode: The ray aiming mode ('paraxial', 'iterative', 'robust'). Returns: The instantiated strategy instance. """ if optic.aperture and isinstance(optic.aperture, FloatByStopAperture): return FloatByStopStrategy(optic) if aiming_mode in ["iterative", "robust"]: return RealReferenceStrategy(optic) return ParaxialReferenceStrategy(optic)