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,222 @@
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
+ Protocol definitions for ray propagators and GPU materials.
36
+
37
+ This module defines:
38
+ - RayPropagatorProtocol: Common interface for CPU/GPU propagators
39
+ - MaterialFieldProtocol: Interface for materials providing refractive index
40
+ - GPUMaterialProtocol: Interface for GPU-accelerated materials
41
+ - GPUMaterialID: Enum for GPU material types
42
+ """
43
+
44
+ from enum import IntEnum
45
+ from typing import Protocol, runtime_checkable
46
+
47
+ import numpy.typing as npt
48
+
49
+
50
+ # =============================================================================
51
+ # GPU Material Registry
52
+ # =============================================================================
53
+
54
+
55
+ class GPUMaterialID(IntEnum):
56
+ """
57
+ Registry of GPU-compatible material types.
58
+
59
+ Each material type has specialized device functions compiled into the
60
+ CUDA kernels. This enum identifies which material's functions to use.
61
+
62
+ Tiers:
63
+ - EXPONENTIAL_ATMOSPHERE (1): Legacy specialized kernels
64
+ - SIMPLE_INHOMOGENEOUS (2): 1D radial profile via LUT interpolation
65
+ - GRID_INHOMOGENEOUS (3): 3D grid data via trilinear interpolation
66
+ - SPECTRAL_INHOMOGENEOUS (4): 2D LUT-based (altitude × wavelength)
67
+ """
68
+
69
+ EXPONENTIAL_ATMOSPHERE = 1 # Legacy: specialized hard-coded kernels
70
+ SIMPLE_INHOMOGENEOUS = 2 # Tier 1: 1D LUT-based (SimpleInhomogeneousModel)
71
+ GRID_INHOMOGENEOUS = 3 # Tier 2: 3D grid interpolation (GridInhomogeneousModel)
72
+ SPECTRAL_INHOMOGENEOUS = 4 # 2D LUT-based (SpectralInhomogeneousModel)
73
+
74
+
75
+ @runtime_checkable
76
+ class RayPropagatorProtocol(Protocol):
77
+ """
78
+ Protocol for ray propagators in inhomogeneous media.
79
+
80
+ All propagators (CPU and GPU) must implement this interface to ensure
81
+ they are interchangeable drop-in replacements.
82
+
83
+ The propagator operates on RayBatch objects and modifies them in-place.
84
+ """
85
+
86
+ def propagate_step(
87
+ self,
88
+ rays, # RayBatch - avoiding import to prevent circular dependency
89
+ step_size: float,
90
+ wavelength: float = 532e-9,
91
+ ) -> None:
92
+ """
93
+ Propagate rays by a single integration step.
94
+
95
+ This is the primary interface method that all propagators must implement.
96
+ It takes a RayBatch object and advances all active rays by one step.
97
+
98
+ Parameters
99
+ ----------
100
+ rays : RayBatch
101
+ Ray batch containing positions, directions, and other ray properties.
102
+ Modified in-place.
103
+ step_size : float
104
+ Integration step size in meters. For gradient-based propagators,
105
+ this is the nominal step size that may be adaptively adjusted.
106
+ wavelength : float, optional
107
+ Wavelength in meters, default 532 nm (green light).
108
+ Used for wavelength-dependent effects like dispersion.
109
+
110
+ Notes
111
+ -----
112
+ - Modifies `rays.positions`, `rays.directions` in-place
113
+ - Updates path length accumulators if present
114
+ - Only propagates rays where `rays.active` is True
115
+ - Implementations may use different integration schemes (Euler, RK4, etc.)
116
+ """
117
+ ...
118
+
119
+ def propagate(
120
+ self,
121
+ rays, # RayBatch
122
+ total_distance: float,
123
+ step_size: float,
124
+ wavelength: float = 532e-9,
125
+ ) -> None:
126
+ """
127
+ Propagate rays through a total distance.
128
+
129
+ This is a convenience method that calls propagate_step repeatedly.
130
+ Implementations may optimize this for batch processing.
131
+
132
+ Parameters
133
+ ----------
134
+ rays : RayBatch
135
+ Ray batch to propagate (modified in-place)
136
+ total_distance : float
137
+ Total distance to propagate in meters
138
+ step_size : float
139
+ Integration step size in meters
140
+ wavelength : float, optional
141
+ Wavelength in meters, default 532 nm
142
+ """
143
+ ...
144
+
145
+
146
+ @runtime_checkable
147
+ class MaterialFieldProtocol(Protocol):
148
+ """
149
+ Protocol for material fields that provide refractive index.
150
+
151
+ Materials must implement these methods to work with gradient propagators.
152
+ """
153
+
154
+ def get_refractive_index(
155
+ self,
156
+ x: npt.NDArray,
157
+ y: npt.NDArray,
158
+ z: npt.NDArray,
159
+ wavelength: float = 532e-9,
160
+ ) -> npt.NDArray:
161
+ """
162
+ Get refractive index at given positions.
163
+
164
+ Parameters
165
+ ----------
166
+ x, y, z : ndarray
167
+ Positions in meters
168
+ wavelength : float
169
+ Wavelength in meters
170
+
171
+ Returns
172
+ -------
173
+ ndarray
174
+ Refractive index at each position
175
+ """
176
+ ...
177
+
178
+ def get_refractive_index_gradient(
179
+ self,
180
+ x: npt.NDArray,
181
+ y: npt.NDArray,
182
+ z: npt.NDArray,
183
+ wavelength: float = 532e-9,
184
+ ) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray]:
185
+ """
186
+ Get gradient of refractive index at given positions.
187
+
188
+ Parameters
189
+ ----------
190
+ x, y, z : ndarray
191
+ Positions in meters
192
+ wavelength : float
193
+ Wavelength in meters
194
+
195
+ Returns
196
+ -------
197
+ tuple of (dn_dx, dn_dy, dn_dz)
198
+ Gradient components in m^-1
199
+ """
200
+ ...
201
+
202
+
203
+ @runtime_checkable
204
+ class GPUMaterialProtocol(Protocol):
205
+ """
206
+ Protocol for GPU-accelerated materials.
207
+
208
+ Materials implementing this protocol can be used with GPU propagators.
209
+ """
210
+
211
+ @property
212
+ def gpu_material_id(self) -> GPUMaterialID:
213
+ """Return GPU material ID enum value."""
214
+ ...
215
+
216
+ def get_gpu_kernels(self) -> dict:
217
+ """Return dict of GPU kernel functions keyed by method name."""
218
+ ...
219
+
220
+ def get_gpu_parameters(self) -> tuple:
221
+ """Return tuple of material-specific parameters for GPU kernels."""
222
+ ...
@@ -0,0 +1,101 @@
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
+ Propagators for ray tracing in gradient media.
36
+
37
+ This subpackage contains:
38
+ - GradientPropagator: CPU-based propagator using NumPy vectorization
39
+ - GPUGradientPropagator: GPU-accelerated propagator using CUDA
40
+ - SpectralGPUGradientPropagator: GPU propagator with per-ray wavelengths
41
+ - SurfacePropagator: GPU propagator with multi-surface intersection detection
42
+ - create_propagator: Factory function for creating propagators based on material compatibility
43
+
44
+ New architecture components:
45
+ - MaterialPropagator: Clean wrapper for in-material propagation with surface detection
46
+ - SurfaceInteractionProcessor: Handles Fresnel physics, detection, absorption
47
+ - PropagationResult, SurfaceCrossing, SurfaceInteractionResult: Data structures
48
+ """
49
+
50
+ from .gradient import GradientPropagator
51
+ from .gpu_gradient import GPUGradientPropagator
52
+ from .spectral_gpu_gradient import SpectralGPUGradientPropagator
53
+ from .surface_propagator import SurfacePropagator, HitData
54
+ from .gpu_surface_propagator import GPUSurfacePropagator
55
+ from .factory import create_propagator
56
+ from .material_propagator import (
57
+ MaterialPropagator,
58
+ CPUMaterialPropagator,
59
+ GPUMaterialPropagator,
60
+ MaterialPropagatorProtocol,
61
+ PropagationResult,
62
+ )
63
+ from .surface_interaction import (
64
+ SurfaceInteractionProcessor,
65
+ SurfaceCrossing,
66
+ SurfaceInteractionResult,
67
+ )
68
+ from .signed_distance_handler import (
69
+ compute_signed_distance_gpu,
70
+ compute_surface_normal_gpu,
71
+ )
72
+
73
+ # Re-export kernel registry types for convenience
74
+ from ..kernels.registry import PropagationKernelID, PropagatorID
75
+
76
+ __all__ = [
77
+ # Factory
78
+ "create_propagator",
79
+ # Kernel/propagator IDs
80
+ "PropagationKernelID",
81
+ "PropagatorID",
82
+ # Legacy propagators
83
+ "GradientPropagator",
84
+ "GPUGradientPropagator",
85
+ "SpectralGPUGradientPropagator",
86
+ "SurfacePropagator",
87
+ "GPUSurfacePropagator",
88
+ "HitData",
89
+ # New architecture
90
+ "MaterialPropagator",
91
+ "CPUMaterialPropagator",
92
+ "GPUMaterialPropagator",
93
+ "MaterialPropagatorProtocol",
94
+ "PropagationResult",
95
+ "SurfaceInteractionProcessor",
96
+ "SurfaceCrossing",
97
+ "SurfaceInteractionResult",
98
+ # Signed distance handlers
99
+ "compute_signed_distance_gpu",
100
+ "compute_surface_normal_gpu",
101
+ ]
@@ -0,0 +1,354 @@
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
+ Detector Handler
36
+
37
+ GPU memory management for ray detection kernels.
38
+ This module provides high-level interfaces that handle cuda.to_device(),
39
+ copy_to_host(), and kernel launching for the detection kernels.
40
+ """
41
+
42
+ import numpy as np
43
+ from numpy.typing import NDArray
44
+
45
+ from ..kernels.detection import (
46
+ kernel_spherical_detect_single,
47
+ kernel_spherical_detect_multi,
48
+ )
49
+
50
+ # GPU support is optional
51
+ try:
52
+ from numba import cuda
53
+
54
+ _HAS_CUDA = cuda.is_available()
55
+ except ImportError:
56
+ _HAS_CUDA = False
57
+
58
+ class _FakeCuda:
59
+ @staticmethod
60
+ def is_available():
61
+ return False
62
+
63
+ cuda = _FakeCuda() # type: ignore[assignment]
64
+
65
+
66
+ def detect_spherical_gpu(
67
+ ray_positions: NDArray[np.float32],
68
+ ray_directions: NDArray[np.float32],
69
+ ray_active: NDArray[np.bool_],
70
+ ray_times: NDArray[np.float32],
71
+ ray_wavelengths: NDArray[np.float32],
72
+ ray_intensities: NDArray[np.float32],
73
+ detector_center: NDArray[np.float32],
74
+ detector_radius: float,
75
+ threads_per_block: int = 256,
76
+ ) -> tuple[NDArray[np.bool_], NDArray[np.float32], NDArray[np.float32]]:
77
+ """
78
+ GPU-accelerated detection for a single spherical detector.
79
+
80
+ Parameters
81
+ ----------
82
+ ray_positions : ndarray, shape (N, 3)
83
+ Ray starting positions
84
+ ray_directions : ndarray, shape (N, 3)
85
+ Ray directions
86
+ ray_active : ndarray, shape (N,)
87
+ Boolean mask of active rays
88
+ ray_times : ndarray, shape (N,)
89
+ Accumulated ray times
90
+ ray_wavelengths : ndarray, shape (N,)
91
+ Ray wavelengths
92
+ ray_intensities : ndarray, shape (N,)
93
+ Ray intensities
94
+ detector_center : ndarray, shape (3,)
95
+ Detector center position
96
+ detector_radius : float
97
+ Detector radius
98
+ threads_per_block : int
99
+ CUDA threads per block
100
+
101
+ Returns
102
+ -------
103
+ hit_mask : ndarray, shape (N,)
104
+ Boolean mask of rays that hit detector
105
+ hit_distances : ndarray, shape (N,)
106
+ Distance to detector for each ray
107
+ hit_times : ndarray, shape (N,)
108
+ Arrival time at detector for each ray
109
+ """
110
+ num_rays = len(ray_positions)
111
+
112
+ # Output arrays
113
+ hit_mask = np.zeros(num_rays, dtype=np.bool_)
114
+ hit_distances = np.zeros(num_rays, dtype=np.float32)
115
+ hit_times = np.zeros(num_rays, dtype=np.float32)
116
+
117
+ if _HAS_CUDA:
118
+ # Transfer to GPU
119
+ d_positions = cuda.to_device(ray_positions.astype(np.float32))
120
+ d_directions = cuda.to_device(ray_directions.astype(np.float32))
121
+ d_active = cuda.to_device(ray_active)
122
+ d_times = cuda.to_device(ray_times.astype(np.float32))
123
+ d_wavelengths = cuda.to_device(ray_wavelengths.astype(np.float32))
124
+ d_intensities = cuda.to_device(ray_intensities.astype(np.float32))
125
+ d_center = cuda.to_device(detector_center.astype(np.float32))
126
+ d_hit_mask = cuda.to_device(hit_mask)
127
+ d_hit_distances = cuda.to_device(hit_distances)
128
+ d_hit_times = cuda.to_device(hit_times)
129
+
130
+ # Launch kernel
131
+ blocks = (num_rays + threads_per_block - 1) // threads_per_block
132
+ kernel_spherical_detect_single[blocks, threads_per_block](
133
+ d_positions,
134
+ d_directions,
135
+ d_active,
136
+ d_times,
137
+ d_wavelengths,
138
+ d_intensities,
139
+ d_center,
140
+ np.float32(detector_radius),
141
+ d_hit_mask,
142
+ d_hit_distances,
143
+ d_hit_times,
144
+ )
145
+
146
+ cuda.synchronize()
147
+ hit_mask = d_hit_mask.copy_to_host()
148
+ hit_distances = d_hit_distances.copy_to_host()
149
+ hit_times = d_hit_times.copy_to_host()
150
+ else:
151
+ # CPU fallback
152
+ hit_mask, hit_distances, hit_times = _detect_spherical_cpu(
153
+ ray_positions,
154
+ ray_directions,
155
+ ray_active,
156
+ ray_times,
157
+ detector_center,
158
+ detector_radius,
159
+ )
160
+
161
+ return hit_mask, hit_distances, hit_times
162
+
163
+
164
+ def detect_multi_spherical_gpu(
165
+ ray_positions: NDArray[np.float32],
166
+ ray_directions: NDArray[np.float32],
167
+ ray_active: NDArray[np.bool_],
168
+ ray_times: NDArray[np.float32],
169
+ ray_intensities: NDArray[np.float32],
170
+ detector_centers: NDArray[np.float32],
171
+ detector_radius: float,
172
+ threads_per_block: int = 256,
173
+ ) -> tuple[NDArray[np.int32], NDArray[np.float32]]:
174
+ """
175
+ GPU-accelerated detection across multiple spherical detectors.
176
+
177
+ This is optimized for detector scans where we want to quickly count
178
+ hits and sum intensities for many detector positions at once.
179
+
180
+ Parameters
181
+ ----------
182
+ ray_positions : ndarray, shape (N, 3)
183
+ Ray starting positions
184
+ ray_directions : ndarray, shape (N, 3)
185
+ Ray directions
186
+ ray_active : ndarray, shape (N,)
187
+ Boolean mask of active rays
188
+ ray_times : ndarray, shape (N,)
189
+ Accumulated ray times
190
+ ray_intensities : ndarray, shape (N,)
191
+ Ray intensities
192
+ detector_centers : ndarray, shape (M, 3)
193
+ Detector center positions
194
+ detector_radius : float
195
+ Detector radius (same for all)
196
+ threads_per_block : int
197
+ CUDA threads per block
198
+
199
+ Returns
200
+ -------
201
+ hit_counts : ndarray, shape (M,)
202
+ Number of rays hitting each detector
203
+ hit_intensities : ndarray, shape (M,)
204
+ Sum of intensities for rays hitting each detector
205
+ """
206
+ num_rays = len(ray_positions)
207
+ num_detectors = len(detector_centers)
208
+
209
+ # Output arrays
210
+ hit_counts = np.zeros(num_detectors, dtype=np.int32)
211
+ hit_intensities = np.zeros(num_detectors, dtype=np.float32)
212
+
213
+ if _HAS_CUDA:
214
+ # Transfer to GPU
215
+ d_positions = cuda.to_device(ray_positions.astype(np.float32))
216
+ d_directions = cuda.to_device(ray_directions.astype(np.float32))
217
+ d_active = cuda.to_device(ray_active)
218
+ d_times = cuda.to_device(ray_times.astype(np.float32))
219
+ d_intensities = cuda.to_device(ray_intensities.astype(np.float32))
220
+ d_centers = cuda.to_device(detector_centers.astype(np.float32))
221
+ d_hit_counts = cuda.to_device(hit_counts)
222
+ d_hit_intensities = cuda.to_device(hit_intensities)
223
+
224
+ # Launch kernel (1D grid over rays)
225
+ blocks = (num_rays + threads_per_block - 1) // threads_per_block
226
+ kernel_spherical_detect_multi[blocks, threads_per_block](
227
+ d_positions,
228
+ d_directions,
229
+ d_active,
230
+ d_times,
231
+ d_centers,
232
+ np.float32(detector_radius),
233
+ d_hit_counts,
234
+ d_hit_intensities,
235
+ d_intensities,
236
+ )
237
+
238
+ cuda.synchronize()
239
+ hit_counts = d_hit_counts.copy_to_host()
240
+ hit_intensities = d_hit_intensities.copy_to_host()
241
+ else:
242
+ # CPU fallback
243
+ hit_counts, hit_intensities = _detect_multi_spherical_cpu(
244
+ ray_positions,
245
+ ray_directions,
246
+ ray_active,
247
+ ray_intensities,
248
+ detector_centers,
249
+ detector_radius,
250
+ )
251
+
252
+ return hit_counts, hit_intensities
253
+
254
+
255
+ # =============================================================================
256
+ # CPU Fallback Implementations
257
+ # =============================================================================
258
+
259
+
260
+ def _detect_spherical_cpu(
261
+ ray_positions: NDArray[np.float32],
262
+ ray_directions: NDArray[np.float32],
263
+ ray_active: NDArray[np.bool_],
264
+ ray_times: NDArray[np.float32],
265
+ detector_center: NDArray[np.float32],
266
+ detector_radius: float,
267
+ ) -> tuple[NDArray[np.bool_], NDArray[np.float32], NDArray[np.float32]]:
268
+ """CPU fallback for single detector detection."""
269
+ num_rays = len(ray_positions)
270
+ hit_mask = np.zeros(num_rays, dtype=np.bool_)
271
+ hit_distances = np.zeros(num_rays, dtype=np.float32)
272
+ hit_times = np.zeros(num_rays, dtype=np.float32)
273
+
274
+ c = 299792458.0
275
+
276
+ for i in range(num_rays):
277
+ if not ray_active[i]:
278
+ continue
279
+
280
+ origin = ray_positions[i]
281
+ direction = ray_directions[i]
282
+ d_len = np.linalg.norm(direction)
283
+ if d_len < 1e-10:
284
+ continue
285
+ direction = direction / d_len
286
+
287
+ oc = detector_center - origin
288
+ t_closest = np.dot(oc, direction)
289
+
290
+ if t_closest < 0:
291
+ continue
292
+
293
+ closest_point = origin + t_closest * direction
294
+ dist = np.linalg.norm(closest_point - detector_center)
295
+
296
+ if dist <= detector_radius:
297
+ hit_mask[i] = True
298
+ hit_distances[i] = t_closest
299
+ hit_times[i] = ray_times[i] + t_closest / c
300
+
301
+ return hit_mask, hit_distances, hit_times
302
+
303
+
304
+ def _detect_multi_spherical_cpu(
305
+ ray_positions: NDArray[np.float32],
306
+ ray_directions: NDArray[np.float32],
307
+ ray_active: NDArray[np.bool_],
308
+ ray_intensities: NDArray[np.float32],
309
+ detector_centers: NDArray[np.float32],
310
+ detector_radius: float,
311
+ ) -> tuple[NDArray[np.int32], NDArray[np.float32]]:
312
+ """CPU fallback for multi-detector detection."""
313
+ num_rays = len(ray_positions)
314
+ num_detectors = len(detector_centers)
315
+
316
+ hit_counts = np.zeros(num_detectors, dtype=np.int32)
317
+ hit_intensities = np.zeros(num_detectors, dtype=np.float32)
318
+
319
+ radius_sq = detector_radius**2
320
+
321
+ for i in range(num_rays):
322
+ if not ray_active[i]:
323
+ continue
324
+
325
+ origin = ray_positions[i]
326
+ direction = ray_directions[i]
327
+ d_len = np.linalg.norm(direction)
328
+ if d_len < 1e-10:
329
+ continue
330
+ direction = direction / d_len
331
+ intensity = ray_intensities[i]
332
+
333
+ for det_idx in range(num_detectors):
334
+ center = detector_centers[det_idx]
335
+ oc = center - origin
336
+ t_closest = np.dot(oc, direction)
337
+
338
+ if t_closest < 0:
339
+ continue
340
+
341
+ closest_point = origin + t_closest * direction
342
+ dist_sq = np.sum((closest_point - center) ** 2)
343
+
344
+ if dist_sq <= radius_sq:
345
+ hit_counts[det_idx] += 1
346
+ hit_intensities[det_idx] += intensity
347
+
348
+ return hit_counts, hit_intensities
349
+
350
+
351
+ __all__ = [
352
+ "detect_spherical_gpu",
353
+ "detect_multi_spherical_gpu",
354
+ ]