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
|
@@ -0,0 +1,299 @@
|
|
|
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 Result Data Structures
|
|
36
|
+
|
|
37
|
+
Contains the result dataclasses returned by the Simulation/Orchestrator classes.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
import numpy.typing as npt
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from ..detectors.results import DetectorResult
|
|
48
|
+
from ..utilities.ray_data import RayBatch
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class SurfaceHitRecord:
|
|
53
|
+
"""
|
|
54
|
+
Record of ray hits on a surface.
|
|
55
|
+
|
|
56
|
+
Stores positions and directions of rays that hit a specific surface,
|
|
57
|
+
useful for visualization and analysis of intermediate surface interactions.
|
|
58
|
+
|
|
59
|
+
Attributes
|
|
60
|
+
----------
|
|
61
|
+
surface_name : str
|
|
62
|
+
Name of the surface.
|
|
63
|
+
positions : ndarray, shape (N, 3)
|
|
64
|
+
Hit positions in world coordinates.
|
|
65
|
+
directions : ndarray, shape (N, 3)
|
|
66
|
+
Ray directions at hit points.
|
|
67
|
+
intensities : ndarray, shape (N,)
|
|
68
|
+
Ray intensities at hit.
|
|
69
|
+
wavelengths : ndarray, shape (N,)
|
|
70
|
+
Ray wavelengths.
|
|
71
|
+
bounce : int
|
|
72
|
+
Which bounce iteration this hit occurred on (0-indexed).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
surface_name: str
|
|
76
|
+
positions: npt.NDArray[np.float32]
|
|
77
|
+
directions: npt.NDArray[np.float32]
|
|
78
|
+
intensities: npt.NDArray[np.float32]
|
|
79
|
+
wavelengths: npt.NDArray[np.float32]
|
|
80
|
+
bounce: int
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def num_hits(self) -> int:
|
|
84
|
+
"""Number of hits recorded."""
|
|
85
|
+
return len(self.positions)
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
return (
|
|
89
|
+
f"SurfaceHitRecord(surface_name={self.surface_name!r}, "
|
|
90
|
+
f"num_hits={self.num_hits}, bounce={self.bounce})"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class SimulationStatistics:
|
|
96
|
+
"""
|
|
97
|
+
Statistics from a simulation run.
|
|
98
|
+
|
|
99
|
+
Attributes
|
|
100
|
+
----------
|
|
101
|
+
total_rays_initial : int
|
|
102
|
+
Number of rays at simulation start.
|
|
103
|
+
total_rays_created : int
|
|
104
|
+
Total rays including split rays.
|
|
105
|
+
rays_detected : int
|
|
106
|
+
Rays that hit detector surfaces.
|
|
107
|
+
rays_absorbed : int
|
|
108
|
+
Rays terminated by absorber surfaces.
|
|
109
|
+
rays_terminated_intensity : int
|
|
110
|
+
Rays terminated due to low intensity.
|
|
111
|
+
rays_terminated_bounds : int
|
|
112
|
+
Rays that exited bounding sphere.
|
|
113
|
+
rays_terminated_max_bounces : int
|
|
114
|
+
Rays that reached max bounce limit.
|
|
115
|
+
bounces_completed : int
|
|
116
|
+
Number of bounce iterations completed.
|
|
117
|
+
max_depth_reached : int
|
|
118
|
+
Maximum tree depth from ray splitting.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
total_rays_initial: int = 0
|
|
122
|
+
total_rays_created: int = 0
|
|
123
|
+
rays_detected: int = 0
|
|
124
|
+
rays_absorbed: int = 0
|
|
125
|
+
rays_terminated_intensity: int = 0
|
|
126
|
+
rays_terminated_bounds: int = 0
|
|
127
|
+
rays_terminated_max_bounces: int = 0
|
|
128
|
+
bounces_completed: int = 0
|
|
129
|
+
max_depth_reached: int = 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class SimulationResult:
|
|
134
|
+
"""
|
|
135
|
+
Complete result from a ray tracing simulation.
|
|
136
|
+
|
|
137
|
+
Attributes
|
|
138
|
+
----------
|
|
139
|
+
detected : DetectorResult
|
|
140
|
+
Rays that were recorded by detector surfaces.
|
|
141
|
+
remaining : RayBatch
|
|
142
|
+
Rays that are still active after simulation completed
|
|
143
|
+
(either didn't hit any surface or exceeded max bounces).
|
|
144
|
+
statistics : SimulationStatistics
|
|
145
|
+
Detailed simulation statistics.
|
|
146
|
+
detections_per_surface : dict
|
|
147
|
+
Number of detections per detector surface name.
|
|
148
|
+
surface_hits : dict or None
|
|
149
|
+
If track_surface_hits was enabled, contains SurfaceHitRecord objects
|
|
150
|
+
for each optical surface, keyed by surface name. Each surface may have
|
|
151
|
+
multiple records (one per bounce). If disabled, this is None.
|
|
152
|
+
|
|
153
|
+
Examples
|
|
154
|
+
--------
|
|
155
|
+
>>> result = sim.run(rays)
|
|
156
|
+
>>> print(f"Detected {result.statistics.rays_detected} rays")
|
|
157
|
+
>>> print(f"Completed in {result.statistics.bounces_completed} bounces")
|
|
158
|
+
>>>
|
|
159
|
+
>>> # Access detection results
|
|
160
|
+
>>> print(f"Total detected intensity: {result.detected.total_intensity:.3e}")
|
|
161
|
+
>>> times = result.detected.times
|
|
162
|
+
>>> positions = result.detected.positions
|
|
163
|
+
>>>
|
|
164
|
+
>>> # Access surface hits (if track_surface_hits=True)
|
|
165
|
+
>>> if result.surface_hits:
|
|
166
|
+
... for name, records in result.surface_hits.items():
|
|
167
|
+
... for rec in records:
|
|
168
|
+
... print(f"{name} bounce {rec.bounce}: {rec.num_hits} hits")
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
detected: "DetectorResult"
|
|
172
|
+
remaining: "RayBatch"
|
|
173
|
+
statistics: SimulationStatistics
|
|
174
|
+
detections_per_surface: dict[str, int] = field(default_factory=dict)
|
|
175
|
+
surface_hits: dict[str, list[SurfaceHitRecord]] | None = None
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def num_detected(self) -> int:
|
|
179
|
+
"""Number of rays that hit detector surfaces."""
|
|
180
|
+
return self.detected.num_rays
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def num_remaining(self) -> int:
|
|
184
|
+
"""Number of rays still active."""
|
|
185
|
+
return self.remaining.num_rays
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def bounces(self) -> int:
|
|
189
|
+
"""Number of bounce iterations performed (backwards compatibility)."""
|
|
190
|
+
return self.statistics.bounces_completed
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def total_rays_processed(self) -> int:
|
|
194
|
+
"""Total rays processed including splits (backwards compatibility)."""
|
|
195
|
+
return self.statistics.total_rays_created
|
|
196
|
+
|
|
197
|
+
def get_surface_hit_positions(self, surface_name: str) -> npt.NDArray[np.float32]:
|
|
198
|
+
"""
|
|
199
|
+
Get all hit positions for a specific surface across all bounces.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
surface_name : str
|
|
204
|
+
Name of the surface.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
ndarray, shape (N, 3)
|
|
209
|
+
Concatenated hit positions from all bounces. Empty array if no hits
|
|
210
|
+
or if track_surface_hits was not enabled.
|
|
211
|
+
"""
|
|
212
|
+
if self.surface_hits is None or surface_name not in self.surface_hits:
|
|
213
|
+
return np.empty((0, 3), dtype=np.float32)
|
|
214
|
+
|
|
215
|
+
records = self.surface_hits[surface_name]
|
|
216
|
+
if not records:
|
|
217
|
+
return np.empty((0, 3), dtype=np.float32)
|
|
218
|
+
|
|
219
|
+
return np.concatenate([r.positions for r in records], axis=0)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class SurfaceHitStats:
|
|
224
|
+
"""
|
|
225
|
+
Statistics about surface hits during a simulation.
|
|
226
|
+
|
|
227
|
+
Attributes
|
|
228
|
+
----------
|
|
229
|
+
surface_name : str
|
|
230
|
+
Name of the surface.
|
|
231
|
+
hit_count : int
|
|
232
|
+
Number of rays that hit this surface.
|
|
233
|
+
mean_intensity : float
|
|
234
|
+
Mean intensity of rays hitting this surface.
|
|
235
|
+
total_intensity : float
|
|
236
|
+
Sum of intensities of rays hitting this surface.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
surface_name: str
|
|
240
|
+
hit_count: int
|
|
241
|
+
mean_intensity: float
|
|
242
|
+
total_intensity: float
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def merge_detector_results(result_list: list) -> "DetectorResult":
|
|
246
|
+
"""
|
|
247
|
+
Merge multiple DetectorResult objects into a single instance.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
result_list : list of DetectorResult
|
|
252
|
+
List of detector results to merge.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
DetectorResult
|
|
257
|
+
Combined detector results.
|
|
258
|
+
"""
|
|
259
|
+
from ..detectors.results import DetectorResult
|
|
260
|
+
|
|
261
|
+
return DetectorResult.merge(result_list)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Backward compatibility: keep merge_recorded_rays as alias
|
|
265
|
+
def merge_recorded_rays(recorded_list: list) -> "DetectorResult":
|
|
266
|
+
"""
|
|
267
|
+
Merge multiple DetectorResult/RecordedRays into a single instance.
|
|
268
|
+
|
|
269
|
+
This function is provided for backward compatibility. New code should
|
|
270
|
+
use DetectorResult.merge() directly.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
recorded_list : list
|
|
275
|
+
List of DetectorResult or RecordedRays to merge.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
DetectorResult
|
|
280
|
+
Combined results.
|
|
281
|
+
"""
|
|
282
|
+
from ..detectors.results import DetectorResult
|
|
283
|
+
|
|
284
|
+
if not recorded_list:
|
|
285
|
+
return DetectorResult.empty()
|
|
286
|
+
|
|
287
|
+
# Check if we have RecordedRays (old format) or DetectorResult (new format)
|
|
288
|
+
first = recorded_list[0]
|
|
289
|
+
if hasattr(first, "to_detection_events"):
|
|
290
|
+
# Already DetectorResult
|
|
291
|
+
return DetectorResult.merge(recorded_list)
|
|
292
|
+
|
|
293
|
+
# Convert RecordedRays to DetectorResult
|
|
294
|
+
converted = []
|
|
295
|
+
for r in recorded_list:
|
|
296
|
+
if r.num_rays > 0:
|
|
297
|
+
converted.append(DetectorResult.from_recorded_rays(r))
|
|
298
|
+
|
|
299
|
+
return DetectorResult.merge(converted)
|
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
Ray Tracing Simulation
|
|
36
|
+
|
|
37
|
+
The Simulation class uses a Geometry object built via GeometryBuilder
|
|
38
|
+
as its first step, ensuring:
|
|
39
|
+
- Material consistency across surfaces via named media
|
|
40
|
+
- Validation of surface configurations
|
|
41
|
+
- Immutable geometry during simulation
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
import warnings
|
|
48
|
+
from typing import TYPE_CHECKING
|
|
49
|
+
|
|
50
|
+
from ..geometry import Geometry
|
|
51
|
+
from ..surfaces import SurfaceRole
|
|
52
|
+
|
|
53
|
+
from .config import SimulationConfig
|
|
54
|
+
from .orchestrator import SimulationOrchestrator
|
|
55
|
+
from .result import SimulationResult
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from ..utilities.ray_data import RayBatch
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Simulation:
|
|
64
|
+
"""
|
|
65
|
+
Ray tracing simulation with geometry-based configuration.
|
|
66
|
+
|
|
67
|
+
The simulation takes a pre-built Geometry object which defines:
|
|
68
|
+
- All optical surfaces with their materials
|
|
69
|
+
- All detector surfaces
|
|
70
|
+
- The background propagation medium
|
|
71
|
+
- Named media for material consistency
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
geometry : Geometry
|
|
76
|
+
Pre-built geometry from GeometryBuilder containing all surfaces,
|
|
77
|
+
detectors, and materials.
|
|
78
|
+
config : SimulationConfig, optional
|
|
79
|
+
Simulation configuration. Uses defaults if not provided.
|
|
80
|
+
|
|
81
|
+
Examples
|
|
82
|
+
--------
|
|
83
|
+
>>> from lsurf.geometry import GeometryBuilder
|
|
84
|
+
>>> from lsurf.materials import LinsleyAtmosphere, WATER
|
|
85
|
+
>>> from lsurf.surfaces import SphereSurface, PlaneSurface, SurfaceRole
|
|
86
|
+
>>> from lsurf.simulation import Simulation, SimulationConfig
|
|
87
|
+
>>>
|
|
88
|
+
>>> # Build geometry
|
|
89
|
+
>>> EARTH_RADIUS = 6.371e6
|
|
90
|
+
>>> atmosphere = LinsleyAtmosphere()
|
|
91
|
+
>>>
|
|
92
|
+
>>> ocean = SphereSurface(
|
|
93
|
+
... center=(0, 0, -EARTH_RADIUS),
|
|
94
|
+
... radius=EARTH_RADIUS,
|
|
95
|
+
... role=SurfaceRole.OPTICAL,
|
|
96
|
+
... name="ocean",
|
|
97
|
+
... )
|
|
98
|
+
>>> detector = PlaneSurface(
|
|
99
|
+
... point=(0, 0, 35000),
|
|
100
|
+
... normal=(0, 0, 1),
|
|
101
|
+
... role=SurfaceRole.DETECTOR,
|
|
102
|
+
... name="detector_35km",
|
|
103
|
+
... )
|
|
104
|
+
>>>
|
|
105
|
+
>>> geometry = (
|
|
106
|
+
... GeometryBuilder()
|
|
107
|
+
... .register_medium("atmosphere", atmosphere)
|
|
108
|
+
... .register_medium("ocean", WATER)
|
|
109
|
+
... .set_background("atmosphere")
|
|
110
|
+
... .add_surface(ocean, front="atmosphere", back="ocean")
|
|
111
|
+
... .add_detector(detector)
|
|
112
|
+
... .build()
|
|
113
|
+
... )
|
|
114
|
+
>>>
|
|
115
|
+
>>> # Create simulation with geometry
|
|
116
|
+
>>> config = SimulationConfig(step_size=100.0, max_bounces=5)
|
|
117
|
+
>>> sim = Simulation(geometry, config)
|
|
118
|
+
>>> result = sim.run(rays)
|
|
119
|
+
>>> print(f"Detected: {result.statistics.rays_detected}")
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
geometry: Geometry,
|
|
125
|
+
config: SimulationConfig | None = None,
|
|
126
|
+
):
|
|
127
|
+
self._geometry = geometry
|
|
128
|
+
self._config = config if config is not None else SimulationConfig()
|
|
129
|
+
|
|
130
|
+
# Extract surfaces list (surfaces + detectors)
|
|
131
|
+
self._all_surfaces = geometry.to_surface_list()
|
|
132
|
+
|
|
133
|
+
# Build surface indices by role for efficient processing
|
|
134
|
+
self._detector_indices: list[int] = []
|
|
135
|
+
self._optical_indices: list[int] = []
|
|
136
|
+
self._absorber_indices: list[int] = []
|
|
137
|
+
|
|
138
|
+
for i, surface in enumerate(self._all_surfaces):
|
|
139
|
+
if surface.role == SurfaceRole.DETECTOR:
|
|
140
|
+
self._detector_indices.append(i)
|
|
141
|
+
elif surface.role == SurfaceRole.OPTICAL:
|
|
142
|
+
self._optical_indices.append(i)
|
|
143
|
+
elif surface.role == SurfaceRole.ABSORBER:
|
|
144
|
+
self._absorber_indices.append(i)
|
|
145
|
+
|
|
146
|
+
# Create orchestrator lazily
|
|
147
|
+
self._orchestrator: SimulationOrchestrator | None = None
|
|
148
|
+
|
|
149
|
+
logger.info(
|
|
150
|
+
"Simulation initialized: %d optical, %d detector, %d absorber surfaces",
|
|
151
|
+
len(self._optical_indices),
|
|
152
|
+
len(self._detector_indices),
|
|
153
|
+
len(self._absorber_indices),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def geometry(self) -> Geometry:
|
|
158
|
+
"""The simulation geometry."""
|
|
159
|
+
return self._geometry
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def config(self) -> SimulationConfig:
|
|
163
|
+
"""The simulation configuration."""
|
|
164
|
+
return self._config
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def num_surfaces(self) -> int:
|
|
168
|
+
"""Total number of surfaces (optical + absorber + detector)."""
|
|
169
|
+
return len(self._all_surfaces)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def detector_surfaces(self) -> list:
|
|
173
|
+
"""List of detector surfaces."""
|
|
174
|
+
return [self._all_surfaces[i] for i in self._detector_indices]
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def optical_surfaces(self) -> list:
|
|
178
|
+
"""List of optical surfaces."""
|
|
179
|
+
return [self._all_surfaces[i] for i in self._optical_indices]
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def absorber_surfaces(self) -> list:
|
|
183
|
+
"""List of absorber surfaces."""
|
|
184
|
+
return [self._all_surfaces[i] for i in self._absorber_indices]
|
|
185
|
+
|
|
186
|
+
def _get_orchestrator(self) -> SimulationOrchestrator:
|
|
187
|
+
"""Get or create the simulation orchestrator."""
|
|
188
|
+
if self._orchestrator is None:
|
|
189
|
+
self._orchestrator = SimulationOrchestrator(
|
|
190
|
+
geometry=self._geometry,
|
|
191
|
+
config=self._config,
|
|
192
|
+
)
|
|
193
|
+
return self._orchestrator
|
|
194
|
+
|
|
195
|
+
def run(self, rays: "RayBatch") -> SimulationResult:
|
|
196
|
+
"""
|
|
197
|
+
Run the ray tracing simulation.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
rays : RayBatch
|
|
202
|
+
Initial rays to trace.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
SimulationResult
|
|
207
|
+
Complete simulation results including:
|
|
208
|
+
- detected: RecordedRays from detector surfaces
|
|
209
|
+
- remaining: RayBatch of rays still active
|
|
210
|
+
- statistics: SimulationStatistics with counts
|
|
211
|
+
- detections_per_surface: dict mapping detector names to hit counts
|
|
212
|
+
|
|
213
|
+
Examples
|
|
214
|
+
--------
|
|
215
|
+
>>> result = sim.run(rays)
|
|
216
|
+
>>> print(f"Detected {result.statistics.rays_detected} rays")
|
|
217
|
+
>>> print(f"Absorbed {result.statistics.rays_absorbed} rays")
|
|
218
|
+
>>> for name, count in result.detections_per_surface.items():
|
|
219
|
+
... print(f" {name}: {count} hits")
|
|
220
|
+
"""
|
|
221
|
+
orchestrator = self._get_orchestrator()
|
|
222
|
+
|
|
223
|
+
# Suppress Numba GPU under-utilization warnings (common with small batches)
|
|
224
|
+
with warnings.catch_warnings():
|
|
225
|
+
warnings.filterwarnings(
|
|
226
|
+
"ignore",
|
|
227
|
+
message=".*Grid size.*GPU under-utilization.*",
|
|
228
|
+
category=UserWarning,
|
|
229
|
+
)
|
|
230
|
+
return orchestrator.run(rays)
|
|
231
|
+
|
|
232
|
+
def run_single_bounce(
|
|
233
|
+
self,
|
|
234
|
+
rays: "RayBatch",
|
|
235
|
+
) -> tuple["RayBatch", SimulationResult]:
|
|
236
|
+
"""
|
|
237
|
+
Run a single propagation + interaction cycle.
|
|
238
|
+
|
|
239
|
+
Useful for step-by-step debugging or custom simulation loops.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
rays : RayBatch
|
|
244
|
+
Rays to propagate.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
continuing_rays : RayBatch
|
|
249
|
+
Rays that should continue (reflected, refracted, no-hit).
|
|
250
|
+
result : SimulationResult
|
|
251
|
+
Results from this single bounce (detections, absorptions).
|
|
252
|
+
"""
|
|
253
|
+
orchestrator = self._get_orchestrator()
|
|
254
|
+
|
|
255
|
+
# Suppress Numba GPU under-utilization warnings (common with small batches)
|
|
256
|
+
with warnings.catch_warnings():
|
|
257
|
+
warnings.filterwarnings(
|
|
258
|
+
"ignore",
|
|
259
|
+
message=".*Grid size.*GPU under-utilization.*",
|
|
260
|
+
category=UserWarning,
|
|
261
|
+
)
|
|
262
|
+
return orchestrator.run_single_bounce(rays)
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
Sources Module - Ray Generation for Raytracing
|
|
36
|
+
|
|
37
|
+
This module provides ray source classes for generating initial ray conditions.
|
|
38
|
+
Each source type generates rays with specific spatial and angular distributions.
|
|
39
|
+
|
|
40
|
+
Available Sources
|
|
41
|
+
-----------------
|
|
42
|
+
RaySource : ABC
|
|
43
|
+
Abstract base class defining the source interface.
|
|
44
|
+
PointSource
|
|
45
|
+
Isotropic point source emitting in all directions.
|
|
46
|
+
CollimatedBeam
|
|
47
|
+
Parallel beam with uniform or Gaussian intensity profile.
|
|
48
|
+
DivergingBeam
|
|
49
|
+
Beam with angular divergence (fiber output, LED).
|
|
50
|
+
UniformDivergingBeam
|
|
51
|
+
Diverging beam with uniform solid angle distribution.
|
|
52
|
+
GaussianBeam
|
|
53
|
+
Gaussian beam following paraxial optics.
|
|
54
|
+
ParallelBeamFromPositions
|
|
55
|
+
Parallel rays from explicit position array (atmospheric studies).
|
|
56
|
+
CustomRaySource
|
|
57
|
+
Fully customizable rays with per-ray position, direction,
|
|
58
|
+
wavelength, and intensity (chromatic dispersion, custom setups).
|
|
59
|
+
|
|
60
|
+
Interface
|
|
61
|
+
---------
|
|
62
|
+
All sources implement the generate() method:
|
|
63
|
+
|
|
64
|
+
>>> rays = source.generate()
|
|
65
|
+
|
|
66
|
+
Returns a RayBatch with initialized positions, directions, wavelengths,
|
|
67
|
+
and intensities.
|
|
68
|
+
|
|
69
|
+
Examples
|
|
70
|
+
--------
|
|
71
|
+
>>> from surface_roughness.sources import CollimatedBeam, PointSource
|
|
72
|
+
>>>
|
|
73
|
+
>>> # Create a collimated laser beam
|
|
74
|
+
>>> beam = CollimatedBeam(
|
|
75
|
+
... center=(0, 0, -10),
|
|
76
|
+
... direction=(0, 0, 1),
|
|
77
|
+
... radius=0.001,
|
|
78
|
+
... num_rays=10000,
|
|
79
|
+
... wavelength=633e-9,
|
|
80
|
+
... power=5e-3
|
|
81
|
+
... )
|
|
82
|
+
>>> rays = beam.generate()
|
|
83
|
+
>>>
|
|
84
|
+
>>> # Create an isotropic point source
|
|
85
|
+
>>> point = PointSource(
|
|
86
|
+
... position=(0, 0, 0),
|
|
87
|
+
... num_rays=5000,
|
|
88
|
+
... wavelength=532e-9,
|
|
89
|
+
... power=1e-3
|
|
90
|
+
... )
|
|
91
|
+
>>> rays = point.generate()
|
|
92
|
+
>>>
|
|
93
|
+
>>> # Create parallel rays from explicit positions (for atmospheric studies)
|
|
94
|
+
>>> import numpy as np
|
|
95
|
+
>>> positions = np.array([
|
|
96
|
+
... [-1000, 0, 100],
|
|
97
|
+
... [-1000, 0, 200],
|
|
98
|
+
... [-1000, 0, 300],
|
|
99
|
+
... ])
|
|
100
|
+
>>> source = ParallelBeamFromPositions(
|
|
101
|
+
... positions=positions,
|
|
102
|
+
... direction=(1, 0, 0),
|
|
103
|
+
... wavelength=532e-9,
|
|
104
|
+
... )
|
|
105
|
+
>>> rays = source.generate()
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
from .base import RaySource
|
|
109
|
+
from .collimated import CollimatedBeam
|
|
110
|
+
from .custom import CustomRaySource
|
|
111
|
+
from .diverging import DivergingBeam
|
|
112
|
+
from .gaussian import GaussianBeam
|
|
113
|
+
from .parallel_from_positions import ParallelBeamFromPositions
|
|
114
|
+
from .point import PointSource
|
|
115
|
+
from .uniform_diverging import UniformDivergingBeam
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
# Base class
|
|
119
|
+
"RaySource",
|
|
120
|
+
# Concrete implementations
|
|
121
|
+
"PointSource",
|
|
122
|
+
"CollimatedBeam",
|
|
123
|
+
"DivergingBeam",
|
|
124
|
+
"UniformDivergingBeam",
|
|
125
|
+
"GaussianBeam",
|
|
126
|
+
"ParallelBeamFromPositions",
|
|
127
|
+
"CustomRaySource",
|
|
128
|
+
]
|