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,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
|