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,602 @@
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
+ Ray Propagation Engine with GPU Acceleration
36
+
37
+ This module implements the core ray propagation algorithms including both
38
+ straight-line propagation (for homogeneous media) and curved-path propagation
39
+ (for gradient media).
40
+ """
41
+
42
+ import math
43
+ from enum import Enum
44
+ from typing import Protocol
45
+
46
+ import numpy as np
47
+ import numpy.typing as npt
48
+
49
+ # GPU support is optional
50
+ try:
51
+ from numba import cuda
52
+
53
+ HAS_CUDA = True
54
+ except ImportError:
55
+
56
+ class _FakeCuda:
57
+ """Fake cuda module for when numba is not installed."""
58
+
59
+ @staticmethod
60
+ def jit(*args, **kwargs):
61
+ """Return a no-op decorator."""
62
+
63
+ def decorator(func):
64
+ return func
65
+
66
+ # Handle both @cuda.jit and @cuda.jit(device=True)
67
+ if args and callable(args[0]):
68
+ return args[0]
69
+ return decorator
70
+
71
+ @staticmethod
72
+ def is_available():
73
+ return False
74
+
75
+ @staticmethod
76
+ def grid(n):
77
+ return 0
78
+
79
+ @staticmethod
80
+ def synchronize():
81
+ pass
82
+
83
+ cuda = _FakeCuda() # type: ignore[assignment]
84
+ HAS_CUDA = False
85
+
86
+ from ..materials.base import MaterialField
87
+ from .ray_data import Float32Array, RayBatch
88
+
89
+
90
+ class PropagationMode(Enum):
91
+ """
92
+ Ray propagation mode selector.
93
+
94
+ Attributes
95
+ ----------
96
+ STRAIGHT_LINE : int
97
+ Use straight-line propagation (homogeneous media)
98
+ CURVED_PATH : int
99
+ Use ray equation integration (gradient media)
100
+ ADAPTIVE : int
101
+ Automatically select based on gradient magnitude
102
+ """
103
+
104
+ STRAIGHT_LINE = 0
105
+ CURVED_PATH = 1
106
+ ADAPTIVE = 2
107
+
108
+
109
+ class IPropagator(Protocol):
110
+ """
111
+ Protocol (interface) for ray propagation implementations.
112
+
113
+ All propagators must implement this interface to ensure compatibility
114
+ with the simulation framework.
115
+ """
116
+
117
+ def propagate_step(
118
+ self,
119
+ rays: RayBatch,
120
+ step_size: float,
121
+ material: MaterialField,
122
+ ) -> None:
123
+ """
124
+ Propagate rays forward by one step.
125
+
126
+ Parameters
127
+ ----------
128
+ rays : RayBatch
129
+ Ray batch to propagate (modified in-place)
130
+ step_size : float
131
+ Step size in meters
132
+ material : MaterialField
133
+ Material field defining refractive index
134
+ """
135
+ ...
136
+
137
+ def compute_gradient_threshold(self, wavelength: float) -> float:
138
+ """
139
+ Compute threshold for switching between propagation modes.
140
+
141
+ Parameters
142
+ ----------
143
+ wavelength : float
144
+ Wavelength in meters
145
+
146
+ Returns
147
+ -------
148
+ float
149
+ Gradient magnitude threshold in m^-1
150
+ """
151
+ ...
152
+
153
+
154
+ class PropagationConfig:
155
+ """
156
+ Configuration for ray propagation.
157
+
158
+ Attributes
159
+ ----------
160
+ mode : PropagationMode
161
+ Propagation mode
162
+ max_step_size : float
163
+ Maximum step size in meters
164
+ min_step_size : float
165
+ Minimum step size in meters
166
+ gradient_threshold : float
167
+ Threshold for adaptive mode switching (m^-1)
168
+ adaptive_stepping : bool
169
+ Enable adaptive step size based on gradient
170
+ rk4_integration : bool
171
+ Use RK4 instead of Euler for curved paths
172
+
173
+ Notes
174
+ -----
175
+ For gradient media, step size should satisfy:
176
+ Δs ≪ R_c where R_c = n/|∇n| is the radius of curvature
177
+ """
178
+
179
+ def __init__(
180
+ self,
181
+ mode: PropagationMode = PropagationMode.ADAPTIVE,
182
+ max_step_size: float = 1e-3,
183
+ min_step_size: float = 1e-6,
184
+ gradient_threshold: float = 1e-6,
185
+ adaptive_stepping: bool = True,
186
+ rk4_integration: bool = True,
187
+ ):
188
+ """
189
+ Initialize propagation configuration.
190
+
191
+ Parameters
192
+ ----------
193
+ mode : PropagationMode, optional
194
+ Propagation mode, default ADAPTIVE
195
+ max_step_size : float, optional
196
+ Maximum step size in meters, default 1 mm
197
+ min_step_size : float, optional
198
+ Minimum step size in meters, default 1 μm
199
+ gradient_threshold : float, optional
200
+ Gradient threshold in m^-1, default 1e-6
201
+ adaptive_stepping : bool, optional
202
+ Enable adaptive stepping, default True
203
+ rk4_integration : bool, optional
204
+ Use RK4 integration, default True
205
+ """
206
+ self.mode = mode
207
+ self.max_step_size = max_step_size
208
+ self.min_step_size = min_step_size
209
+ self.gradient_threshold = gradient_threshold
210
+ self.adaptive_stepping = adaptive_stepping
211
+ self.rk4_integration = rk4_integration
212
+
213
+ def compute_step_size(
214
+ self, gradient_magnitude: float, refractive_index: float, wavelength: float
215
+ ) -> float:
216
+ """
217
+ Compute adaptive step size based on local conditions.
218
+
219
+ Parameters
220
+ ----------
221
+ gradient_magnitude : float
222
+ |∇n| at current position (m^-1)
223
+ refractive_index : float
224
+ n at current position
225
+ wavelength : float
226
+ Wavelength in meters
227
+
228
+ Returns
229
+ -------
230
+ float
231
+ Recommended step size in meters
232
+
233
+ Notes
234
+ -----
235
+ Step size is chosen to resolve ray curvature:
236
+ Δs ≤ min(R_c/10, λ_medium) where R_c = n/|∇n|
237
+ """
238
+ if not self.adaptive_stepping:
239
+ return self.max_step_size
240
+
241
+ # Radius of curvature
242
+ if gradient_magnitude > 1e-12:
243
+ radius_of_curvature = refractive_index / gradient_magnitude
244
+ step_from_curvature = radius_of_curvature / 10.0
245
+ else:
246
+ step_from_curvature = self.max_step_size
247
+
248
+ # Wavelength in medium
249
+ wavelength_medium = wavelength / refractive_index
250
+
251
+ # Take minimum, but clamp to configured limits
252
+ step_size = min(step_from_curvature, wavelength_medium)
253
+ step_size = np.clip(step_size, self.min_step_size, self.max_step_size)
254
+
255
+ return float(step_size)
256
+
257
+
258
+ # GPU device functions for ray propagation
259
+ # These are defined even when CUDA is not available (decorators become no-ops)
260
+
261
+
262
+ @cuda.jit(device=True)
263
+ def device_propagate_straight_line(
264
+ x: float, y: float, z: float, dx: float, dy: float, dz: float, step_size: float
265
+ ) -> tuple[float, float, float]:
266
+ """
267
+ Propagate ray in straight line on GPU device.
268
+
269
+ Parameters
270
+ ----------
271
+ x, y, z : float
272
+ Current position
273
+ dx, dy, dz : float
274
+ Direction vector (must be normalized)
275
+ step_size : float
276
+ Distance to propagate
277
+
278
+ Returns
279
+ -------
280
+ tuple of float
281
+ New position (x', y', z')
282
+ """
283
+ new_x = x + dx * step_size
284
+ new_y = y + dy * step_size
285
+ new_z = z + dz * step_size
286
+ return new_x, new_y, new_z
287
+
288
+
289
+ @cuda.jit(device=True)
290
+ def device_compute_ray_curvature(
291
+ n: float,
292
+ grad_n_x: float,
293
+ grad_n_y: float,
294
+ grad_n_z: float,
295
+ dx: float,
296
+ dy: float,
297
+ dz: float,
298
+ ) -> tuple[float, float, float]:
299
+ """
300
+ Compute ray curvature vector on GPU device.
301
+
302
+ The curvature vector determines how the ray direction changes:
303
+ κ = (∇n - (d̂·∇n)d̂) / n
304
+
305
+ Parameters
306
+ ----------
307
+ n : float
308
+ Refractive index at current position
309
+ grad_n_x, grad_n_y, grad_n_z : float
310
+ Gradient of refractive index ∇n
311
+ dx, dy, dz : float
312
+ Current ray direction (unit vector)
313
+
314
+ Returns
315
+ -------
316
+ tuple of float
317
+ Curvature vector (κx, κy, κz) in m^-1
318
+
319
+ References
320
+ ----------
321
+ .. [1] Born, M., & Wolf, E. (1999). Principles of Optics. Section 3.1.
322
+ """
323
+ # Project gradient onto direction: d̂·∇n
324
+ dot_product = dx * grad_n_x + dy * grad_n_y + dz * grad_n_z
325
+
326
+ # Perpendicular component: ∇n - (d̂·∇n)d̂
327
+ perp_x = grad_n_x - dot_product * dx
328
+ perp_y = grad_n_y - dot_product * dy
329
+ perp_z = grad_n_z - dot_product * dz
330
+
331
+ # Curvature: κ = ∇n_perp / n
332
+ kappa_x = perp_x / n
333
+ kappa_y = perp_y / n
334
+ kappa_z = perp_z / n
335
+
336
+ return kappa_x, kappa_y, kappa_z
337
+
338
+
339
+ @cuda.jit(device=True)
340
+ def device_euler_step_gradient(
341
+ x: float,
342
+ y: float,
343
+ z: float,
344
+ dx: float,
345
+ dy: float,
346
+ dz: float,
347
+ n: float,
348
+ grad_n_x: float,
349
+ grad_n_y: float,
350
+ grad_n_z: float,
351
+ step_size: float,
352
+ ) -> tuple[float, float, float, float, float, float]:
353
+ """
354
+ Single Euler step for ray in gradient medium on GPU device.
355
+
356
+ Updates position and direction according to:
357
+ dr/ds = d̂
358
+ dd̂/ds = κ = ∇n_perp / n
359
+
360
+ Parameters
361
+ ----------
362
+ x, y, z : float
363
+ Current position
364
+ dx, dy, dz : float
365
+ Current direction (unit vector)
366
+ n : float
367
+ Refractive index at current position
368
+ grad_n_x, grad_n_y, grad_n_z : float
369
+ Gradient ∇n at current position
370
+ step_size : float
371
+ Integration step size
372
+
373
+ Returns
374
+ -------
375
+ tuple of float
376
+ (new_x, new_y, new_z, new_dx, new_dy, new_dz)
377
+ """
378
+ # Compute curvature
379
+ kappa_x, kappa_y, kappa_z = device_compute_ray_curvature(
380
+ n, grad_n_x, grad_n_y, grad_n_z, dx, dy, dz
381
+ )
382
+
383
+ # Update position: r' = r + d̂ * Δs
384
+ new_x = x + dx * step_size
385
+ new_y = y + dy * step_size
386
+ new_z = z + dz * step_size
387
+
388
+ # Update direction: d̂' = d̂ + κ * Δs
389
+ new_dx = dx + kappa_x * step_size
390
+ new_dy = dy + kappa_y * step_size
391
+ new_dz = dz + kappa_z * step_size
392
+
393
+ # Renormalize direction
394
+ norm = math.sqrt(new_dx**2 + new_dy**2 + new_dz**2)
395
+ if norm > 1e-12:
396
+ new_dx /= norm
397
+ new_dy /= norm
398
+ new_dz /= norm
399
+
400
+ return new_x, new_y, new_z, new_dx, new_dy, new_dz
401
+
402
+
403
+ @cuda.jit(device=True)
404
+ def device_normalize_vector(
405
+ vx: float, vy: float, vz: float
406
+ ) -> tuple[float, float, float]:
407
+ """
408
+ Normalize a 3D vector on GPU device.
409
+
410
+ Parameters
411
+ ----------
412
+ vx, vy, vz : float
413
+ Vector components
414
+
415
+ Returns
416
+ -------
417
+ tuple of float
418
+ Normalized vector (or original if norm too small)
419
+ """
420
+ norm = math.sqrt(vx**2 + vy**2 + vz**2)
421
+ if norm < 1e-12:
422
+ return vx, vy, vz
423
+ return vx / norm, vy / norm, vz / norm
424
+
425
+
426
+ @cuda.jit
427
+ def kernel_propagate_straight_line(
428
+ positions: Float32Array,
429
+ directions: Float32Array,
430
+ active: npt.NDArray[np.bool_],
431
+ geometric_path_lengths: Float32Array,
432
+ step_size: float,
433
+ ) -> None:
434
+ """
435
+ GPU kernel for straight-line ray propagation.
436
+
437
+ Parameters
438
+ ----------
439
+ positions : Float32Array
440
+ Ray positions array (N, 3), modified in-place
441
+ directions : Float32Array
442
+ Ray directions array (N, 3)
443
+ active : BoolArray
444
+ Active ray mask (N,)
445
+ geometric_path_lengths : Float32Array
446
+ Geometric path lengths (N,), modified in-place
447
+ step_size : float
448
+ Distance to propagate
449
+ """
450
+ idx = cuda.grid(1)
451
+
452
+ if idx >= positions.shape[0]:
453
+ return
454
+
455
+ if not active[idx]:
456
+ return
457
+
458
+ # Read current state
459
+ x = positions[idx, 0]
460
+ y = positions[idx, 1]
461
+ z = positions[idx, 2]
462
+ dx = directions[idx, 0]
463
+ dy = directions[idx, 1]
464
+ dz = directions[idx, 2]
465
+
466
+ # Propagate
467
+ new_x, new_y, new_z = device_propagate_straight_line(x, y, z, dx, dy, dz, step_size)
468
+
469
+ # Write new position
470
+ positions[idx, 0] = new_x
471
+ positions[idx, 1] = new_y
472
+ positions[idx, 2] = new_z
473
+
474
+ # Update path length
475
+ geometric_path_lengths[idx] += step_size
476
+
477
+
478
+ def propagate_straight_line_cpu(rays: RayBatch, step_size: float) -> None:
479
+ """
480
+ Propagate rays in straight lines using CPU.
481
+
482
+ Parameters
483
+ ----------
484
+ rays : RayBatch
485
+ Ray batch to propagate (modified in-place)
486
+ step_size : float
487
+ Distance to propagate in meters
488
+
489
+ Notes
490
+ -----
491
+ This function is appropriate for homogeneous media only.
492
+ """
493
+ # Only process active rays
494
+ active_mask = rays.active
495
+
496
+ # Update positions: pos += step_size * direction
497
+ rays.positions[active_mask] += step_size * rays.directions[active_mask]
498
+
499
+ # Update geometric path lengths
500
+ rays.geometric_path_lengths[active_mask] += step_size
501
+
502
+
503
+ def propagate_straight_line_gpu(
504
+ rays: RayBatch, step_size: float, threads_per_block: int = 256
505
+ ) -> None:
506
+ """
507
+ Propagate rays in straight lines using GPU.
508
+
509
+ Parameters
510
+ ----------
511
+ rays : RayBatch
512
+ Ray batch to propagate (modified in-place)
513
+ step_size : float
514
+ Distance to propagate in meters
515
+ threads_per_block : int, optional
516
+ CUDA threads per block, default 256
517
+
518
+ Notes
519
+ -----
520
+ This function is appropriate for homogeneous media only.
521
+ Use propagate_gradient_gpu for inhomogeneous media.
522
+ """
523
+ if not cuda.is_available():
524
+ raise RuntimeError("CUDA is not available")
525
+
526
+ n_rays = rays.num_rays
527
+ blocks_per_grid = (n_rays + threads_per_block - 1) // threads_per_block
528
+
529
+ # Launch kernel
530
+ kernel_propagate_straight_line[blocks_per_grid, threads_per_block](
531
+ rays.positions,
532
+ rays.directions,
533
+ rays.active,
534
+ rays.geometric_path_lengths,
535
+ step_size,
536
+ )
537
+
538
+ cuda.synchronize()
539
+
540
+
541
+ class StraightLinePropagator:
542
+ """
543
+ Propagator for homogeneous media using straight-line propagation.
544
+
545
+ This is the simplest and fastest propagation mode, appropriate when
546
+ the refractive index gradient is negligible.
547
+
548
+ Automatically falls back to CPU if CUDA is not available.
549
+ """
550
+
551
+ def __init__(self, threads_per_block: int = 256, prefer_gpu: bool = True):
552
+ """
553
+ Initialize straight-line propagator.
554
+
555
+ Parameters
556
+ ----------
557
+ threads_per_block : int, optional
558
+ CUDA threads per block, default 256
559
+ prefer_gpu : bool, optional
560
+ If True, use GPU when available. If False, always use CPU.
561
+ """
562
+ self.threads_per_block = threads_per_block
563
+ self._use_gpu = prefer_gpu and cuda.is_available()
564
+
565
+ def propagate_step(
566
+ self,
567
+ rays: RayBatch,
568
+ step_size: float,
569
+ material: MaterialField,
570
+ ) -> None:
571
+ """
572
+ Propagate rays in straight lines.
573
+
574
+ Parameters
575
+ ----------
576
+ rays : RayBatch
577
+ Ray batch to propagate (modified in-place)
578
+ step_size : float
579
+ Step size in meters
580
+ material : MaterialField
581
+ Material field (not used for straight-line propagation)
582
+ """
583
+ if self._use_gpu:
584
+ propagate_straight_line_gpu(rays, step_size, self.threads_per_block)
585
+ else:
586
+ propagate_straight_line_cpu(rays, step_size)
587
+
588
+ def compute_gradient_threshold(self, wavelength: float) -> float:
589
+ """
590
+ Compute gradient threshold.
591
+
592
+ Parameters
593
+ ----------
594
+ wavelength : float
595
+ Wavelength in meters
596
+
597
+ Returns
598
+ -------
599
+ float
600
+ Threshold value (infinity for straight-line only mode)
601
+ """
602
+ return float("inf") # Always use straight line