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,707 @@
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
+ GPU Surface Propagator
36
+
37
+ Fully GPU-resident propagator with surface detection.
38
+ Uses shared GPUDeviceRays structure for both material propagation
39
+ and surface intersection - no duplicate memory allocation.
40
+
41
+ Architecture
42
+ ------------
43
+ This propagator minimizes CPU-GPU transfers:
44
+ 1. One-time upload of ray data at start
45
+ 2. GPU-resident loop: propagation + detection + bisection
46
+ 3. Small reduction transfer per step (8 bytes) for loop termination
47
+ 4. One-time download of results at end
48
+
49
+ For N=1M rays, 10k steps:
50
+ - Previous: ~240 GB transferred
51
+ - This: ~48 MB transferred (5000x reduction)
52
+ """
53
+
54
+ from typing import TYPE_CHECKING
55
+
56
+ import numpy as np
57
+
58
+ from ..gpu_device_rays import GPUDeviceRays
59
+ from .surface_propagator import HitData
60
+
61
+ # Import MAX_SURFACE_PARAMS
62
+ MAX_SURFACE_PARAMS = 64
63
+
64
+ # Speed of light
65
+ SPEED_OF_LIGHT = 299792458.0
66
+
67
+ # GPU support
68
+ try:
69
+ from numba import cuda
70
+
71
+ _HAS_CUDA = cuda.is_available()
72
+
73
+ if _HAS_CUDA:
74
+
75
+ @cuda.jit
76
+ def kernel_homogeneous_step(
77
+ positions, directions, active, geo_path, opt_path, acc_time, step_size, n, c
78
+ ):
79
+ """
80
+ Simple straight-line propagation for homogeneous materials.
81
+
82
+ For homogeneous materials (constant n, zero gradient):
83
+ - Position advances in straight line
84
+ - Direction stays constant
85
+ - Path lengths and time accumulate
86
+ """
87
+ idx = cuda.grid(1)
88
+ if idx >= positions.shape[0]:
89
+ return
90
+ if not active[idx]:
91
+ return
92
+
93
+ # Update position (straight line)
94
+ positions[idx, 0] += directions[idx, 0] * step_size
95
+ positions[idx, 1] += directions[idx, 1] * step_size
96
+ positions[idx, 2] += directions[idx, 2] * step_size
97
+
98
+ # Update path lengths
99
+ geo_path[idx] += step_size
100
+ opt_path[idx] += n * step_size
101
+ acc_time[idx] += n * step_size / c
102
+
103
+ @cuda.jit
104
+ def kernel_homogeneous_step_adaptive(
105
+ positions,
106
+ directions,
107
+ active,
108
+ geo_path,
109
+ opt_path,
110
+ acc_time,
111
+ step_sizes,
112
+ n,
113
+ c,
114
+ ):
115
+ """
116
+ Straight-line propagation with per-ray adaptive step sizes.
117
+
118
+ Same as kernel_homogeneous_step but uses per-ray step_sizes array
119
+ instead of a scalar step size. This enables adaptive stepping
120
+ where rays near surfaces use smaller steps for precision.
121
+ """
122
+ idx = cuda.grid(1)
123
+ if idx >= positions.shape[0]:
124
+ return
125
+ if not active[idx]:
126
+ return
127
+
128
+ step_size = step_sizes[idx]
129
+
130
+ # Update position (straight line)
131
+ positions[idx, 0] += directions[idx, 0] * step_size
132
+ positions[idx, 1] += directions[idx, 1] * step_size
133
+ positions[idx, 2] += directions[idx, 2] * step_size
134
+
135
+ # Update path lengths
136
+ geo_path[idx] += step_size
137
+ opt_path[idx] += n * step_size
138
+ acc_time[idx] += n * step_size / c
139
+
140
+ except ImportError:
141
+ _HAS_CUDA = False
142
+
143
+ class _FakeCuda:
144
+ @staticmethod
145
+ def is_available():
146
+ return False
147
+
148
+ @staticmethod
149
+ def synchronize():
150
+ pass
151
+
152
+ @staticmethod
153
+ def to_device(arr):
154
+ return arr
155
+
156
+ cuda = _FakeCuda() # type: ignore[assignment]
157
+ kernel_homogeneous_step = None # type: ignore[assignment]
158
+ kernel_homogeneous_step_adaptive = None # type: ignore[assignment]
159
+
160
+
161
+ if TYPE_CHECKING:
162
+ from ...utilities.ray_data import RayBatch
163
+
164
+
165
+ class GPUSurfacePropagator:
166
+ """
167
+ Fully GPU-resident propagator with surface detection.
168
+
169
+ Uses shared GPUDeviceRays structure for both material propagation
170
+ and surface intersection - no duplicate memory allocation.
171
+
172
+ Parameters
173
+ ----------
174
+ material : MaterialField
175
+ Material to propagate through. Must support get_gpu_kernels().
176
+ surfaces : list of Surface
177
+ List of surfaces to check for intersections. All must be gpu_capable.
178
+ threads_per_block : int
179
+ CUDA threads per block. Default: 256.
180
+ apply_absorption : bool
181
+ Whether to apply Beer-Lambert absorption during propagation. Default: False.
182
+
183
+ Notes
184
+ -----
185
+ Falls back to CPU SurfacePropagator if:
186
+ - CUDA is not available
187
+ - Material doesn't support GPU propagation
188
+ - Any surface is not gpu_capable
189
+
190
+ Example
191
+ -------
192
+ >>> propagator = GPUSurfacePropagator(material, surfaces)
193
+ >>> hit_data = propagator.propagate_to_surface(rays, step_size=100.0, max_steps=10000)
194
+ """
195
+
196
+ def __init__(
197
+ self,
198
+ material,
199
+ surfaces: list,
200
+ threads_per_block: int = 256,
201
+ apply_absorption: bool = False,
202
+ ):
203
+ self._material = material
204
+ self._surfaces = list(surfaces)
205
+ self._threads_per_block = threads_per_block
206
+ self._apply_absorption = apply_absorption
207
+ self._use_gpu = self._check_gpu_support()
208
+
209
+ # Initialize absorption kernel (will be set in _setup_gpu_resources if enabled)
210
+ self._absorption_kernel = None
211
+
212
+ if self._use_gpu:
213
+ self._setup_gpu_resources()
214
+
215
+ def _check_gpu_support(self) -> bool:
216
+ """Check if GPU propagation is possible."""
217
+ if not _HAS_CUDA:
218
+ return False
219
+
220
+ # Check material: either has GPU kernels OR is homogeneous
221
+ self._is_homogeneous_material = getattr(
222
+ self._material, "_is_homogeneous", False
223
+ )
224
+ has_gpu_kernels = hasattr(self._material, "get_gpu_kernels")
225
+
226
+ if not (has_gpu_kernels or self._is_homogeneous_material):
227
+ return False
228
+
229
+ # Check all surfaces are GPU-capable
230
+ for surface in self._surfaces:
231
+ if not getattr(surface, "gpu_capable", False):
232
+ return False
233
+
234
+ return True
235
+
236
+ def _setup_gpu_resources(self) -> None:
237
+ """Pre-upload surface parameters to GPU (one-time setup)."""
238
+ num_surfaces = len(self._surfaces)
239
+
240
+ # Collect geometry IDs
241
+ geometry_ids = np.array([s.geometry_id for s in self._surfaces], dtype=np.int32)
242
+ self._d_geometry_ids = cuda.to_device(geometry_ids)
243
+
244
+ # Concatenate all surface parameters
245
+ all_params = np.zeros(num_surfaces * MAX_SURFACE_PARAMS, dtype=np.float32)
246
+ for i, surface in enumerate(self._surfaces):
247
+ params = surface.get_gpu_parameters()
248
+ offset = i * MAX_SURFACE_PARAMS
249
+ all_params[offset : offset + len(params)] = params
250
+
251
+ self._d_surface_params = cuda.to_device(all_params)
252
+
253
+ # Upload material LUT arrays to GPU if available (for spectral/grid materials)
254
+ self._d_material_arrays = {}
255
+ if hasattr(self._material, "get_gpu_arrays"):
256
+ gpu_arrays = self._material.get_gpu_arrays()
257
+ for name, arr in gpu_arrays.items():
258
+ if arr is not None:
259
+ self._d_material_arrays[name] = cuda.to_device(
260
+ np.ascontiguousarray(arr, dtype=np.float32)
261
+ )
262
+
263
+ # Import kernels
264
+ from ..kernels.surface import (
265
+ kernel_save_prev_positions,
266
+ kernel_detect_crossing,
267
+ kernel_init_signed_distances,
268
+ kernel_bisect_crossing,
269
+ kernel_reduce_status,
270
+ kernel_compute_min_surface_distance,
271
+ kernel_compute_adaptive_steps,
272
+ )
273
+
274
+ self._kernel_save_prev = kernel_save_prev_positions
275
+ self._kernel_detect = kernel_detect_crossing
276
+ self._kernel_init_sd = kernel_init_signed_distances
277
+ self._kernel_bisect = kernel_bisect_crossing
278
+ self._kernel_reduce = kernel_reduce_status
279
+ self._kernel_min_dist = kernel_compute_min_surface_distance
280
+ self._kernel_adaptive_steps = kernel_compute_adaptive_steps
281
+
282
+ # Set up absorption kernel if enabled
283
+ if self._apply_absorption:
284
+ self._absorption_kernel = self._get_absorption_kernel()
285
+
286
+ def _get_absorption_kernel(self):
287
+ """Get the appropriate absorption kernel based on material type."""
288
+ from ..propagator_protocol import GPUMaterialID
289
+ from ..kernels.absorption.simple import _kernel_absorption_simple
290
+ from ..kernels.absorption.grid import _kernel_absorption_grid
291
+ from ..kernels.absorption.spectral import _kernel_absorption_spectral_perray
292
+
293
+ material_id = getattr(self._material, "gpu_material_id", None)
294
+
295
+ if material_id == GPUMaterialID.SIMPLE_INHOMOGENEOUS:
296
+ return _kernel_absorption_simple
297
+ elif material_id == GPUMaterialID.EXPONENTIAL_ATMOSPHERE:
298
+ return _kernel_absorption_simple # Uses same LUT structure
299
+ elif material_id == GPUMaterialID.GRID_INHOMOGENEOUS:
300
+ return _kernel_absorption_grid
301
+ elif material_id == GPUMaterialID.SPECTRAL_INHOMOGENEOUS:
302
+ return _kernel_absorption_spectral_perray
303
+ else:
304
+ # No absorption kernel available for this material type
305
+ return None
306
+
307
+ def _apply_absorption_step(
308
+ self,
309
+ gpu_rays: "GPUDeviceRays",
310
+ step_size: float,
311
+ num_steps: int,
312
+ blocks: int,
313
+ ) -> None:
314
+ """Apply absorption using the appropriate kernel."""
315
+ from ..propagator_protocol import GPUMaterialID
316
+
317
+ if self._absorption_kernel is None:
318
+ return
319
+
320
+ material_params = self._material.get_gpu_parameters()
321
+ material_id = getattr(self._material, "gpu_material_id", None)
322
+
323
+ if material_id in (
324
+ GPUMaterialID.SIMPLE_INHOMOGENEOUS,
325
+ GPUMaterialID.EXPONENTIAL_ATMOSPHERE,
326
+ ):
327
+ # Simple kernel: positions, directions, active, intensities, optical_depth,
328
+ # step_size, num_steps, center_x, center_y, center_z, ref_radius,
329
+ # min_alt, delta_h, lut_size, lut_alpha
330
+ if "lut_alpha" not in self._d_material_arrays:
331
+ return # No absorption LUT available
332
+ self._absorption_kernel[blocks, self._threads_per_block](
333
+ gpu_rays.d_positions,
334
+ gpu_rays.d_directions,
335
+ gpu_rays.d_active,
336
+ gpu_rays.d_intensities,
337
+ gpu_rays.d_optical_depth,
338
+ float(step_size),
339
+ num_steps,
340
+ material_params[0], # center_x
341
+ material_params[1], # center_y
342
+ material_params[2], # center_z
343
+ material_params[3], # ref_radius
344
+ material_params[4], # min_alt
345
+ material_params[5], # delta_h
346
+ int(material_params[6]), # lut_size
347
+ self._d_material_arrays["lut_alpha"],
348
+ )
349
+ elif material_id == GPUMaterialID.GRID_INHOMOGENEOUS:
350
+ # Grid kernel: positions, directions, active, intensities, optical_depth,
351
+ # step_size, num_steps, x_min, y_min, z_min, grid_dx, grid_dy, grid_dz,
352
+ # nx, ny, nz, alpha_grid
353
+ if "alpha_grid" not in self._d_material_arrays:
354
+ return # No absorption grid available
355
+ self._absorption_kernel[blocks, self._threads_per_block](
356
+ gpu_rays.d_positions,
357
+ gpu_rays.d_directions,
358
+ gpu_rays.d_active,
359
+ gpu_rays.d_intensities,
360
+ gpu_rays.d_optical_depth,
361
+ float(step_size),
362
+ num_steps,
363
+ material_params[0], # x_min
364
+ material_params[1], # y_min
365
+ material_params[2], # z_min
366
+ material_params[3], # grid_dx
367
+ material_params[4], # grid_dy
368
+ material_params[5], # grid_dz
369
+ int(material_params[6]), # nx
370
+ int(material_params[7]), # ny
371
+ int(material_params[8]), # nz
372
+ self._d_material_arrays["alpha_grid"],
373
+ )
374
+ elif material_id == GPUMaterialID.SPECTRAL_INHOMOGENEOUS:
375
+ # Spectral kernel with per-ray wavelengths
376
+ if "lut_alpha" not in self._d_material_arrays:
377
+ return # No absorption LUT available
378
+ self._absorption_kernel[blocks, self._threads_per_block](
379
+ gpu_rays.d_positions,
380
+ gpu_rays.d_directions,
381
+ gpu_rays.d_wavelengths,
382
+ gpu_rays.d_active,
383
+ gpu_rays.d_intensities,
384
+ gpu_rays.d_optical_depth,
385
+ float(step_size),
386
+ num_steps,
387
+ material_params[0], # center_x
388
+ material_params[1], # center_y
389
+ material_params[2], # center_z
390
+ material_params[3], # ref_radius
391
+ material_params[4], # alt_min
392
+ material_params[5], # alt_delta
393
+ int(material_params[6]), # n_alt
394
+ material_params[7], # wl_min
395
+ material_params[8], # wl_delta
396
+ int(material_params[9]), # n_wl
397
+ self._d_material_arrays["lut_alpha"],
398
+ )
399
+
400
+ @property
401
+ def use_gpu(self) -> bool:
402
+ """Whether GPU acceleration is active."""
403
+ return self._use_gpu
404
+
405
+ @property
406
+ def material(self):
407
+ """The material being propagated through."""
408
+ return self._material
409
+
410
+ @property
411
+ def surfaces(self) -> list:
412
+ """List of surfaces being checked for intersection."""
413
+ return self._surfaces
414
+
415
+ def propagate_to_surface(
416
+ self,
417
+ rays: "RayBatch",
418
+ step_size: float,
419
+ max_steps: int,
420
+ wavelength: float = 532e-9,
421
+ adaptive_stepping: bool = False,
422
+ min_step_size: float = 3e-4,
423
+ surface_proximity_factor: float = 0.5,
424
+ surface_proximity_threshold: float = 10.0,
425
+ ) -> HitData:
426
+ """
427
+ Propagate rays until any surface is hit (or max_steps reached).
428
+
429
+ Uses GPU-resident loop with minimal CPU-GPU transfers.
430
+
431
+ Parameters
432
+ ----------
433
+ rays : RayBatch
434
+ Ray batch to propagate. Modified in-place.
435
+ step_size : float
436
+ Maximum integration step size in meters.
437
+ max_steps : int
438
+ Maximum number of propagation steps.
439
+ wavelength : float, optional
440
+ Default wavelength for material queries.
441
+ adaptive_stepping : bool, optional
442
+ Whether to use adaptive step sizing near surfaces (default False).
443
+ min_step_size : float, optional
444
+ Minimum step size in meters (default 3e-4 = 0.3mm → ~1ps resolution).
445
+ surface_proximity_factor : float, optional
446
+ Step = distance * factor when within threshold (default 0.5).
447
+ surface_proximity_threshold : float, optional
448
+ Distance within which to start adaptive stepping (default 10.0 m).
449
+
450
+ Returns
451
+ -------
452
+ HitData
453
+ Information about which rays hit which surfaces.
454
+ """
455
+ if self._use_gpu:
456
+ return self._propagate_gpu(
457
+ rays,
458
+ step_size,
459
+ max_steps,
460
+ wavelength,
461
+ adaptive_stepping=adaptive_stepping,
462
+ min_step_size=min_step_size,
463
+ surface_proximity_factor=surface_proximity_factor,
464
+ surface_proximity_threshold=surface_proximity_threshold,
465
+ )
466
+ else:
467
+ # Fall back to CPU propagator
468
+ from .surface_propagator import SurfacePropagator
469
+
470
+ cpu_prop = SurfacePropagator(
471
+ material=self._material,
472
+ surfaces=self._surfaces,
473
+ use_gpu=False,
474
+ )
475
+ return cpu_prop.propagate_to_surface(
476
+ rays,
477
+ step_size,
478
+ max_steps,
479
+ adaptive_stepping=adaptive_stepping,
480
+ min_step_size=min_step_size,
481
+ surface_proximity_factor=surface_proximity_factor,
482
+ surface_proximity_threshold=surface_proximity_threshold,
483
+ )
484
+
485
+ def _propagate_gpu(
486
+ self,
487
+ rays: "RayBatch",
488
+ step_size: float,
489
+ max_steps: int,
490
+ wavelength: float,
491
+ adaptive_stepping: bool = False,
492
+ min_step_size: float = 3e-4,
493
+ surface_proximity_factor: float = 0.5,
494
+ surface_proximity_threshold: float = 10.0,
495
+ ) -> HitData:
496
+ """GPU-resident propagation loop with optional adaptive stepping."""
497
+ num_rays = rays.num_rays
498
+ num_surfaces = len(self._surfaces)
499
+ blocks = (num_rays + self._threads_per_block - 1) // self._threads_per_block
500
+
501
+ # === ONE-TIME UPLOAD ===
502
+ gpu_rays = GPUDeviceRays.from_ray_batch(rays, num_surfaces)
503
+
504
+ # Initialize signed distances for all surfaces
505
+ self._kernel_init_sd[blocks, self._threads_per_block](
506
+ gpu_rays.d_positions,
507
+ gpu_rays.d_active,
508
+ self._d_geometry_ids,
509
+ self._d_surface_params,
510
+ num_surfaces,
511
+ MAX_SURFACE_PARAMS,
512
+ gpu_rays.d_prev_signed_dist,
513
+ )
514
+ cuda.synchronize()
515
+
516
+ # Determine propagation mode: homogeneous (straight-line) or gradient-based
517
+ use_homogeneous = getattr(self, "_is_homogeneous_material", False)
518
+
519
+ # Allocate GPU arrays for adaptive stepping if needed
520
+ if adaptive_stepping:
521
+ d_min_distances = cuda.to_device(np.zeros(num_rays, dtype=np.float32))
522
+ d_adaptive_steps = cuda.to_device(
523
+ np.full(num_rays, step_size, dtype=np.float32)
524
+ )
525
+
526
+ if use_homogeneous:
527
+ # For homogeneous materials, use simple straight-line kernel
528
+ # Get refractive index (constant for homogeneous materials)
529
+ n = self._material.get_refractive_index(0, 0, 0, wavelength)
530
+ else:
531
+ # Get material propagation kernel for inhomogeneous materials
532
+ material_kernels = self._material.get_gpu_kernels()
533
+ prop_kernel = material_kernels.get("euler") or material_kernels.get("rk4")
534
+ if prop_kernel is None:
535
+ raise RuntimeError("Material does not provide GPU propagation kernel")
536
+ material_params = self._material.get_gpu_parameters()
537
+ # Get reference refractive index for time correction in bisection
538
+ # Use value at reference position (e.g., sea level for atmosphere)
539
+ n = self._material.get_refractive_index(0, 0, 0, wavelength)
540
+
541
+ # === GPU-RESIDENT LOOP ===
542
+ for step in range(max_steps):
543
+ # 1. Save positions before propagation (for bisection)
544
+ self._kernel_save_prev[blocks, self._threads_per_block](
545
+ gpu_rays.d_positions,
546
+ gpu_rays.d_prev_positions,
547
+ gpu_rays.d_active,
548
+ )
549
+
550
+ # 1b. Compute adaptive step sizes if enabled
551
+ if adaptive_stepping:
552
+ # Compute minimum distance to any surface
553
+ self._kernel_min_dist[blocks, self._threads_per_block](
554
+ gpu_rays.d_positions,
555
+ gpu_rays.d_active,
556
+ self._d_geometry_ids,
557
+ self._d_surface_params,
558
+ num_surfaces,
559
+ MAX_SURFACE_PARAMS,
560
+ d_min_distances,
561
+ )
562
+
563
+ # Compute adaptive step sizes based on distances
564
+ self._kernel_adaptive_steps[blocks, self._threads_per_block](
565
+ d_min_distances,
566
+ gpu_rays.d_active,
567
+ step_size,
568
+ min_step_size,
569
+ surface_proximity_factor,
570
+ surface_proximity_threshold,
571
+ d_adaptive_steps,
572
+ )
573
+
574
+ # 2. Propagate one step using appropriate kernel
575
+ if use_homogeneous:
576
+ if adaptive_stepping:
577
+ # Use per-ray step sizes
578
+ kernel_homogeneous_step_adaptive[blocks, self._threads_per_block](
579
+ gpu_rays.d_positions,
580
+ gpu_rays.d_directions,
581
+ gpu_rays.d_active,
582
+ gpu_rays.d_geo_path,
583
+ gpu_rays.d_opt_path,
584
+ gpu_rays.d_acc_time,
585
+ d_adaptive_steps,
586
+ float(n),
587
+ SPEED_OF_LIGHT,
588
+ )
589
+ else:
590
+ # Use fixed step size
591
+ kernel_homogeneous_step[blocks, self._threads_per_block](
592
+ gpu_rays.d_positions,
593
+ gpu_rays.d_directions,
594
+ gpu_rays.d_active,
595
+ gpu_rays.d_geo_path,
596
+ gpu_rays.d_opt_path,
597
+ gpu_rays.d_acc_time,
598
+ step_size,
599
+ float(n),
600
+ SPEED_OF_LIGHT,
601
+ )
602
+ else:
603
+ # Inhomogeneous: use material kernel with gradient-based bending
604
+ # Note: Adaptive stepping for inhomogeneous materials would require
605
+ # per-ray step size support in material kernels (future enhancement)
606
+ #
607
+ # Build kernel arguments: scalar params + device arrays (LUTs)
608
+ # Convention matches GPUGradientPropagator:
609
+ # - lut_n, lut_dn_dh for SimpleAtmosphere/SpectralAtmosphere
610
+ # - n_grid, grad_grid for GridAtmosphere
611
+ kernel_args = list(material_params)
612
+ for name in ["lut_n", "lut_dn_dh", "n_grid", "grad_grid"]:
613
+ if name in self._d_material_arrays:
614
+ kernel_args.append(self._d_material_arrays[name])
615
+
616
+ prop_kernel[blocks, self._threads_per_block](
617
+ gpu_rays.d_positions,
618
+ gpu_rays.d_directions,
619
+ gpu_rays.d_active,
620
+ gpu_rays.d_geo_path,
621
+ gpu_rays.d_opt_path,
622
+ gpu_rays.d_acc_time,
623
+ step_size,
624
+ 1, # num_steps
625
+ *kernel_args,
626
+ wavelength,
627
+ )
628
+
629
+ # 2b. Apply absorption if enabled
630
+ if self._apply_absorption and self._absorption_kernel is not None:
631
+ self._apply_absorption_step(gpu_rays, step_size, 1, blocks)
632
+
633
+ # 3. Detect surface crossings
634
+ self._kernel_detect[blocks, self._threads_per_block](
635
+ gpu_rays.d_positions,
636
+ gpu_rays.d_prev_signed_dist,
637
+ gpu_rays.d_active,
638
+ self._d_geometry_ids,
639
+ self._d_surface_params,
640
+ num_surfaces,
641
+ MAX_SURFACE_PARAMS,
642
+ gpu_rays.d_crossing_mask,
643
+ gpu_rays.d_hit_surface_idx,
644
+ )
645
+
646
+ # 4. Reduce to check status (ONLY transfer: 8 bytes)
647
+ gpu_rays.d_reduction_result.copy_to_device(np.zeros(2, dtype=np.int32))
648
+ self._kernel_reduce[blocks, self._threads_per_block](
649
+ gpu_rays.d_active,
650
+ gpu_rays.d_crossing_mask,
651
+ gpu_rays.d_reduction_result,
652
+ )
653
+ num_active, num_crossing = gpu_rays.get_reduction_result()
654
+
655
+ # 5. Refine crossings if any
656
+ if num_crossing > 0:
657
+ # Get step sizes for time correction
658
+ if adaptive_stepping:
659
+ d_step_sizes_for_correction = d_adaptive_steps
660
+ else:
661
+ # Create array with fixed step size for correction
662
+ d_step_sizes_for_correction = cuda.to_device(
663
+ np.full(num_rays, step_size, dtype=np.float32)
664
+ )
665
+
666
+ self._kernel_bisect[blocks, self._threads_per_block](
667
+ gpu_rays.d_prev_positions,
668
+ gpu_rays.d_positions,
669
+ gpu_rays.d_crossing_mask,
670
+ gpu_rays.d_hit_surface_idx,
671
+ self._d_geometry_ids,
672
+ self._d_surface_params,
673
+ MAX_SURFACE_PARAMS,
674
+ gpu_rays.d_hit_positions,
675
+ gpu_rays.d_active,
676
+ 20, # num_iterations (20 gives ~nm precision from 10m step)
677
+ gpu_rays.d_geo_path, # for time correction
678
+ gpu_rays.d_opt_path,
679
+ gpu_rays.d_acc_time,
680
+ d_step_sizes_for_correction,
681
+ float(n),
682
+ SPEED_OF_LIGHT,
683
+ )
684
+
685
+ # 6. Early exit if all rays done
686
+ if num_active == 0:
687
+ break
688
+
689
+ # Reset crossing mask for next iteration
690
+ gpu_rays.d_crossing_mask.copy_to_device(np.zeros(num_rays, dtype=np.bool_))
691
+
692
+ # === ONE-TIME DOWNLOAD ===
693
+ gpu_rays.to_ray_batch(rays)
694
+
695
+ # Build hit data
696
+ hit_surface_idx, hit_positions, _ = gpu_rays.get_hit_data()
697
+ hit_directions = gpu_rays.d_directions.copy_to_host()
698
+
699
+ return HitData(
700
+ hit_surface_idx=hit_surface_idx,
701
+ hit_positions=hit_positions.astype(np.float32),
702
+ hit_directions=hit_directions.astype(np.float32),
703
+ num_rays=num_rays,
704
+ )
705
+
706
+
707
+ __all__ = ["GPUSurfacePropagator"]