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