Source code for optiland.nonsequential.components.absorbing

"""Absorbing component for Non-Sequential Raytracing.

Terminates rays on contact (light traps, baffles, aperture stops).

Kramer Harrison, 2026
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import optiland.backend as be
from optiland.nonsequential.components.base import BaseComponent
from optiland.nonsequential.materials.nsq_material import VACUUM

if TYPE_CHECKING:
    import numpy as np

    from optiland.coordinate_system import CoordinateSystem
    from optiland.nonsequential.components.geometry.base import ComponentGeometry
    from optiland.nonsequential.ir.bsdf_ir import BsdfIR
    from optiland.nonsequential.ir.scene_ir import SamplingPolicy
    from optiland.nonsequential.materials.nsq_material import NSQMaterial
    from optiland.nonsequential.ray_bundle import NSQRayBundle
    from optiland.nonsequential.rng import NSQRng


[docs] class AbsorbingComponent(BaseComponent): """Absorbing surface that terminates all rays on contact. Use for light traps, aperture stops, and baffles. Attributes: cs: Coordinate system. geometry: Surface geometry. name: Optional label. """ def __init__( self, cs: CoordinateSystem, geometry: ComponentGeometry, material_front: NSQMaterial = VACUUM, name: str = "", ) -> None: """Initialize AbsorbingComponent. Args: cs: Coordinate system. geometry: Surface geometry. material_front: Surrounding medium (default: vacuum). name: Optional label. """ super().__init__(cs, geometry, material_front, material_front, None, name) self._absorbed_count: int = 0 self._absorbed_flux: float = 0.0
[docs] def interact( self, rays: NSQRayBundle, t: np.ndarray, normals: np.ndarray, hit_mask: np.ndarray, rng: NSQRng, bsdf_ir: BsdfIR, n_geom: np.ndarray, sampling: SamplingPolicy | None = None, forced_branch: str | None = None, ) -> None: """Kill all rays that hit this component (in-place). Args: rays: Ray bundle updated in-place. t: Hit distances [mm], shape (N,). normals: Surface normals in global frame, shape (N, 3). hit_mask: True for rays hitting this component, shape (N,). rng: Keyed PCG32 RNG (unused). bsdf_ir: Unused -- absorbing surfaces never scatter. n_geom: Unused -- absorbing surfaces never determine sidedness. sampling: Unused -- an absorber has no stochastic branch (D2, PR11). forced_branch: Unused -- bounded splitting only applies to ``RefractiveComponent``. """ # Missed rays carry t = inf; zero it before the position update so the # masked-out be.where branch cannot backpropagate 0 * inf = NaN into # the ray directions. t = be.where(hit_mask, t, be.zeros_like(t)) # Advance to hit point before killing rays.x = be.where(hit_mask, rays.x + t * rays.L, rays.x) rays.y = be.where(hit_mask, rays.y + t * rays.M, rays.y) rays.z = be.where(hit_mask, rays.z + t * rays.N, rays.z) # Count absorbed rays (must be alive when they hit) hit_alive = hit_mask & rays.alive self._absorbed_count += int(hit_alive.sum()) self._absorbed_flux += float(rays.flux[hit_alive].sum()) # Terminate rays rays.alive = rays.alive & ~hit_mask rays.flux = be.where(hit_mask, be.zeros_like(rays.flux), rays.flux) rays.bounce = be.where(hit_mask, rays.bounce + 1, rays.bounce)
[docs] def reset_stats(self) -> None: """Reset per-simulation absorbed ray and flux counters.""" self._absorbed_count = 0 self._absorbed_flux = 0.0