Source code for optiland.nonsequential.bsdf.base

"""Base BSDF for Non-Sequential Raytracing.

Kramer Harrison, 2026
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import numpy as np

    from optiland.nonsequential.rng import NSQRng


[docs] class BaseBSDF(ABC): """Abstract base class for bidirectional scattering distribution functions. All BSDF implementations must support vectorized operation over N rays. Array operations must be compatible with both NumPy and CuPy arrays. Lobes are explicitly REFLECT or TRANSMIT: ``sample()`` returns, alongside each scattered direction, whether that particular ray's draw landed in the reflective hemisphere (same side as the incident ray) or the transmissive one (far side). A surface's own optical topology decides what that means physically -- a mirror has no far side to transmit into, so ``ReflectiveComponent`` ignores the flag, while ``RefractiveComponent`` uses it to pick which of its two adjacent media (``material_front``/``material_back``) a scattered ray is now in: the medium a scattered ray ends up in is decided by its own lobe choice, never by the independent Fresnel branch draw that only applies to unscattered rays. """
[docs] @abstractmethod def sample( self, num_rays: int, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, rng: NSQRng, ray_id: np.ndarray, bounce: np.ndarray, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Sample scattered ray directions, flux weights, and lobe side. Args: num_rays: Number of rays to scatter. incident_dirs: Incident ray directions, shape (N, 3), unit vectors. normals: Surface normals at hit points, shape (N, 3), unit vectors pointing toward the incoming ray side. wavelengths: Per-ray wavelengths [nm], shape (N,). rng: Keyed PCG32 RNG. ray_id: Per-ray identifiers, shape (N,), for keying the draw. bounce: Per-ray bounce/step index, shape (N,), for keying the draw. Returns: A tuple (scattered_dirs, flux_weights, transmitted) where: - scattered_dirs: Scattered unit direction vectors, shape (N, 3). - flux_weights: Relative flux weights in [0, 1], shape (N,). - transmitted: Boolean mask, shape (N,). True where the returned direction is on the transmissive (far) side of the surface; False for the reflective (incident) side. A purely reflective BSDF (e.g. ``SpecularBRDF``) returns all-False. """
[docs] @abstractmethod def reflectance( self, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, ) -> np.ndarray: """Total hemispherical reflectance for Russian-roulette decisions. Args: incident_dirs: Incident ray directions, shape (N, 3), unit vectors. normals: Surface normals at hit points, shape (N, 3), unit vectors. wavelengths: Per-ray wavelengths [nm], shape (N,). Returns: Total reflectance values in [0, 1], shape (N,). """