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