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,402 @@
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
+ Surface Crossing Detection Kernels
36
+
37
+ GPU kernels for detecting when rays cross surfaces by monitoring
38
+ sign changes in signed distance values.
39
+ """
40
+
41
+ import math
42
+
43
+ # GPU support is optional
44
+ try:
45
+ from numba import cuda
46
+
47
+ HAS_CUDA = cuda.is_available()
48
+ except ImportError:
49
+ HAS_CUDA = False
50
+
51
+ class _FakeCuda:
52
+ @staticmethod
53
+ def jit(*args, **kwargs):
54
+ def decorator(func):
55
+ return func
56
+
57
+ if args and callable(args[0]):
58
+ return args[0]
59
+ return decorator
60
+
61
+ @staticmethod
62
+ def grid(n):
63
+ return 0
64
+
65
+ cuda = _FakeCuda() # type: ignore[assignment]
66
+
67
+
68
+ # Import device functions for signed distance computation
69
+ from ..intersection.signed_distance import (
70
+ _device_plane_sd,
71
+ _device_sphere_sd,
72
+ _device_gerstner_sd,
73
+ _device_curved_wave_sd,
74
+ _device_multi_curved_wave_sd,
75
+ _device_annular_plane_sd,
76
+ )
77
+
78
+
79
+ # =============================================================================
80
+ # Device Functions
81
+ # =============================================================================
82
+
83
+
84
+ @cuda.jit(device=True)
85
+ def _device_signed_distance(
86
+ x: float, y: float, z: float, geometry_id: int, params, param_offset: int
87
+ ) -> float:
88
+ """
89
+ Compute signed distance for a surface with parameters at given offset.
90
+
91
+ Parameters
92
+ ----------
93
+ x, y, z : float
94
+ Point coordinates
95
+ geometry_id : int
96
+ Surface geometry type (1-5)
97
+ params : device array
98
+ All surface parameters concatenated
99
+ param_offset : int
100
+ Offset into params for this surface (surface_idx * MAX_SURFACE_PARAMS)
101
+
102
+ Returns
103
+ -------
104
+ float
105
+ Signed distance to surface
106
+ """
107
+ # Create a local view of params for this surface
108
+ # Note: We pass the full params array and geometry_id handles the layout
109
+ if geometry_id == 1: # Plane
110
+ return _device_plane_sd(x, y, z, params[param_offset:])
111
+ elif geometry_id == 2: # Sphere
112
+ return _device_sphere_sd(x, y, z, params[param_offset:])
113
+ elif geometry_id == 3: # Gerstner wave
114
+ return _device_gerstner_sd(x, y, z, params[param_offset:])
115
+ elif geometry_id == 4: # Curved wave
116
+ return _device_curved_wave_sd(x, y, z, params[param_offset:])
117
+ elif geometry_id == 5: # Multi-wave curved
118
+ return _device_multi_curved_wave_sd(x, y, z, params[param_offset:])
119
+ elif geometry_id == 6: # Bounded plane (same SD formula as infinite plane)
120
+ return _device_plane_sd(x, y, z, params[param_offset:])
121
+ elif geometry_id == 7: # Annular plane
122
+ return _device_annular_plane_sd(x, y, z, params[param_offset:])
123
+ else:
124
+ return math.inf
125
+
126
+
127
+ # =============================================================================
128
+ # Main Kernels
129
+ # =============================================================================
130
+
131
+
132
+ @cuda.jit
133
+ def kernel_save_prev_positions(
134
+ positions, # (N, 3) current positions
135
+ prev_positions, # (N, 3) output: saved positions
136
+ active, # (N,) active mask
137
+ ):
138
+ """
139
+ Save current positions before propagation step.
140
+
141
+ Simple copy kernel for bisection - saves positions so we can
142
+ interpolate between prev and current to find exact crossing point.
143
+
144
+ Parameters
145
+ ----------
146
+ positions : (N, 3) float32 device array
147
+ Current ray positions
148
+ prev_positions : (N, 3) float32 device array
149
+ Output array to store previous positions
150
+ active : (N,) bool device array
151
+ Active ray mask (only save for active rays)
152
+ """
153
+ idx = cuda.grid(1)
154
+ if idx >= positions.shape[0] or not active[idx]:
155
+ return
156
+
157
+ prev_positions[idx, 0] = positions[idx, 0]
158
+ prev_positions[idx, 1] = positions[idx, 1]
159
+ prev_positions[idx, 2] = positions[idx, 2]
160
+
161
+
162
+ @cuda.jit
163
+ def kernel_detect_crossing(
164
+ positions, # (N, 3) current positions
165
+ prev_sd, # (N, S) previous signed distances
166
+ active, # (N,) active mask
167
+ geometry_ids, # (S,) geometry ID per surface
168
+ surface_params, # (S * MAX_PARAMS,) all surface parameters
169
+ num_surfaces, # int
170
+ max_params, # int: MAX_SURFACE_PARAMS
171
+ crossing_mask, # (N,) output: rays that crossed
172
+ hit_surface_idx, # (N,) output: which surface was hit
173
+ ):
174
+ """
175
+ Check all surfaces for sign changes in signed distance.
176
+
177
+ For each active ray, compute signed distance to all surfaces and
178
+ check for sign changes (crossings) compared to previous step.
179
+ First surface crossed wins (in surface order).
180
+
181
+ Parameters
182
+ ----------
183
+ positions : (N, 3) float32 device array
184
+ Current ray positions
185
+ prev_sd : (N, S) float32 device array
186
+ Previous signed distances per surface
187
+ active : (N,) bool device array
188
+ Active ray mask
189
+ geometry_ids : (S,) int32 device array
190
+ Geometry ID for each surface
191
+ surface_params : (S * MAX_PARAMS,) float32 device array
192
+ Concatenated surface parameters
193
+ num_surfaces : int
194
+ Number of surfaces
195
+ max_params : int
196
+ MAX_SURFACE_PARAMS constant
197
+ crossing_mask : (N,) bool device array
198
+ Output: True for rays that crossed any surface
199
+ hit_surface_idx : (N,) int32 device array
200
+ Output: Surface index that was crossed (-1 if none)
201
+ """
202
+ idx = cuda.grid(1)
203
+ if idx >= positions.shape[0] or not active[idx]:
204
+ return
205
+
206
+ x = positions[idx, 0]
207
+ y = positions[idx, 1]
208
+ z = positions[idx, 2]
209
+
210
+ crossing_mask[idx] = False
211
+
212
+ # Check each surface in order
213
+ for surf in range(num_surfaces):
214
+ geo_id = geometry_ids[surf]
215
+ param_offset = surf * max_params
216
+
217
+ # Compute current signed distance
218
+ curr_sd = _device_signed_distance(x, y, z, geo_id, surface_params, param_offset)
219
+ prev = prev_sd[idx, surf]
220
+
221
+ # Check for sign change (crossing)
222
+ if (prev >= 0 and curr_sd < 0) or (prev < 0 and curr_sd >= 0):
223
+ crossing_mask[idx] = True
224
+ hit_surface_idx[idx] = surf
225
+ return # First surface crossed wins
226
+
227
+ # Save current as prev for next step
228
+ prev_sd[idx, surf] = curr_sd
229
+
230
+
231
+ @cuda.jit
232
+ def kernel_init_signed_distances(
233
+ positions, # (N, 3) current positions
234
+ active, # (N,) active mask
235
+ geometry_ids, # (S,) geometry ID per surface
236
+ surface_params, # (S * MAX_PARAMS,) all surface parameters
237
+ num_surfaces, # int
238
+ max_params, # int: MAX_SURFACE_PARAMS
239
+ prev_sd, # (N, S) output: initial signed distances
240
+ ):
241
+ """
242
+ Initialize signed distances for all surfaces.
243
+
244
+ Called once at the start to populate prev_sd before the first
245
+ propagation step.
246
+
247
+ Parameters
248
+ ----------
249
+ positions : (N, 3) float32 device array
250
+ Initial ray positions
251
+ active : (N,) bool device array
252
+ Active ray mask
253
+ geometry_ids : (S,) int32 device array
254
+ Geometry ID for each surface
255
+ surface_params : (S * MAX_PARAMS,) float32 device array
256
+ Concatenated surface parameters
257
+ num_surfaces : int
258
+ Number of surfaces
259
+ max_params : int
260
+ MAX_SURFACE_PARAMS constant
261
+ prev_sd : (N, S) float32 device array
262
+ Output: Initial signed distances per surface
263
+ """
264
+ idx = cuda.grid(1)
265
+ if idx >= positions.shape[0] or not active[idx]:
266
+ return
267
+
268
+ x = positions[idx, 0]
269
+ y = positions[idx, 1]
270
+ z = positions[idx, 2]
271
+
272
+ for surf in range(num_surfaces):
273
+ geo_id = geometry_ids[surf]
274
+ param_offset = surf * max_params
275
+ prev_sd[idx, surf] = _device_signed_distance(
276
+ x, y, z, geo_id, surface_params, param_offset
277
+ )
278
+
279
+
280
+ @cuda.jit
281
+ def kernel_compute_min_surface_distance(
282
+ positions, # (N, 3) current positions
283
+ active, # (N,) active mask
284
+ geometry_ids, # (S,) geometry ID per surface
285
+ surface_params, # (S * MAX_PARAMS,) all surface parameters
286
+ num_surfaces, # int
287
+ max_params, # int: MAX_SURFACE_PARAMS
288
+ min_distances, # (N,) output: minimum distance to any surface
289
+ ):
290
+ """
291
+ Compute minimum absolute distance to any surface for each ray.
292
+
293
+ Used for adaptive step sizing - rays closer to surfaces use smaller steps.
294
+
295
+ Parameters
296
+ ----------
297
+ positions : (N, 3) float32 device array
298
+ Current ray positions
299
+ active : (N,) bool device array
300
+ Active ray mask
301
+ geometry_ids : (S,) int32 device array
302
+ Geometry ID for each surface
303
+ surface_params : (S * MAX_PARAMS,) float32 device array
304
+ Concatenated surface parameters
305
+ num_surfaces : int
306
+ Number of surfaces
307
+ max_params : int
308
+ MAX_SURFACE_PARAMS constant
309
+ min_distances : (N,) float32 device array
310
+ Output: Minimum absolute distance to any surface
311
+ """
312
+ idx = cuda.grid(1)
313
+ if idx >= positions.shape[0]:
314
+ return
315
+
316
+ if not active[idx]:
317
+ min_distances[idx] = math.inf
318
+ return
319
+
320
+ x = positions[idx, 0]
321
+ y = positions[idx, 1]
322
+ z = positions[idx, 2]
323
+
324
+ min_dist = math.inf
325
+
326
+ # Check all surfaces and find minimum absolute distance
327
+ for surf in range(num_surfaces):
328
+ geo_id = geometry_ids[surf]
329
+ param_offset = surf * max_params
330
+
331
+ sd = _device_signed_distance(x, y, z, geo_id, surface_params, param_offset)
332
+ abs_sd = abs(sd)
333
+
334
+ if abs_sd < min_dist:
335
+ min_dist = abs_sd
336
+
337
+ min_distances[idx] = min_dist
338
+
339
+
340
+ @cuda.jit
341
+ def kernel_compute_adaptive_steps(
342
+ min_distances, # (N,) minimum surface distances
343
+ active, # (N,) active mask
344
+ max_step_size, # float: maximum step size
345
+ min_step_size, # float: minimum step size
346
+ proximity_factor, # float: step = distance * factor
347
+ proximity_threshold, # float: threshold for adaptive stepping
348
+ adaptive_steps, # (N,) output: per-ray step sizes
349
+ ):
350
+ """
351
+ Compute adaptive step sizes based on surface proximity.
352
+
353
+ For rays within the proximity threshold, step size is proportional to
354
+ distance from surface. Otherwise, maximum step size is used.
355
+
356
+ Parameters
357
+ ----------
358
+ min_distances : (N,) float32 device array
359
+ Minimum distance to any surface per ray
360
+ active : (N,) bool device array
361
+ Active ray mask
362
+ max_step_size : float
363
+ Maximum step size (far from surfaces)
364
+ min_step_size : float
365
+ Minimum step size (very close to surfaces, ~0.3mm for 1ps resolution)
366
+ proximity_factor : float
367
+ Step size = distance * factor when within threshold
368
+ proximity_threshold : float
369
+ Distance within which to start adaptive stepping
370
+ adaptive_steps : (N,) float32 device array
371
+ Output: Per-ray step sizes
372
+ """
373
+ idx = cuda.grid(1)
374
+ if idx >= min_distances.shape[0]:
375
+ return
376
+
377
+ if not active[idx]:
378
+ adaptive_steps[idx] = max_step_size
379
+ return
380
+
381
+ dist = min_distances[idx]
382
+
383
+ if dist < proximity_threshold:
384
+ # Within threshold: step proportional to distance
385
+ step = dist * proximity_factor
386
+ # Clamp to [min_step_size, max_step_size]
387
+ if step < min_step_size:
388
+ step = min_step_size
389
+ elif step > max_step_size:
390
+ step = max_step_size
391
+ adaptive_steps[idx] = step
392
+ else:
393
+ adaptive_steps[idx] = max_step_size
394
+
395
+
396
+ __all__ = [
397
+ "kernel_save_prev_positions",
398
+ "kernel_detect_crossing",
399
+ "kernel_init_signed_distances",
400
+ "kernel_compute_min_surface_distance",
401
+ "kernel_compute_adaptive_steps",
402
+ ]
@@ -0,0 +1,166 @@
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
+ Status Reduction Kernel
36
+
37
+ GPU kernel for counting active rays and detecting crossings.
38
+ Uses parallel reduction to efficiently compute loop termination conditions.
39
+ """
40
+
41
+ # GPU support is optional
42
+ try:
43
+ from numba import cuda
44
+
45
+ HAS_CUDA = cuda.is_available()
46
+ except ImportError:
47
+ HAS_CUDA = False
48
+
49
+ class _FakeCuda:
50
+ @staticmethod
51
+ def jit(*args, **kwargs):
52
+ def decorator(func):
53
+ return func
54
+
55
+ if args and callable(args[0]):
56
+ return args[0]
57
+ return decorator
58
+
59
+ @staticmethod
60
+ def grid(n):
61
+ return 0
62
+
63
+ class shared:
64
+ @staticmethod
65
+ def array(size, dtype):
66
+ return None
67
+
68
+ class atomic:
69
+ @staticmethod
70
+ def add(arr, idx, val):
71
+ pass
72
+
73
+ cuda = _FakeCuda() # type: ignore[assignment]
74
+
75
+
76
+ @cuda.jit
77
+ def kernel_reduce_status(
78
+ active, # (N,) active mask
79
+ crossing_mask, # (N,) rays that crossed
80
+ result, # (2,) output: [num_active, num_crossing]
81
+ ):
82
+ """
83
+ Count active rays and check for crossings.
84
+
85
+ Uses atomic operations for simplicity. For large ray counts,
86
+ a proper tree reduction would be more efficient.
87
+
88
+ Parameters
89
+ ----------
90
+ active : (N,) bool device array
91
+ Active ray mask
92
+ crossing_mask : (N,) bool device array
93
+ Rays that crossed a surface
94
+ result : (2,) int32 device array
95
+ Output: [num_active, num_crossing]
96
+ Must be initialized to zeros before calling.
97
+ """
98
+ idx = cuda.grid(1)
99
+ if idx >= active.shape[0]:
100
+ return
101
+
102
+ if active[idx]:
103
+ cuda.atomic.add(result, 0, 1)
104
+
105
+ if crossing_mask[idx]:
106
+ cuda.atomic.add(result, 1, 1)
107
+
108
+
109
+ @cuda.jit
110
+ def kernel_reduce_status_shared(
111
+ active, # (N,) active mask
112
+ crossing_mask, # (N,) rays that crossed
113
+ result, # (2,) output: [num_active, num_crossing]
114
+ block_size, # int: threads per block
115
+ ):
116
+ """
117
+ Count active rays and crossings using shared memory reduction.
118
+
119
+ More efficient than atomic-only version for large ray counts.
120
+ Uses tree reduction within each block, then atomic add for final sum.
121
+
122
+ Parameters
123
+ ----------
124
+ active : (N,) bool device array
125
+ Active ray mask
126
+ crossing_mask : (N,) bool device array
127
+ Rays that crossed a surface
128
+ result : (2,) int32 device array
129
+ Output: [num_active, num_crossing]
130
+ Must be initialized to zeros before calling.
131
+ block_size : int
132
+ Number of threads per block (must match launch config)
133
+ """
134
+ # Shared memory for reduction
135
+ shared_active = cuda.shared.array(256, dtype=cuda.int32)
136
+ shared_crossing = cuda.shared.array(256, dtype=cuda.int32)
137
+
138
+ tid = cuda.threadIdx.x
139
+ idx = cuda.grid(1)
140
+
141
+ # Load data
142
+ if idx < active.shape[0]:
143
+ shared_active[tid] = 1 if active[idx] else 0
144
+ shared_crossing[tid] = 1 if crossing_mask[idx] else 0
145
+ else:
146
+ shared_active[tid] = 0
147
+ shared_crossing[tid] = 0
148
+
149
+ cuda.syncthreads()
150
+
151
+ # Tree reduction
152
+ s = cuda.blockDim.x // 2
153
+ while s > 0:
154
+ if tid < s:
155
+ shared_active[tid] += shared_active[tid + s]
156
+ shared_crossing[tid] += shared_crossing[tid + s]
157
+ cuda.syncthreads()
158
+ s //= 2
159
+
160
+ # First thread in block adds to global result
161
+ if tid == 0:
162
+ cuda.atomic.add(result, 0, shared_active[0])
163
+ cuda.atomic.add(result, 1, shared_crossing[0])
164
+
165
+
166
+ __all__ = ["kernel_reduce_status", "kernel_reduce_status_shared"]