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)"
)