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,566 @@
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-accelerated Gradient Propagator for inhomogeneous media.
36
+
37
+ This module provides a high-performance CUDA-based propagator for
38
+ materials with spatially-varying refractive indices.
39
+ """
40
+
41
+ import numpy as np
42
+ import numpy.typing as npt
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
+ @staticmethod
55
+ def jit(*args, **kwargs):
56
+ """Return a no-op decorator."""
57
+
58
+ def decorator(func):
59
+ return func
60
+
61
+ if args and callable(args[0]):
62
+ return args[0]
63
+ return decorator
64
+
65
+ @staticmethod
66
+ def is_available():
67
+ return False
68
+
69
+ @staticmethod
70
+ def grid(n):
71
+ return 0
72
+
73
+ @staticmethod
74
+ def synchronize():
75
+ pass
76
+
77
+ @staticmethod
78
+ def to_device(arr):
79
+ """Fake to_device that returns a wrapper."""
80
+ return _FakeDeviceArray(arr)
81
+
82
+ class _FakeDeviceArray:
83
+ """Fake device array for when numba is not installed."""
84
+
85
+ def __init__(self, arr):
86
+ self._arr = arr
87
+
88
+ def copy_to_host(self):
89
+ return self._arr
90
+
91
+ cuda = _FakeCuda() # type: ignore[assignment]
92
+ HAS_CUDA = False
93
+
94
+ from ..propagator_protocol import GPUMaterialProtocol, GPUMaterialID
95
+
96
+
97
+ class GPUGradientPropagator:
98
+ """
99
+ High-performance GPU-based propagator for inhomogeneous materials.
100
+
101
+ This propagator keeps all computation on the GPU for maximum performance.
102
+ It works with any material that implements GPUMaterialProtocol, using
103
+ the material's gpu_material_id to dispatch to the appropriate GPU kernels.
104
+
105
+ Currently Supported Materials
106
+ -----------------------------
107
+ - ExponentialAtmosphere (GPUMaterialID.EXPONENTIAL_ATMOSPHERE)
108
+
109
+ For materials without GPU support, use GradientPropagator.propagate_step_cpu()
110
+ which works with any MaterialField via the CPU.
111
+
112
+ Parameters
113
+ ----------
114
+ material : GPUMaterialProtocol
115
+ The material to propagate through. Must implement GPUMaterialProtocol.
116
+ method : str
117
+ Integration method: 'euler' or 'rk4'. Default: 'rk4'
118
+ threads_per_block : int
119
+ CUDA threads per block. Default: 256
120
+
121
+ Example
122
+ -------
123
+ >>> from lsurf.materials import ExponentialAtmosphere
124
+ >>> atmosphere = ExponentialAtmosphere()
125
+ >>> propagator = GPUGradientPropagator(atmosphere, method='rk4')
126
+ >>> propagator.propagate(rays, total_distance=500e3, step_size=100.0)
127
+
128
+ Raises
129
+ ------
130
+ ValueError
131
+ If the material does not support GPU acceleration.
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ material: GPUMaterialProtocol,
137
+ method: str = "rk4",
138
+ threads_per_block: int = 256,
139
+ enable_absorption: bool = False,
140
+ ):
141
+ # Check if CUDA is available
142
+ if not HAS_CUDA:
143
+ raise ImportError(
144
+ "CUDA support requires numba with CUDA. "
145
+ "Use GradientPropagator for CPU-based propagation."
146
+ )
147
+
148
+ # Store material reference for CPU fallback methods
149
+ self._material = material
150
+
151
+ # Validate material has GPU support
152
+ if not hasattr(material, "gpu_material_id"):
153
+ raise ValueError(
154
+ f"Material {type(material).__name__} does not support GPU acceleration. "
155
+ f"Use GradientPropagator.propagate_step_cpu() instead."
156
+ )
157
+
158
+ # Validate material provides kernels
159
+ if not hasattr(material, "get_gpu_kernels"):
160
+ raise ValueError(
161
+ f"Material {type(material).__name__} does not provide GPU kernels. "
162
+ f"Implement get_gpu_kernels() method."
163
+ )
164
+
165
+ self._material_id = material.gpu_material_id
166
+
167
+ if method not in ("euler", "rk4"):
168
+ raise ValueError(f"method must be 'euler' or 'rk4', got {method}")
169
+ self.method = method
170
+ self.threads_per_block = threads_per_block
171
+ self.enable_absorption = enable_absorption
172
+
173
+ # Get kernels from the material
174
+ self._kernels = material.get_gpu_kernels()
175
+ self._kernel = self._kernels.get(method)
176
+ if self._kernel is None:
177
+ raise ValueError(
178
+ f"Material {type(material).__name__} does not provide '{method}' kernel. "
179
+ f"Available: {list(self._kernels.keys())}"
180
+ )
181
+
182
+ # Get absorption kernel if enabled
183
+ self._absorption_kernel = None
184
+ if enable_absorption:
185
+ self._absorption_kernel = self._get_absorption_kernel()
186
+
187
+ # Check if material uses device arrays (LUTs, grids, etc.)
188
+ self._gpu_arrays = None
189
+ self._device_arrays = None
190
+ if hasattr(material, "get_gpu_arrays"):
191
+ self._gpu_arrays = material.get_gpu_arrays()
192
+ # Device arrays will be created on first propagate call
193
+
194
+ def _get_absorption_kernel(self):
195
+ """Get the appropriate absorption kernel based on material type."""
196
+ from ..kernels.absorption.simple import _kernel_absorption_simple
197
+ from ..kernels.absorption.grid import _kernel_absorption_grid
198
+
199
+ if self._material_id == GPUMaterialID.SIMPLE_INHOMOGENEOUS:
200
+ return _kernel_absorption_simple
201
+ elif self._material_id == GPUMaterialID.EXPONENTIAL_ATMOSPHERE:
202
+ return _kernel_absorption_simple # Uses same LUT structure
203
+ elif self._material_id == GPUMaterialID.GRID_INHOMOGENEOUS:
204
+ return _kernel_absorption_grid
205
+ else:
206
+ raise ValueError(
207
+ f"No absorption kernel available for material ID {self._material_id}"
208
+ )
209
+
210
+ def _apply_absorption(
211
+ self,
212
+ positions_d,
213
+ directions_d,
214
+ active_d,
215
+ intensities_d,
216
+ optical_depth_d,
217
+ step_size: float,
218
+ num_steps: int,
219
+ blocks: int,
220
+ ) -> None:
221
+ """Apply absorption using the appropriate kernel."""
222
+ material_params = self._material.get_gpu_parameters()
223
+
224
+ if self._material_id in (
225
+ GPUMaterialID.SIMPLE_INHOMOGENEOUS,
226
+ GPUMaterialID.EXPONENTIAL_ATMOSPHERE,
227
+ ):
228
+ # Simple kernel: positions, directions, active, intensities, optical_depth,
229
+ # step_size, num_steps, center_x, center_y, center_z, ref_radius,
230
+ # min_alt, delta_h, lut_size, lut_alpha
231
+ self._absorption_kernel[blocks, self.threads_per_block](
232
+ positions_d,
233
+ directions_d,
234
+ active_d,
235
+ intensities_d,
236
+ optical_depth_d,
237
+ float(step_size),
238
+ num_steps,
239
+ material_params[0], # center_x
240
+ material_params[1], # center_y
241
+ material_params[2], # center_z
242
+ material_params[3], # ref_radius
243
+ material_params[4], # min_alt
244
+ material_params[5], # delta_h
245
+ material_params[6], # lut_size
246
+ self._device_arrays["lut_alpha"],
247
+ )
248
+ elif self._material_id == GPUMaterialID.GRID_INHOMOGENEOUS:
249
+ # Grid kernel: positions, directions, active, intensities, optical_depth,
250
+ # step_size, num_steps, x_min, y_min, z_min, grid_dx, grid_dy, grid_dz,
251
+ # nx, ny, nz, alpha_grid
252
+ self._absorption_kernel[blocks, self.threads_per_block](
253
+ positions_d,
254
+ directions_d,
255
+ active_d,
256
+ intensities_d,
257
+ optical_depth_d,
258
+ float(step_size),
259
+ num_steps,
260
+ material_params[0], # x_min
261
+ material_params[1], # y_min
262
+ material_params[2], # z_min
263
+ material_params[3], # grid_dx
264
+ material_params[4], # grid_dy
265
+ material_params[5], # grid_dz
266
+ material_params[6], # nx
267
+ material_params[7], # ny
268
+ material_params[8], # nz
269
+ self._device_arrays["alpha_grid"],
270
+ )
271
+
272
+ @property
273
+ def material(self) -> GPUMaterialProtocol:
274
+ """The material being propagated through."""
275
+ return self._material
276
+
277
+ def propagate_step(
278
+ self,
279
+ rays, # RayBatch
280
+ step_size: float,
281
+ wavelength: float = 532e-9,
282
+ ) -> None:
283
+ """
284
+ Propagate rays by a single integration step.
285
+
286
+ This is the unified interface method compatible with CPU propagators.
287
+ Performs a single step propagation on GPU.
288
+
289
+ Parameters
290
+ ----------
291
+ rays : RayBatch
292
+ Ray batch containing positions, directions, and other ray properties.
293
+ Modified in-place.
294
+ step_size : float
295
+ Integration step size in meters.
296
+ wavelength : float, optional
297
+ Wavelength in meters, default 532 nm.
298
+ """
299
+ # Single step is just propagate with total_distance = step_size
300
+ self.propagate(
301
+ rays=rays,
302
+ total_distance=step_size,
303
+ step_size=step_size,
304
+ wavelength=wavelength,
305
+ )
306
+
307
+ def propagate(
308
+ self,
309
+ rays, # RayBatch
310
+ total_distance: float,
311
+ step_size: float,
312
+ wavelength: float = 532e-9,
313
+ steps_per_kernel: int = 100,
314
+ ) -> None:
315
+ """
316
+ Propagate rays through material on GPU.
317
+
318
+ Modifies the rays object in-place.
319
+
320
+ Parameters
321
+ ----------
322
+ rays : RayBatch
323
+ Ray batch to propagate (modified in-place)
324
+ total_distance : float
325
+ Total distance to propagate in meters
326
+ step_size : float
327
+ Integration step size in meters
328
+ wavelength : float, optional
329
+ Wavelength in meters, default 532 nm (green light)
330
+ steps_per_kernel : int
331
+ Number of steps per kernel launch. Higher values reduce
332
+ kernel launch overhead but increase register pressure.
333
+ """
334
+ num_rays = rays.num_rays
335
+ total_steps = int(total_distance / step_size)
336
+
337
+ # Prepare GPU arrays for ray state
338
+ positions_d = cuda.to_device(rays.positions.astype(np.float32))
339
+ directions_d = cuda.to_device(rays.directions.astype(np.float32))
340
+ active_d = cuda.to_device(rays.active)
341
+ geo_path_d = cuda.to_device(rays.geometric_path_lengths.astype(np.float32))
342
+ opt_path_d = cuda.to_device(rays.optical_path_lengths.astype(np.float32))
343
+ acc_time_d = cuda.to_device(rays.accumulated_time.astype(np.float32))
344
+
345
+ # Prepare absorption arrays if enabled
346
+ intensities_d = None
347
+ optical_depth_d = None
348
+ if self.enable_absorption:
349
+ intensities_d = cuda.to_device(rays.intensities.astype(np.float32))
350
+ if rays.optical_depth is not None:
351
+ optical_depth_d = cuda.to_device(rays.optical_depth.astype(np.float32))
352
+ else:
353
+ optical_depth_d = cuda.to_device(np.zeros(num_rays, dtype=np.float32))
354
+
355
+ # Transfer material arrays to device if needed (LUTs, grids, etc.)
356
+ if self._gpu_arrays is not None and self._device_arrays is None:
357
+ self._device_arrays = {}
358
+ for name, arr in self._gpu_arrays.items():
359
+ self._device_arrays[name] = cuda.to_device(arr.astype(np.float32))
360
+
361
+ # Get material-specific kernel parameters from the material
362
+ material_params = self._material.get_gpu_parameters()
363
+
364
+ # Build kernel arguments: scalar params + device arrays
365
+ kernel_args = list(material_params)
366
+ if self._device_arrays is not None:
367
+ # Append device arrays in the order the kernel expects
368
+ # Convention: lut_n, lut_dn_dh for SimpleAtmosphere
369
+ # n_grid, grad_grid for GridAtmosphere
370
+ for name in ["lut_n", "lut_dn_dh", "n_grid", "grad_grid"]:
371
+ if name in self._device_arrays:
372
+ kernel_args.append(self._device_arrays[name])
373
+
374
+ # Configure kernel launch
375
+ blocks = (num_rays + self.threads_per_block - 1) // self.threads_per_block
376
+
377
+ # Launch kernels in batches
378
+ remaining_steps = total_steps
379
+ while remaining_steps > 0:
380
+ steps_this_call = min(remaining_steps, steps_per_kernel)
381
+ self._kernel[blocks, self.threads_per_block](
382
+ positions_d,
383
+ directions_d,
384
+ active_d,
385
+ geo_path_d,
386
+ opt_path_d,
387
+ acc_time_d,
388
+ float(step_size),
389
+ steps_this_call,
390
+ *kernel_args,
391
+ float(wavelength),
392
+ )
393
+
394
+ # Apply absorption AFTER each batch (for proper path integration)
395
+ if self.enable_absorption and self._absorption_kernel is not None:
396
+ self._apply_absorption(
397
+ positions_d,
398
+ directions_d,
399
+ active_d,
400
+ intensities_d,
401
+ optical_depth_d,
402
+ step_size,
403
+ steps_this_call,
404
+ blocks,
405
+ )
406
+
407
+ remaining_steps -= steps_this_call
408
+
409
+ # Synchronize and copy back
410
+ cuda.synchronize()
411
+ rays.positions[:] = positions_d.copy_to_host()
412
+ rays.directions[:] = directions_d.copy_to_host()
413
+ rays.geometric_path_lengths[:] = geo_path_d.copy_to_host()
414
+ rays.optical_path_lengths[:] = opt_path_d.copy_to_host()
415
+ rays.accumulated_time[:] = acc_time_d.copy_to_host()
416
+
417
+ # Copy back absorption data if enabled
418
+ if self.enable_absorption:
419
+ rays.intensities[:] = intensities_d.copy_to_host()
420
+ if rays.optical_depth is not None:
421
+ rays.optical_depth[:] = optical_depth_d.copy_to_host()
422
+
423
+ def propagate_with_history(
424
+ self,
425
+ rays, # RayBatch
426
+ total_distance: float,
427
+ step_size: float,
428
+ history_interval: int = 100,
429
+ wavelength: float = 1.0e-6,
430
+ ) -> dict:
431
+ """
432
+ Propagate rays and record position history at intervals.
433
+
434
+ Parameters
435
+ ----------
436
+ rays : RayBatch
437
+ Ray batch to propagate (modified in-place)
438
+ total_distance : float
439
+ Total distance to propagate in meters
440
+ step_size : float
441
+ Integration step size in meters
442
+ history_interval : int
443
+ Record history every N steps
444
+ wavelength : float
445
+ Wavelength in meters
446
+
447
+ Returns
448
+ -------
449
+ dict
450
+ Dictionary with 'positions', 'directions', 'distances' arrays
451
+ """
452
+ num_rays = rays.num_rays
453
+ total_steps = int(total_distance / step_size)
454
+ num_records = total_steps // history_interval + 1
455
+
456
+ # Prepare GPU arrays for ray state
457
+ positions_d = cuda.to_device(rays.positions.astype(np.float32))
458
+ directions_d = cuda.to_device(rays.directions.astype(np.float32))
459
+ active_d = cuda.to_device(rays.active)
460
+ geo_path_d = cuda.to_device(rays.geometric_path_lengths.astype(np.float32))
461
+ opt_path_d = cuda.to_device(rays.optical_path_lengths.astype(np.float32))
462
+ acc_time_d = cuda.to_device(rays.accumulated_time.astype(np.float32))
463
+
464
+ # Transfer material arrays to device if needed (LUTs, grids, etc.)
465
+ if self._gpu_arrays is not None and self._device_arrays is None:
466
+ self._device_arrays = {}
467
+ for name, arr in self._gpu_arrays.items():
468
+ self._device_arrays[name] = cuda.to_device(arr.astype(np.float32))
469
+
470
+ # Get material-specific kernel parameters
471
+ material_params = self._material.get_gpu_parameters()
472
+
473
+ # Build kernel arguments: scalar params + device arrays
474
+ kernel_args = list(material_params)
475
+ if self._device_arrays is not None:
476
+ for name in ["lut_n", "lut_dn_dh", "n_grid", "grad_grid"]:
477
+ if name in self._device_arrays:
478
+ kernel_args.append(self._device_arrays[name])
479
+
480
+ # Configure kernel launch
481
+ blocks = (num_rays + self.threads_per_block - 1) // self.threads_per_block
482
+
483
+ # Storage for history
484
+ position_history = np.zeros((num_records, num_rays, 3), dtype=np.float32)
485
+ direction_history = np.zeros((num_records, num_rays, 3), dtype=np.float32)
486
+ distance_history = np.zeros(num_records, dtype=np.float32)
487
+
488
+ # Record initial state
489
+ position_history[0] = positions_d.copy_to_host()
490
+ direction_history[0] = directions_d.copy_to_host()
491
+ distance_history[0] = 0.0
492
+
493
+ # Propagate with recording
494
+ record_idx = 1
495
+ steps_done = 0
496
+
497
+ while steps_done < total_steps:
498
+ steps_to_next_record = min(
499
+ history_interval - (steps_done % history_interval),
500
+ total_steps - steps_done,
501
+ )
502
+
503
+ self._kernel[blocks, self.threads_per_block](
504
+ positions_d,
505
+ directions_d,
506
+ active_d,
507
+ geo_path_d,
508
+ opt_path_d,
509
+ acc_time_d,
510
+ float(step_size),
511
+ steps_to_next_record,
512
+ *kernel_args,
513
+ float(wavelength),
514
+ )
515
+ steps_done += steps_to_next_record
516
+
517
+ # Record if at interval
518
+ if steps_done % history_interval == 0 and record_idx < num_records:
519
+ cuda.synchronize()
520
+ position_history[record_idx] = positions_d.copy_to_host()
521
+ direction_history[record_idx] = directions_d.copy_to_host()
522
+ distance_history[record_idx] = steps_done * step_size
523
+ record_idx += 1
524
+
525
+ # Final copy back
526
+ cuda.synchronize()
527
+ rays.positions[:] = positions_d.copy_to_host()
528
+ rays.directions[:] = directions_d.copy_to_host()
529
+ rays.geometric_path_lengths[:] = geo_path_d.copy_to_host()
530
+ rays.optical_path_lengths[:] = opt_path_d.copy_to_host()
531
+ rays.accumulated_time[:] = acc_time_d.copy_to_host()
532
+
533
+ return {
534
+ "positions": position_history[:record_idx],
535
+ "directions": direction_history[:record_idx],
536
+ "distances": distance_history[:record_idx],
537
+ }
538
+
539
+ def get_refractive_index(
540
+ self,
541
+ positions: npt.NDArray[np.floating],
542
+ wavelength: float = 1.0e-6,
543
+ ) -> npt.NDArray[np.float64]:
544
+ """
545
+ Compute refractive index at positions (CPU, delegates to material).
546
+
547
+ Parameters
548
+ ----------
549
+ positions : ndarray (N, 3)
550
+ Position coordinates
551
+ wavelength : float
552
+ Wavelength in meters
553
+
554
+ Returns
555
+ -------
556
+ ndarray (N,)
557
+ Refractive index at each position
558
+ """
559
+ x = positions[:, 0]
560
+ y = positions[:, 1]
561
+ z = positions[:, 2]
562
+ return self._material.get_refractive_index(x, y, z, wavelength)
563
+
564
+
565
+ # Convenience alias
566
+ GPUInhomogeneousPropagator = GPUGradientPropagator