Source code for interactions.base

"""Base Interaction Model

Defines the abstract base class for ray-surface interaction models.

Kramer Harrison, 2025
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # pragma: no cover
    from optiland.coatings import BaseCoating
    from optiland.rays import ParaxialRays, RealRays
    from optiland.scatter import BaseBSDF
    from optiland.surfaces import Surface


[docs] class BaseInteractionModel(ABC): """Abstract base class for ray-surface interaction models.""" _registry = {} def __init__( self, parent_surface: Surface | None, is_reflective: bool, coating: BaseCoating | None = None, bsdf: BaseBSDF | None = None, ): self.parent_surface = parent_surface self.is_reflective = is_reflective self.coating = coating self.bsdf = bsdf @property def material_pre(self): return ( self.parent_surface.material_post if self.parent_surface.previous_surface is None else self.parent_surface.previous_surface.material_post ) @property def material_post(self): return self.parent_surface.material_post @property def geometry(self): return self.parent_surface.geometry def __init_subclass__(cls, **kwargs): """Automatically register subclasses.""" super().__init_subclass__(**kwargs) BaseInteractionModel._registry[cls.__name__] = cls
[docs] @abstractmethod def interact_real_rays(self, rays: RealRays) -> RealRays: """Interact with real rays.""" pass # pragma: no cover
[docs] @abstractmethod def interact_paraxial_rays(self, rays: ParaxialRays) -> ParaxialRays: """Interact with paraxial rays.""" pass # pragma: no cover
[docs] @abstractmethod def flip(self): """Flip the interaction model.""" pass # pragma: no cover
[docs] def to_dict(self): """Returns a dictionary representation of the interaction model.""" return { "type": self.__class__.__name__, "is_reflective": self.is_reflective, "coating": self.coating.to_dict() if self.coating else None, "bsdf": self.bsdf.to_dict() if self.bsdf else None, }
[docs] @classmethod def from_dict(cls, data, parent_surface): """Creates an interaction model from a dictionary representation.""" from optiland.coatings import BaseCoating from optiland.scatter import BaseBSDF interaction_type = data["type"] subclass = cls._registry.get(interaction_type) if subclass is None: raise ValueError(f"Unknown interaction model type: {interaction_type}") # Remove 'type' from data to avoid passing it to the constructor init_data = data.copy() init_data.pop("type") # Ignore 'material_pre' that might be present in older files but is obsolete: if "material_pre" in init_data: init_data.pop("material_pre") if "coating" in init_data and init_data["coating"] is not None: init_data["coating"] = BaseCoating.from_dict(init_data["coating"]) if "bsdf" in init_data and init_data["bsdf"] is not None: init_data["bsdf"] = BaseBSDF.from_dict(init_data["bsdf"]) return subclass( parent_surface=parent_surface, **init_data, )
def _apply_coating_and_bsdf( self, rays: RealRays, nx: float, ny: float, nz: float ) -> RealRays: """Apply coating and BSDF to the rays.""" if self.bsdf: rays = self.bsdf.scatter(rays, nx, ny, nz) if self.coating: rays = self.coating.interact( rays, reflect=self.is_reflective, nx=nx, ny=ny, nz=nz, ) else: rays.update() return rays