Source code for lsurf.sources.collimated

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

"""
Collimated Beam Source Implementation

Provides a collimated (parallel) beam source with optional spatial intensity
profiles. Useful for modeling laser beams and plane wave illumination.

Examples
--------
>>> from surface_roughness.sources import CollimatedBeam
>>>
>>> source = CollimatedBeam(
...     center=(0, 0, -10),
...     direction=(0, 0, 1),
...     radius=0.001,  # 1 mm
...     num_rays=5000,
...     wavelength=633e-9,  # HeNe laser
...     power=5e-3
... )
>>> rays = source.generate()
"""

from typing import Literal

import numpy as np

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


[docs] class CollimatedBeam(RaySource): """ Collimated beam with parallel rays. Generates rays with identical directions and positions distributed in a circular cross-section. Supports uniform and Gaussian intensity profiles. Parameters ---------- center : tuple of float Beam center position (x, y, z) in meters. direction : tuple of float Beam propagation direction (dx, dy, dz), will be normalized. radius : float Beam radius in meters. 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 beam power in watts. Default is 1.0. profile : {'uniform', 'gaussian'}, optional Spatial intensity profile. Default is 'uniform'. Attributes ---------- center : ndarray, shape (3,) Beam center position. direction : ndarray, shape (3,) Normalized beam direction. radius : float Beam radius. profile : str Intensity profile type. Notes ----- For Gaussian profile, the radius corresponds to 2σ (where σ is the standard deviation of the Gaussian). Ray intensities are weighted according to the Gaussian distribution. Ray timing is initialized so that all rays cross the reference plane (at center) at time=0. This ensures coherent phase fronts. Examples -------- >>> # Uniform circular beam >>> source = CollimatedBeam( ... center=(0, 0, 0), ... direction=(0, 0, 1), ... radius=1e-3, ... num_rays=5000, ... wavelength=633e-9, ... power=5e-3 ... ) >>> # Gaussian beam profile >>> source = CollimatedBeam( ... center=(0, 0, 0), ... direction=(0, 0, 1), ... radius=2e-3, ... num_rays=10000, ... wavelength=1064e-9, ... power=1.0, ... profile='gaussian' ... ) """
[docs] def __init__( self, center: tuple[float, float, float], direction: tuple[float, float, float], radius: float, num_rays: int, wavelength: float | tuple[float, float], power: float = 1.0, profile: Literal["uniform", "gaussian"] = "uniform", ): """ Initialize collimated beam. Parameters ---------- center : tuple of float Beam center position (x, y, z) in meters. direction : tuple of float Beam propagation direction, will be normalized. radius : float Beam radius in meters. 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 beam power in watts. Default is 1.0. profile : {'uniform', 'gaussian'}, optional Spatial intensity profile. Default is 'uniform'. Raises ------ ValueError If radius <= 0 or profile not in {'uniform', 'gaussian'}. """ super().__init__(num_rays, wavelength, power) self.center = np.array(center, dtype=np.float32) self.radius = radius self.profile = profile # Normalize direction direction_arr = np.array(direction, dtype=np.float32) self.direction = direction_arr / np.linalg.norm(direction_arr) if radius <= 0: raise ValueError("radius must be positive") if profile not in ("uniform", "gaussian"): raise ValueError("profile must be 'uniform' or 'gaussian'")
[docs] def generate(self) -> RayBatch: """ Generate collimated beam. Creates rays with parallel directions and positions sampled in a disk perpendicular to the beam direction. Returns ------- RayBatch Ray batch with collimated ray directions. Notes ----- For uniform profile, positions are uniformly distributed in a disk. For Gaussian profile, positions follow a 2D Gaussian distribution and intensities are weighted accordingly. """ rays = self._allocate_rays() # All rays have same direction rays.directions[:] = self.direction # Create perpendicular basis v1, v2 = self._create_perpendicular_basis(self.direction) if self.profile == "uniform": # Uniform disk sampling r = self.radius * np.sqrt(np.random.uniform(0, 1, self.num_rays)) theta = np.random.uniform(0, 2 * np.pi, self.num_rays) x_local = r * np.cos(theta) y_local = r * np.sin(theta) else: # gaussian # Gaussian profile (radius = 2*sigma) sigma = self.radius / 2 x_local = np.random.normal(0, sigma, self.num_rays) y_local = np.random.normal(0, sigma, self.num_rays) # Adjust intensities for Gaussian profile r_squared = x_local**2 + y_local**2 rays.intensities[:] *= np.exp(-r_squared / (2 * sigma**2)) # Renormalize to conserve power rays.intensities[:] *= self.power / np.sum(rays.intensities) # Convert to 3D positions rays.positions[:] = ( self.center + x_local[:, np.newaxis] * v1 + y_local[:, np.newaxis] * v2 ) # Initialize accumulated_time for coherent phase front # All rays should cross the reference plane (at center) at t=0 c = 299792458.0 # Speed of light in vacuum n = 1.0 # Assume initial medium is air/vacuum # Distance along beam direction from center to each ray position offset_along_beam = np.sum( (rays.positions - self.center) * self.direction, axis=1 ) # Time offset: negative for rays ahead, positive for rays behind rays.accumulated_time[:] = -offset_along_beam * n / c self._assign_wavelengths(rays) return rays
[docs] def __repr__(self) -> str: """Return string representation.""" return ( f"CollimatedBeam(center={self.center.tolist()}, " f"radius={self.radius}, profile='{self.profile}')" )