Propagation Module
The propagation module provides ray propagation engines for tracing rays through materials with varying refractive indices.
Overview
The propagation system includes:
Propagators - Engines that advance rays through materials
Kernels - GPU-accelerated computation kernels
Factory -
create_propagator()for automatic propagator selection
Propagators
GradientPropagator (CPU)
CPU-based propagator for gradient index materials using RK4 integration.
- class lsurf.propagation.GradientPropagator(use_rk4=None, method=None, adaptive_stepping=True, min_step_size=1e-06, max_step_size=0.001, threads_per_block=256)[source]
Propagator for inhomogeneous media using ray equation integration.
This propagator handles materials with spatially-varying refractive indices by numerically integrating the ray equation (eikonal equation). Supports both Euler and RK4 integration methods.
References
- __init__(use_rk4=None, method=None, adaptive_stepping=True, min_step_size=1e-06, max_step_size=0.001, threads_per_block=256)[source]
Initialize gradient propagator.
- Parameters:
use_rk4 (bool, optional) – Use RK4 instead of Euler (deprecated, use method instead)
method (str, optional) – Integration method: “euler” or “rk4”, default “rk4”
adaptive_stepping (bool, optional) – Enable adaptive step sizing, default True
min_step_size (float, optional) – Minimum step size in meters, default 1 μm
max_step_size (float, optional) – Maximum step size in meters, default 1 mm
threads_per_block (int, optional) – CUDA threads per block (legacy), default 256
- compute_gradient_threshold(wavelength)[source]
Compute gradient threshold for switching to curved-path mode.
- Parameters:
wavelength (float) – Wavelength in meters
- Returns:
Gradient magnitude threshold in m^-1
- Return type:
Notes
Threshold is chosen such that ray curvature becomes significant over a distance of ~wavelength: |∇n|/n > 1/λ
- propagate_step_cpu(positions, directions, active, material, step_size, wavelength, geometric_path_lengths=None, optical_path_lengths=None, accumulated_time=None)[source]
Propagate rays through gradient medium using CPU (vectorized NumPy).
This method modifies arrays in-place using Euler or RK4 integration.
- Parameters:
positions (ndarray) – Ray positions, shape (N, 3), modified in-place.
directions (ndarray) – Ray directions, shape (N, 3), modified in-place.
active (ndarray) – Active ray mask, shape (N,).
material (MaterialFieldProtocol) – Material with get_refractive_index and get_refractive_index_gradient.
step_size (float) – Step size in meters.
wavelength (float) – Wavelength in meters.
geometric_path_lengths (ndarray, optional) – Geometric path lengths, shape (N,), updated in-place.
optical_path_lengths (ndarray, optional) – Optical path lengths, shape (N,), updated in-place.
accumulated_time (ndarray, optional) – Accumulated time, shape (N,), updated in-place.
- propagate_step(rays, step_size, wavelength=5.32e-07, material=None)[source]
Propagate rays by a single integration step.
This is the unified interface method compatible with GPU propagators. Delegates to propagate_step_cpu internally.
- Parameters:
rays (RayBatch) – Ray batch containing positions, directions, and other ray properties. Modified in-place.
step_size (float) – Integration step size in meters.
wavelength (float, optional) – Wavelength in meters, default 532 nm.
material (MaterialFieldProtocol, optional) – Material to propagate through. If not provided, must be set via a separate mechanism (e.g., stored during initialization).
- propagate(rays, total_distance, step_size, wavelength=5.32e-07, material=None)[source]
Propagate rays through a total distance.
This is the unified interface method compatible with GPU propagators.
- Parameters:
rays (RayBatch) – Ray batch to propagate (modified in-place)
total_distance (float) – Total distance to propagate in meters
step_size (float) – Integration step size in meters
wavelength (float, optional) – Wavelength in meters, default 532 nm
material (MaterialFieldProtocol, optional) – Material to propagate through
GPUGradientPropagator (GPU)
GPU-accelerated propagator for gradient index materials.
- class lsurf.propagation.GPUGradientPropagator(material, method='rk4', threads_per_block=256, enable_absorption=False)[source]
High-performance GPU-based propagator for inhomogeneous materials.
This propagator keeps all computation on the GPU for maximum performance. It works with any material that implements GPUMaterialProtocol, using the material’s gpu_material_id to dispatch to the appropriate GPU kernels.
Currently Supported Materials
ExponentialAtmosphere (GPUMaterialID.EXPONENTIAL_ATMOSPHERE)
For materials without GPU support, use GradientPropagator.propagate_step_cpu() which works with any MaterialField via the CPU.
- param material:
The material to propagate through. Must implement GPUMaterialProtocol.
- type material:
GPUMaterialProtocol
- param method:
Integration method: ‘euler’ or ‘rk4’. Default: ‘rk4’
- type method:
str
- param threads_per_block:
CUDA threads per block. Default: 256
- type threads_per_block:
int
Example
>>> from lsurf.materials import ExponentialAtmosphere >>> atmosphere = ExponentialAtmosphere() >>> propagator = GPUGradientPropagator(atmosphere, method='rk4') >>> propagator.propagate(rays, total_distance=500e3, step_size=100.0)
- raises ValueError:
If the material does not support GPU acceleration.
- property material: GPUMaterialProtocol
The material being propagated through.
- propagate_step(rays, step_size, wavelength=5.32e-07)[source]
Propagate rays by a single integration step.
This is the unified interface method compatible with CPU propagators. Performs a single step propagation on GPU.
- propagate(rays, total_distance, step_size, wavelength=5.32e-07, steps_per_kernel=100, target_dn_frac=None)[source]
Propagate rays through material on GPU.
Modifies the rays object in-place.
- Parameters:
rays (RayBatch) – Ray batch to propagate (modified in-place)
total_distance (float) – Total distance to propagate in meters
step_size (float) – Integration step size in meters (max_step in adaptive mode)
wavelength (float, optional) – Wavelength in meters, default 532 nm (green light)
steps_per_kernel (int) – Number of steps per kernel launch. Higher values reduce kernel launch overhead but increase register pressure.
target_dn_frac (float or None) – If provided, use adaptive stepping with this fractional refractivity change target. The step_size becomes max_step and min_step_size from the material is used as min_step.
- property last_adaptive_step_stats
Return adaptive step stats from the most recent propagate() call.
- propagate_with_history(rays, total_distance, step_size, history_interval=100, wavelength=1e-06, target_dn_frac=None, history_distance_interval=None)[source]
Propagate rays and record position history at intervals.
- Parameters:
rays (RayBatch) – Ray batch to propagate (modified in-place)
total_distance (float) – Total distance to propagate in meters
step_size (float) – Integration step size in meters (max_step in adaptive mode)
history_interval (int) – Record history every N steps (fixed-step mode only)
wavelength (float) – Wavelength in meters
target_dn_frac (float or None) – If provided, use adaptive stepping with this fractional refractivity change target.
history_distance_interval (float or None) – Distance between history records in adaptive mode. If None in adaptive mode, defaults to step_size * history_interval.
- Returns:
Dictionary with ‘positions’, ‘directions’, ‘distances’ arrays
- Return type:
Propagator Factory
- lsurf.propagation.create_propagator(material, propagator_id=None, kernel_id=None, method=None, prefer_gpu=True, **kwargs)[source]
Create the appropriate propagator for a material.
Selection priority (highest to lowest): 1. If propagator_id/kernel_id passed here → use those (override) 2. Else use material’s stored preference (material._propagator_id, material._kernel_id) 3. Else use material’s class defaults
- Parameters:
material (MaterialField) – The material to propagate through.
propagator_id (PropagatorID, optional) – Override propagator selection. If None, uses material’s preference.
kernel_id (PropagationKernelID, optional) – Override kernel selection. If None, uses material’s preference. Note: Only used for GPU propagators.
method (str, optional) – Integration method (“euler” or “rk4”). If None, determined from kernel_id.
prefer_gpu (bool, default True) – If True and material supports GPU, use GPU propagator. If False, force CPU propagator.
**kwargs (Any) – Additional propagator configuration (e.g., threads_per_block).
- Returns:
Configured propagator instance appropriate for the material.
- Return type:
Propagator
- Raises:
ValueError – If propagator_id or kernel_id is not supported by the material.
Examples
>>> from lsurf.materials import ExponentialAtmosphere >>> from lsurf.propagation.propagators import create_propagator >>> from lsurf.propagation.kernels import PropagatorID, PropagationKernelID
>>> # Use defaults (RK4 kernel, GPU propagator) >>> atmo = ExponentialAtmosphere(n_sea_level=1.000293) >>> propagator = create_propagator(atmo)
>>> # Override kernel at propagator creation time >>> propagator_euler = create_propagator( ... atmo, ... kernel_id=PropagationKernelID.SIMPLE_EULER ... )
>>> # Force CPU propagator >>> cpu_propagator = create_propagator(atmo, propagator_id=PropagatorID.CPU_GRADIENT)
>>> # Or use prefer_gpu=False >>> cpu_propagator = create_propagator(atmo, prefer_gpu=False)
Usage Example
from lsurf.propagation import create_propagator, GPUMaterialID
from lsurf.materials import ExponentialAtmosphere
# Create material
atmosphere = ExponentialAtmosphere(n_sea_level=1.000293)
# Create propagator (auto-selects GPU if available)
propagator = create_propagator(atmosphere)
# Or explicitly request CPU
from lsurf.propagation.kernels.registry import PropagatorID
cpu_propagator = create_propagator(
atmosphere,
propagator_id=PropagatorID.CPU_GRADIENT
)
Material IDs
- class lsurf.propagation.GPUMaterialID(*values)[source]
Registry of GPU-compatible material types.
Each material type has specialized device functions compiled into the CUDA kernels. This enum identifies which material’s functions to use.
Tiers: - EXPONENTIAL_ATMOSPHERE (1): Legacy specialized kernels - SIMPLE_INHOMOGENEOUS (2): 1D radial profile via LUT interpolation - GRID_INHOMOGENEOUS (3): 3D grid data via trilinear interpolation - SPECTRAL_INHOMOGENEOUS (4): 2D LUT-based (altitude × wavelength)
- EXPONENTIAL_ATMOSPHERE = 1
- SIMPLE_INHOMOGENEOUS = 2
- GRID_INHOMOGENEOUS = 3
- SPECTRAL_INHOMOGENEOUS = 4
- DUCT_ATMOSPHERE = 5
- US_DUCT_ATMOSPHERE = 6
Kernel Registry
The kernel registry manages available propagation kernels:
Kernel and Propagator Registry
Central registry for all kernel types in the ray tracing system: - PropagationKernelID: Material propagation kernels - IntersectionKernelID: Surface intersection kernels - DetectionKernelID: Ray detection kernels - FresnelKernelID: Fresnel reflection/refraction kernels - PropagatorID: Propagator implementations
This module provides: - Kernel ID enums for each category - Registration decorator and lookup functions - Unified registry for all kernel types
- class lsurf.propagation.kernels.registry.PropagationKernelID(*values)[source]
Identifiers for material propagation kernels.
Each kernel corresponds to a specific integration method and material type. Materials declare which kernels they support, and propagators use these IDs to select the appropriate CUDA kernel.
Naming convention: {MATERIAL_TYPE}_{INTEGRATION_METHOD}
Examples
>>> from lsurf.propagation.kernels import PropagationKernelID >>> PropagationKernelID.SIMPLE_RK4 <PropagationKernelID.SIMPLE_RK4: ...>
- SIMPLE_EULER = 1
- SIMPLE_RK4 = 2
- SPECTRAL_EULER = 3
- SPECTRAL_RK4 = 4
- SPECTRAL_EULER_PERRAY = 5
- SPECTRAL_RK4_PERRAY = 6
- GRID_EULER = 7
- GRID_RK4 = 8
- DUCT_EULER = 9
- DUCT_RK4 = 10
- DUCT_EULER_ADAPTIVE = 11
- DUCT_RK4_ADAPTIVE = 12
- US_DUCT_EULER = 13
- US_DUCT_RK4 = 14
- US_DUCT_EULER_ADAPTIVE = 15
- US_DUCT_RK4_ADAPTIVE = 16
- class lsurf.propagation.kernels.registry.IntersectionKernelID(*values)[source]
Identifiers for surface intersection kernels.
Each kernel corresponds to a specific geometry type and intersection method. Surfaces declare which kernels they support for ray-surface intersection.
Note: Complex surfaces (waves, curved surfaces) use the generic signed distance kernels which dispatch to device functions based on geometry_id.
Naming convention: {GEOMETRY_TYPE}_{METHOD}
Examples
>>> from lsurf.propagation.kernels import IntersectionKernelID >>> IntersectionKernelID.PLANE_ANALYTICAL <IntersectionKernelID.PLANE_ANALYTICAL: ...>
- PLANE_ANALYTICAL = 1
- SPHERE_ANALYTICAL = 2
- BOUNDED_PLANE_ANALYTICAL = 3
- ANNULAR_PLANE_ANALYTICAL = 4
- SIGNED_DISTANCE_GENERIC = 5
- SURFACE_NORMAL_GENERIC = 6
- class lsurf.propagation.kernels.registry.DetectionKernelID(*values)[source]
Identifiers for ray detection kernels.
Each kernel corresponds to a specific detector geometry.
Naming convention: {GEOMETRY_TYPE}_{VARIANT}
Examples
>>> from lsurf.propagation.kernels import DetectionKernelID >>> DetectionKernelID.SPHERICAL_SINGLE <DetectionKernelID.SPHERICAL_SINGLE: ...>
- SPHERICAL_SINGLE = 1
- SPHERICAL_MULTI = 2
- PLANAR_SINGLE = 3
- class lsurf.propagation.kernels.registry.FresnelKernelID(*values)[source]
Identifiers for Fresnel reflection/refraction kernels.
Each kernel implements a specific approach to computing Fresnel coefficients.
Examples
>>> from lsurf.propagation.kernels import FresnelKernelID >>> FresnelKernelID.STANDARD <FresnelKernelID.STANDARD: ...>
- STANDARD = 1
- POLARIZED = 2
- class lsurf.propagation.kernels.registry.PropagatorID(*values)[source]
Identifiers for available propagator implementations.
Each propagator ID corresponds to a specific propagator class that handles ray propagation through materials.
Examples
>>> from lsurf.propagation.kernels import PropagatorID >>> PropagatorID.GPU_GRADIENT <PropagatorID.GPU_GRADIENT: ...>
- CPU_GRADIENT = 1
- GPU_GRADIENT = 2
- GPU_SPECTRAL = 3
- GPU_SURFACE = 4
- lsurf.propagation.kernels.registry.register_kernel(kernel_id)[source]
Decorator to register a kernel function with the global registry.
- Parameters:
kernel_id (KernelID) – The unique identifier for this kernel. Can be any kernel ID type: PropagationKernelID, IntersectionKernelID, DetectionKernelID, or FresnelKernelID.
- Returns:
A decorator that registers the function and returns it unchanged.
- Return type:
decorator
Examples
>>> @register_kernel(PropagationKernelID.SIMPLE_EULER) ... def my_euler_kernel(positions, directions, ...): ... ...
>>> @register_kernel(DetectionKernelID.SPHERICAL_SINGLE) ... def my_detection_kernel(positions, directions, ...): ... ...
Notes
Registration happens at import time. The registered function can be retrieved later using get_kernel().
- lsurf.propagation.kernels.registry.get_kernel(kernel_id)[source]
Retrieve a registered kernel function by ID.
- Parameters:
kernel_id (KernelID) – The identifier of the kernel to retrieve. Can be any kernel ID type.
- Returns:
The registered kernel function.
- Return type:
Callable
- Raises:
KeyError – If the kernel ID is not registered.
Examples
>>> kernel = get_kernel(PropagationKernelID.SIMPLE_RK4) >>> kernel(positions, directions, ...)
>>> detect = get_kernel(DetectionKernelID.SPHERICAL_SINGLE) >>> detect(positions, directions, ...)
- lsurf.propagation.kernels.registry.get_registered_kernels()[source]
Return a copy of all registered kernels.
- Returns:
Dictionary mapping kernel IDs to kernel functions.
- Return type:
- lsurf.propagation.kernels.registry.list_kernels(kernel_type=None)[source]
List all registered kernels, optionally filtered by type.
- Parameters:
kernel_type (type[Enum], optional) – If provided, only return kernels of this enum type. For example, PropagationKernelID to list only propagation kernels.
- Returns:
List of registered kernel IDs.
- Return type:
list[KernelID]
Examples
>>> # List all registered kernels >>> all_kernels = list_kernels()
>>> # List only propagation kernels >>> prop_kernels = list_kernels(PropagationKernelID)
>>> # List only detection kernels >>> detect_kernels = list_kernels(DetectionKernelID)
Available Kernels
Propagation Kernels
SIMPLE_EULER- First-order Euler integrationSIMPLE_RK4- Fourth-order Runge-Kutta integrationSPECTRAL_EULER- Wavelength-dependent EulerSPECTRAL_RK4- Wavelength-dependent RK4GRID_EULER- Grid-based refractive index lookup (Euler)GRID_RK4- Grid-based refractive index lookup (RK4)
Intersection Kernels
PLANE_ANALYTICAL- Analytical plane intersectionSPHERE_ANALYTICAL- Analytical sphere intersectionSDF_BISECTION- Signed distance function with bisection refinement
Architecture
The propagation system follows a layered architecture:
Simulation calls MaterialPropagator
MaterialPropagator uses registered Kernels for:
Propagation (advancing rays)
Intersection (finding surface hits)
Detection (recording hits)
Kernels are GPU-accelerated via Numba CUDA
Device Functions
Low-level device functions used by GPU kernels:
Shared GPU Device Functions
This module consolidates device functions used across GPU kernels: - Integration functions (Euler, RK4 steps) - Dispersion models (Sellmeier, Cauchy equations) - Common utilities (normalization, interpolation)
All functions are designed to be used with @cuda.jit(device=True).
- lsurf.propagation.kernels.device_functions.device_adaptive_step_size(gradient_magnitude, refractive_index, wavelength, min_step, max_step)[source]
Compute adaptive step size based on local gradient on GPU device.
- lsurf.propagation.kernels.device_functions.device_euler_step(x, y, z, dx, dy, dz, n, grad_x, grad_y, grad_z, step_size)[source]
Single Euler integration step for ray equation on GPU.
- The ray equation in gradient media is:
dr/ds = d̂ dd̂/ds = (∇n - (d̂·∇n)d̂) / n
- Parameters:
x (float) – Current position
y (float) – Current position
z (float) – Current position
dx (float) – Current direction (unit vector)
dy (float) – Current direction (unit vector)
dz (float) – Current direction (unit vector)
n (float) – Refractive index at current position
grad_x (float) – Gradient of n at current position
grad_y (float) – Gradient of n at current position
grad_z (float) – Gradient of n at current position
step_size (float) – Step size ds
- Returns:
(new_x, new_y, new_z, new_dx, new_dy, new_dz)
- Return type:
- lsurf.propagation.kernels.device_functions.device_rk4_step(x, y, z, dx, dy, dz, step_size, n_func, grad_func, *material_params)[source]
RK4 integration step for ray equation on GPU.
This is a generic RK4 step that takes material evaluation functions as parameters. For material-specific implementations with better performance, materials should define their own device functions.
- Parameters:
x (float) – Current position
y (float) – Current position
z (float) – Current position
dx (float) – Current direction (unit vector)
dy (float) – Current direction (unit vector)
dz (float) – Current direction (unit vector)
step_size (float) – Integration step size
n_func (device function) – Function to evaluate n(x, y, z, *material_params)
grad_func (device function) – Function to evaluate ∇n(x, y, z, *material_params) -> (gx, gy, gz)
material_params (tuple) – Material-specific parameters passed to n_func and grad_func
- Returns:
(new_x, new_y, new_z, new_dx, new_dy, new_dz, n_avg)
- Return type:
Notes
For best performance, materials should implement their own specialized RK4 device functions that inline the n and gradient calculations. This generic version has function call overhead.
- lsurf.propagation.kernels.device_functions.device_sellmeier_equation(wl_um, B1, B2, B3, C1, C2, C3)[source]
GPU-compatible Sellmeier equation.
Computes refractive index from wavelength and Sellmeier coefficients.
- Parameters:
- Returns:
n – Refractive index.
- Return type:
- lsurf.propagation.kernels.device_functions.device_cauchy_equation(wl_um, A, B, C)[source]
GPU-compatible Cauchy equation.
Computes refractive index from wavelength and Cauchy coefficients.
- lsurf.propagation.kernels.device_functions.euler_step(x, y, z, dx, dy, dz, n, grad_x, grad_y, grad_z, step_size)[source]
Single Euler integration step for ray equation.
- Parameters:
x (float) – Current position
y (float) – Current position
z (float) – Current position
dx (float) – Current direction (unit vector)
dy (float) – Current direction (unit vector)
dz (float) – Current direction (unit vector)
n (float) – Refractive index at current position
grad_x (float) – Gradient of n at current position
grad_y (float) – Gradient of n at current position
grad_z (float) – Gradient of n at current position
step_size (float) – Step size ds
- Returns:
(new_x, new_y, new_z, new_dx, new_dy, new_dz)
- Return type:
- lsurf.propagation.kernels.device_functions.rk4_step(x, y, z, dx, dy, dz, n_func, grad_func, wavelength, step_size)[source]
RK4 integration step for ray equation in gradient medium.
- Solves the coupled ODEs:
dr/ds = d̂ dd̂/ds = (∇n - (d̂·∇n)d̂) / n
- Parameters:
x (float) – Current position
y (float) – Current position
z (float) – Current position
dx (float) – Current direction (unit vector)
dy (float) – Current direction (unit vector)
dz (float) – Current direction (unit vector)
n_func (callable) – Function to evaluate n(x, y, z, wavelength)
grad_func (callable) – Function to evaluate ∇n(x, y, z, wavelength) -> (gx, gy, gz)
wavelength (float) – Wavelength
step_size (float) – Integration step size
- Returns:
(new_x, new_y, new_z, new_dx, new_dy, new_dz, optical_path_increment)
- Return type:
- lsurf.propagation.kernels.device_functions.compute_adaptive_step_size(gradient_magnitude, refractive_index, wavelength, min_step, max_step)[source]
Compute adaptive step size based on local gradient.
- Parameters:
- Returns:
Recommended step size
- Return type:
Notes
Step size is chosen to resolve curvature: Δs ≤ R_c / 10 where R_c = n / |∇n| is the radius of curvature.
- lsurf.propagation.kernels.device_functions.normalize_directions(directions)[source]
Normalize direction vectors to unit length.
- lsurf.propagation.kernels.device_functions.euler_step_batch(positions, directions, active_mask, n, grad_n, step_size)[source]
Vectorized Euler step for batch of rays.
- Parameters:
positions (ndarray of shape (N, 3)) – Ray positions
directions (ndarray of shape (N, 3)) – Ray directions (unit vectors)
active_mask (ndarray of shape (N,)) – Boolean mask for active rays
n (ndarray of shape (N,)) – Refractive index at each position
grad_n (ndarray of shape (N, 3)) – Gradient of n at each position
step_size (float) – Step size
- Returns:
new_positions (ndarray of shape (N, 3))
new_directions (ndarray of shape (N, 3))
- Return type:
tuple[ndarray[tuple[Any, …], dtype[float32]], ndarray[tuple[Any, …], dtype[float32]]]
- lsurf.propagation.kernels.device_functions.rk4_step_batch(positions, directions, active_mask, material, step_size, wavelength)[source]
Vectorized RK4 step for batch of rays.
- Parameters:
positions (ndarray of shape (N, 3)) – Ray positions
directions (ndarray of shape (N, 3)) – Ray directions (unit vectors)
active_mask (ndarray of shape (N,)) – Boolean mask for active rays
material (MaterialFieldProtocol) – Material providing n and ∇n evaluation
step_size (float) – Step size
wavelength (float) – Wavelength
- Returns:
new_positions (ndarray of shape (N, 3))
new_directions (ndarray of shape (N, 3))
n_avg (ndarray of shape (N,)) – Average refractive index over step (for optical path)
- Return type:
tuple[ndarray[tuple[Any, …], dtype[float32]], ndarray[tuple[Any, …], dtype[float32]], ndarray[tuple[Any, …], dtype[float32]]]
Integration with Simulation
The Simulation class automatically creates and manages propagators:
from lsurf.simulation import Simulation, SimulationConfig
# Simulation creates propagator internally
config = SimulationConfig(use_gpu=True) # Use GPU if available
sim = Simulation(geometry, config)
# Or force CPU-only
config = SimulationConfig(use_gpu=False)
sim = Simulation(geometry, config)