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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. 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
+ ]