# 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.
"""
Simulation Configuration
Contains the SimulationConfig dataclass for configuring ray tracing simulations.
"""
from dataclasses import dataclass
[docs]
@dataclass
class SimulationConfig:
"""
Configuration for ray tracing simulation.
Attributes
----------
step_size : float
Maximum integration step size in meters (default 100.0).
min_step_size : float
Minimum step size in meters for adaptive stepping (default 3e-4 = 0.3mm).
This provides ~1ps time resolution near surfaces.
adaptive_stepping : bool
Whether to use adaptive step sizing near surfaces (default True).
When enabled, steps decrease as rays approach surfaces for precise timing.
surface_proximity_factor : float
Step size = distance * factor when within proximity threshold (default 0.5).
surface_proximity_threshold : float
Distance (in meters) within which adaptive stepping activates (default 10.0).
max_steps_per_leg : int
Maximum steps before forcing surface check (default 10000).
max_bounces : int
Maximum surface interactions before termination (default 10).
min_intensity : float
Intensity threshold below which rays are terminated (default 1e-10).
bounding_radius : float
Radius of bounding sphere in meters (default 500_000.0).
bounding_center : tuple[float, float, float]
Center of bounding sphere (default (0.0, 0.0, -6.371e6) for Earth center).
apply_absorption : bool
Whether to apply Beer-Lambert absorption (default True).
polarization : str
Polarization state: 's', 'p', or 'unpolarized' (default 'unpolarized').
track_polarization_vector : bool
Whether to track 3D polarization vectors through interactions (default False).
track_surface_hits : bool
Whether to store intermediate surface hit positions (default False).
When enabled, SimulationResult.surface_hits will contain hit positions
for all optical surfaces, useful for visualization.
track_refracted_rays : bool
Whether to continue propagating refracted rays from optical surfaces (default False).
When False, only reflected rays continue; refracted rays are discarded.
Set to False for simulations where only reflected light is of interest
(e.g., ocean surface reflection where underwater propagation is not needed).
use_gpu : bool
Whether to use GPU acceleration if available (default True).
Examples
--------
>>> config = SimulationConfig(step_size=50.0, max_bounces=5)
>>> sim = Simulation(geometry, config)
Notes
-----
Adaptive stepping provides sub-nanosecond timing precision near surfaces:
| Distance to Surface | Step Size | Time Resolution |
|---------------------|-----------|-----------------|
| > 10m | 100m | ~333ns |
| 5m | 2.5m | ~8ns |
| 1m | 0.5m | ~1.7ns |
| 0.1m | 0.05m | ~167ps |
| < 0.6mm | 0.3mm | ~1ps (minimum) |
"""
step_size: float = 100.0
min_step_size: float = 3e-4 # 0.3mm → ~1ps time resolution
adaptive_stepping: bool = True
surface_proximity_factor: float = 0.5 # Step = distance * factor when near
surface_proximity_threshold: float = 10.0 # Start adapting within this distance (m)
max_steps_per_leg: int = 10000
max_bounces: int = 10
min_intensity: float = 1e-10
bounding_radius: float = 500_000.0
bounding_center: tuple[float, float, float] = (0.0, 0.0, -6.371e6)
apply_absorption: bool = True
polarization: str = "unpolarized"
track_polarization_vector: bool = False
track_surface_hits: bool = False
track_refracted_rays: bool = False
use_gpu: bool = True
gradient_adaptive_stepping: bool = False
target_dn_frac: float = 0.01
[docs]
def __post_init__(self) -> None:
"""Validate configuration."""
if self.step_size <= 0:
raise ValueError(f"step_size must be positive, got {self.step_size}")
if self.min_step_size <= 0:
raise ValueError(
f"min_step_size must be positive, got {self.min_step_size}"
)
if self.min_step_size > self.step_size:
raise ValueError(
f"min_step_size ({self.min_step_size}) must be <= step_size ({self.step_size})"
)
if self.surface_proximity_factor <= 0 or self.surface_proximity_factor > 1:
raise ValueError(
f"surface_proximity_factor must be in (0, 1], got {self.surface_proximity_factor}"
)
if self.surface_proximity_threshold <= 0:
raise ValueError(
f"surface_proximity_threshold must be positive, got {self.surface_proximity_threshold}"
)
if self.max_steps_per_leg <= 0:
raise ValueError(
f"max_steps_per_leg must be positive, got {self.max_steps_per_leg}"
)
if self.max_bounces < 0:
raise ValueError(
f"max_bounces must be non-negative, got {self.max_bounces}"
)
if self.min_intensity < 0:
raise ValueError(
f"min_intensity must be non-negative, got {self.min_intensity}"
)
if self.bounding_radius <= 0:
raise ValueError(
f"bounding_radius must be positive, got {self.bounding_radius}"
)
if self.polarization not in ("s", "p", "unpolarized"):
raise ValueError(
f"polarization must be 's', 'p', or 'unpolarized', got '{self.polarization}'"
)
if self.target_dn_frac <= 0:
raise ValueError(
f"target_dn_frac must be positive, got {self.target_dn_frac}"
)