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