Source code for lsurf.sources.parallel_from_positions

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

"""
Parallel Beam from Explicit Position Array

Provides a source where ray positions are explicitly specified and all rays
share a common direction. Useful for atmospheric propagation studies where
rays are launched from specific impact parameters or grid points.

Examples
--------
>>> from lsurf.sources import ParallelBeamFromPositions
>>> import numpy as np
>>>
>>> # Create rays at different altitudes, all traveling horizontally
>>> positions = np.array([
...     [-1000, 0, 100],
...     [-1000, 0, 200],
...     [-1000, 0, 300],
... ])
>>> source = ParallelBeamFromPositions(
...     positions=positions,
...     direction=(1, 0, 0),
...     wavelength=532e-9,
... )
>>> rays = source.generate()
"""

import numpy as np
import numpy.typing as npt

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


[docs] class ParallelBeamFromPositions(RaySource): """ Parallel rays from explicitly specified positions. All rays share the same direction, making this ideal for: - Atmospheric refraction studies with specific impact parameters - Plane wave propagation through inhomogeneous media - Grid-based ray launching for wavefront analysis Unlike CollimatedBeam which generates positions in a disk, this source accepts arbitrary position arrays, enabling custom spatial distributions. Parameters ---------- positions : array_like, shape (N, 3) Starting positions for each ray in meters. direction : tuple of float Direction vector for all rays (will be normalized). wavelength : float or tuple of float, optional Single wavelength (m) or (min, max) range. Default is 532 nm. power : float, optional Total source power in watts. Default is 1.0. Attributes ---------- positions : ndarray, shape (N, 3) Ray starting positions. direction : ndarray, shape (3,) Normalized ray direction. Examples -------- >>> # Rays at different impact parameters for atmospheric study >>> impact_params = np.linspace(0, 10000, 100) >>> positions = np.column_stack([ ... -np.sqrt((R + 100e3)**2 - (R + impact_params)**2), # x ... np.zeros_like(impact_params), # y ... impact_params # z ... ]) >>> source = ParallelBeamFromPositions(positions, direction=(1, 0, 0)) >>> rays = source.generate() >>> propagator.propagate(rays, total_distance=500e3, step_size=100) >>> # Regular grid of rays >>> x, y = np.meshgrid(np.linspace(-1, 1, 10), np.linspace(-1, 1, 10)) >>> positions = np.column_stack([x.ravel(), y.ravel(), np.zeros(100)]) >>> source = ParallelBeamFromPositions(positions, direction=(0, 0, 1)) """
[docs] def __init__( self, positions: npt.ArrayLike, direction: tuple[float, float, float], wavelength: float | tuple[float, float] = 532e-9, power: float = 1.0, ): """ Initialize parallel ray source. Parameters ---------- positions : array_like, shape (N, 3) Starting positions for each ray in meters. direction : tuple of float Direction vector for all rays (will be normalized). wavelength : float or tuple of float, optional Wavelength in meters or (min, max) range. Default is 532 nm. power : float, optional Total source power in watts. Default is 1.0. Raises ------ ValueError If positions shape is invalid or direction is zero vector. """ # Convert and validate positions self._positions = np.asarray(positions, dtype=np.float32) if self._positions.ndim == 1: self._positions = self._positions.reshape(1, 3) if self._positions.ndim != 2 or self._positions.shape[1] != 3: raise ValueError( f"positions must have shape (N, 3), got {self._positions.shape}" ) num_rays = self._positions.shape[0] # Initialize base class super().__init__(num_rays, wavelength, power) # Normalize direction direction_arr = np.array(direction, dtype=np.float32) norm = np.linalg.norm(direction_arr) if norm < 1e-10: raise ValueError("direction cannot be zero vector") self.direction = direction_arr / norm
@property def positions(self) -> npt.NDArray[np.float32]: """Ray starting positions, shape (N, 3).""" return self._positions
[docs] def generate(self) -> RayBatch: """ Generate parallel ray batch. Creates rays at the specified positions, all with the same direction. Returns ------- RayBatch Ray batch with parallel rays ready for propagation. """ rays = self._allocate_rays() # Set positions from input array rays.positions[:] = self._positions # All rays have same direction rays.directions[:] = self.direction # Assign wavelengths self._assign_wavelengths(rays) return rays
[docs] def __repr__(self) -> str: """Return string representation.""" return ( f"ParallelBeamFromPositions(num_rays={self.num_rays}, " f"direction={self.direction.tolist()})" )