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,875 @@
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 Kernels for SpectralInhomogeneousModel
36
+
37
+ CUDA kernels for GPU-accelerated ray propagation through
38
+ radially-symmetric materials with wavelength-dependent refractive index,
39
+ using 2D lookup table interpolation (altitude × wavelength).
40
+ """
41
+
42
+ import math
43
+
44
+ # GPU support is optional
45
+ try:
46
+ from numba import cuda
47
+
48
+ HAS_CUDA = True
49
+ except ImportError:
50
+
51
+ class _FakeCuda:
52
+ """Fake cuda module for when numba is not installed."""
53
+
54
+ class devicearray:
55
+ """Fake devicearray submodule."""
56
+
57
+ DeviceNDArray = object
58
+
59
+ @staticmethod
60
+ def jit(*args, **kwargs):
61
+ """Return a no-op decorator."""
62
+
63
+ def decorator(func):
64
+ return func
65
+
66
+ if args and callable(args[0]):
67
+ return args[0]
68
+ return decorator
69
+
70
+ @staticmethod
71
+ def is_available():
72
+ return False
73
+
74
+ @staticmethod
75
+ def grid(n):
76
+ return 0
77
+
78
+ @staticmethod
79
+ def synchronize():
80
+ pass
81
+
82
+ cuda = _FakeCuda() # type: ignore[assignment]
83
+ HAS_CUDA = False
84
+
85
+ from ..device_functions import device_euler_step
86
+ from ..registry import PropagationKernelID, register_kernel
87
+
88
+ SPEED_OF_LIGHT = 299792458.0
89
+
90
+
91
+ @cuda.jit(device=True)
92
+ def _device_bilinear_interpolate(
93
+ val1: float,
94
+ val2: float,
95
+ lut: cuda.devicearray.DeviceNDArray,
96
+ min1: float,
97
+ delta1: float,
98
+ n1: int,
99
+ min2: float,
100
+ delta2: float,
101
+ n2: int,
102
+ ) -> float:
103
+ """
104
+ Generic bilinear interpolation in 2D LUT.
105
+
106
+ Parameters
107
+ ----------
108
+ val1 : float
109
+ Value in first dimension (e.g., altitude)
110
+ val2 : float
111
+ Value in second dimension (e.g., wavelength)
112
+ lut : device array
113
+ 2D lookup table [dim1, dim2]
114
+ min1 : float
115
+ Minimum value in first dimension
116
+ delta1 : float
117
+ Spacing in first dimension
118
+ n1 : int
119
+ Number of entries in first dimension
120
+ min2 : float
121
+ Minimum value in second dimension
122
+ delta2 : float
123
+ Spacing in second dimension
124
+ n2 : int
125
+ Number of entries in second dimension
126
+
127
+ Returns
128
+ -------
129
+ float
130
+ Interpolated value
131
+ """
132
+ # Normalize coordinates to fractional indices
133
+ idx1 = (val1 - min1) / delta1
134
+ idx2 = (val2 - min2) / delta2
135
+
136
+ # Clamp to valid bounds
137
+ if idx1 < 0.0:
138
+ idx1 = 0.0
139
+ if idx1 > n1 - 1.001:
140
+ idx1 = n1 - 1.001
141
+ if idx2 < 0.0:
142
+ idx2 = 0.0
143
+ if idx2 > n2 - 1.001:
144
+ idx2 = n2 - 1.001
145
+
146
+ # Get integer indices
147
+ i0 = int(idx1)
148
+ j0 = int(idx2)
149
+ i1 = i0 + 1
150
+ j1 = j0 + 1
151
+
152
+ # Clamp upper indices
153
+ if i1 > n1 - 1:
154
+ i1 = n1 - 1
155
+ if j1 > n2 - 1:
156
+ j1 = n2 - 1
157
+
158
+ # Fractional parts
159
+ fi = idx1 - i0
160
+ fj = idx2 - j0
161
+
162
+ # Bilinear interpolation
163
+ c00 = lut[i0, j0]
164
+ c01 = lut[i0, j1]
165
+ c10 = lut[i1, j0]
166
+ c11 = lut[i1, j1]
167
+
168
+ # Interpolate along second dimension first
169
+ c0 = c00 * (1.0 - fj) + c01 * fj
170
+ c1 = c10 * (1.0 - fj) + c11 * fj
171
+
172
+ # Then interpolate along first dimension
173
+ return c0 * (1.0 - fi) + c1 * fi
174
+
175
+
176
+ @cuda.jit(device=True)
177
+ def _device_spectral_n_and_gradient(
178
+ x: float,
179
+ y: float,
180
+ z: float,
181
+ wavelength: float,
182
+ center_x: float,
183
+ center_y: float,
184
+ center_z: float,
185
+ ref_radius: float,
186
+ alt_min: float,
187
+ alt_delta: float,
188
+ n_alt: int,
189
+ wl_min: float,
190
+ wl_delta: float,
191
+ n_wl: int,
192
+ lut_n: cuda.devicearray.DeviceNDArray,
193
+ lut_dn_dh: cuda.devicearray.DeviceNDArray,
194
+ ) -> tuple[float, float, float, float]:
195
+ """
196
+ Compute n and gradient for any SpectralInhomogeneousModel.
197
+
198
+ Parameters
199
+ ----------
200
+ x, y, z : float
201
+ Position in Cartesian coordinates
202
+ wavelength : float
203
+ Wavelength in meters
204
+ center_x, center_y, center_z : float
205
+ Center of spherical symmetry
206
+ ref_radius : float
207
+ Reference radius (e.g., Earth radius)
208
+ alt_min : float
209
+ Minimum altitude in LUT
210
+ alt_delta : float
211
+ Altitude spacing in LUT
212
+ n_alt : int
213
+ Number of altitude samples in LUT
214
+ wl_min : float
215
+ Minimum wavelength in LUT
216
+ wl_delta : float
217
+ Wavelength spacing in LUT
218
+ n_wl : int
219
+ Number of wavelength samples in LUT
220
+ lut_n : device array
221
+ 2D lookup table for n [altitude, wavelength]
222
+ lut_dn_dh : device array
223
+ 2D lookup table for dn/dh [altitude, wavelength]
224
+
225
+ Returns
226
+ -------
227
+ tuple
228
+ (n, grad_x, grad_y, grad_z)
229
+ """
230
+ # Compute distance from center
231
+ dx = x - center_x
232
+ dy = y - center_y
233
+ dz = z - center_z
234
+ r = math.sqrt(dx * dx + dy * dy + dz * dz)
235
+
236
+ # Compute altitude
237
+ altitude = r - ref_radius
238
+ if altitude < 0.0:
239
+ altitude = 0.0
240
+
241
+ # 2D interpolation for n and dn/dh
242
+ n = _device_bilinear_interpolate(
243
+ altitude, wavelength, lut_n, alt_min, alt_delta, n_alt, wl_min, wl_delta, n_wl
244
+ )
245
+ dn_dh = _device_bilinear_interpolate(
246
+ altitude,
247
+ wavelength,
248
+ lut_dn_dh,
249
+ alt_min,
250
+ alt_delta,
251
+ n_alt,
252
+ wl_min,
253
+ wl_delta,
254
+ n_wl,
255
+ )
256
+
257
+ # Compute radial unit vector (gradient direction)
258
+ if r < 1e-10:
259
+ r_hat_x, r_hat_y, r_hat_z = 0.0, 0.0, 1.0
260
+ else:
261
+ r_inv = 1.0 / r
262
+ r_hat_x = dx * r_inv
263
+ r_hat_y = dy * r_inv
264
+ r_hat_z = dz * r_inv
265
+
266
+ # Gradient = dn/dh * r_hat
267
+ grad_x = dn_dh * r_hat_x
268
+ grad_y = dn_dh * r_hat_y
269
+ grad_z = dn_dh * r_hat_z
270
+
271
+ return n, grad_x, grad_y, grad_z
272
+
273
+
274
+ @cuda.jit(device=True)
275
+ def _device_spectral_euler_step(
276
+ x: float,
277
+ y: float,
278
+ z: float,
279
+ dir_x: float,
280
+ dir_y: float,
281
+ dir_z: float,
282
+ step_size: float,
283
+ wavelength: float,
284
+ center_x: float,
285
+ center_y: float,
286
+ center_z: float,
287
+ ref_radius: float,
288
+ alt_min: float,
289
+ alt_delta: float,
290
+ n_alt: int,
291
+ wl_min: float,
292
+ wl_delta: float,
293
+ n_wl: int,
294
+ lut_n: cuda.devicearray.DeviceNDArray,
295
+ lut_dn_dh: cuda.devicearray.DeviceNDArray,
296
+ ) -> tuple[float, float, float, float, float, float, float]:
297
+ """Euler step for SpectralInhomogeneousModel."""
298
+ n, grad_x, grad_y, grad_z = _device_spectral_n_and_gradient(
299
+ x,
300
+ y,
301
+ z,
302
+ wavelength,
303
+ center_x,
304
+ center_y,
305
+ center_z,
306
+ ref_radius,
307
+ alt_min,
308
+ alt_delta,
309
+ n_alt,
310
+ wl_min,
311
+ wl_delta,
312
+ n_wl,
313
+ lut_n,
314
+ lut_dn_dh,
315
+ )
316
+
317
+ new_x, new_y, new_z, new_dx, new_dy, new_dz = device_euler_step(
318
+ x, y, z, dir_x, dir_y, dir_z, n, grad_x, grad_y, grad_z, step_size
319
+ )
320
+
321
+ return new_x, new_y, new_z, new_dx, new_dy, new_dz, n
322
+
323
+
324
+ @cuda.jit(device=True)
325
+ def _device_spectral_rk4_step(
326
+ x: float,
327
+ y: float,
328
+ z: float,
329
+ dir_x: float,
330
+ dir_y: float,
331
+ dir_z: float,
332
+ step_size: float,
333
+ wavelength: float,
334
+ center_x: float,
335
+ center_y: float,
336
+ center_z: float,
337
+ ref_radius: float,
338
+ alt_min: float,
339
+ alt_delta: float,
340
+ n_alt: int,
341
+ wl_min: float,
342
+ wl_delta: float,
343
+ n_wl: int,
344
+ lut_n: cuda.devicearray.DeviceNDArray,
345
+ lut_dn_dh: cuda.devicearray.DeviceNDArray,
346
+ ) -> tuple[float, float, float, float, float, float, float]:
347
+ """RK4 step for SpectralInhomogeneousModel."""
348
+ h = step_size
349
+ h2 = h / 2.0
350
+
351
+ def get_n_and_kappa(px, py, pz, dx, dy, dz):
352
+ n, gx, gy, gz = _device_spectral_n_and_gradient(
353
+ px,
354
+ py,
355
+ pz,
356
+ wavelength,
357
+ center_x,
358
+ center_y,
359
+ center_z,
360
+ ref_radius,
361
+ alt_min,
362
+ alt_delta,
363
+ n_alt,
364
+ wl_min,
365
+ wl_delta,
366
+ n_wl,
367
+ lut_n,
368
+ lut_dn_dh,
369
+ )
370
+ dot = dx * gx + dy * gy + dz * gz
371
+ kx = (gx - dot * dx) / n
372
+ ky = (gy - dot * dy) / n
373
+ kz = (gz - dot * dz) / n
374
+ return n, kx, ky, kz
375
+
376
+ def normalize(dx, dy, dz):
377
+ norm = math.sqrt(dx * dx + dy * dy + dz * dz)
378
+ if norm < 1e-12:
379
+ norm = 1.0
380
+ return dx / norm, dy / norm, dz / norm
381
+
382
+ # k1
383
+ n0, kx1, ky1, kz1 = get_n_and_kappa(x, y, z, dir_x, dir_y, dir_z)
384
+ k1_rx, k1_ry, k1_rz = dir_x, dir_y, dir_z
385
+ k1_dx, k1_dy, k1_dz = kx1, ky1, kz1
386
+
387
+ # k2
388
+ px = x + h2 * k1_rx
389
+ py = y + h2 * k1_ry
390
+ pz = z + h2 * k1_rz
391
+ dx, dy, dz = normalize(dir_x + h2 * k1_dx, dir_y + h2 * k1_dy, dir_z + h2 * k1_dz)
392
+ n1, kx2, ky2, kz2 = get_n_and_kappa(px, py, pz, dx, dy, dz)
393
+ k2_rx, k2_ry, k2_rz = dx, dy, dz
394
+ k2_dx, k2_dy, k2_dz = kx2, ky2, kz2
395
+
396
+ # k3
397
+ px = x + h2 * k2_rx
398
+ py = y + h2 * k2_ry
399
+ pz = z + h2 * k2_rz
400
+ dx, dy, dz = normalize(dir_x + h2 * k2_dx, dir_y + h2 * k2_dy, dir_z + h2 * k2_dz)
401
+ n2, kx3, ky3, kz3 = get_n_and_kappa(px, py, pz, dx, dy, dz)
402
+ k3_rx, k3_ry, k3_rz = dx, dy, dz
403
+ k3_dx, k3_dy, k3_dz = kx3, ky3, kz3
404
+
405
+ # k4
406
+ px = x + h * k3_rx
407
+ py = y + h * k3_ry
408
+ pz = z + h * k3_rz
409
+ dx, dy, dz = normalize(dir_x + h * k3_dx, dir_y + h * k3_dy, dir_z + h * k3_dz)
410
+ n3, kx4, ky4, kz4 = get_n_and_kappa(px, py, pz, dx, dy, dz)
411
+
412
+ # Final RK4 combination
413
+ new_x = x + (h / 6.0) * (k1_rx + 2 * k2_rx + 2 * k3_rx + dx)
414
+ new_y = y + (h / 6.0) * (k1_ry + 2 * k2_ry + 2 * k3_ry + dy)
415
+ new_z = z + (h / 6.0) * (k1_rz + 2 * k2_rz + 2 * k3_rz + dz)
416
+
417
+ new_dx = dir_x + (h / 6.0) * (k1_dx + 2 * k2_dx + 2 * k3_dx + kx4)
418
+ new_dy = dir_y + (h / 6.0) * (k1_dy + 2 * k2_dy + 2 * k3_dy + ky4)
419
+ new_dz = dir_z + (h / 6.0) * (k1_dz + 2 * k2_dz + 2 * k3_dz + kz4)
420
+
421
+ new_dx, new_dy, new_dz = normalize(new_dx, new_dy, new_dz)
422
+
423
+ # Simpson's rule for average n
424
+ n_avg = (n0 + 4 * n1 + n2) / 6.0
425
+
426
+ return new_x, new_y, new_z, new_dx, new_dy, new_dz, n_avg
427
+
428
+
429
+ @register_kernel(PropagationKernelID.SPECTRAL_EULER)
430
+ @cuda.jit
431
+ def _kernel_spectral_inhomogeneous_euler(
432
+ positions,
433
+ directions,
434
+ active,
435
+ geo_path,
436
+ opt_path,
437
+ acc_time,
438
+ step_size: float,
439
+ num_steps: int,
440
+ center_x: float,
441
+ center_y: float,
442
+ center_z: float,
443
+ ref_radius: float,
444
+ alt_min: float,
445
+ alt_delta: float,
446
+ n_alt: int,
447
+ wl_min: float,
448
+ wl_delta: float,
449
+ n_wl: int,
450
+ lut_n,
451
+ lut_dn_dh,
452
+ wavelength: float,
453
+ ):
454
+ """
455
+ GPU kernel for SpectralInhomogeneousModel using Euler integration.
456
+
457
+ Parameters
458
+ ----------
459
+ positions : device array, shape (N, 3)
460
+ Ray positions
461
+ directions : device array, shape (N, 3)
462
+ Ray directions (unit vectors)
463
+ active : device array, shape (N,)
464
+ Boolean mask for active rays
465
+ geo_path : device array, shape (N,)
466
+ Accumulated geometric path length
467
+ opt_path : device array, shape (N,)
468
+ Accumulated optical path length
469
+ acc_time : device array, shape (N,)
470
+ Accumulated travel time
471
+ step_size : float
472
+ Integration step size in meters
473
+ num_steps : int
474
+ Number of steps to take
475
+ center_x, center_y, center_z : float
476
+ Center of spherical symmetry
477
+ ref_radius : float
478
+ Reference radius
479
+ alt_min : float
480
+ Minimum altitude in LUT
481
+ alt_delta : float
482
+ Altitude spacing in LUT
483
+ n_alt : int
484
+ Number of altitude samples
485
+ wl_min : float
486
+ Minimum wavelength in LUT
487
+ wl_delta : float
488
+ Wavelength spacing in LUT
489
+ n_wl : int
490
+ Number of wavelength samples
491
+ lut_n : device array, shape (n_alt, n_wl)
492
+ 2D lookup table for n
493
+ lut_dn_dh : device array, shape (n_alt, n_wl)
494
+ 2D lookup table for dn/dh
495
+ wavelength : float
496
+ Wavelength in meters (same for all rays in this kernel call)
497
+ """
498
+ c = SPEED_OF_LIGHT
499
+
500
+ idx = cuda.grid(1)
501
+ if idx >= positions.shape[0]:
502
+ return
503
+ if not active[idx]:
504
+ return
505
+
506
+ # Load ray state
507
+ x = positions[idx, 0]
508
+ y = positions[idx, 1]
509
+ z = positions[idx, 2]
510
+ dx = directions[idx, 0]
511
+ dy = directions[idx, 1]
512
+ dz = directions[idx, 2]
513
+ gp = geo_path[idx]
514
+ op = opt_path[idx]
515
+ at = acc_time[idx]
516
+
517
+ for _ in range(num_steps):
518
+ x, y, z, dx, dy, dz, n = _device_spectral_euler_step(
519
+ x,
520
+ y,
521
+ z,
522
+ dx,
523
+ dy,
524
+ dz,
525
+ step_size,
526
+ wavelength,
527
+ center_x,
528
+ center_y,
529
+ center_z,
530
+ ref_radius,
531
+ alt_min,
532
+ alt_delta,
533
+ n_alt,
534
+ wl_min,
535
+ wl_delta,
536
+ n_wl,
537
+ lut_n,
538
+ lut_dn_dh,
539
+ )
540
+ gp += step_size
541
+ op += n * step_size
542
+ at += n * step_size / c
543
+
544
+ # Store updated state
545
+ positions[idx, 0] = x
546
+ positions[idx, 1] = y
547
+ positions[idx, 2] = z
548
+ directions[idx, 0] = dx
549
+ directions[idx, 1] = dy
550
+ directions[idx, 2] = dz
551
+ geo_path[idx] = gp
552
+ opt_path[idx] = op
553
+ acc_time[idx] = at
554
+
555
+
556
+ @register_kernel(PropagationKernelID.SPECTRAL_RK4)
557
+ @cuda.jit
558
+ def _kernel_spectral_inhomogeneous_rk4(
559
+ positions,
560
+ directions,
561
+ active,
562
+ geo_path,
563
+ opt_path,
564
+ acc_time,
565
+ step_size: float,
566
+ num_steps: int,
567
+ center_x: float,
568
+ center_y: float,
569
+ center_z: float,
570
+ ref_radius: float,
571
+ alt_min: float,
572
+ alt_delta: float,
573
+ n_alt: int,
574
+ wl_min: float,
575
+ wl_delta: float,
576
+ n_wl: int,
577
+ lut_n,
578
+ lut_dn_dh,
579
+ wavelength: float,
580
+ ):
581
+ """
582
+ GPU kernel for SpectralInhomogeneousModel using RK4 integration.
583
+
584
+ Parameters
585
+ ----------
586
+ Same as _kernel_spectral_inhomogeneous_euler
587
+ """
588
+ c = SPEED_OF_LIGHT
589
+
590
+ idx = cuda.grid(1)
591
+ if idx >= positions.shape[0]:
592
+ return
593
+ if not active[idx]:
594
+ return
595
+
596
+ # Load ray state
597
+ x = positions[idx, 0]
598
+ y = positions[idx, 1]
599
+ z = positions[idx, 2]
600
+ dx = directions[idx, 0]
601
+ dy = directions[idx, 1]
602
+ dz = directions[idx, 2]
603
+ gp = geo_path[idx]
604
+ op = opt_path[idx]
605
+ at = acc_time[idx]
606
+
607
+ for _ in range(num_steps):
608
+ x, y, z, dx, dy, dz, n_avg = _device_spectral_rk4_step(
609
+ x,
610
+ y,
611
+ z,
612
+ dx,
613
+ dy,
614
+ dz,
615
+ step_size,
616
+ wavelength,
617
+ center_x,
618
+ center_y,
619
+ center_z,
620
+ ref_radius,
621
+ alt_min,
622
+ alt_delta,
623
+ n_alt,
624
+ wl_min,
625
+ wl_delta,
626
+ n_wl,
627
+ lut_n,
628
+ lut_dn_dh,
629
+ )
630
+ gp += step_size
631
+ op += n_avg * step_size
632
+ at += n_avg * step_size / c
633
+
634
+ # Store updated state
635
+ positions[idx, 0] = x
636
+ positions[idx, 1] = y
637
+ positions[idx, 2] = z
638
+ directions[idx, 0] = dx
639
+ directions[idx, 1] = dy
640
+ directions[idx, 2] = dz
641
+ geo_path[idx] = gp
642
+ opt_path[idx] = op
643
+ acc_time[idx] = at
644
+
645
+
646
+ # =============================================================================
647
+ # PER-RAY WAVELENGTH KERNELS
648
+ # =============================================================================
649
+ # These variants take wavelength as a per-ray array instead of a scalar.
650
+ # Use with SpectralGPUGradientPropagator for chromatic dispersion simulation.
651
+
652
+
653
+ @register_kernel(PropagationKernelID.SPECTRAL_EULER_PERRAY)
654
+ @cuda.jit
655
+ def _kernel_spectral_inhomogeneous_euler_perray(
656
+ positions,
657
+ directions,
658
+ wavelengths, # Per-ray wavelength array, shape (N,)
659
+ active,
660
+ geo_path,
661
+ opt_path,
662
+ acc_time,
663
+ step_size: float,
664
+ num_steps: int,
665
+ center_x: float,
666
+ center_y: float,
667
+ center_z: float,
668
+ ref_radius: float,
669
+ alt_min: float,
670
+ alt_delta: float,
671
+ n_alt: int,
672
+ wl_min: float,
673
+ wl_delta: float,
674
+ n_wl: int,
675
+ lut_n,
676
+ lut_dn_dh,
677
+ ):
678
+ """
679
+ GPU kernel for SpectralInhomogeneousModel with per-ray wavelengths.
680
+
681
+ Uses Euler integration. Each ray has its own wavelength value,
682
+ allowing simulation of chromatic dispersion.
683
+
684
+ Parameters
685
+ ----------
686
+ positions : device array, shape (N, 3)
687
+ Ray positions
688
+ directions : device array, shape (N, 3)
689
+ Ray directions (unit vectors)
690
+ wavelengths : device array, shape (N,)
691
+ Per-ray wavelengths in meters
692
+ active : device array, shape (N,)
693
+ Boolean mask for active rays
694
+ geo_path : device array, shape (N,)
695
+ Accumulated geometric path length
696
+ opt_path : device array, shape (N,)
697
+ Accumulated optical path length
698
+ acc_time : device array, shape (N,)
699
+ Accumulated travel time
700
+ step_size : float
701
+ Integration step size in meters
702
+ num_steps : int
703
+ Number of steps to take
704
+ center_x, center_y, center_z : float
705
+ Center of spherical symmetry
706
+ ref_radius : float
707
+ Reference radius
708
+ alt_min : float
709
+ Minimum altitude in LUT
710
+ alt_delta : float
711
+ Altitude spacing in LUT
712
+ n_alt : int
713
+ Number of altitude samples
714
+ wl_min : float
715
+ Minimum wavelength in LUT
716
+ wl_delta : float
717
+ Wavelength spacing in LUT
718
+ n_wl : int
719
+ Number of wavelength samples
720
+ lut_n : device array, shape (n_alt, n_wl)
721
+ 2D lookup table for n
722
+ lut_dn_dh : device array, shape (n_alt, n_wl)
723
+ 2D lookup table for dn/dh
724
+ """
725
+ c = SPEED_OF_LIGHT
726
+
727
+ idx = cuda.grid(1)
728
+ if idx >= positions.shape[0]:
729
+ return
730
+ if not active[idx]:
731
+ return
732
+
733
+ # Load ray state including per-ray wavelength
734
+ x = positions[idx, 0]
735
+ y = positions[idx, 1]
736
+ z = positions[idx, 2]
737
+ dx = directions[idx, 0]
738
+ dy = directions[idx, 1]
739
+ dz = directions[idx, 2]
740
+ wavelength = wavelengths[idx] # Per-ray wavelength
741
+ gp = geo_path[idx]
742
+ op = opt_path[idx]
743
+ at = acc_time[idx]
744
+
745
+ for _ in range(num_steps):
746
+ x, y, z, dx, dy, dz, n = _device_spectral_euler_step(
747
+ x,
748
+ y,
749
+ z,
750
+ dx,
751
+ dy,
752
+ dz,
753
+ step_size,
754
+ wavelength,
755
+ center_x,
756
+ center_y,
757
+ center_z,
758
+ ref_radius,
759
+ alt_min,
760
+ alt_delta,
761
+ n_alt,
762
+ wl_min,
763
+ wl_delta,
764
+ n_wl,
765
+ lut_n,
766
+ lut_dn_dh,
767
+ )
768
+ gp += step_size
769
+ op += n * step_size
770
+ at += n * step_size / c
771
+
772
+ # Store updated state
773
+ positions[idx, 0] = x
774
+ positions[idx, 1] = y
775
+ positions[idx, 2] = z
776
+ directions[idx, 0] = dx
777
+ directions[idx, 1] = dy
778
+ directions[idx, 2] = dz
779
+ geo_path[idx] = gp
780
+ opt_path[idx] = op
781
+ acc_time[idx] = at
782
+
783
+
784
+ @register_kernel(PropagationKernelID.SPECTRAL_RK4_PERRAY)
785
+ @cuda.jit
786
+ def _kernel_spectral_inhomogeneous_rk4_perray(
787
+ positions,
788
+ directions,
789
+ wavelengths, # Per-ray wavelength array, shape (N,)
790
+ active,
791
+ geo_path,
792
+ opt_path,
793
+ acc_time,
794
+ step_size: float,
795
+ num_steps: int,
796
+ center_x: float,
797
+ center_y: float,
798
+ center_z: float,
799
+ ref_radius: float,
800
+ alt_min: float,
801
+ alt_delta: float,
802
+ n_alt: int,
803
+ wl_min: float,
804
+ wl_delta: float,
805
+ n_wl: int,
806
+ lut_n,
807
+ lut_dn_dh,
808
+ ):
809
+ """
810
+ GPU kernel for SpectralInhomogeneousModel with per-ray wavelengths.
811
+
812
+ Uses RK4 integration. Each ray has its own wavelength value,
813
+ allowing simulation of chromatic dispersion.
814
+
815
+ Parameters
816
+ ----------
817
+ Same as _kernel_spectral_inhomogeneous_euler_perray
818
+ """
819
+ c = SPEED_OF_LIGHT
820
+
821
+ idx = cuda.grid(1)
822
+ if idx >= positions.shape[0]:
823
+ return
824
+ if not active[idx]:
825
+ return
826
+
827
+ # Load ray state including per-ray wavelength
828
+ x = positions[idx, 0]
829
+ y = positions[idx, 1]
830
+ z = positions[idx, 2]
831
+ dx = directions[idx, 0]
832
+ dy = directions[idx, 1]
833
+ dz = directions[idx, 2]
834
+ wavelength = wavelengths[idx] # Per-ray wavelength
835
+ gp = geo_path[idx]
836
+ op = opt_path[idx]
837
+ at = acc_time[idx]
838
+
839
+ for _ in range(num_steps):
840
+ x, y, z, dx, dy, dz, n_avg = _device_spectral_rk4_step(
841
+ x,
842
+ y,
843
+ z,
844
+ dx,
845
+ dy,
846
+ dz,
847
+ step_size,
848
+ wavelength,
849
+ center_x,
850
+ center_y,
851
+ center_z,
852
+ ref_radius,
853
+ alt_min,
854
+ alt_delta,
855
+ n_alt,
856
+ wl_min,
857
+ wl_delta,
858
+ n_wl,
859
+ lut_n,
860
+ lut_dn_dh,
861
+ )
862
+ gp += step_size
863
+ op += n_avg * step_size
864
+ at += n_avg * step_size / c
865
+
866
+ # Store updated state
867
+ positions[idx, 0] = x
868
+ positions[idx, 1] = y
869
+ positions[idx, 2] = z
870
+ directions[idx, 0] = dx
871
+ directions[idx, 1] = dy
872
+ directions[idx, 2] = dz
873
+ geo_path[idx] = gp
874
+ opt_path[idx] = op
875
+ acc_time[idx] = at