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