Source code for lsurf.sources.diverging

# 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.

"""
Diverging Beam Source Implementation

Provides a diverging beam source with angular spread, suitable for modeling
fiber optic outputs, LEDs, and other extended sources.

Examples
--------
>>> from surface_roughness.sources import DivergingBeam
>>>
>>> source = DivergingBeam(
...     origin=(0, 0, 0),
...     mean_direction=(0, 0, 1),
...     divergence_angle=0.05,  # ~2.9 degrees
...     num_rays=1000,
...     wavelength=850e-9,
...     power=1.0
... )
>>> rays = source.generate()
"""

import numpy as np

from ..utilities.ray_data import RayBatch
from .base import RaySource


[docs] class DivergingBeam(RaySource): """ Beam with angular divergence. Generates rays from a single point with directions distributed within a cone around the mean direction. Suitable for modeling fiber optic outputs, LEDs, and similar diverging sources. Parameters ---------- origin : tuple of float Source position (x, y, z) in meters. mean_direction : tuple of float Mean beam direction (dx, dy, dz), will be normalized. divergence_angle : float Half-angle divergence in radians (cone half-angle). Must be in range (0, π/2). num_rays : int Number of rays to generate. wavelength : float or tuple of float Single wavelength (m) or (min, max) range. power : float, optional Total source power in watts. Default is 1.0. Attributes ---------- origin : ndarray, shape (3,) Source position. mean_direction : ndarray, shape (3,) Normalized mean beam direction. divergence_angle : float Cone half-angle in radians. Notes ----- The angular distribution is uniform within the cone. For Lambertian sources (cosine distribution), a different implementation would be needed. Examples -------- >>> # Fiber output with 0.1 rad NA >>> source = DivergingBeam( ... origin=(0, 0, 0), ... mean_direction=(0, 0, 1), ... divergence_angle=0.1, ... num_rays=5000, ... wavelength=1550e-9, ... power=1e-3 ... ) >>> # LED with wide angle >>> source = DivergingBeam( ... origin=(0, 0.1, 0), ... mean_direction=(0, 0, 1), ... divergence_angle=np.radians(30), # 30 degrees ... num_rays=10000, ... wavelength=(400e-9, 700e-9), ... power=0.5 ... ) """
[docs] def __init__( self, origin: tuple[float, float, float], mean_direction: tuple[float, float, float], divergence_angle: float, num_rays: int, wavelength: float | tuple[float, float], power: float = 1.0, ): """ Initialize diverging beam. Parameters ---------- origin : tuple of float Source position (x, y, z) in meters. mean_direction : tuple of float Mean beam direction, will be normalized. divergence_angle : float Cone half-angle in radians. num_rays : int Number of rays to generate. wavelength : float or tuple of float Wavelength in meters or (min, max) range. power : float, optional Total source power in watts. Default is 1.0. Raises ------ ValueError If divergence_angle not in (0, π/2). """ super().__init__(num_rays, wavelength, power) self.origin = np.array(origin, dtype=np.float32) self.divergence_angle = divergence_angle # Normalize mean direction mean_direction_arr = np.array(mean_direction, dtype=np.float32) self.mean_direction = mean_direction_arr / np.linalg.norm(mean_direction_arr) if divergence_angle <= 0 or divergence_angle >= np.pi / 2: raise ValueError("divergence_angle must be in (0, π/2)")
[docs] def generate(self) -> RayBatch: """ Generate diverging beam. Creates rays from the origin with directions uniformly distributed within a cone around the mean direction. Returns ------- RayBatch Ray batch with diverging direction distribution. Notes ----- Uses spherical coordinates relative to the mean direction to generate uniformly distributed directions within the cone. """ rays = self._allocate_rays() # All rays start at origin rays.positions[:] = self.origin # Create perpendicular basis for direction perturbation v1, v2 = self._create_perpendicular_basis(self.mean_direction) # Generate random angles within cone # Azimuthal angle: uniform in [0, 2π) theta = np.random.uniform(0, 2 * np.pi, self.num_rays) # Polar angle: uniform in [0, divergence_angle] # For uniform distribution on cone cap, should sample cos(phi) uniformly # but for simplicity we use uniform phi for now phi = np.random.uniform(0, self.divergence_angle, self.num_rays) # Compute perturbed directions sin_phi = np.sin(phi) cos_phi = np.cos(phi) # Direction in local coordinates (z = mean_direction) local_x = sin_phi * np.cos(theta) local_y = sin_phi * np.sin(theta) local_z = cos_phi # Transform to global coordinates rays.directions[:] = ( local_x[:, np.newaxis] * v1 + local_y[:, np.newaxis] * v2 + local_z[:, np.newaxis] * self.mean_direction ) # Normalize (should already be normalized, but ensure numerical stability) norms = np.linalg.norm(rays.directions, axis=1, keepdims=True) rays.directions[:] /= norms self._assign_wavelengths(rays) return rays
[docs] def __repr__(self) -> str: """Return string representation.""" return ( f"DivergingBeam(origin={self.origin.tolist()}, " f"divergence_angle={self.divergence_angle:.4f})" )