Source code for optiland.aperture.base

"""Base System Aperture Module

Defines the abstract base class for all system aperture types.

Kramer Harrison, 2026
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from optiland.paraxial import Paraxial


[docs] class BaseSystemAperture(ABC): """Abstract base class for system aperture type definitions. Each concrete subclass encapsulates the logic for computing the entrance pupil diameter (EPD) for one aperture specification style (e.g. EPD, image-space F-number, object-space NA, or float-by-stop-size). Subclasses must declare a class-level ``ap_type`` string (e.g. ``"EPD"``) that matches the legacy type key used in serialized lens files. This string is used both for serialization round-trips and for the ``__init_subclass__`` auto-registration that powers :meth:`from_dict`. Attributes: _registry (dict[str, type[BaseSystemAperture]]): Class-level registry mapping ``ap_type`` strings to concrete subclass types. Populated automatically via ``__init_subclass__``. """ _registry: dict[str, type[BaseSystemAperture]] = {} def __init_subclass__(cls, **kwargs: object) -> None: super().__init_subclass__(**kwargs) # Register subclasses that declare a _ap_type_key class variable. # This is separate from the ap_type property to avoid descriptor conflicts. key = cls.__dict__.get("_ap_type_key") if isinstance(key, str): BaseSystemAperture._registry[key] = cls # ── Identity ─────────────────────────────────────────────────────────── @property @abstractmethod def ap_type(self) -> str: """String identifier matching the legacy type key (e.g. ``'EPD'``).""" @property @abstractmethod def value(self) -> float: """Raw aperture value as supplied by the user.""" # ── Capability flags ─────────────────────────────────────────────────── @property @abstractmethod def supports_telecentric(self) -> bool: """True if this aperture type is compatible with telecentric object space.""" @property @abstractmethod def is_scalable(self) -> bool: """True if the stored value should be scaled during ``optic.scale()``.""" # ── Core computation ───────────────────────────────────────────────────
[docs] @abstractmethod def compute_epd(self, paraxial: Paraxial, wavelength: float | None = None) -> float: """Return the entrance pupil diameter using paraxial context. Args: paraxial: The paraxial engine for the current optical system. wavelength: Primary wavelength in micrometers. When ``None``, implementations may fall back to ``paraxial.optic.primary_wavelength``. Returns: Entrance pupil diameter in lens units. """
[docs] def direct_fno(self) -> float | None: """Return the F-number directly if this type stores it, else ``None``. Overridden only by :class:`~optiland.aperture.image_fno.ImageFNOAperture` to avoid a redundant EPD computation in :meth:`~optiland.paraxial.Paraxial.FNO`. Returns: F-number value, or ``None`` if this type does not store one directly. """ return None
# ── System scaling ─────────────────────────────────────────────────────
[docs] @abstractmethod def scale(self, factor: float) -> BaseSystemAperture: """Return a new aperture with the value scaled by *factor*. Non-scalable types (where :attr:`is_scalable` is ``False``) return ``self`` unchanged since they are immutable and scaling has no effect. Args: factor: Multiplicative scale factor (e.g. 0.001 to convert mm to m). Returns: A new :class:`BaseSystemAperture` instance, or ``self`` for non-scalable types. """
# ── Serialization ──────────────────────────────────────────────────────
[docs] @abstractmethod def to_dict(self) -> dict: """Serialize to a JSON-compatible dictionary. Returns: A dict with at least ``"type"`` and ``"value"`` keys. """
[docs] @classmethod def from_dict(cls, data: dict | None) -> BaseSystemAperture | None: """Deserialize from a dictionary, dispatching by the ``"type"`` field. Args: data: Dictionary produced by :meth:`to_dict`. Passing ``None`` returns ``None`` (mirrors legacy ``Aperture.from_dict`` behaviour). Returns: A concrete :class:`BaseSystemAperture` instance, or ``None`` if *data* is ``None``. Raises: ValueError: If required keys are missing or the type is not registered. """ if data is None: return None required_keys = {"type", "value"} if not required_keys.issubset(data): missing = required_keys - data.keys() raise ValueError(f"Missing required keys in aperture data: {missing}") type_str = data["type"] if type_str not in cls._registry: raise ValueError( f"Unknown aperture type '{type_str}'. Available: {list(cls._registry)}" ) return cls._registry[type_str]._from_dict(data)
@classmethod @abstractmethod def _from_dict(cls, data: dict) -> BaseSystemAperture: """Construct an instance from a raw dict (called by :meth:`from_dict`). Args: data: The validated raw dictionary. Returns: A concrete instance of this subclass. """