Source code for lsurf.materials.base.homogeneous

# The Clear BSD License
#
# Copyright (c) 2026 Tobias Heibges
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted (subject to the limitations in the disclaimer
# below) provided that the following conditions are met:
#
#      * Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#
#      * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#
#      * Neither the name of the copyright holder nor the names of its
#      contributors may be used to endorse or promote products derived from this
#      software without specific prior written permission.
#
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""
Homogeneous Material Implementation

A material with uniform optical properties throughout the volume.
Supports wavelength-dependent refractive index via Sellmeier or Cauchy models.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING

import numpy as np
from numpy.typing import NDArray

from .material_field import MaterialField

if TYPE_CHECKING:
    from ...propagation.kernels.registry import PropagatorID


[docs] class HomogeneousMaterial(MaterialField): """ Homogeneous material with constant optical properties. Properties are uniform throughout space but can depend on wavelength. Supports analytic dispersion models (Sellmeier, Cauchy) or custom functions. Parameters ---------- name : str Descriptive name for this material. refractive_index : float or callable Refractive index value or function f(wavelength) -> n. If float, uses constant n for all wavelengths. If callable, wavelength is in meters. absorption_coef : float, optional Absorption coefficient α in m⁻¹. Default is 0. scattering_coef : float, optional Scattering coefficient μ_s in m⁻¹. Default is 0. anisotropy : float, optional Henyey-Greenstein anisotropy factor g ∈ [-1, 1]. Default is 0. Attributes ---------- refractive_index : float or callable Refractive index specification. absorption_coef : float Absorption coefficient in m⁻¹. scattering_coef : float Scattering coefficient in m⁻¹. anisotropy : float Scattering anisotropy factor. Examples -------- >>> # Constant refractive index >>> glass = HomogeneousMaterial("Glass", 1.5) >>> # Wavelength-dependent (Cauchy dispersion) >>> def cauchy_n(wavelength): ... wl_um = wavelength * 1e6 ... return 1.5 + 0.01 / wl_um**2 >>> glass = HomogeneousMaterial("Glass", cauchy_n) >>> # With absorption (colored glass) >>> colored_glass = HomogeneousMaterial( ... "Red Glass", 1.52, absorption_coef=0.1 ... ) Notes ----- For homogeneous materials: - Rays propagate in straight lines (no gradient-driven bending) - Reflection/refraction occurs only at interfaces - Beer-Lambert absorption: I(d) = I₀ exp(-αd) - No GPU kernels are needed (straight-line propagation) """ # ========================================================================= # COMPATIBILITY DECLARATIONS # ========================================================================= @classmethod def _init_compatibility(cls): """Initialize compatibility declarations (called once on first use).""" from ...propagation.kernels.registry import PropagatorID if not cls._supported_propagators: # Homogeneous materials don't need GPU kernels - straight-line propagation cls._supported_kernels = [] # No GPU kernels needed cls._default_kernel = None cls._supported_propagators = [PropagatorID.CPU_GRADIENT] cls._default_propagator = PropagatorID.CPU_GRADIENT
[docs] def __init__( self, name: str, refractive_index: float | Callable[[float], float], absorption_coef: float = 0.0, scattering_coef: float = 0.0, anisotropy: float = 0.0, propagator: PropagatorID | None = None, ): """ Initialize homogeneous material. Parameters ---------- name : str Descriptive name for this material. refractive_index : float or callable Refractive index value or function f(wavelength) -> n. absorption_coef : float, optional Absorption coefficient α in m⁻¹. Default is 0. scattering_coef : float, optional Scattering coefficient μ_s in m⁻¹. Default is 0. anisotropy : float, optional Henyey-Greenstein anisotropy factor g ∈ [-1, 1]. Default is 0. propagator : PropagatorID, optional Override the default propagator. Only CPU_GRADIENT is supported. Raises ------ ValueError If anisotropy is outside [-1, 1]. """ # Initialize compatibility declarations before calling super().__init__ self._init_compatibility() # Homogeneous materials don't use GPU kernels super().__init__(name, kernel=None, propagator=propagator) self._is_homogeneous = True # Validate inputs if not -1.0 <= anisotropy <= 1.0: raise ValueError(f"Anisotropy must be in [-1, 1], got {anisotropy}") if absorption_coef < 0: raise ValueError( f"Absorption coefficient must be >= 0, got {absorption_coef}" ) if scattering_coef < 0: raise ValueError( f"Scattering coefficient must be >= 0, got {scattering_coef}" ) # Store properties self.refractive_index = refractive_index self.absorption_coef = absorption_coef self.scattering_coef = scattering_coef self.anisotropy = anisotropy # Check if n is callable or constant self._n_is_callable = callable(refractive_index)
[docs] def get_refractive_index( self, x: float | NDArray[np.float64], y: float | NDArray[np.float64], z: float | NDArray[np.float64], wavelength: float | NDArray[np.float64], ) -> float | NDArray[np.float64]: """ Get refractive index (position-independent, wavelength-dependent). Parameters ---------- x, y, z : float or ndarray Position coordinates in meters (ignored for homogeneous). wavelength : float or ndarray Wavelength in meters. Returns ------- n : float or ndarray Refractive index. """ if self._n_is_callable: if isinstance(wavelength, np.ndarray): # Vectorize the callable return np.array([self.refractive_index(wl) for wl in wavelength]) return self.refractive_index(wavelength) else: if isinstance(x, np.ndarray): return np.full_like(x, self.refractive_index) return self.refractive_index
[docs] def get_refractive_index_gradient( self, x: float | NDArray[np.float64], y: float | NDArray[np.float64], z: float | NDArray[np.float64], wavelength: float | NDArray[np.float64], ) -> tuple[ float | NDArray[np.float64], float | NDArray[np.float64], float | NDArray[np.float64], ]: """ Get refractive index gradient (always zero for homogeneous). Parameters ---------- x, y, z : float or ndarray Position coordinates in meters. wavelength : float or ndarray Wavelength in meters (unused). Returns ------- grad_n : tuple of (float or ndarray) (0, 0, 0) since ∇n = 0 for homogeneous materials. """ if isinstance(x, np.ndarray): zeros = np.zeros_like(x) return (zeros, zeros, zeros) return (0.0, 0.0, 0.0)
[docs] def get_absorption_coefficient( self, x: float | NDArray[np.float64], y: float | NDArray[np.float64], z: float | NDArray[np.float64], wavelength: float | NDArray[np.float64], ) -> float | NDArray[np.float64]: """ Get absorption coefficient (constant for homogeneous). Parameters ---------- x, y, z : float or ndarray Position coordinates in meters (ignored). wavelength : float or ndarray Wavelength in meters (currently ignored, could extend). Returns ------- alpha : float or ndarray Absorption coefficient in m⁻¹. """ if isinstance(x, np.ndarray): return np.full_like(x, self.absorption_coef) return self.absorption_coef
[docs] def get_scattering_coefficient( self, x: float | NDArray[np.float64], y: float | NDArray[np.float64], z: float | NDArray[np.float64], wavelength: float | NDArray[np.float64], ) -> float | NDArray[np.float64]: """ Get scattering coefficient (constant for homogeneous). Parameters ---------- x, y, z : float or ndarray Position coordinates in meters (ignored). wavelength : float or ndarray Wavelength in meters (currently ignored, could extend). Returns ------- mu_s : float or ndarray Scattering coefficient in m⁻¹. """ if isinstance(x, np.ndarray): return np.full_like(x, self.scattering_coef) return self.scattering_coef
[docs] def get_anisotropy_factor( self, x: float | NDArray[np.float64], y: float | NDArray[np.float64], z: float | NDArray[np.float64], wavelength: float | NDArray[np.float64], ) -> float | NDArray[np.float64]: """ Get scattering anisotropy factor (constant for homogeneous). Parameters ---------- x, y, z : float or ndarray Position coordinates in meters (ignored). wavelength : float or ndarray Wavelength in meters (ignored). Returns ------- g : float or ndarray Anisotropy factor. """ if isinstance(x, np.ndarray): return np.full_like(x, self.anisotropy) return self.anisotropy
[docs] def __repr__(self) -> str: """Return string representation with key properties.""" if self._n_is_callable: n_str = "n(λ)" else: n_str = f"n={self.refractive_index:.4f}" props = [n_str] if self.absorption_coef > 0: props.append(f"α={self.absorption_coef:.2e}") if self.scattering_coef > 0: props.append(f"μ_s={self.scattering_coef:.2e}") return f"<HomogeneousMaterial('{self.name}', {', '.join(props)})>"