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,742 @@
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 Signed Distance Functions for All Surface Types
36
+
37
+ Generic dispatch kernel that computes signed distance for any surface type
38
+ based on geometry_id. This allows GPU-accelerated intersection detection
39
+ for all surfaces via bisection on signed distance values.
40
+
41
+ Device functions compute the signed distance formula for each geometry type.
42
+ The main kernel dispatches to the appropriate device function based on ID.
43
+
44
+ Geometry IDs:
45
+ 1 = Plane: signed distance to infinite plane
46
+ 2 = Sphere: signed distance to sphere surface
47
+ 3 = Gerstner wave: flat-earth wave surface
48
+ 4 = Curved wave: spherical-earth wave surface (single wave)
49
+ 5 = Multi-wave curved: spherical-earth with up to 8 wave components
50
+ 7 = Annular plane: ring-shaped planar surface (detector)
51
+
52
+ This module contains ONLY pure CUDA kernels - no GPU memory management.
53
+ The propagator layer is responsible for cuda.to_device() and copy_to_host().
54
+ """
55
+
56
+ import math
57
+
58
+ from ..registry import IntersectionKernelID, register_kernel
59
+
60
+ # GPU support is optional
61
+ try:
62
+ from numba import cuda
63
+
64
+ HAS_CUDA = True
65
+ except ImportError:
66
+
67
+ class _FakeCuda:
68
+ """Fake cuda module for when numba is not installed."""
69
+
70
+ @staticmethod
71
+ def jit(*args, **kwargs):
72
+ """Return a no-op decorator."""
73
+
74
+ def decorator(func):
75
+ return func
76
+
77
+ if args and callable(args[0]):
78
+ return args[0]
79
+ return decorator
80
+
81
+ @staticmethod
82
+ def is_available():
83
+ return False
84
+
85
+ @staticmethod
86
+ def grid(n):
87
+ return 0
88
+
89
+ @staticmethod
90
+ def synchronize():
91
+ pass
92
+
93
+ cuda = _FakeCuda() # type: ignore[assignment]
94
+ HAS_CUDA = False
95
+
96
+
97
+ # =============================================================================
98
+ # Constants
99
+ # =============================================================================
100
+
101
+ GRAVITY = 9.81 # Gravity for deep water dispersion
102
+
103
+
104
+ # =============================================================================
105
+ # Device Functions - Signed Distance
106
+ # =============================================================================
107
+
108
+
109
+ @cuda.jit(device=True)
110
+ def _device_plane_sd(x: float, y: float, z: float, params) -> float:
111
+ """
112
+ Signed distance from point to plane.
113
+
114
+ Plane: signed_distance = dot(p - point, normal)
115
+ Positive on normal side, negative on back side.
116
+
117
+ Parameters (from PlaneSurface.get_gpu_parameters):
118
+ p0: normal_x
119
+ p1: normal_y
120
+ p2: normal_z
121
+ p3: point_x
122
+ p4: point_y
123
+ p5: point_z
124
+ """
125
+ nx, ny, nz = params[0], params[1], params[2]
126
+ px, py, pz = params[3], params[4], params[5]
127
+
128
+ return nx * (x - px) + ny * (y - py) + nz * (z - pz)
129
+
130
+
131
+ @cuda.jit(device=True)
132
+ def _device_sphere_sd(x: float, y: float, z: float, params) -> float:
133
+ """
134
+ Signed distance from point to sphere surface.
135
+
136
+ Sphere: signed_distance = |p - center| - radius
137
+ Positive outside, negative inside.
138
+
139
+ Parameters (from SphereSurface.get_gpu_parameters):
140
+ p0: center_x
141
+ p1: center_y
142
+ p2: center_z
143
+ p3: radius
144
+ """
145
+ cx, cy, cz = params[0], params[1], params[2]
146
+ r = params[3]
147
+
148
+ dx = x - cx
149
+ dy = y - cy
150
+ dz = z - cz
151
+
152
+ dist = math.sqrt(dx * dx + dy * dy + dz * dz)
153
+ return dist - abs(r)
154
+
155
+
156
+ @cuda.jit(device=True)
157
+ def _device_gerstner_sd(x: float, y: float, z: float, params) -> float:
158
+ """
159
+ Signed distance from point to Gerstner wave surface.
160
+
161
+ Wave: z = ref_z + A * cos(k * (dx*x + dy*y) - omega*t + phase)
162
+ Signed distance: z - wave_height (positive above, negative below)
163
+
164
+ Parameters (from GPUGerstnerWaveSurface.get_gpu_parameters):
165
+ p0: amplitude
166
+ p1: wave_number (k)
167
+ p2: dir_x (normalized)
168
+ p3: dir_y (normalized)
169
+ p4: reference_z
170
+ p5: phase
171
+ p6: time
172
+ """
173
+ A = params[0]
174
+ k = params[1]
175
+ dir_x, dir_y = params[2], params[3]
176
+ ref_z = params[4]
177
+ phase = params[5]
178
+ time = params[6]
179
+
180
+ # Angular frequency from deep water dispersion: omega = sqrt(g*k)
181
+ omega = math.sqrt(GRAVITY * k)
182
+
183
+ # Phase at this position
184
+ phase_val = k * (dir_x * x + dir_y * y) - omega * time + phase
185
+
186
+ # Wave height
187
+ wave_height = ref_z + A * math.cos(phase_val)
188
+
189
+ return z - wave_height
190
+
191
+
192
+ @cuda.jit(device=True)
193
+ def _device_curved_wave_sd(x: float, y: float, z: float, params) -> float:
194
+ """
195
+ Signed distance from point to curved-earth wave surface.
196
+
197
+ The surface is Earth's sphere + wave perturbation.
198
+ Signed distance: |p - center| - (earth_radius + wave_height)
199
+ Positive outside, negative inside.
200
+
201
+ Uses numerically stable formulation to avoid catastrophic cancellation
202
+ when computing dist - R with large Earth radius values in float32.
203
+
204
+ For earth_center = (cx, cy, cz):
205
+ dist² - R² = (x-cx)² + (y-cy)² + (z-cz)² - R²
206
+ = x² + y² + z² - 2(x·cx + y·cy + z·cz) + (cx² + cy² + cz²) - R²
207
+
208
+ For the typical case center = (0, 0, -R):
209
+ dist² - R² = x² + y² + z² + 2zR (since cz = -R, so -2z·cz = 2zR, and cx²+cy²+cz²-R² = 0)
210
+
211
+ Parameters (from GPUCurvedWaveSurface.get_gpu_parameters):
212
+ p0: earth_center_x
213
+ p1: earth_center_y
214
+ p2: earth_center_z
215
+ p3: earth_radius
216
+ p4: amplitude
217
+ p5: wave_number (k)
218
+ p6: dir_x (normalized)
219
+ p7: dir_y (normalized)
220
+ p8: time
221
+ """
222
+ cx, cy, cz = params[0], params[1], params[2]
223
+ earth_radius = params[3]
224
+ A = params[4]
225
+ k = params[5]
226
+ dir_x, dir_y = params[6], params[7]
227
+ time = params[8]
228
+
229
+ # Vector from Earth center to point
230
+ dx = x - cx
231
+ dy = y - cy
232
+ dz = z - cz
233
+
234
+ # Compute dist² and dist
235
+ dist_sq = dx * dx + dy * dy + dz * dz
236
+ dist = math.sqrt(dist_sq)
237
+
238
+ # Numerically stable computation of dist² - R²
239
+ # Expand: dist² - R² = x² + y² + z² - 2(x·cx + y·cy + z·cz) + (cx² + cy² + cz²) - R²
240
+ # For typical case (cx=0, cy=0, cz=-R), this simplifies to x² + y² + z² + 2zR
241
+ # which avoids subtracting two large ~O(R²) numbers
242
+ pos_sq = x * x + y * y + z * z
243
+ dot_pc = x * cx + y * cy + z * cz
244
+ center_sq = cx * cx + cy * cy + cz * cz
245
+ R_sq = earth_radius * earth_radius
246
+
247
+ # dist² - R² computed stably (small terms when near surface)
248
+ dist_sq_minus_R_sq = pos_sq - 2.0 * dot_pc + center_sq - R_sq
249
+
250
+ # Altitude above Earth: (dist² - R²) / (dist + R) = dist - R
251
+ altitude = dist_sq_minus_R_sq / (dist + earth_radius)
252
+
253
+ # Angular frequency from deep water dispersion
254
+ omega = math.sqrt(GRAVITY * k)
255
+
256
+ # Wave phase using x,y as local tangent coordinates
257
+ dot_val = dir_x * x + dir_y * y
258
+ phase_val = k * dot_val - omega * time
259
+
260
+ # Wave height
261
+ wave_height = A * math.cos(phase_val)
262
+
263
+ # Signed distance = altitude above Earth - wave_height
264
+ # Positive when above wave surface, negative when below
265
+ return altitude - wave_height
266
+
267
+
268
+ @cuda.jit(device=True)
269
+ def _device_multi_curved_wave_sd(x: float, y: float, z: float, params) -> float:
270
+ """
271
+ Signed distance from point to multi-wave curved-earth surface.
272
+
273
+ Supports up to 8 superimposed wave components on Earth's sphere.
274
+ Signed distance: |p - center| - (earth_radius + sum_of_wave_heights)
275
+ Positive outside, negative inside.
276
+
277
+ Uses numerically stable formulation to avoid catastrophic cancellation
278
+ when computing dist - R with large Earth radius values in float32.
279
+
280
+ Parameters (from GPUMultiCurvedWaveSurface.get_gpu_parameters):
281
+ p0: earth_center_x
282
+ p1: earth_center_y
283
+ p2: earth_center_z
284
+ p3: earth_radius
285
+ p4: time
286
+ p5: num_waves
287
+ p6-p7: reserved
288
+
289
+ For each wave i (i=0..7), starting at offset 8 + i*8:
290
+ - p[offset+0]: amplitude
291
+ - p[offset+1]: wave_number (k)
292
+ - p[offset+2]: dir_x (normalized)
293
+ - p[offset+3]: dir_y (normalized)
294
+ - p[offset+4]: phase
295
+ - p[offset+5]: steepness (unused in SD calculation)
296
+ - p[offset+6-7]: reserved
297
+ """
298
+ cx, cy, cz = params[0], params[1], params[2]
299
+ earth_radius = params[3]
300
+ time = params[4]
301
+ num_waves = int(params[5])
302
+
303
+ # Vector from Earth center to point
304
+ dx = x - cx
305
+ dy = y - cy
306
+ dz = z - cz
307
+
308
+ # Compute dist² and dist
309
+ dist_sq = dx * dx + dy * dy + dz * dz
310
+ dist = math.sqrt(dist_sq)
311
+
312
+ # Numerically stable computation of dist² - R²
313
+ # Expand: dist² - R² = x² + y² + z² - 2(x·cx + y·cy + z·cz) + (cx² + cy² + cz²) - R²
314
+ pos_sq = x * x + y * y + z * z
315
+ dot_pc = x * cx + y * cy + z * cz
316
+ center_sq = cx * cx + cy * cy + cz * cz
317
+ R_sq = earth_radius * earth_radius
318
+
319
+ # dist² - R² computed stably (small terms when near surface)
320
+ dist_sq_minus_R_sq = pos_sq - 2.0 * dot_pc + center_sq - R_sq
321
+
322
+ # Altitude above Earth: (dist² - R²) / (dist + R) = dist - R
323
+ altitude = dist_sq_minus_R_sq / (dist + earth_radius)
324
+
325
+ # Sum wave heights from all components
326
+ total_wave_height = 0.0
327
+ for i in range(num_waves):
328
+ offset = 8 + i * 8
329
+ A = params[offset + 0]
330
+ k = params[offset + 1]
331
+ dir_x = params[offset + 2]
332
+ dir_y = params[offset + 3]
333
+ phase = params[offset + 4]
334
+
335
+ # Angular frequency from deep water dispersion
336
+ omega = math.sqrt(GRAVITY * k)
337
+
338
+ # Wave phase using x,y as local tangent coordinates
339
+ dot_val = dir_x * x + dir_y * y
340
+ phase_val = k * dot_val - omega * time + phase
341
+
342
+ # Accumulate wave height
343
+ total_wave_height += A * math.cos(phase_val)
344
+
345
+ # Signed distance = altitude above Earth - wave_height
346
+ # Positive when above wave surface, negative when below
347
+ return altitude - total_wave_height
348
+
349
+
350
+ # =============================================================================
351
+ # Device Functions - Surface Normal
352
+ # =============================================================================
353
+
354
+
355
+ @cuda.jit(device=True)
356
+ def _device_plane_normal(
357
+ x: float, y: float, z: float, params, out_nx, out_ny, out_nz
358
+ ) -> tuple:
359
+ """
360
+ Compute normal for plane surface.
361
+
362
+ For a plane, normal is constant everywhere.
363
+ Returns (nx, ny, nz) tuple.
364
+ """
365
+ # Normal is stored directly in params
366
+ return params[0], params[1], params[2]
367
+
368
+
369
+ @cuda.jit(device=True)
370
+ def _device_sphere_normal(
371
+ x: float, y: float, z: float, params, out_nx, out_ny, out_nz
372
+ ) -> tuple:
373
+ """
374
+ Compute normal for sphere surface.
375
+
376
+ Normal points radially outward from center.
377
+ Returns (nx, ny, nz) tuple.
378
+ """
379
+ cx, cy, cz = params[0], params[1], params[2]
380
+ r = params[3]
381
+
382
+ dx = x - cx
383
+ dy = y - cy
384
+ dz = z - cz
385
+
386
+ norm = math.sqrt(dx * dx + dy * dy + dz * dz)
387
+ if norm < 1e-12:
388
+ return 0.0, 0.0, 1.0
389
+
390
+ nx = dx / norm
391
+ ny = dy / norm
392
+ nz = dz / norm
393
+
394
+ # For negative radius (concave), flip normals
395
+ if r < 0:
396
+ nx, ny, nz = -nx, -ny, -nz
397
+
398
+ return nx, ny, nz
399
+
400
+
401
+ @cuda.jit(device=True)
402
+ def _device_gerstner_normal(
403
+ x: float, y: float, z: float, params, out_nx, out_ny, out_nz
404
+ ) -> tuple:
405
+ """
406
+ Compute normal for Gerstner wave surface.
407
+
408
+ Normal is computed from gradient of height field.
409
+ For z = ref_z + A*cos(theta):
410
+ dz/dx = -A * k * dir_x * sin(theta)
411
+ dz/dy = -A * k * dir_y * sin(theta)
412
+ Normal = (-dz/dx, -dz/dy, 1) normalized
413
+
414
+ Returns (nx, ny, nz) tuple.
415
+ """
416
+ A = params[0]
417
+ k = params[1]
418
+ dir_x, dir_y = params[2], params[3]
419
+ phase = params[5]
420
+ time = params[6]
421
+
422
+ omega = math.sqrt(GRAVITY * k)
423
+ phase_val = k * (dir_x * x + dir_y * y) - omega * time + phase
424
+
425
+ sin_theta = math.sin(phase_val)
426
+ dz_dx = -A * k * dir_x * sin_theta
427
+ dz_dy = -A * k * dir_y * sin_theta
428
+
429
+ # Normal = (-dz/dx, -dz/dy, 1) normalized
430
+ nx = -dz_dx
431
+ ny = -dz_dy
432
+ nz = 1.0
433
+
434
+ norm = math.sqrt(nx * nx + ny * ny + nz * nz)
435
+ return nx / norm, ny / norm, nz / norm
436
+
437
+
438
+ @cuda.jit(device=True)
439
+ def _device_curved_wave_normal(
440
+ x: float, y: float, z: float, params, out_nx, out_ny, out_nz
441
+ ) -> tuple:
442
+ """
443
+ Compute normal for curved-earth wave surface.
444
+
445
+ This is a simplified version - for full accuracy, transform to local
446
+ tangent space. Here we compute radial + wave perturbation.
447
+
448
+ Returns (nx, ny, nz) tuple.
449
+ """
450
+ cx, cy, cz = params[0], params[1], params[2]
451
+ A = params[4]
452
+ k = params[5]
453
+ dir_x, dir_y = params[6], params[7]
454
+ time = params[8]
455
+
456
+ # Radial direction from Earth center
457
+ dx = x - cx
458
+ dy = y - cy
459
+ dz = z - cz
460
+ dist = math.sqrt(dx * dx + dy * dy + dz * dz)
461
+ if dist < 1e-10:
462
+ return 0.0, 0.0, 1.0
463
+
464
+ radial_x = dx / dist
465
+ radial_y = dy / dist
466
+ radial_z = dz / dist
467
+
468
+ # Wave normal perturbation (simplified)
469
+ omega = math.sqrt(GRAVITY * k)
470
+ dot_val = dir_x * x + dir_y * y
471
+ phase_val = k * dot_val - omega * time
472
+
473
+ sin_theta = math.sin(phase_val)
474
+ WA = k * A
475
+
476
+ # Local wave gradient contribution (small perturbation)
477
+ # This is simplified - full version would transform to tangent space
478
+ nx_local = dir_x * WA * sin_theta
479
+ ny_local = dir_y * WA * sin_theta
480
+ nz_local = 1.0 - 0.5 * WA * sin_theta
481
+
482
+ # Combine radial with local perturbation (simplified blend)
483
+ nx = radial_x + 0.01 * nx_local
484
+ ny = radial_y + 0.01 * ny_local
485
+ nz = radial_z + 0.01 * nz_local
486
+
487
+ norm = math.sqrt(nx * nx + ny * ny + nz * nz)
488
+ if norm < 1e-12:
489
+ return radial_x, radial_y, radial_z
490
+
491
+ return nx / norm, ny / norm, nz / norm
492
+
493
+
494
+ @cuda.jit(device=True)
495
+ def _device_multi_curved_wave_normal(
496
+ x: float, y: float, z: float, params, out_nx, out_ny, out_nz
497
+ ) -> tuple:
498
+ """
499
+ Compute normal for multi-wave curved-earth surface.
500
+
501
+ Combines radial direction with wave gradient perturbations from
502
+ all wave components.
503
+
504
+ Returns (nx, ny, nz) tuple.
505
+ """
506
+ cx, cy, cz = params[0], params[1], params[2]
507
+ time = params[4]
508
+ num_waves = int(params[5])
509
+
510
+ # Radial direction from Earth center
511
+ dx = x - cx
512
+ dy = y - cy
513
+ dz = z - cz
514
+ dist = math.sqrt(dx * dx + dy * dy + dz * dz)
515
+ if dist < 1e-10:
516
+ return 0.0, 0.0, 1.0
517
+
518
+ radial_x = dx / dist
519
+ radial_y = dy / dist
520
+ radial_z = dz / dist
521
+
522
+ # Accumulate wave normal perturbations
523
+ nx_local = 0.0
524
+ ny_local = 0.0
525
+ nz_local = 1.0
526
+
527
+ for i in range(num_waves):
528
+ offset = 8 + i * 8
529
+ A = params[offset + 0]
530
+ k = params[offset + 1]
531
+ dir_x = params[offset + 2]
532
+ dir_y = params[offset + 3]
533
+ phase = params[offset + 4]
534
+
535
+ omega = math.sqrt(GRAVITY * k)
536
+ dot_val = dir_x * x + dir_y * y
537
+ phase_val = k * dot_val - omega * time + phase
538
+
539
+ sin_theta = math.sin(phase_val)
540
+ WA = k * A
541
+
542
+ # Accumulate gradient contributions
543
+ nx_local += dir_x * WA * sin_theta
544
+ ny_local += dir_y * WA * sin_theta
545
+ nz_local -= 0.5 * WA * sin_theta
546
+
547
+ # Normalize local normal
548
+ local_norm = math.sqrt(
549
+ nx_local * nx_local + ny_local * ny_local + nz_local * nz_local
550
+ )
551
+ if local_norm > 1e-10:
552
+ nx_local /= local_norm
553
+ ny_local /= local_norm
554
+ nz_local /= local_norm
555
+
556
+ # Combine radial with local perturbation (simplified blend)
557
+ nx = radial_x + 0.01 * nx_local
558
+ ny = radial_y + 0.01 * ny_local
559
+ nz = radial_z + 0.01 * nz_local
560
+
561
+ norm = math.sqrt(nx * nx + ny * ny + nz * nz)
562
+ if norm < 1e-12:
563
+ return radial_x, radial_y, radial_z
564
+
565
+ return nx / norm, ny / norm, nz / norm
566
+
567
+
568
+ @cuda.jit(device=True)
569
+ def _device_annular_plane_sd(x: float, y: float, z: float, params) -> float:
570
+ """
571
+ Signed distance from point to annular plane.
572
+
573
+ For signed distance, we compute distance to the infinite plane.
574
+ Bounds checking (inner/outer radius) is done separately in intersection.
575
+
576
+ Plane: signed_distance = dot(p - center, normal)
577
+ Positive on normal side, negative on back side.
578
+
579
+ Parameters (from AnnularPlaneSurface.get_gpu_parameters):
580
+ p0: normal_x
581
+ p1: normal_y
582
+ p2: normal_z
583
+ p3: center_x
584
+ p4: center_y
585
+ p5: center_z
586
+ p6-11: u_axis, v_axis (not needed for signed distance)
587
+ p12: inner_radius_sq (not used in SD)
588
+ p13: outer_radius_sq (not used in SD)
589
+ """
590
+ nx, ny, nz = params[0], params[1], params[2]
591
+ cx, cy, cz = params[3], params[4], params[5]
592
+
593
+ return nx * (x - cx) + ny * (y - cy) + nz * (z - cz)
594
+
595
+
596
+ @cuda.jit(device=True)
597
+ def _device_annular_plane_normal(
598
+ x: float, y: float, z: float, params, out_nx, out_ny, out_nz
599
+ ) -> tuple:
600
+ """
601
+ Compute normal for annular plane surface.
602
+
603
+ For a plane, normal is constant everywhere.
604
+ Returns (nx, ny, nz) tuple.
605
+ """
606
+ # Normal is stored directly in params
607
+ return params[0], params[1], params[2]
608
+
609
+
610
+ # =============================================================================
611
+ # Main Dispatch Kernels
612
+ # =============================================================================
613
+
614
+
615
+ @register_kernel(IntersectionKernelID.SIGNED_DISTANCE_GENERIC)
616
+ @cuda.jit
617
+ def kernel_signed_distance(
618
+ positions, # (N, 3) input positions
619
+ signed_dist_out, # (N,) output signed distances
620
+ geometry_id, # int: 1=plane, 2=sphere, 3=gerstner, 4=curved_wave, 5=multi_curved_wave
621
+ params, # (64,) surface parameters from get_gpu_parameters()
622
+ ):
623
+ """
624
+ CUDA kernel to compute signed distance from points to a surface.
625
+
626
+ Dispatches to appropriate device function based on geometry_id.
627
+ This kernel enables GPU-accelerated intersection detection for all
628
+ surface types via bisection on signed distance values.
629
+
630
+ Parameters
631
+ ----------
632
+ positions : (N, 3) float32 device array
633
+ Points to compute signed distance for
634
+ signed_dist_out : (N,) float32 device array
635
+ Output array for signed distances
636
+ geometry_id : int
637
+ Surface geometry type:
638
+ 1 = Plane
639
+ 2 = Sphere
640
+ 3 = Gerstner wave (flat earth)
641
+ 4 = Curved wave (spherical earth)
642
+ 5 = Multi-wave curved (up to 8 waves)
643
+ params : (64,) float32 device array
644
+ Surface parameters from surface.get_gpu_parameters()
645
+ """
646
+ idx = cuda.grid(1)
647
+ if idx >= positions.shape[0]:
648
+ return
649
+
650
+ x = positions[idx, 0]
651
+ y = positions[idx, 1]
652
+ z = positions[idx, 2]
653
+
654
+ if geometry_id == 1:
655
+ signed_dist_out[idx] = _device_plane_sd(x, y, z, params)
656
+ elif geometry_id == 2:
657
+ signed_dist_out[idx] = _device_sphere_sd(x, y, z, params)
658
+ elif geometry_id == 3:
659
+ signed_dist_out[idx] = _device_gerstner_sd(x, y, z, params)
660
+ elif geometry_id == 4:
661
+ signed_dist_out[idx] = _device_curved_wave_sd(x, y, z, params)
662
+ elif geometry_id == 5:
663
+ signed_dist_out[idx] = _device_multi_curved_wave_sd(x, y, z, params)
664
+ elif geometry_id == 6:
665
+ # Bounded plane uses same SD formula as infinite plane
666
+ signed_dist_out[idx] = _device_plane_sd(x, y, z, params)
667
+ elif geometry_id == 7:
668
+ signed_dist_out[idx] = _device_annular_plane_sd(x, y, z, params)
669
+ else:
670
+ # Unknown geometry - set to inf (no intersection possible)
671
+ signed_dist_out[idx] = math.inf
672
+
673
+
674
+ @register_kernel(IntersectionKernelID.SURFACE_NORMAL_GENERIC)
675
+ @cuda.jit
676
+ def kernel_surface_normal(
677
+ positions, # (N, 3) input positions
678
+ normals_out, # (N, 3) output normals
679
+ geometry_id, # int: 1=plane, 2=sphere, 3=gerstner, 4=curved_wave, 5=multi_curved_wave
680
+ params, # (64,) surface parameters
681
+ ):
682
+ """
683
+ CUDA kernel to compute surface normal at given positions.
684
+
685
+ Dispatches to appropriate device function based on geometry_id.
686
+ Used after intersection detection to compute normals for Fresnel/reflection.
687
+
688
+ Parameters
689
+ ----------
690
+ positions : (N, 3) float32 device array
691
+ Points on the surface (or close to it)
692
+ normals_out : (N, 3) float32 device array
693
+ Output array for normal vectors
694
+ geometry_id : int
695
+ Surface geometry type (1-5)
696
+ params : (64,) float32 device array
697
+ Surface parameters from surface.get_gpu_parameters()
698
+ """
699
+ idx = cuda.grid(1)
700
+ if idx >= positions.shape[0]:
701
+ return
702
+
703
+ x = positions[idx, 0]
704
+ y = positions[idx, 1]
705
+ z = positions[idx, 2]
706
+
707
+ # Placeholder for output (not used by device functions, just for signature)
708
+ out_nx, out_ny, out_nz = 0.0, 0.0, 0.0
709
+
710
+ if geometry_id == 1:
711
+ nx, ny, nz = _device_plane_normal(x, y, z, params, out_nx, out_ny, out_nz)
712
+ elif geometry_id == 2:
713
+ nx, ny, nz = _device_sphere_normal(x, y, z, params, out_nx, out_ny, out_nz)
714
+ elif geometry_id == 3:
715
+ nx, ny, nz = _device_gerstner_normal(x, y, z, params, out_nx, out_ny, out_nz)
716
+ elif geometry_id == 4:
717
+ nx, ny, nz = _device_curved_wave_normal(x, y, z, params, out_nx, out_ny, out_nz)
718
+ elif geometry_id == 5:
719
+ nx, ny, nz = _device_multi_curved_wave_normal(
720
+ x, y, z, params, out_nx, out_ny, out_nz
721
+ )
722
+ elif geometry_id == 6:
723
+ # Bounded plane uses same normal as infinite plane
724
+ nx, ny, nz = _device_plane_normal(x, y, z, params, out_nx, out_ny, out_nz)
725
+ elif geometry_id == 7:
726
+ nx, ny, nz = _device_annular_plane_normal(
727
+ x, y, z, params, out_nx, out_ny, out_nz
728
+ )
729
+ else:
730
+ # Unknown geometry - default to +Z normal
731
+ nx, ny, nz = 0.0, 0.0, 1.0
732
+
733
+ normals_out[idx, 0] = nx
734
+ normals_out[idx, 1] = ny
735
+ normals_out[idx, 2] = nz
736
+
737
+
738
+ __all__ = [
739
+ "kernel_signed_distance",
740
+ "kernel_surface_normal",
741
+ "HAS_CUDA",
742
+ ]