Source code for lsurf.sources.uniform_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.

"""
Uniform Diverging Beam Source Implementation

Provides a diverging beam source with uniform solid angle distribution,
suitable for modeling sources that emit uniformly over a cone.

This differs from DivergingBeam which samples phi uniformly (creating
higher ray density near the cone axis). UniformDivergingBeam samples
cos(phi) uniformly, giving uniform ray density per solid angle.

Examples
--------
>>> from lsurf.sources import UniformDivergingBeam
>>>
>>> source = UniformDivergingBeam(
...     origin=(0, 0, 0),
...     mean_direction=(0, 0, 1),
...     divergence_angle=0.1,  # ~5.7 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 UniformDivergingBeam(RaySource): """ Beam with angular divergence and uniform solid angle distribution. Generates rays from a single point with directions uniformly distributed over the solid angle of a cone around the mean direction. Each element of solid angle receives equal ray density. This is physically correct for sources that emit uniformly over a cone (e.g., idealized point sources with angular limits). 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. solid_angle : float Solid angle of the cone in steradians: Ω = 2π(1 - cos(θ)) Notes ----- The key difference from DivergingBeam is the sampling of the polar angle: - DivergingBeam: phi ~ Uniform(0, divergence_angle) This creates higher ray density near the cone axis. - UniformDivergingBeam: cos(phi) ~ Uniform(cos(divergence_angle), 1) This creates uniform ray density per solid angle. The solid angle of a cone with half-angle θ is: Ω = 2π(1 - cos(θ)) For uniform sampling over this solid angle, we need: dN/dΩ = constant This requires sampling cos(phi) uniformly, not phi. Examples -------- >>> # Uniform emission over 10 degree cone >>> source = UniformDivergingBeam( ... origin=(0, 0, 0), ... mean_direction=(0, 0, 1), ... divergence_angle=np.radians(10), ... num_rays=5000, ... wavelength=1550e-9, ... power=1e-3 ... ) >>> # Compare solid angle coverage >>> # Expected: uniform distribution in cos(phi) """
[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 uniform 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)")
@property def solid_angle(self) -> float: """ Solid angle of the cone in steradians. Returns ------- float Ω = 2π(1 - cos(θ)) where θ is the divergence half-angle. """ return 2 * np.pi * (1 - np.cos(self.divergence_angle))
[docs] def generate(self) -> RayBatch: """ Generate uniformly diverging beam. Creates rays from the origin with directions uniformly distributed over the solid angle of a cone around the mean direction. Returns ------- RayBatch Ray batch with uniform solid angle distribution. Notes ----- The sampling uses: - theta ~ Uniform(0, 2π) for azimuthal angle - cos(phi) ~ Uniform(cos(divergence_angle), 1) for polar angle This ensures dN/dΩ = constant over 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: sample cos(phi) uniformly for uniform solid angle distribution # cos(phi) ~ Uniform(cos(divergence_angle), 1) cos_phi = np.random.uniform(np.cos(self.divergence_angle), 1.0, self.num_rays) phi = np.arccos(cos_phi) # Compute perturbed directions sin_phi = np.sin(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"UniformDivergingBeam(origin={self.origin.tolist()}, " f"divergence_angle={self.divergence_angle:.4f}, " f"solid_angle={self.solid_angle:.6f} sr)" )