Source code for lsurf.simulation.simulation

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

"""
Ray Tracing Simulation

The Simulation class uses a Geometry object built via GeometryBuilder
as its first step, ensuring:
- Material consistency across surfaces via named media
- Validation of surface configurations
- Immutable geometry during simulation
"""

from __future__ import annotations

import logging
import warnings
from typing import TYPE_CHECKING

from ..geometry import Geometry
from ..surfaces import SurfaceRole

from .config import SimulationConfig
from .orchestrator import SimulationOrchestrator
from .result import SimulationResult

if TYPE_CHECKING:
    from ..utilities.ray_data import RayBatch

logger = logging.getLogger(__name__)


[docs] class Simulation: """ Ray tracing simulation with geometry-based configuration. The simulation takes a pre-built Geometry object which defines: - All optical surfaces with their materials - All detector surfaces - The background propagation medium - Named media for material consistency Parameters ---------- geometry : Geometry Pre-built geometry from GeometryBuilder containing all surfaces, detectors, and materials. config : SimulationConfig, optional Simulation configuration. Uses defaults if not provided. Examples -------- >>> from lsurf.geometry import GeometryBuilder >>> from lsurf.materials import LinsleyAtmosphere, WATER >>> from lsurf.surfaces import SphereSurface, PlaneSurface, SurfaceRole >>> from lsurf.simulation import Simulation, SimulationConfig >>> >>> # Build geometry >>> EARTH_RADIUS = 6.371e6 >>> atmosphere = LinsleyAtmosphere() >>> >>> ocean = SphereSurface( ... center=(0, 0, -EARTH_RADIUS), ... radius=EARTH_RADIUS, ... role=SurfaceRole.OPTICAL, ... name="ocean", ... ) >>> detector = PlaneSurface( ... point=(0, 0, 35000), ... normal=(0, 0, 1), ... role=SurfaceRole.DETECTOR, ... name="detector_35km", ... ) >>> >>> geometry = ( ... GeometryBuilder() ... .register_medium("atmosphere", atmosphere) ... .register_medium("ocean", WATER) ... .set_background("atmosphere") ... .add_surface(ocean, front="atmosphere", back="ocean") ... .add_detector(detector) ... .build() ... ) >>> >>> # Create simulation with geometry >>> config = SimulationConfig(step_size=100.0, max_bounces=5) >>> sim = Simulation(geometry, config) >>> result = sim.run(rays) >>> print(f"Detected: {result.statistics.rays_detected}") """
[docs] def __init__( self, geometry: Geometry, config: SimulationConfig | None = None, ): self._geometry = geometry self._config = config if config is not None else SimulationConfig() # Extract surfaces list (surfaces + detectors) self._all_surfaces = geometry.to_surface_list() # Build surface indices by role for efficient processing self._detector_indices: list[int] = [] self._optical_indices: list[int] = [] self._absorber_indices: list[int] = [] for i, surface in enumerate(self._all_surfaces): if surface.role == SurfaceRole.DETECTOR: self._detector_indices.append(i) elif surface.role == SurfaceRole.OPTICAL: self._optical_indices.append(i) elif surface.role == SurfaceRole.ABSORBER: self._absorber_indices.append(i) # Create orchestrator lazily self._orchestrator: SimulationOrchestrator | None = None logger.info( "Simulation initialized: %d optical, %d detector, %d absorber surfaces", len(self._optical_indices), len(self._detector_indices), len(self._absorber_indices), )
@property def geometry(self) -> Geometry: """The simulation geometry.""" return self._geometry @property def config(self) -> SimulationConfig: """The simulation configuration.""" return self._config @property def num_surfaces(self) -> int: """Total number of surfaces (optical + absorber + detector).""" return len(self._all_surfaces) @property def detector_surfaces(self) -> list: """List of detector surfaces.""" return [self._all_surfaces[i] for i in self._detector_indices] @property def optical_surfaces(self) -> list: """List of optical surfaces.""" return [self._all_surfaces[i] for i in self._optical_indices] @property def absorber_surfaces(self) -> list: """List of absorber surfaces.""" return [self._all_surfaces[i] for i in self._absorber_indices] def _get_orchestrator(self) -> SimulationOrchestrator: """Get or create the simulation orchestrator.""" if self._orchestrator is None: self._orchestrator = SimulationOrchestrator( geometry=self._geometry, config=self._config, ) return self._orchestrator
[docs] def run(self, rays: "RayBatch") -> SimulationResult: """ Run the ray tracing simulation. Parameters ---------- rays : RayBatch Initial rays to trace. Returns ------- SimulationResult Complete simulation results including: - detected: RecordedRays from detector surfaces - remaining: RayBatch of rays still active - statistics: SimulationStatistics with counts - detections_per_surface: dict mapping detector names to hit counts Examples -------- >>> result = sim.run(rays) >>> print(f"Detected {result.statistics.rays_detected} rays") >>> print(f"Absorbed {result.statistics.rays_absorbed} rays") >>> for name, count in result.detections_per_surface.items(): ... print(f" {name}: {count} hits") """ orchestrator = self._get_orchestrator() # Suppress Numba GPU under-utilization warnings (common with small batches) with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message=".*Grid size.*GPU under-utilization.*", category=UserWarning, ) return orchestrator.run(rays)
[docs] def run_single_bounce( self, rays: "RayBatch", ) -> tuple["RayBatch", SimulationResult]: """ Run a single propagation + interaction cycle. Useful for step-by-step debugging or custom simulation loops. Parameters ---------- rays : RayBatch Rays to propagate. Returns ------- continuing_rays : RayBatch Rays that should continue (reflected, refracted, no-hit). result : SimulationResult Results from this single bounce (detections, absorptions). """ orchestrator = self._get_orchestrator() # Suppress Numba GPU under-utilization warnings (common with small batches) with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message=".*Grid size.*GPU under-utilization.*", category=UserWarning, ) return orchestrator.run_single_bounce(rays)