lsurf 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsurf/__init__.py +471 -0
- lsurf/analysis/__init__.py +107 -0
- lsurf/analysis/healpix_utils.py +418 -0
- lsurf/analysis/sphere_viz.py +1280 -0
- lsurf/cli/__init__.py +48 -0
- lsurf/cli/build.py +398 -0
- lsurf/cli/config_schema.py +318 -0
- lsurf/cli/gui_cmd.py +76 -0
- lsurf/cli/interactive.py +850 -0
- lsurf/cli/main.py +81 -0
- lsurf/cli/run.py +806 -0
- lsurf/detectors/__init__.py +266 -0
- lsurf/detectors/analysis.py +289 -0
- lsurf/detectors/base.py +284 -0
- lsurf/detectors/constant_size_rings.py +485 -0
- lsurf/detectors/directional.py +45 -0
- lsurf/detectors/extended/__init__.py +73 -0
- lsurf/detectors/extended/local_sphere.py +353 -0
- lsurf/detectors/extended/recording_sphere.py +368 -0
- lsurf/detectors/planar.py +45 -0
- lsurf/detectors/protocol.py +187 -0
- lsurf/detectors/recording_spheres.py +63 -0
- lsurf/detectors/results.py +1140 -0
- lsurf/detectors/small/__init__.py +79 -0
- lsurf/detectors/small/directional.py +330 -0
- lsurf/detectors/small/planar.py +401 -0
- lsurf/detectors/small/spherical.py +450 -0
- lsurf/detectors/spherical.py +45 -0
- lsurf/geometry/__init__.py +199 -0
- lsurf/geometry/builder.py +478 -0
- lsurf/geometry/cell.py +228 -0
- lsurf/geometry/cell_geometry.py +247 -0
- lsurf/geometry/detector_arrays.py +1785 -0
- lsurf/geometry/geometry.py +222 -0
- lsurf/geometry/surface_analysis.py +375 -0
- lsurf/geometry/validation.py +91 -0
- lsurf/gui/__init__.py +51 -0
- lsurf/gui/app.py +903 -0
- lsurf/gui/core/__init__.py +39 -0
- lsurf/gui/core/scene.py +343 -0
- lsurf/gui/core/simulation.py +264 -0
- lsurf/gui/renderers/__init__.py +40 -0
- lsurf/gui/renderers/ray_renderer.py +353 -0
- lsurf/gui/renderers/source_renderer.py +505 -0
- lsurf/gui/renderers/surface_renderer.py +477 -0
- lsurf/gui/views/__init__.py +48 -0
- lsurf/gui/views/config_editor.py +3199 -0
- lsurf/gui/views/properties.py +257 -0
- lsurf/gui/views/results.py +291 -0
- lsurf/gui/views/scene_tree.py +180 -0
- lsurf/gui/views/viewport_3d.py +555 -0
- lsurf/gui/views/visualizations.py +712 -0
- lsurf/materials/__init__.py +169 -0
- lsurf/materials/base/__init__.py +64 -0
- lsurf/materials/base/full_inhomogeneous.py +208 -0
- lsurf/materials/base/grid_inhomogeneous.py +319 -0
- lsurf/materials/base/homogeneous.py +342 -0
- lsurf/materials/base/material_field.py +527 -0
- lsurf/materials/base/simple_inhomogeneous.py +418 -0
- lsurf/materials/base/spectral_inhomogeneous.py +497 -0
- lsurf/materials/implementations/__init__.py +120 -0
- lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
- lsurf/materials/implementations/duct_atmosphere.py +390 -0
- lsurf/materials/implementations/exponential_atmosphere.py +435 -0
- lsurf/materials/implementations/gaussian_lens.py +120 -0
- lsurf/materials/implementations/interpolated_data.py +123 -0
- lsurf/materials/implementations/layered_atmosphere.py +134 -0
- lsurf/materials/implementations/linear_gradient.py +109 -0
- lsurf/materials/implementations/linsley_atmosphere.py +764 -0
- lsurf/materials/implementations/standard_materials.py +126 -0
- lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
- lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
- lsurf/materials/utils/__init__.py +77 -0
- lsurf/materials/utils/constants.py +45 -0
- lsurf/materials/utils/device_functions.py +117 -0
- lsurf/materials/utils/dispersion.py +160 -0
- lsurf/materials/utils/factories.py +142 -0
- lsurf/propagation/__init__.py +91 -0
- lsurf/propagation/detector_gpu.py +67 -0
- lsurf/propagation/gpu_device_rays.py +294 -0
- lsurf/propagation/kernels/__init__.py +175 -0
- lsurf/propagation/kernels/absorption/__init__.py +61 -0
- lsurf/propagation/kernels/absorption/grid.py +240 -0
- lsurf/propagation/kernels/absorption/simple.py +232 -0
- lsurf/propagation/kernels/absorption/spectral.py +410 -0
- lsurf/propagation/kernels/detection/__init__.py +64 -0
- lsurf/propagation/kernels/detection/protocol.py +102 -0
- lsurf/propagation/kernels/detection/spherical.py +255 -0
- lsurf/propagation/kernels/device_functions.py +790 -0
- lsurf/propagation/kernels/fresnel/__init__.py +64 -0
- lsurf/propagation/kernels/fresnel/protocol.py +97 -0
- lsurf/propagation/kernels/fresnel/standard.py +258 -0
- lsurf/propagation/kernels/intersection/__init__.py +79 -0
- lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
- lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
- lsurf/propagation/kernels/intersection/plane.py +166 -0
- lsurf/propagation/kernels/intersection/protocol.py +95 -0
- lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
- lsurf/propagation/kernels/intersection/sphere.py +190 -0
- lsurf/propagation/kernels/propagation/__init__.py +85 -0
- lsurf/propagation/kernels/propagation/grid.py +527 -0
- lsurf/propagation/kernels/propagation/protocol.py +105 -0
- lsurf/propagation/kernels/propagation/simple.py +460 -0
- lsurf/propagation/kernels/propagation/spectral.py +875 -0
- lsurf/propagation/kernels/registry.py +331 -0
- lsurf/propagation/kernels/surface/__init__.py +72 -0
- lsurf/propagation/kernels/surface/bisection.py +232 -0
- lsurf/propagation/kernels/surface/detection.py +402 -0
- lsurf/propagation/kernels/surface/reduction.py +166 -0
- lsurf/propagation/propagator_protocol.py +222 -0
- lsurf/propagation/propagators/__init__.py +101 -0
- lsurf/propagation/propagators/detector_handler.py +354 -0
- lsurf/propagation/propagators/factory.py +200 -0
- lsurf/propagation/propagators/fresnel_handler.py +305 -0
- lsurf/propagation/propagators/gpu_gradient.py +566 -0
- lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
- lsurf/propagation/propagators/gradient.py +429 -0
- lsurf/propagation/propagators/intersection_handler.py +327 -0
- lsurf/propagation/propagators/material_propagator.py +398 -0
- lsurf/propagation/propagators/signed_distance_handler.py +522 -0
- lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
- lsurf/propagation/propagators/surface_interaction.py +616 -0
- lsurf/propagation/propagators/surface_propagator.py +719 -0
- lsurf/py.typed +1 -0
- lsurf/simulation/__init__.py +70 -0
- lsurf/simulation/config.py +164 -0
- lsurf/simulation/orchestrator.py +462 -0
- lsurf/simulation/result.py +299 -0
- lsurf/simulation/simulation.py +262 -0
- lsurf/sources/__init__.py +128 -0
- lsurf/sources/base.py +264 -0
- lsurf/sources/collimated.py +252 -0
- lsurf/sources/custom.py +409 -0
- lsurf/sources/diverging.py +228 -0
- lsurf/sources/gaussian.py +272 -0
- lsurf/sources/parallel_from_positions.py +197 -0
- lsurf/sources/point.py +172 -0
- lsurf/sources/uniform_diverging.py +258 -0
- lsurf/surfaces/__init__.py +184 -0
- lsurf/surfaces/cpu/__init__.py +50 -0
- lsurf/surfaces/cpu/curved_wave.py +463 -0
- lsurf/surfaces/cpu/gerstner_wave.py +381 -0
- lsurf/surfaces/cpu/wave_params.py +118 -0
- lsurf/surfaces/gpu/__init__.py +72 -0
- lsurf/surfaces/gpu/annular_plane.py +453 -0
- lsurf/surfaces/gpu/bounded_plane.py +390 -0
- lsurf/surfaces/gpu/curved_wave.py +483 -0
- lsurf/surfaces/gpu/gerstner_wave.py +377 -0
- lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
- lsurf/surfaces/gpu/plane.py +299 -0
- lsurf/surfaces/gpu/recording_sphere.py +587 -0
- lsurf/surfaces/gpu/sphere.py +311 -0
- lsurf/surfaces/protocol.py +336 -0
- lsurf/surfaces/registry.py +373 -0
- lsurf/utilities/__init__.py +175 -0
- lsurf/utilities/detector_analysis.py +814 -0
- lsurf/utilities/fresnel.py +628 -0
- lsurf/utilities/interactions.py +1215 -0
- lsurf/utilities/propagation.py +602 -0
- lsurf/utilities/ray_data.py +532 -0
- lsurf/utilities/recording_sphere.py +745 -0
- lsurf/utilities/time_spread.py +463 -0
- lsurf/visualization/__init__.py +329 -0
- lsurf/visualization/absorption_plots.py +334 -0
- lsurf/visualization/atmospheric_plots.py +754 -0
- lsurf/visualization/common.py +348 -0
- lsurf/visualization/detector_plots.py +1350 -0
- lsurf/visualization/detector_sphere_plots.py +1173 -0
- lsurf/visualization/fresnel_plots.py +1061 -0
- lsurf/visualization/ocean_simulation_plots.py +999 -0
- lsurf/visualization/polarization_plots.py +916 -0
- lsurf/visualization/raytracing_plots.py +1521 -0
- lsurf/visualization/ring_detector_plots.py +1867 -0
- lsurf/visualization/time_spread_plots.py +531 -0
- lsurf-1.0.0.dist-info/METADATA +381 -0
- lsurf-1.0.0.dist-info/RECORD +180 -0
- lsurf-1.0.0.dist-info/WHEEL +5 -0
- lsurf-1.0.0.dist-info/entry_points.txt +2 -0
- lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
- lsurf-1.0.0.dist-info/top_level.txt +1 -0
lsurf/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# PEP 561 marker file - this package supports type checking
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# The Clear BSD License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Tobias Heibges
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted (subject to the limitations in the disclaimer
|
|
8
|
+
# below) provided that the following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
18
|
+
# contributors may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
22
|
+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
24
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
27
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
28
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
29
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
30
|
+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
Simulation Module
|
|
36
|
+
|
|
37
|
+
Provides the main Simulation class and supporting components for
|
|
38
|
+
ray tracing simulations.
|
|
39
|
+
|
|
40
|
+
Components:
|
|
41
|
+
- Simulation: Main class that coordinates ray tracing
|
|
42
|
+
- SimulationConfig: Configuration dataclass
|
|
43
|
+
- SimulationOrchestrator: Low-level orchestration (advanced use)
|
|
44
|
+
- SimulationResult: Result dataclass with statistics
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from .config import SimulationConfig
|
|
48
|
+
from .orchestrator import SimulationOrchestrator
|
|
49
|
+
from .result import (
|
|
50
|
+
SimulationResult,
|
|
51
|
+
SimulationStatistics,
|
|
52
|
+
SurfaceHitRecord,
|
|
53
|
+
SurfaceHitStats,
|
|
54
|
+
merge_recorded_rays,
|
|
55
|
+
)
|
|
56
|
+
from .simulation import Simulation
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Main classes
|
|
60
|
+
"Simulation",
|
|
61
|
+
"SimulationConfig",
|
|
62
|
+
"SimulationOrchestrator",
|
|
63
|
+
# Results
|
|
64
|
+
"SimulationResult",
|
|
65
|
+
"SimulationStatistics",
|
|
66
|
+
"SurfaceHitRecord",
|
|
67
|
+
"SurfaceHitStats",
|
|
68
|
+
# Utilities
|
|
69
|
+
"merge_recorded_rays",
|
|
70
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# The Clear BSD License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Tobias Heibges
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted (subject to the limitations in the disclaimer
|
|
8
|
+
# below) provided that the following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
18
|
+
# contributors may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
22
|
+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
24
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
27
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
28
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
29
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
30
|
+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
Simulation Configuration
|
|
36
|
+
|
|
37
|
+
Contains the SimulationConfig dataclass for configuring ray tracing simulations.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SimulationConfig:
|
|
45
|
+
"""
|
|
46
|
+
Configuration for ray tracing simulation.
|
|
47
|
+
|
|
48
|
+
Attributes
|
|
49
|
+
----------
|
|
50
|
+
step_size : float
|
|
51
|
+
Maximum integration step size in meters (default 100.0).
|
|
52
|
+
min_step_size : float
|
|
53
|
+
Minimum step size in meters for adaptive stepping (default 3e-4 = 0.3mm).
|
|
54
|
+
This provides ~1ps time resolution near surfaces.
|
|
55
|
+
adaptive_stepping : bool
|
|
56
|
+
Whether to use adaptive step sizing near surfaces (default True).
|
|
57
|
+
When enabled, steps decrease as rays approach surfaces for precise timing.
|
|
58
|
+
surface_proximity_factor : float
|
|
59
|
+
Step size = distance * factor when within proximity threshold (default 0.5).
|
|
60
|
+
surface_proximity_threshold : float
|
|
61
|
+
Distance (in meters) within which adaptive stepping activates (default 10.0).
|
|
62
|
+
max_steps_per_leg : int
|
|
63
|
+
Maximum steps before forcing surface check (default 10000).
|
|
64
|
+
max_bounces : int
|
|
65
|
+
Maximum surface interactions before termination (default 10).
|
|
66
|
+
min_intensity : float
|
|
67
|
+
Intensity threshold below which rays are terminated (default 1e-10).
|
|
68
|
+
bounding_radius : float
|
|
69
|
+
Radius of bounding sphere in meters (default 500_000.0).
|
|
70
|
+
bounding_center : tuple[float, float, float]
|
|
71
|
+
Center of bounding sphere (default (0.0, 0.0, -6.371e6) for Earth center).
|
|
72
|
+
apply_absorption : bool
|
|
73
|
+
Whether to apply Beer-Lambert absorption (default True).
|
|
74
|
+
polarization : str
|
|
75
|
+
Polarization state: 's', 'p', or 'unpolarized' (default 'unpolarized').
|
|
76
|
+
track_polarization_vector : bool
|
|
77
|
+
Whether to track 3D polarization vectors through interactions (default False).
|
|
78
|
+
track_surface_hits : bool
|
|
79
|
+
Whether to store intermediate surface hit positions (default False).
|
|
80
|
+
When enabled, SimulationResult.surface_hits will contain hit positions
|
|
81
|
+
for all optical surfaces, useful for visualization.
|
|
82
|
+
track_refracted_rays : bool
|
|
83
|
+
Whether to continue propagating refracted rays from optical surfaces (default False).
|
|
84
|
+
When False, only reflected rays continue; refracted rays are discarded.
|
|
85
|
+
Set to False for simulations where only reflected light is of interest
|
|
86
|
+
(e.g., ocean surface reflection where underwater propagation is not needed).
|
|
87
|
+
use_gpu : bool
|
|
88
|
+
Whether to use GPU acceleration if available (default True).
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> config = SimulationConfig(step_size=50.0, max_bounces=5)
|
|
93
|
+
>>> sim = Simulation(geometry, config)
|
|
94
|
+
|
|
95
|
+
Notes
|
|
96
|
+
-----
|
|
97
|
+
Adaptive stepping provides sub-nanosecond timing precision near surfaces:
|
|
98
|
+
|
|
99
|
+
| Distance to Surface | Step Size | Time Resolution |
|
|
100
|
+
|---------------------|-----------|-----------------|
|
|
101
|
+
| > 10m | 100m | ~333ns |
|
|
102
|
+
| 5m | 2.5m | ~8ns |
|
|
103
|
+
| 1m | 0.5m | ~1.7ns |
|
|
104
|
+
| 0.1m | 0.05m | ~167ps |
|
|
105
|
+
| < 0.6mm | 0.3mm | ~1ps (minimum) |
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
step_size: float = 100.0
|
|
109
|
+
min_step_size: float = 3e-4 # 0.3mm → ~1ps time resolution
|
|
110
|
+
adaptive_stepping: bool = True
|
|
111
|
+
surface_proximity_factor: float = 0.5 # Step = distance * factor when near
|
|
112
|
+
surface_proximity_threshold: float = 10.0 # Start adapting within this distance (m)
|
|
113
|
+
max_steps_per_leg: int = 10000
|
|
114
|
+
max_bounces: int = 10
|
|
115
|
+
min_intensity: float = 1e-10
|
|
116
|
+
bounding_radius: float = 500_000.0
|
|
117
|
+
bounding_center: tuple[float, float, float] = (0.0, 0.0, -6.371e6)
|
|
118
|
+
apply_absorption: bool = True
|
|
119
|
+
polarization: str = "unpolarized"
|
|
120
|
+
track_polarization_vector: bool = False
|
|
121
|
+
track_surface_hits: bool = False
|
|
122
|
+
track_refracted_rays: bool = False
|
|
123
|
+
use_gpu: bool = True
|
|
124
|
+
|
|
125
|
+
def __post_init__(self) -> None:
|
|
126
|
+
"""Validate configuration."""
|
|
127
|
+
if self.step_size <= 0:
|
|
128
|
+
raise ValueError(f"step_size must be positive, got {self.step_size}")
|
|
129
|
+
if self.min_step_size <= 0:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"min_step_size must be positive, got {self.min_step_size}"
|
|
132
|
+
)
|
|
133
|
+
if self.min_step_size > self.step_size:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"min_step_size ({self.min_step_size}) must be <= step_size ({self.step_size})"
|
|
136
|
+
)
|
|
137
|
+
if self.surface_proximity_factor <= 0 or self.surface_proximity_factor > 1:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"surface_proximity_factor must be in (0, 1], got {self.surface_proximity_factor}"
|
|
140
|
+
)
|
|
141
|
+
if self.surface_proximity_threshold <= 0:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"surface_proximity_threshold must be positive, got {self.surface_proximity_threshold}"
|
|
144
|
+
)
|
|
145
|
+
if self.max_steps_per_leg <= 0:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"max_steps_per_leg must be positive, got {self.max_steps_per_leg}"
|
|
148
|
+
)
|
|
149
|
+
if self.max_bounces < 0:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"max_bounces must be non-negative, got {self.max_bounces}"
|
|
152
|
+
)
|
|
153
|
+
if self.min_intensity < 0:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"min_intensity must be non-negative, got {self.min_intensity}"
|
|
156
|
+
)
|
|
157
|
+
if self.bounding_radius <= 0:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"bounding_radius must be positive, got {self.bounding_radius}"
|
|
160
|
+
)
|
|
161
|
+
if self.polarization not in ("s", "p", "unpolarized"):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"polarization must be 's', 'p', or 'unpolarized', got '{self.polarization}'"
|
|
164
|
+
)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# The Clear BSD License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Tobias Heibges
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted (subject to the limitations in the disclaimer
|
|
8
|
+
# below) provided that the following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
18
|
+
# contributors may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
22
|
+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
24
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
27
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
28
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
29
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
30
|
+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
Simulation Orchestrator
|
|
36
|
+
|
|
37
|
+
Coordinates the propagation loop for ray tracing simulations.
|
|
38
|
+
|
|
39
|
+
Main loop:
|
|
40
|
+
1. MaterialPropagator.propagate_to_surface(rays, max_steps)
|
|
41
|
+
-> Returns HitData (which rays hit which surfaces)
|
|
42
|
+
2. SurfaceInteractionProcessor.process_hits(rays, hit_data)
|
|
43
|
+
-> DETECTOR: record hits
|
|
44
|
+
-> ABSORBER: terminate
|
|
45
|
+
-> OPTICAL: create reflected/refracted rays
|
|
46
|
+
3. Merge new rays (reflected/refracted) into active batch
|
|
47
|
+
4. Apply termination filters (bounds, min_intensity)
|
|
48
|
+
5. Compact batch (remove inactive rays)
|
|
49
|
+
6. Repeat until no active rays or max_bounces
|
|
50
|
+
|
|
51
|
+
This is the third component in the propagator architecture:
|
|
52
|
+
MaterialPropagator -> SurfaceInteractionProcessor -> SimulationOrchestrator
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import logging
|
|
58
|
+
from typing import TYPE_CHECKING
|
|
59
|
+
|
|
60
|
+
import numpy as np
|
|
61
|
+
|
|
62
|
+
from .config import SimulationConfig
|
|
63
|
+
from .result import (
|
|
64
|
+
SimulationResult,
|
|
65
|
+
SimulationStatistics,
|
|
66
|
+
SurfaceHitRecord,
|
|
67
|
+
merge_recorded_rays,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from ..geometry import Geometry
|
|
72
|
+
from ..utilities.ray_data import RayBatch
|
|
73
|
+
|
|
74
|
+
logger = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SimulationOrchestrator:
|
|
78
|
+
"""
|
|
79
|
+
Coordinates the ray tracing simulation loop.
|
|
80
|
+
|
|
81
|
+
Uses MaterialPropagator for GPU-accelerated propagation with surface
|
|
82
|
+
detection, and SurfaceInteractionProcessor for handling surface
|
|
83
|
+
interactions (Fresnel, detection, absorption).
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
geometry : Geometry
|
|
88
|
+
Pre-built geometry from GeometryBuilder.
|
|
89
|
+
config : SimulationConfig, optional
|
|
90
|
+
Simulation configuration. Uses defaults if not provided.
|
|
91
|
+
use_gpu : bool, optional
|
|
92
|
+
Override GPU setting from config.
|
|
93
|
+
|
|
94
|
+
Examples
|
|
95
|
+
--------
|
|
96
|
+
>>> from lsurf.geometry import GeometryBuilder
|
|
97
|
+
>>> from lsurf.simulation import SimulationOrchestrator, SimulationConfig
|
|
98
|
+
>>>
|
|
99
|
+
>>> geometry = builder.build()
|
|
100
|
+
>>> config = SimulationConfig(step_size=100.0, max_bounces=5)
|
|
101
|
+
>>> orchestrator = SimulationOrchestrator(geometry, config)
|
|
102
|
+
>>> result = orchestrator.run(rays)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
geometry: "Geometry",
|
|
108
|
+
config: SimulationConfig | None = None,
|
|
109
|
+
use_gpu: bool | None = None,
|
|
110
|
+
):
|
|
111
|
+
from ..propagation.propagators.material_propagator import MaterialPropagator
|
|
112
|
+
from ..propagation.propagators.surface_interaction import (
|
|
113
|
+
SurfaceInteractionProcessor,
|
|
114
|
+
)
|
|
115
|
+
from ..utilities.recording_sphere import RecordedRays
|
|
116
|
+
|
|
117
|
+
self._geometry = geometry
|
|
118
|
+
self._config = config if config is not None else SimulationConfig()
|
|
119
|
+
|
|
120
|
+
# Override GPU setting if specified
|
|
121
|
+
effective_use_gpu = use_gpu if use_gpu is not None else self._config.use_gpu
|
|
122
|
+
|
|
123
|
+
# Get all surfaces
|
|
124
|
+
surfaces = geometry.to_surface_list()
|
|
125
|
+
|
|
126
|
+
# Create propagator with background material
|
|
127
|
+
self._propagator = MaterialPropagator(
|
|
128
|
+
material=geometry.background_material,
|
|
129
|
+
surfaces=surfaces,
|
|
130
|
+
use_gpu=effective_use_gpu,
|
|
131
|
+
apply_absorption=self._config.apply_absorption,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Create interaction processor
|
|
135
|
+
self._interaction_processor = SurfaceInteractionProcessor(
|
|
136
|
+
surfaces=surfaces,
|
|
137
|
+
polarization=self._config.polarization,
|
|
138
|
+
track_polarization_vector=self._config.track_polarization_vector,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# RecordedRays class for creating empty results
|
|
142
|
+
self._RecordedRays = RecordedRays
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def geometry(self) -> "Geometry":
|
|
146
|
+
"""The simulation geometry."""
|
|
147
|
+
return self._geometry
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def config(self) -> SimulationConfig:
|
|
151
|
+
"""The simulation configuration."""
|
|
152
|
+
return self._config
|
|
153
|
+
|
|
154
|
+
def run(self, rays: "RayBatch") -> SimulationResult:
|
|
155
|
+
"""
|
|
156
|
+
Run the ray tracing simulation.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
rays : RayBatch
|
|
161
|
+
Initial rays to trace.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
SimulationResult
|
|
166
|
+
Complete simulation results including detected rays and statistics.
|
|
167
|
+
"""
|
|
168
|
+
from ..utilities.ray_data import merge_ray_batches, create_ray_batch
|
|
169
|
+
|
|
170
|
+
# Initialize statistics
|
|
171
|
+
stats = SimulationStatistics(
|
|
172
|
+
total_rays_initial=rays.num_rays,
|
|
173
|
+
total_rays_created=rays.num_rays,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
logger.info(
|
|
177
|
+
"Starting simulation: %d rays, max %d bounces, step_size=%.3g m",
|
|
178
|
+
rays.num_rays,
|
|
179
|
+
self._config.max_bounces,
|
|
180
|
+
self._config.step_size,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Clone rays to avoid modifying input
|
|
184
|
+
active_rays = rays.clone()
|
|
185
|
+
|
|
186
|
+
# Storage for collected results
|
|
187
|
+
all_detector_hits: list = []
|
|
188
|
+
detector_counts: dict[str, int] = {}
|
|
189
|
+
|
|
190
|
+
# Storage for surface hits if tracking enabled
|
|
191
|
+
surface_hits: dict[str, list[SurfaceHitRecord]] | None = None
|
|
192
|
+
if self._config.track_surface_hits:
|
|
193
|
+
surface_hits = {}
|
|
194
|
+
|
|
195
|
+
# Parse bounding center
|
|
196
|
+
bounding_center = np.array(self._config.bounding_center, dtype=np.float64)
|
|
197
|
+
|
|
198
|
+
# Main simulation loop
|
|
199
|
+
for bounce in range(self._config.max_bounces):
|
|
200
|
+
stats.bounces_completed = bounce + 1
|
|
201
|
+
|
|
202
|
+
# Check for active rays
|
|
203
|
+
if active_rays.num_active == 0:
|
|
204
|
+
logger.debug("Bounce %d: No active rays remaining", bounce + 1)
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
"Bounce %d/%d: %d active rays",
|
|
209
|
+
bounce + 1,
|
|
210
|
+
self._config.max_bounces,
|
|
211
|
+
active_rays.num_active,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Compact batch if many inactive rays
|
|
215
|
+
if active_rays.num_active < active_rays.num_rays * 0.5:
|
|
216
|
+
active_rays = active_rays.compact()
|
|
217
|
+
logger.debug(" Compacted batch to %d rays", active_rays.num_rays)
|
|
218
|
+
|
|
219
|
+
if active_rays.num_rays == 0:
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
# Step 1: Propagate to surface
|
|
223
|
+
hit_data = self._propagator.propagate_to_surface(
|
|
224
|
+
rays=active_rays,
|
|
225
|
+
step_size=self._config.step_size,
|
|
226
|
+
max_steps=self._config.max_steps_per_leg,
|
|
227
|
+
adaptive_stepping=self._config.adaptive_stepping,
|
|
228
|
+
min_step_size=self._config.min_step_size,
|
|
229
|
+
surface_proximity_factor=self._config.surface_proximity_factor,
|
|
230
|
+
surface_proximity_threshold=self._config.surface_proximity_threshold,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Step 2: Process surface interactions
|
|
234
|
+
interaction_result = self._interaction_processor.process_hits(
|
|
235
|
+
rays=active_rays,
|
|
236
|
+
hit_data=hit_data,
|
|
237
|
+
track_surface_hits=self._config.track_surface_hits,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Log interaction results
|
|
241
|
+
logger.debug(
|
|
242
|
+
" Interactions: %d detected, %d absorbed, %d reflected",
|
|
243
|
+
interaction_result.detected_count,
|
|
244
|
+
interaction_result.absorbed_count,
|
|
245
|
+
(
|
|
246
|
+
interaction_result.reflected_rays.num_rays
|
|
247
|
+
if interaction_result.reflected_rays
|
|
248
|
+
else 0
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Collect detector hits
|
|
253
|
+
for name, recorded in interaction_result.detector_hits.items():
|
|
254
|
+
all_detector_hits.append(recorded)
|
|
255
|
+
detector_counts[name] = detector_counts.get(name, 0) + recorded.num_rays
|
|
256
|
+
|
|
257
|
+
# Collect optical surface hits if tracking enabled
|
|
258
|
+
if surface_hits is not None:
|
|
259
|
+
for hit in interaction_result.optical_hits:
|
|
260
|
+
if hit.surface_name not in surface_hits:
|
|
261
|
+
surface_hits[hit.surface_name] = []
|
|
262
|
+
surface_hits[hit.surface_name].append(
|
|
263
|
+
SurfaceHitRecord(
|
|
264
|
+
surface_name=hit.surface_name,
|
|
265
|
+
positions=hit.positions,
|
|
266
|
+
directions=hit.directions,
|
|
267
|
+
intensities=hit.intensities,
|
|
268
|
+
wavelengths=hit.wavelengths,
|
|
269
|
+
bounce=bounce,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Update statistics
|
|
274
|
+
stats.rays_detected += interaction_result.detected_count
|
|
275
|
+
stats.rays_absorbed += interaction_result.absorbed_count
|
|
276
|
+
|
|
277
|
+
# Step 3: Collect rays for next iteration
|
|
278
|
+
next_rays_list = []
|
|
279
|
+
|
|
280
|
+
# Rays that didn't hit any surface continue
|
|
281
|
+
no_hit_rays = self._propagator.extract_no_hits(active_rays, hit_data)
|
|
282
|
+
if no_hit_rays.num_rays > 0:
|
|
283
|
+
next_rays_list.append(no_hit_rays)
|
|
284
|
+
|
|
285
|
+
# Reflected rays continue
|
|
286
|
+
if (
|
|
287
|
+
interaction_result.reflected_rays is not None
|
|
288
|
+
and interaction_result.reflected_rays.num_rays > 0
|
|
289
|
+
):
|
|
290
|
+
next_rays_list.append(interaction_result.reflected_rays)
|
|
291
|
+
stats.total_rays_created += interaction_result.reflected_rays.num_rays
|
|
292
|
+
|
|
293
|
+
# Refracted rays continue (only if tracking is enabled)
|
|
294
|
+
if (
|
|
295
|
+
self._config.track_refracted_rays
|
|
296
|
+
and interaction_result.refracted_rays is not None
|
|
297
|
+
and interaction_result.refracted_rays.num_rays > 0
|
|
298
|
+
):
|
|
299
|
+
next_rays_list.append(interaction_result.refracted_rays)
|
|
300
|
+
stats.total_rays_created += interaction_result.refracted_rays.num_rays
|
|
301
|
+
|
|
302
|
+
# Merge all continuing rays
|
|
303
|
+
if not next_rays_list:
|
|
304
|
+
active_rays = create_ray_batch(0)
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
active_rays = merge_ray_batches(next_rays_list)
|
|
308
|
+
|
|
309
|
+
# Step 4: Apply termination filters
|
|
310
|
+
|
|
311
|
+
# Bounding sphere check
|
|
312
|
+
distances_from_center = np.linalg.norm(
|
|
313
|
+
active_rays.positions - bounding_center, axis=1
|
|
314
|
+
)
|
|
315
|
+
outside_bounds = distances_from_center >= self._config.bounding_radius
|
|
316
|
+
num_outside = np.sum(outside_bounds & active_rays.active)
|
|
317
|
+
active_rays.active[outside_bounds] = False
|
|
318
|
+
stats.rays_terminated_bounds += int(num_outside)
|
|
319
|
+
|
|
320
|
+
# Intensity threshold
|
|
321
|
+
weak_mask = active_rays.active & (
|
|
322
|
+
active_rays.intensities < self._config.min_intensity
|
|
323
|
+
)
|
|
324
|
+
num_weak = np.sum(weak_mask)
|
|
325
|
+
active_rays.active[weak_mask] = False
|
|
326
|
+
stats.rays_terminated_intensity += int(num_weak)
|
|
327
|
+
|
|
328
|
+
# Log termination counts if any
|
|
329
|
+
if num_outside > 0 or num_weak > 0:
|
|
330
|
+
logger.debug(
|
|
331
|
+
" Terminated: %d out-of-bounds, %d below intensity threshold",
|
|
332
|
+
num_outside,
|
|
333
|
+
num_weak,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Track max depth
|
|
337
|
+
if active_rays.num_rays > 0:
|
|
338
|
+
max_gen = np.max(active_rays.generations)
|
|
339
|
+
stats.max_depth_reached = max(stats.max_depth_reached, int(max_gen))
|
|
340
|
+
|
|
341
|
+
# Handle rays remaining after max bounces
|
|
342
|
+
if active_rays.num_rays > 0:
|
|
343
|
+
remaining_active = np.sum(active_rays.active)
|
|
344
|
+
stats.rays_terminated_max_bounces += int(remaining_active)
|
|
345
|
+
|
|
346
|
+
# Merge all detector hits
|
|
347
|
+
detected = merge_recorded_rays(all_detector_hits)
|
|
348
|
+
|
|
349
|
+
# Log final summary
|
|
350
|
+
logger.info(
|
|
351
|
+
"Simulation complete: %d bounces, %d detected, %d absorbed, %d remaining",
|
|
352
|
+
stats.bounces_completed,
|
|
353
|
+
stats.rays_detected,
|
|
354
|
+
stats.rays_absorbed,
|
|
355
|
+
active_rays.num_active,
|
|
356
|
+
)
|
|
357
|
+
if detector_counts:
|
|
358
|
+
for name, count in detector_counts.items():
|
|
359
|
+
logger.info(" Detector '%s': %d hits", name, count)
|
|
360
|
+
|
|
361
|
+
# Create final result
|
|
362
|
+
return SimulationResult(
|
|
363
|
+
detected=detected,
|
|
364
|
+
remaining=active_rays,
|
|
365
|
+
statistics=stats,
|
|
366
|
+
detections_per_surface=detector_counts,
|
|
367
|
+
surface_hits=surface_hits,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def run_single_bounce(
|
|
371
|
+
self,
|
|
372
|
+
rays: "RayBatch",
|
|
373
|
+
) -> tuple["RayBatch", "SimulationResult"]:
|
|
374
|
+
"""
|
|
375
|
+
Run a single propagation + interaction cycle.
|
|
376
|
+
|
|
377
|
+
Useful for step-by-step debugging or custom simulation loops.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
rays : RayBatch
|
|
382
|
+
Rays to propagate.
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
continuing_rays : RayBatch
|
|
387
|
+
Rays that should continue (reflected, refracted, no-hit).
|
|
388
|
+
result : SimulationResult
|
|
389
|
+
Results from this single bounce (detections, absorptions).
|
|
390
|
+
"""
|
|
391
|
+
from ..utilities.ray_data import merge_ray_batches, create_ray_batch
|
|
392
|
+
|
|
393
|
+
stats = SimulationStatistics(
|
|
394
|
+
total_rays_initial=rays.num_rays,
|
|
395
|
+
total_rays_created=rays.num_rays,
|
|
396
|
+
bounces_completed=1,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Propagate to surface
|
|
400
|
+
hit_data = self._propagator.propagate_to_surface(
|
|
401
|
+
rays=rays,
|
|
402
|
+
step_size=self._config.step_size,
|
|
403
|
+
max_steps=self._config.max_steps_per_leg,
|
|
404
|
+
adaptive_stepping=self._config.adaptive_stepping,
|
|
405
|
+
min_step_size=self._config.min_step_size,
|
|
406
|
+
surface_proximity_factor=self._config.surface_proximity_factor,
|
|
407
|
+
surface_proximity_threshold=self._config.surface_proximity_threshold,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Process interactions
|
|
411
|
+
interaction_result = self._interaction_processor.process_hits(
|
|
412
|
+
rays=rays,
|
|
413
|
+
hit_data=hit_data,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Collect statistics
|
|
417
|
+
stats.rays_detected = interaction_result.detected_count
|
|
418
|
+
stats.rays_absorbed = interaction_result.absorbed_count
|
|
419
|
+
|
|
420
|
+
# Collect detector hits
|
|
421
|
+
detector_counts: dict[str, int] = {}
|
|
422
|
+
for name, recorded in interaction_result.detector_hits.items():
|
|
423
|
+
detector_counts[name] = recorded.num_rays
|
|
424
|
+
|
|
425
|
+
# Merge detector hits
|
|
426
|
+
detected = merge_recorded_rays(list(interaction_result.detector_hits.values()))
|
|
427
|
+
|
|
428
|
+
# Collect continuing rays
|
|
429
|
+
next_rays_list = []
|
|
430
|
+
|
|
431
|
+
no_hit_rays = self._propagator.extract_no_hits(rays, hit_data)
|
|
432
|
+
if no_hit_rays.num_rays > 0:
|
|
433
|
+
next_rays_list.append(no_hit_rays)
|
|
434
|
+
|
|
435
|
+
if (
|
|
436
|
+
interaction_result.reflected_rays is not None
|
|
437
|
+
and interaction_result.reflected_rays.num_rays > 0
|
|
438
|
+
):
|
|
439
|
+
next_rays_list.append(interaction_result.reflected_rays)
|
|
440
|
+
stats.total_rays_created += interaction_result.reflected_rays.num_rays
|
|
441
|
+
|
|
442
|
+
if (
|
|
443
|
+
self._config.track_refracted_rays
|
|
444
|
+
and interaction_result.refracted_rays is not None
|
|
445
|
+
and interaction_result.refracted_rays.num_rays > 0
|
|
446
|
+
):
|
|
447
|
+
next_rays_list.append(interaction_result.refracted_rays)
|
|
448
|
+
stats.total_rays_created += interaction_result.refracted_rays.num_rays
|
|
449
|
+
|
|
450
|
+
if next_rays_list:
|
|
451
|
+
continuing_rays = merge_ray_batches(next_rays_list)
|
|
452
|
+
else:
|
|
453
|
+
continuing_rays = create_ray_batch(0)
|
|
454
|
+
|
|
455
|
+
result = SimulationResult(
|
|
456
|
+
detected=detected,
|
|
457
|
+
remaining=continuing_rays,
|
|
458
|
+
statistics=stats,
|
|
459
|
+
detections_per_surface=detector_counts,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return continuing_rays, result
|