Source code for optiland.nonsequential.components.doublet

"""Doublet compound component for Non-Sequential Raytracing.

A cemented achromatic doublet: front face, cemented interface, back face,
edge (cylindrical frustum).

Kramer Harrison, 2026
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from optiland.nonsequential.components.compound import CompoundComponent
from optiland.nonsequential.components.configs import DoubletConfig, InteractionType
from optiland.nonsequential.components.geometry.analytic.conic import ConicGeometry
from optiland.nonsequential.components.geometry.analytic.frustum import (
    CylindricalFrustumGeometry,
)
from optiland.nonsequential.components.lens import (
    _make_surface,
    _offset_cs,
    _resolve_material,
    _sag_at_rim,
)
from optiland.nonsequential.components.volume import Volume
from optiland.nonsequential.materials.nsq_material import VACUUM

if TYPE_CHECKING:
    from optiland.coordinate_system import CoordinateSystem
    from optiland.nonsequential.components.base import BaseComponent


[docs] class Doublet(CompoundComponent): """Cemented achromatic doublet. Assembles five physical surfaces: 1. **Front face** -- refractive, conic (crown element). 2. **Cemented interface** -- refractive, conic (crown->flint). 3. **Back face** -- refractive, conic (flint element). 4. **Crown edge** -- cylindrical frustum, absorbing. 5. **Flint edge** -- cylindrical frustum, absorbing. Unlike ``Lens``, a doublet is two closed solids (the crown element and the flint element) sharing the cemented interface as a common boundary surface -- so the edge is split into a crown segment and a flint segment at the cemented interface's axial position, and each element is validated as its own :class:`Volume`. Attributes: _name: Registry name. _cs: Front-vertex coordinate system. _config: DoubletConfig. _surfaces: Built list of sub-surfaces. _volumes: The two validated ``Volume`` instances (crown, flint). """ def __init__( self, name: str, cs: CoordinateSystem, config: DoubletConfig, ) -> None: """Initialize Doublet from a DoubletConfig. Args: name: Unique identifier in the registry. cs: Front-vertex coordinate system. config: Doublet geometry and material configuration. Raises: NonWatertightVolumeError: If either element's surfaces do not form a closed, consistently outward-oriented solid. """ self._name = name self._cs = cs self._config = config mat1 = _resolve_material(config.material1) mat2 = _resolve_material(config.material2) self._surfaces: list[BaseComponent] = self._build() front, cemented, back, edge_crown, edge_flint = self._surfaces self._volumes = [ Volume( name=f"{name}.crown", boundary=[front, cemented, edge_crown], interior=mat1, ), Volume( name=f"{name}.flint", boundary=[cemented, back, edge_flint], interior=mat2, ), ] @property def name(self) -> str: """Registry name.""" return self._name @property def surfaces(self) -> list[BaseComponent]: """Ordered flat list of sub-surfaces.""" return self._surfaces @property def coordinate_system(self) -> CoordinateSystem: """Front-vertex coordinate system.""" return self._cs def _build(self) -> list[BaseComponent]: """Construct all four sub-surfaces from the config. Returns: Ordered list of BaseComponent sub-surfaces. """ cfg = self._config mat1 = _resolve_material(cfg.material1) mat2 = _resolve_material(cfg.material2) r = cfg.aperture_radius cs_front = self._cs cs_cemented = _offset_cs(cs_front, cfg.thickness1) cs_back = _offset_cs(cs_cemented, cfg.thickness2) surfaces: list[BaseComponent] = [] # 1. Front face (vacuum -> crown) surfaces.append( _make_surface( cs_front, ConicGeometry(cfg.r1, cfg.conic1, r), VACUUM, mat1, cfg.front, InteractionType.REFRACTIVE, f"{self._name}.front", ) ) # 2. Cemented interface (crown -> flint) surfaces.append( _make_surface( cs_cemented, ConicGeometry(cfg.r2, cfg.conic2, r), mat1, mat2, cfg.cemented, InteractionType.REFRACTIVE, f"{self._name}.cemented", ) ) # 3. Back face (flint -> vacuum) surfaces.append( _make_surface( cs_back, ConicGeometry(cfg.r3, cfg.conic3, r), mat2, VACUUM, cfg.back, InteractionType.REFRACTIVE, f"{self._name}.back", ) ) # 4. Crown edge (cylindrical frustum, absorbing) -- front face to # the cemented interface, so the crown element's own boundary # closes without the flint element. sag_front = _sag_at_rim(cfg.r1, cfg.conic1, r) sag_cemented = _sag_at_rim(cfg.r2, cfg.conic2, r) sag_back = _sag_at_rim(cfg.r3, cfg.conic3, r) crown_edge_geom = CylindricalFrustumGeometry( r_front=r, r_back=r, z_front=sag_front, z_back=cfg.thickness1 + sag_cemented, ) surfaces.append( _make_surface( cs_front, crown_edge_geom, VACUUM, VACUUM, cfg.edge, InteractionType.ABSORBING, f"{self._name}.edge_crown", ) ) # 5. Flint edge -- cemented interface to back face. total_thickness = cfg.thickness1 + cfg.thickness2 flint_edge_geom = CylindricalFrustumGeometry( r_front=r, r_back=r, z_front=cfg.thickness1 + sag_cemented, z_back=total_thickness + sag_back, ) surfaces.append( _make_surface( cs_front, flint_edge_geom, VACUUM, VACUUM, cfg.edge, InteractionType.ABSORBING, f"{self._name}.edge_flint", ) ) return surfaces