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.
- lsurf/__init__.py +471 -0
- lsurf/analysis/__init__.py +107 -0
- lsurf/analysis/healpix_utils.py +418 -0
- lsurf/analysis/sphere_viz.py +1280 -0
- lsurf/cli/__init__.py +48 -0
- lsurf/cli/build.py +398 -0
- lsurf/cli/config_schema.py +318 -0
- lsurf/cli/gui_cmd.py +76 -0
- lsurf/cli/interactive.py +850 -0
- lsurf/cli/main.py +81 -0
- lsurf/cli/run.py +806 -0
- lsurf/detectors/__init__.py +266 -0
- lsurf/detectors/analysis.py +289 -0
- lsurf/detectors/base.py +284 -0
- lsurf/detectors/constant_size_rings.py +485 -0
- lsurf/detectors/directional.py +45 -0
- lsurf/detectors/extended/__init__.py +73 -0
- lsurf/detectors/extended/local_sphere.py +353 -0
- lsurf/detectors/extended/recording_sphere.py +368 -0
- lsurf/detectors/planar.py +45 -0
- lsurf/detectors/protocol.py +187 -0
- lsurf/detectors/recording_spheres.py +63 -0
- lsurf/detectors/results.py +1140 -0
- lsurf/detectors/small/__init__.py +79 -0
- lsurf/detectors/small/directional.py +330 -0
- lsurf/detectors/small/planar.py +401 -0
- lsurf/detectors/small/spherical.py +450 -0
- lsurf/detectors/spherical.py +45 -0
- lsurf/geometry/__init__.py +199 -0
- lsurf/geometry/builder.py +478 -0
- lsurf/geometry/cell.py +228 -0
- lsurf/geometry/cell_geometry.py +247 -0
- lsurf/geometry/detector_arrays.py +1785 -0
- lsurf/geometry/geometry.py +222 -0
- lsurf/geometry/surface_analysis.py +375 -0
- lsurf/geometry/validation.py +91 -0
- lsurf/gui/__init__.py +51 -0
- lsurf/gui/app.py +903 -0
- lsurf/gui/core/__init__.py +39 -0
- lsurf/gui/core/scene.py +343 -0
- lsurf/gui/core/simulation.py +264 -0
- lsurf/gui/renderers/__init__.py +40 -0
- lsurf/gui/renderers/ray_renderer.py +353 -0
- lsurf/gui/renderers/source_renderer.py +505 -0
- lsurf/gui/renderers/surface_renderer.py +477 -0
- lsurf/gui/views/__init__.py +48 -0
- lsurf/gui/views/config_editor.py +3199 -0
- lsurf/gui/views/properties.py +257 -0
- lsurf/gui/views/results.py +291 -0
- lsurf/gui/views/scene_tree.py +180 -0
- lsurf/gui/views/viewport_3d.py +555 -0
- lsurf/gui/views/visualizations.py +712 -0
- lsurf/materials/__init__.py +169 -0
- lsurf/materials/base/__init__.py +64 -0
- lsurf/materials/base/full_inhomogeneous.py +208 -0
- lsurf/materials/base/grid_inhomogeneous.py +319 -0
- lsurf/materials/base/homogeneous.py +342 -0
- lsurf/materials/base/material_field.py +527 -0
- lsurf/materials/base/simple_inhomogeneous.py +418 -0
- lsurf/materials/base/spectral_inhomogeneous.py +497 -0
- lsurf/materials/implementations/__init__.py +120 -0
- lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
- lsurf/materials/implementations/duct_atmosphere.py +390 -0
- lsurf/materials/implementations/exponential_atmosphere.py +435 -0
- lsurf/materials/implementations/gaussian_lens.py +120 -0
- lsurf/materials/implementations/interpolated_data.py +123 -0
- lsurf/materials/implementations/layered_atmosphere.py +134 -0
- lsurf/materials/implementations/linear_gradient.py +109 -0
- lsurf/materials/implementations/linsley_atmosphere.py +764 -0
- lsurf/materials/implementations/standard_materials.py +126 -0
- lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
- lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
- lsurf/materials/utils/__init__.py +77 -0
- lsurf/materials/utils/constants.py +45 -0
- lsurf/materials/utils/device_functions.py +117 -0
- lsurf/materials/utils/dispersion.py +160 -0
- lsurf/materials/utils/factories.py +142 -0
- lsurf/propagation/__init__.py +91 -0
- lsurf/propagation/detector_gpu.py +67 -0
- lsurf/propagation/gpu_device_rays.py +294 -0
- lsurf/propagation/kernels/__init__.py +175 -0
- lsurf/propagation/kernels/absorption/__init__.py +61 -0
- lsurf/propagation/kernels/absorption/grid.py +240 -0
- lsurf/propagation/kernels/absorption/simple.py +232 -0
- lsurf/propagation/kernels/absorption/spectral.py +410 -0
- lsurf/propagation/kernels/detection/__init__.py +64 -0
- lsurf/propagation/kernels/detection/protocol.py +102 -0
- lsurf/propagation/kernels/detection/spherical.py +255 -0
- lsurf/propagation/kernels/device_functions.py +790 -0
- lsurf/propagation/kernels/fresnel/__init__.py +64 -0
- lsurf/propagation/kernels/fresnel/protocol.py +97 -0
- lsurf/propagation/kernels/fresnel/standard.py +258 -0
- lsurf/propagation/kernels/intersection/__init__.py +79 -0
- lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
- lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
- lsurf/propagation/kernels/intersection/plane.py +166 -0
- lsurf/propagation/kernels/intersection/protocol.py +95 -0
- lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
- lsurf/propagation/kernels/intersection/sphere.py +190 -0
- lsurf/propagation/kernels/propagation/__init__.py +85 -0
- lsurf/propagation/kernels/propagation/grid.py +527 -0
- lsurf/propagation/kernels/propagation/protocol.py +105 -0
- lsurf/propagation/kernels/propagation/simple.py +460 -0
- lsurf/propagation/kernels/propagation/spectral.py +875 -0
- lsurf/propagation/kernels/registry.py +331 -0
- lsurf/propagation/kernels/surface/__init__.py +72 -0
- lsurf/propagation/kernels/surface/bisection.py +232 -0
- lsurf/propagation/kernels/surface/detection.py +402 -0
- lsurf/propagation/kernels/surface/reduction.py +166 -0
- lsurf/propagation/propagator_protocol.py +222 -0
- lsurf/propagation/propagators/__init__.py +101 -0
- lsurf/propagation/propagators/detector_handler.py +354 -0
- lsurf/propagation/propagators/factory.py +200 -0
- lsurf/propagation/propagators/fresnel_handler.py +305 -0
- lsurf/propagation/propagators/gpu_gradient.py +566 -0
- lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
- lsurf/propagation/propagators/gradient.py +429 -0
- lsurf/propagation/propagators/intersection_handler.py +327 -0
- lsurf/propagation/propagators/material_propagator.py +398 -0
- lsurf/propagation/propagators/signed_distance_handler.py +522 -0
- lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
- lsurf/propagation/propagators/surface_interaction.py +616 -0
- lsurf/propagation/propagators/surface_propagator.py +719 -0
- lsurf/py.typed +1 -0
- lsurf/simulation/__init__.py +70 -0
- lsurf/simulation/config.py +164 -0
- lsurf/simulation/orchestrator.py +462 -0
- lsurf/simulation/result.py +299 -0
- lsurf/simulation/simulation.py +262 -0
- lsurf/sources/__init__.py +128 -0
- lsurf/sources/base.py +264 -0
- lsurf/sources/collimated.py +252 -0
- lsurf/sources/custom.py +409 -0
- lsurf/sources/diverging.py +228 -0
- lsurf/sources/gaussian.py +272 -0
- lsurf/sources/parallel_from_positions.py +197 -0
- lsurf/sources/point.py +172 -0
- lsurf/sources/uniform_diverging.py +258 -0
- lsurf/surfaces/__init__.py +184 -0
- lsurf/surfaces/cpu/__init__.py +50 -0
- lsurf/surfaces/cpu/curved_wave.py +463 -0
- lsurf/surfaces/cpu/gerstner_wave.py +381 -0
- lsurf/surfaces/cpu/wave_params.py +118 -0
- lsurf/surfaces/gpu/__init__.py +72 -0
- lsurf/surfaces/gpu/annular_plane.py +453 -0
- lsurf/surfaces/gpu/bounded_plane.py +390 -0
- lsurf/surfaces/gpu/curved_wave.py +483 -0
- lsurf/surfaces/gpu/gerstner_wave.py +377 -0
- lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
- lsurf/surfaces/gpu/plane.py +299 -0
- lsurf/surfaces/gpu/recording_sphere.py +587 -0
- lsurf/surfaces/gpu/sphere.py +311 -0
- lsurf/surfaces/protocol.py +336 -0
- lsurf/surfaces/registry.py +373 -0
- lsurf/utilities/__init__.py +175 -0
- lsurf/utilities/detector_analysis.py +814 -0
- lsurf/utilities/fresnel.py +628 -0
- lsurf/utilities/interactions.py +1215 -0
- lsurf/utilities/propagation.py +602 -0
- lsurf/utilities/ray_data.py +532 -0
- lsurf/utilities/recording_sphere.py +745 -0
- lsurf/utilities/time_spread.py +463 -0
- lsurf/visualization/__init__.py +329 -0
- lsurf/visualization/absorption_plots.py +334 -0
- lsurf/visualization/atmospheric_plots.py +754 -0
- lsurf/visualization/common.py +348 -0
- lsurf/visualization/detector_plots.py +1350 -0
- lsurf/visualization/detector_sphere_plots.py +1173 -0
- lsurf/visualization/fresnel_plots.py +1061 -0
- lsurf/visualization/ocean_simulation_plots.py +999 -0
- lsurf/visualization/polarization_plots.py +916 -0
- lsurf/visualization/raytracing_plots.py +1521 -0
- lsurf/visualization/ring_detector_plots.py +1867 -0
- lsurf/visualization/time_spread_plots.py +531 -0
- lsurf-1.0.0.dist-info/METADATA +381 -0
- lsurf-1.0.0.dist-info/RECORD +180 -0
- lsurf-1.0.0.dist-info/WHEEL +5 -0
- lsurf-1.0.0.dist-info/entry_points.txt +2 -0
- lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
- lsurf-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,707 @@
|
|
|
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 Surface Propagator
|
|
36
|
+
|
|
37
|
+
Fully GPU-resident propagator with surface detection.
|
|
38
|
+
Uses shared GPUDeviceRays structure for both material propagation
|
|
39
|
+
and surface intersection - no duplicate memory allocation.
|
|
40
|
+
|
|
41
|
+
Architecture
|
|
42
|
+
------------
|
|
43
|
+
This propagator minimizes CPU-GPU transfers:
|
|
44
|
+
1. One-time upload of ray data at start
|
|
45
|
+
2. GPU-resident loop: propagation + detection + bisection
|
|
46
|
+
3. Small reduction transfer per step (8 bytes) for loop termination
|
|
47
|
+
4. One-time download of results at end
|
|
48
|
+
|
|
49
|
+
For N=1M rays, 10k steps:
|
|
50
|
+
- Previous: ~240 GB transferred
|
|
51
|
+
- This: ~48 MB transferred (5000x reduction)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from typing import TYPE_CHECKING
|
|
55
|
+
|
|
56
|
+
import numpy as np
|
|
57
|
+
|
|
58
|
+
from ..gpu_device_rays import GPUDeviceRays
|
|
59
|
+
from .surface_propagator import HitData
|
|
60
|
+
|
|
61
|
+
# Import MAX_SURFACE_PARAMS
|
|
62
|
+
MAX_SURFACE_PARAMS = 64
|
|
63
|
+
|
|
64
|
+
# Speed of light
|
|
65
|
+
SPEED_OF_LIGHT = 299792458.0
|
|
66
|
+
|
|
67
|
+
# GPU support
|
|
68
|
+
try:
|
|
69
|
+
from numba import cuda
|
|
70
|
+
|
|
71
|
+
_HAS_CUDA = cuda.is_available()
|
|
72
|
+
|
|
73
|
+
if _HAS_CUDA:
|
|
74
|
+
|
|
75
|
+
@cuda.jit
|
|
76
|
+
def kernel_homogeneous_step(
|
|
77
|
+
positions, directions, active, geo_path, opt_path, acc_time, step_size, n, c
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Simple straight-line propagation for homogeneous materials.
|
|
81
|
+
|
|
82
|
+
For homogeneous materials (constant n, zero gradient):
|
|
83
|
+
- Position advances in straight line
|
|
84
|
+
- Direction stays constant
|
|
85
|
+
- Path lengths and time accumulate
|
|
86
|
+
"""
|
|
87
|
+
idx = cuda.grid(1)
|
|
88
|
+
if idx >= positions.shape[0]:
|
|
89
|
+
return
|
|
90
|
+
if not active[idx]:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Update position (straight line)
|
|
94
|
+
positions[idx, 0] += directions[idx, 0] * step_size
|
|
95
|
+
positions[idx, 1] += directions[idx, 1] * step_size
|
|
96
|
+
positions[idx, 2] += directions[idx, 2] * step_size
|
|
97
|
+
|
|
98
|
+
# Update path lengths
|
|
99
|
+
geo_path[idx] += step_size
|
|
100
|
+
opt_path[idx] += n * step_size
|
|
101
|
+
acc_time[idx] += n * step_size / c
|
|
102
|
+
|
|
103
|
+
@cuda.jit
|
|
104
|
+
def kernel_homogeneous_step_adaptive(
|
|
105
|
+
positions,
|
|
106
|
+
directions,
|
|
107
|
+
active,
|
|
108
|
+
geo_path,
|
|
109
|
+
opt_path,
|
|
110
|
+
acc_time,
|
|
111
|
+
step_sizes,
|
|
112
|
+
n,
|
|
113
|
+
c,
|
|
114
|
+
):
|
|
115
|
+
"""
|
|
116
|
+
Straight-line propagation with per-ray adaptive step sizes.
|
|
117
|
+
|
|
118
|
+
Same as kernel_homogeneous_step but uses per-ray step_sizes array
|
|
119
|
+
instead of a scalar step size. This enables adaptive stepping
|
|
120
|
+
where rays near surfaces use smaller steps for precision.
|
|
121
|
+
"""
|
|
122
|
+
idx = cuda.grid(1)
|
|
123
|
+
if idx >= positions.shape[0]:
|
|
124
|
+
return
|
|
125
|
+
if not active[idx]:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
step_size = step_sizes[idx]
|
|
129
|
+
|
|
130
|
+
# Update position (straight line)
|
|
131
|
+
positions[idx, 0] += directions[idx, 0] * step_size
|
|
132
|
+
positions[idx, 1] += directions[idx, 1] * step_size
|
|
133
|
+
positions[idx, 2] += directions[idx, 2] * step_size
|
|
134
|
+
|
|
135
|
+
# Update path lengths
|
|
136
|
+
geo_path[idx] += step_size
|
|
137
|
+
opt_path[idx] += n * step_size
|
|
138
|
+
acc_time[idx] += n * step_size / c
|
|
139
|
+
|
|
140
|
+
except ImportError:
|
|
141
|
+
_HAS_CUDA = False
|
|
142
|
+
|
|
143
|
+
class _FakeCuda:
|
|
144
|
+
@staticmethod
|
|
145
|
+
def is_available():
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def synchronize():
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def to_device(arr):
|
|
154
|
+
return arr
|
|
155
|
+
|
|
156
|
+
cuda = _FakeCuda() # type: ignore[assignment]
|
|
157
|
+
kernel_homogeneous_step = None # type: ignore[assignment]
|
|
158
|
+
kernel_homogeneous_step_adaptive = None # type: ignore[assignment]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if TYPE_CHECKING:
|
|
162
|
+
from ...utilities.ray_data import RayBatch
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class GPUSurfacePropagator:
|
|
166
|
+
"""
|
|
167
|
+
Fully GPU-resident propagator with surface detection.
|
|
168
|
+
|
|
169
|
+
Uses shared GPUDeviceRays structure for both material propagation
|
|
170
|
+
and surface intersection - no duplicate memory allocation.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
material : MaterialField
|
|
175
|
+
Material to propagate through. Must support get_gpu_kernels().
|
|
176
|
+
surfaces : list of Surface
|
|
177
|
+
List of surfaces to check for intersections. All must be gpu_capable.
|
|
178
|
+
threads_per_block : int
|
|
179
|
+
CUDA threads per block. Default: 256.
|
|
180
|
+
apply_absorption : bool
|
|
181
|
+
Whether to apply Beer-Lambert absorption during propagation. Default: False.
|
|
182
|
+
|
|
183
|
+
Notes
|
|
184
|
+
-----
|
|
185
|
+
Falls back to CPU SurfacePropagator if:
|
|
186
|
+
- CUDA is not available
|
|
187
|
+
- Material doesn't support GPU propagation
|
|
188
|
+
- Any surface is not gpu_capable
|
|
189
|
+
|
|
190
|
+
Example
|
|
191
|
+
-------
|
|
192
|
+
>>> propagator = GPUSurfacePropagator(material, surfaces)
|
|
193
|
+
>>> hit_data = propagator.propagate_to_surface(rays, step_size=100.0, max_steps=10000)
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
material,
|
|
199
|
+
surfaces: list,
|
|
200
|
+
threads_per_block: int = 256,
|
|
201
|
+
apply_absorption: bool = False,
|
|
202
|
+
):
|
|
203
|
+
self._material = material
|
|
204
|
+
self._surfaces = list(surfaces)
|
|
205
|
+
self._threads_per_block = threads_per_block
|
|
206
|
+
self._apply_absorption = apply_absorption
|
|
207
|
+
self._use_gpu = self._check_gpu_support()
|
|
208
|
+
|
|
209
|
+
# Initialize absorption kernel (will be set in _setup_gpu_resources if enabled)
|
|
210
|
+
self._absorption_kernel = None
|
|
211
|
+
|
|
212
|
+
if self._use_gpu:
|
|
213
|
+
self._setup_gpu_resources()
|
|
214
|
+
|
|
215
|
+
def _check_gpu_support(self) -> bool:
|
|
216
|
+
"""Check if GPU propagation is possible."""
|
|
217
|
+
if not _HAS_CUDA:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# Check material: either has GPU kernels OR is homogeneous
|
|
221
|
+
self._is_homogeneous_material = getattr(
|
|
222
|
+
self._material, "_is_homogeneous", False
|
|
223
|
+
)
|
|
224
|
+
has_gpu_kernels = hasattr(self._material, "get_gpu_kernels")
|
|
225
|
+
|
|
226
|
+
if not (has_gpu_kernels or self._is_homogeneous_material):
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
# Check all surfaces are GPU-capable
|
|
230
|
+
for surface in self._surfaces:
|
|
231
|
+
if not getattr(surface, "gpu_capable", False):
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
def _setup_gpu_resources(self) -> None:
|
|
237
|
+
"""Pre-upload surface parameters to GPU (one-time setup)."""
|
|
238
|
+
num_surfaces = len(self._surfaces)
|
|
239
|
+
|
|
240
|
+
# Collect geometry IDs
|
|
241
|
+
geometry_ids = np.array([s.geometry_id for s in self._surfaces], dtype=np.int32)
|
|
242
|
+
self._d_geometry_ids = cuda.to_device(geometry_ids)
|
|
243
|
+
|
|
244
|
+
# Concatenate all surface parameters
|
|
245
|
+
all_params = np.zeros(num_surfaces * MAX_SURFACE_PARAMS, dtype=np.float32)
|
|
246
|
+
for i, surface in enumerate(self._surfaces):
|
|
247
|
+
params = surface.get_gpu_parameters()
|
|
248
|
+
offset = i * MAX_SURFACE_PARAMS
|
|
249
|
+
all_params[offset : offset + len(params)] = params
|
|
250
|
+
|
|
251
|
+
self._d_surface_params = cuda.to_device(all_params)
|
|
252
|
+
|
|
253
|
+
# Upload material LUT arrays to GPU if available (for spectral/grid materials)
|
|
254
|
+
self._d_material_arrays = {}
|
|
255
|
+
if hasattr(self._material, "get_gpu_arrays"):
|
|
256
|
+
gpu_arrays = self._material.get_gpu_arrays()
|
|
257
|
+
for name, arr in gpu_arrays.items():
|
|
258
|
+
if arr is not None:
|
|
259
|
+
self._d_material_arrays[name] = cuda.to_device(
|
|
260
|
+
np.ascontiguousarray(arr, dtype=np.float32)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Import kernels
|
|
264
|
+
from ..kernels.surface import (
|
|
265
|
+
kernel_save_prev_positions,
|
|
266
|
+
kernel_detect_crossing,
|
|
267
|
+
kernel_init_signed_distances,
|
|
268
|
+
kernel_bisect_crossing,
|
|
269
|
+
kernel_reduce_status,
|
|
270
|
+
kernel_compute_min_surface_distance,
|
|
271
|
+
kernel_compute_adaptive_steps,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self._kernel_save_prev = kernel_save_prev_positions
|
|
275
|
+
self._kernel_detect = kernel_detect_crossing
|
|
276
|
+
self._kernel_init_sd = kernel_init_signed_distances
|
|
277
|
+
self._kernel_bisect = kernel_bisect_crossing
|
|
278
|
+
self._kernel_reduce = kernel_reduce_status
|
|
279
|
+
self._kernel_min_dist = kernel_compute_min_surface_distance
|
|
280
|
+
self._kernel_adaptive_steps = kernel_compute_adaptive_steps
|
|
281
|
+
|
|
282
|
+
# Set up absorption kernel if enabled
|
|
283
|
+
if self._apply_absorption:
|
|
284
|
+
self._absorption_kernel = self._get_absorption_kernel()
|
|
285
|
+
|
|
286
|
+
def _get_absorption_kernel(self):
|
|
287
|
+
"""Get the appropriate absorption kernel based on material type."""
|
|
288
|
+
from ..propagator_protocol import GPUMaterialID
|
|
289
|
+
from ..kernels.absorption.simple import _kernel_absorption_simple
|
|
290
|
+
from ..kernels.absorption.grid import _kernel_absorption_grid
|
|
291
|
+
from ..kernels.absorption.spectral import _kernel_absorption_spectral_perray
|
|
292
|
+
|
|
293
|
+
material_id = getattr(self._material, "gpu_material_id", None)
|
|
294
|
+
|
|
295
|
+
if material_id == GPUMaterialID.SIMPLE_INHOMOGENEOUS:
|
|
296
|
+
return _kernel_absorption_simple
|
|
297
|
+
elif material_id == GPUMaterialID.EXPONENTIAL_ATMOSPHERE:
|
|
298
|
+
return _kernel_absorption_simple # Uses same LUT structure
|
|
299
|
+
elif material_id == GPUMaterialID.GRID_INHOMOGENEOUS:
|
|
300
|
+
return _kernel_absorption_grid
|
|
301
|
+
elif material_id == GPUMaterialID.SPECTRAL_INHOMOGENEOUS:
|
|
302
|
+
return _kernel_absorption_spectral_perray
|
|
303
|
+
else:
|
|
304
|
+
# No absorption kernel available for this material type
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def _apply_absorption_step(
|
|
308
|
+
self,
|
|
309
|
+
gpu_rays: "GPUDeviceRays",
|
|
310
|
+
step_size: float,
|
|
311
|
+
num_steps: int,
|
|
312
|
+
blocks: int,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Apply absorption using the appropriate kernel."""
|
|
315
|
+
from ..propagator_protocol import GPUMaterialID
|
|
316
|
+
|
|
317
|
+
if self._absorption_kernel is None:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
material_params = self._material.get_gpu_parameters()
|
|
321
|
+
material_id = getattr(self._material, "gpu_material_id", None)
|
|
322
|
+
|
|
323
|
+
if material_id in (
|
|
324
|
+
GPUMaterialID.SIMPLE_INHOMOGENEOUS,
|
|
325
|
+
GPUMaterialID.EXPONENTIAL_ATMOSPHERE,
|
|
326
|
+
):
|
|
327
|
+
# Simple kernel: positions, directions, active, intensities, optical_depth,
|
|
328
|
+
# step_size, num_steps, center_x, center_y, center_z, ref_radius,
|
|
329
|
+
# min_alt, delta_h, lut_size, lut_alpha
|
|
330
|
+
if "lut_alpha" not in self._d_material_arrays:
|
|
331
|
+
return # No absorption LUT available
|
|
332
|
+
self._absorption_kernel[blocks, self._threads_per_block](
|
|
333
|
+
gpu_rays.d_positions,
|
|
334
|
+
gpu_rays.d_directions,
|
|
335
|
+
gpu_rays.d_active,
|
|
336
|
+
gpu_rays.d_intensities,
|
|
337
|
+
gpu_rays.d_optical_depth,
|
|
338
|
+
float(step_size),
|
|
339
|
+
num_steps,
|
|
340
|
+
material_params[0], # center_x
|
|
341
|
+
material_params[1], # center_y
|
|
342
|
+
material_params[2], # center_z
|
|
343
|
+
material_params[3], # ref_radius
|
|
344
|
+
material_params[4], # min_alt
|
|
345
|
+
material_params[5], # delta_h
|
|
346
|
+
int(material_params[6]), # lut_size
|
|
347
|
+
self._d_material_arrays["lut_alpha"],
|
|
348
|
+
)
|
|
349
|
+
elif material_id == GPUMaterialID.GRID_INHOMOGENEOUS:
|
|
350
|
+
# Grid kernel: positions, directions, active, intensities, optical_depth,
|
|
351
|
+
# step_size, num_steps, x_min, y_min, z_min, grid_dx, grid_dy, grid_dz,
|
|
352
|
+
# nx, ny, nz, alpha_grid
|
|
353
|
+
if "alpha_grid" not in self._d_material_arrays:
|
|
354
|
+
return # No absorption grid available
|
|
355
|
+
self._absorption_kernel[blocks, self._threads_per_block](
|
|
356
|
+
gpu_rays.d_positions,
|
|
357
|
+
gpu_rays.d_directions,
|
|
358
|
+
gpu_rays.d_active,
|
|
359
|
+
gpu_rays.d_intensities,
|
|
360
|
+
gpu_rays.d_optical_depth,
|
|
361
|
+
float(step_size),
|
|
362
|
+
num_steps,
|
|
363
|
+
material_params[0], # x_min
|
|
364
|
+
material_params[1], # y_min
|
|
365
|
+
material_params[2], # z_min
|
|
366
|
+
material_params[3], # grid_dx
|
|
367
|
+
material_params[4], # grid_dy
|
|
368
|
+
material_params[5], # grid_dz
|
|
369
|
+
int(material_params[6]), # nx
|
|
370
|
+
int(material_params[7]), # ny
|
|
371
|
+
int(material_params[8]), # nz
|
|
372
|
+
self._d_material_arrays["alpha_grid"],
|
|
373
|
+
)
|
|
374
|
+
elif material_id == GPUMaterialID.SPECTRAL_INHOMOGENEOUS:
|
|
375
|
+
# Spectral kernel with per-ray wavelengths
|
|
376
|
+
if "lut_alpha" not in self._d_material_arrays:
|
|
377
|
+
return # No absorption LUT available
|
|
378
|
+
self._absorption_kernel[blocks, self._threads_per_block](
|
|
379
|
+
gpu_rays.d_positions,
|
|
380
|
+
gpu_rays.d_directions,
|
|
381
|
+
gpu_rays.d_wavelengths,
|
|
382
|
+
gpu_rays.d_active,
|
|
383
|
+
gpu_rays.d_intensities,
|
|
384
|
+
gpu_rays.d_optical_depth,
|
|
385
|
+
float(step_size),
|
|
386
|
+
num_steps,
|
|
387
|
+
material_params[0], # center_x
|
|
388
|
+
material_params[1], # center_y
|
|
389
|
+
material_params[2], # center_z
|
|
390
|
+
material_params[3], # ref_radius
|
|
391
|
+
material_params[4], # alt_min
|
|
392
|
+
material_params[5], # alt_delta
|
|
393
|
+
int(material_params[6]), # n_alt
|
|
394
|
+
material_params[7], # wl_min
|
|
395
|
+
material_params[8], # wl_delta
|
|
396
|
+
int(material_params[9]), # n_wl
|
|
397
|
+
self._d_material_arrays["lut_alpha"],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def use_gpu(self) -> bool:
|
|
402
|
+
"""Whether GPU acceleration is active."""
|
|
403
|
+
return self._use_gpu
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def material(self):
|
|
407
|
+
"""The material being propagated through."""
|
|
408
|
+
return self._material
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def surfaces(self) -> list:
|
|
412
|
+
"""List of surfaces being checked for intersection."""
|
|
413
|
+
return self._surfaces
|
|
414
|
+
|
|
415
|
+
def propagate_to_surface(
|
|
416
|
+
self,
|
|
417
|
+
rays: "RayBatch",
|
|
418
|
+
step_size: float,
|
|
419
|
+
max_steps: int,
|
|
420
|
+
wavelength: float = 532e-9,
|
|
421
|
+
adaptive_stepping: bool = False,
|
|
422
|
+
min_step_size: float = 3e-4,
|
|
423
|
+
surface_proximity_factor: float = 0.5,
|
|
424
|
+
surface_proximity_threshold: float = 10.0,
|
|
425
|
+
) -> HitData:
|
|
426
|
+
"""
|
|
427
|
+
Propagate rays until any surface is hit (or max_steps reached).
|
|
428
|
+
|
|
429
|
+
Uses GPU-resident loop with minimal CPU-GPU transfers.
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
rays : RayBatch
|
|
434
|
+
Ray batch to propagate. Modified in-place.
|
|
435
|
+
step_size : float
|
|
436
|
+
Maximum integration step size in meters.
|
|
437
|
+
max_steps : int
|
|
438
|
+
Maximum number of propagation steps.
|
|
439
|
+
wavelength : float, optional
|
|
440
|
+
Default wavelength for material queries.
|
|
441
|
+
adaptive_stepping : bool, optional
|
|
442
|
+
Whether to use adaptive step sizing near surfaces (default False).
|
|
443
|
+
min_step_size : float, optional
|
|
444
|
+
Minimum step size in meters (default 3e-4 = 0.3mm → ~1ps resolution).
|
|
445
|
+
surface_proximity_factor : float, optional
|
|
446
|
+
Step = distance * factor when within threshold (default 0.5).
|
|
447
|
+
surface_proximity_threshold : float, optional
|
|
448
|
+
Distance within which to start adaptive stepping (default 10.0 m).
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
HitData
|
|
453
|
+
Information about which rays hit which surfaces.
|
|
454
|
+
"""
|
|
455
|
+
if self._use_gpu:
|
|
456
|
+
return self._propagate_gpu(
|
|
457
|
+
rays,
|
|
458
|
+
step_size,
|
|
459
|
+
max_steps,
|
|
460
|
+
wavelength,
|
|
461
|
+
adaptive_stepping=adaptive_stepping,
|
|
462
|
+
min_step_size=min_step_size,
|
|
463
|
+
surface_proximity_factor=surface_proximity_factor,
|
|
464
|
+
surface_proximity_threshold=surface_proximity_threshold,
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
# Fall back to CPU propagator
|
|
468
|
+
from .surface_propagator import SurfacePropagator
|
|
469
|
+
|
|
470
|
+
cpu_prop = SurfacePropagator(
|
|
471
|
+
material=self._material,
|
|
472
|
+
surfaces=self._surfaces,
|
|
473
|
+
use_gpu=False,
|
|
474
|
+
)
|
|
475
|
+
return cpu_prop.propagate_to_surface(
|
|
476
|
+
rays,
|
|
477
|
+
step_size,
|
|
478
|
+
max_steps,
|
|
479
|
+
adaptive_stepping=adaptive_stepping,
|
|
480
|
+
min_step_size=min_step_size,
|
|
481
|
+
surface_proximity_factor=surface_proximity_factor,
|
|
482
|
+
surface_proximity_threshold=surface_proximity_threshold,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def _propagate_gpu(
|
|
486
|
+
self,
|
|
487
|
+
rays: "RayBatch",
|
|
488
|
+
step_size: float,
|
|
489
|
+
max_steps: int,
|
|
490
|
+
wavelength: float,
|
|
491
|
+
adaptive_stepping: bool = False,
|
|
492
|
+
min_step_size: float = 3e-4,
|
|
493
|
+
surface_proximity_factor: float = 0.5,
|
|
494
|
+
surface_proximity_threshold: float = 10.0,
|
|
495
|
+
) -> HitData:
|
|
496
|
+
"""GPU-resident propagation loop with optional adaptive stepping."""
|
|
497
|
+
num_rays = rays.num_rays
|
|
498
|
+
num_surfaces = len(self._surfaces)
|
|
499
|
+
blocks = (num_rays + self._threads_per_block - 1) // self._threads_per_block
|
|
500
|
+
|
|
501
|
+
# === ONE-TIME UPLOAD ===
|
|
502
|
+
gpu_rays = GPUDeviceRays.from_ray_batch(rays, num_surfaces)
|
|
503
|
+
|
|
504
|
+
# Initialize signed distances for all surfaces
|
|
505
|
+
self._kernel_init_sd[blocks, self._threads_per_block](
|
|
506
|
+
gpu_rays.d_positions,
|
|
507
|
+
gpu_rays.d_active,
|
|
508
|
+
self._d_geometry_ids,
|
|
509
|
+
self._d_surface_params,
|
|
510
|
+
num_surfaces,
|
|
511
|
+
MAX_SURFACE_PARAMS,
|
|
512
|
+
gpu_rays.d_prev_signed_dist,
|
|
513
|
+
)
|
|
514
|
+
cuda.synchronize()
|
|
515
|
+
|
|
516
|
+
# Determine propagation mode: homogeneous (straight-line) or gradient-based
|
|
517
|
+
use_homogeneous = getattr(self, "_is_homogeneous_material", False)
|
|
518
|
+
|
|
519
|
+
# Allocate GPU arrays for adaptive stepping if needed
|
|
520
|
+
if adaptive_stepping:
|
|
521
|
+
d_min_distances = cuda.to_device(np.zeros(num_rays, dtype=np.float32))
|
|
522
|
+
d_adaptive_steps = cuda.to_device(
|
|
523
|
+
np.full(num_rays, step_size, dtype=np.float32)
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if use_homogeneous:
|
|
527
|
+
# For homogeneous materials, use simple straight-line kernel
|
|
528
|
+
# Get refractive index (constant for homogeneous materials)
|
|
529
|
+
n = self._material.get_refractive_index(0, 0, 0, wavelength)
|
|
530
|
+
else:
|
|
531
|
+
# Get material propagation kernel for inhomogeneous materials
|
|
532
|
+
material_kernels = self._material.get_gpu_kernels()
|
|
533
|
+
prop_kernel = material_kernels.get("euler") or material_kernels.get("rk4")
|
|
534
|
+
if prop_kernel is None:
|
|
535
|
+
raise RuntimeError("Material does not provide GPU propagation kernel")
|
|
536
|
+
material_params = self._material.get_gpu_parameters()
|
|
537
|
+
# Get reference refractive index for time correction in bisection
|
|
538
|
+
# Use value at reference position (e.g., sea level for atmosphere)
|
|
539
|
+
n = self._material.get_refractive_index(0, 0, 0, wavelength)
|
|
540
|
+
|
|
541
|
+
# === GPU-RESIDENT LOOP ===
|
|
542
|
+
for step in range(max_steps):
|
|
543
|
+
# 1. Save positions before propagation (for bisection)
|
|
544
|
+
self._kernel_save_prev[blocks, self._threads_per_block](
|
|
545
|
+
gpu_rays.d_positions,
|
|
546
|
+
gpu_rays.d_prev_positions,
|
|
547
|
+
gpu_rays.d_active,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# 1b. Compute adaptive step sizes if enabled
|
|
551
|
+
if adaptive_stepping:
|
|
552
|
+
# Compute minimum distance to any surface
|
|
553
|
+
self._kernel_min_dist[blocks, self._threads_per_block](
|
|
554
|
+
gpu_rays.d_positions,
|
|
555
|
+
gpu_rays.d_active,
|
|
556
|
+
self._d_geometry_ids,
|
|
557
|
+
self._d_surface_params,
|
|
558
|
+
num_surfaces,
|
|
559
|
+
MAX_SURFACE_PARAMS,
|
|
560
|
+
d_min_distances,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Compute adaptive step sizes based on distances
|
|
564
|
+
self._kernel_adaptive_steps[blocks, self._threads_per_block](
|
|
565
|
+
d_min_distances,
|
|
566
|
+
gpu_rays.d_active,
|
|
567
|
+
step_size,
|
|
568
|
+
min_step_size,
|
|
569
|
+
surface_proximity_factor,
|
|
570
|
+
surface_proximity_threshold,
|
|
571
|
+
d_adaptive_steps,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# 2. Propagate one step using appropriate kernel
|
|
575
|
+
if use_homogeneous:
|
|
576
|
+
if adaptive_stepping:
|
|
577
|
+
# Use per-ray step sizes
|
|
578
|
+
kernel_homogeneous_step_adaptive[blocks, self._threads_per_block](
|
|
579
|
+
gpu_rays.d_positions,
|
|
580
|
+
gpu_rays.d_directions,
|
|
581
|
+
gpu_rays.d_active,
|
|
582
|
+
gpu_rays.d_geo_path,
|
|
583
|
+
gpu_rays.d_opt_path,
|
|
584
|
+
gpu_rays.d_acc_time,
|
|
585
|
+
d_adaptive_steps,
|
|
586
|
+
float(n),
|
|
587
|
+
SPEED_OF_LIGHT,
|
|
588
|
+
)
|
|
589
|
+
else:
|
|
590
|
+
# Use fixed step size
|
|
591
|
+
kernel_homogeneous_step[blocks, self._threads_per_block](
|
|
592
|
+
gpu_rays.d_positions,
|
|
593
|
+
gpu_rays.d_directions,
|
|
594
|
+
gpu_rays.d_active,
|
|
595
|
+
gpu_rays.d_geo_path,
|
|
596
|
+
gpu_rays.d_opt_path,
|
|
597
|
+
gpu_rays.d_acc_time,
|
|
598
|
+
step_size,
|
|
599
|
+
float(n),
|
|
600
|
+
SPEED_OF_LIGHT,
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
# Inhomogeneous: use material kernel with gradient-based bending
|
|
604
|
+
# Note: Adaptive stepping for inhomogeneous materials would require
|
|
605
|
+
# per-ray step size support in material kernels (future enhancement)
|
|
606
|
+
#
|
|
607
|
+
# Build kernel arguments: scalar params + device arrays (LUTs)
|
|
608
|
+
# Convention matches GPUGradientPropagator:
|
|
609
|
+
# - lut_n, lut_dn_dh for SimpleAtmosphere/SpectralAtmosphere
|
|
610
|
+
# - n_grid, grad_grid for GridAtmosphere
|
|
611
|
+
kernel_args = list(material_params)
|
|
612
|
+
for name in ["lut_n", "lut_dn_dh", "n_grid", "grad_grid"]:
|
|
613
|
+
if name in self._d_material_arrays:
|
|
614
|
+
kernel_args.append(self._d_material_arrays[name])
|
|
615
|
+
|
|
616
|
+
prop_kernel[blocks, self._threads_per_block](
|
|
617
|
+
gpu_rays.d_positions,
|
|
618
|
+
gpu_rays.d_directions,
|
|
619
|
+
gpu_rays.d_active,
|
|
620
|
+
gpu_rays.d_geo_path,
|
|
621
|
+
gpu_rays.d_opt_path,
|
|
622
|
+
gpu_rays.d_acc_time,
|
|
623
|
+
step_size,
|
|
624
|
+
1, # num_steps
|
|
625
|
+
*kernel_args,
|
|
626
|
+
wavelength,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# 2b. Apply absorption if enabled
|
|
630
|
+
if self._apply_absorption and self._absorption_kernel is not None:
|
|
631
|
+
self._apply_absorption_step(gpu_rays, step_size, 1, blocks)
|
|
632
|
+
|
|
633
|
+
# 3. Detect surface crossings
|
|
634
|
+
self._kernel_detect[blocks, self._threads_per_block](
|
|
635
|
+
gpu_rays.d_positions,
|
|
636
|
+
gpu_rays.d_prev_signed_dist,
|
|
637
|
+
gpu_rays.d_active,
|
|
638
|
+
self._d_geometry_ids,
|
|
639
|
+
self._d_surface_params,
|
|
640
|
+
num_surfaces,
|
|
641
|
+
MAX_SURFACE_PARAMS,
|
|
642
|
+
gpu_rays.d_crossing_mask,
|
|
643
|
+
gpu_rays.d_hit_surface_idx,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# 4. Reduce to check status (ONLY transfer: 8 bytes)
|
|
647
|
+
gpu_rays.d_reduction_result.copy_to_device(np.zeros(2, dtype=np.int32))
|
|
648
|
+
self._kernel_reduce[blocks, self._threads_per_block](
|
|
649
|
+
gpu_rays.d_active,
|
|
650
|
+
gpu_rays.d_crossing_mask,
|
|
651
|
+
gpu_rays.d_reduction_result,
|
|
652
|
+
)
|
|
653
|
+
num_active, num_crossing = gpu_rays.get_reduction_result()
|
|
654
|
+
|
|
655
|
+
# 5. Refine crossings if any
|
|
656
|
+
if num_crossing > 0:
|
|
657
|
+
# Get step sizes for time correction
|
|
658
|
+
if adaptive_stepping:
|
|
659
|
+
d_step_sizes_for_correction = d_adaptive_steps
|
|
660
|
+
else:
|
|
661
|
+
# Create array with fixed step size for correction
|
|
662
|
+
d_step_sizes_for_correction = cuda.to_device(
|
|
663
|
+
np.full(num_rays, step_size, dtype=np.float32)
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
self._kernel_bisect[blocks, self._threads_per_block](
|
|
667
|
+
gpu_rays.d_prev_positions,
|
|
668
|
+
gpu_rays.d_positions,
|
|
669
|
+
gpu_rays.d_crossing_mask,
|
|
670
|
+
gpu_rays.d_hit_surface_idx,
|
|
671
|
+
self._d_geometry_ids,
|
|
672
|
+
self._d_surface_params,
|
|
673
|
+
MAX_SURFACE_PARAMS,
|
|
674
|
+
gpu_rays.d_hit_positions,
|
|
675
|
+
gpu_rays.d_active,
|
|
676
|
+
20, # num_iterations (20 gives ~nm precision from 10m step)
|
|
677
|
+
gpu_rays.d_geo_path, # for time correction
|
|
678
|
+
gpu_rays.d_opt_path,
|
|
679
|
+
gpu_rays.d_acc_time,
|
|
680
|
+
d_step_sizes_for_correction,
|
|
681
|
+
float(n),
|
|
682
|
+
SPEED_OF_LIGHT,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# 6. Early exit if all rays done
|
|
686
|
+
if num_active == 0:
|
|
687
|
+
break
|
|
688
|
+
|
|
689
|
+
# Reset crossing mask for next iteration
|
|
690
|
+
gpu_rays.d_crossing_mask.copy_to_device(np.zeros(num_rays, dtype=np.bool_))
|
|
691
|
+
|
|
692
|
+
# === ONE-TIME DOWNLOAD ===
|
|
693
|
+
gpu_rays.to_ray_batch(rays)
|
|
694
|
+
|
|
695
|
+
# Build hit data
|
|
696
|
+
hit_surface_idx, hit_positions, _ = gpu_rays.get_hit_data()
|
|
697
|
+
hit_directions = gpu_rays.d_directions.copy_to_host()
|
|
698
|
+
|
|
699
|
+
return HitData(
|
|
700
|
+
hit_surface_idx=hit_surface_idx,
|
|
701
|
+
hit_positions=hit_positions.astype(np.float32),
|
|
702
|
+
hit_directions=hit_directions.astype(np.float32),
|
|
703
|
+
num_rays=num_rays,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
__all__ = ["GPUSurfacePropagator"]
|