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,790 @@
|
|
|
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
|
+
Shared GPU Device Functions
|
|
36
|
+
|
|
37
|
+
This module consolidates device functions used across GPU kernels:
|
|
38
|
+
- Integration functions (Euler, RK4 steps)
|
|
39
|
+
- Dispersion models (Sellmeier, Cauchy equations)
|
|
40
|
+
- Common utilities (normalization, interpolation)
|
|
41
|
+
|
|
42
|
+
All functions are designed to be used with @cuda.jit(device=True).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import math
|
|
46
|
+
from collections.abc import Callable
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
import numpy.typing as npt
|
|
50
|
+
|
|
51
|
+
# GPU support is optional
|
|
52
|
+
try:
|
|
53
|
+
from numba import cuda
|
|
54
|
+
|
|
55
|
+
HAS_CUDA = True
|
|
56
|
+
except ImportError:
|
|
57
|
+
|
|
58
|
+
class _FakeCuda:
|
|
59
|
+
"""Fake cuda module for when numba is not installed."""
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def jit(*args, **kwargs):
|
|
63
|
+
"""Return a no-op decorator."""
|
|
64
|
+
|
|
65
|
+
def decorator(func):
|
|
66
|
+
return func
|
|
67
|
+
|
|
68
|
+
if args and callable(args[0]):
|
|
69
|
+
return args[0]
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def is_available():
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def grid(n):
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def synchronize():
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
cuda = _FakeCuda() # type: ignore[assignment]
|
|
85
|
+
HAS_CUDA = False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# CUDA Device Functions for Integration
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@cuda.jit(device=True)
|
|
94
|
+
def device_adaptive_step_size(
|
|
95
|
+
gradient_magnitude: float,
|
|
96
|
+
refractive_index: float,
|
|
97
|
+
wavelength: float,
|
|
98
|
+
min_step: float,
|
|
99
|
+
max_step: float,
|
|
100
|
+
) -> float:
|
|
101
|
+
"""
|
|
102
|
+
Compute adaptive step size based on local gradient on GPU device.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
gradient_magnitude : float
|
|
107
|
+
|∇n| at current position
|
|
108
|
+
refractive_index : float
|
|
109
|
+
n at current position
|
|
110
|
+
wavelength : float
|
|
111
|
+
Wavelength in meters
|
|
112
|
+
min_step : float
|
|
113
|
+
Minimum allowed step size
|
|
114
|
+
max_step : float
|
|
115
|
+
Maximum allowed step size
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
float
|
|
120
|
+
Recommended step size
|
|
121
|
+
"""
|
|
122
|
+
# Radius of curvature
|
|
123
|
+
if gradient_magnitude > 1e-12:
|
|
124
|
+
radius_curvature = refractive_index / gradient_magnitude
|
|
125
|
+
step_from_curvature = radius_curvature / 10.0
|
|
126
|
+
else:
|
|
127
|
+
step_from_curvature = max_step
|
|
128
|
+
|
|
129
|
+
# Wavelength in medium
|
|
130
|
+
wavelength_medium = wavelength / refractive_index
|
|
131
|
+
|
|
132
|
+
# Take minimum
|
|
133
|
+
step = min(step_from_curvature, wavelength_medium)
|
|
134
|
+
|
|
135
|
+
# Clamp to limits
|
|
136
|
+
if step < min_step:
|
|
137
|
+
step = min_step
|
|
138
|
+
if step > max_step:
|
|
139
|
+
step = max_step
|
|
140
|
+
|
|
141
|
+
return step
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@cuda.jit(device=True)
|
|
145
|
+
def device_euler_step(
|
|
146
|
+
x: float,
|
|
147
|
+
y: float,
|
|
148
|
+
z: float,
|
|
149
|
+
dx: float,
|
|
150
|
+
dy: float,
|
|
151
|
+
dz: float,
|
|
152
|
+
n: float,
|
|
153
|
+
grad_x: float,
|
|
154
|
+
grad_y: float,
|
|
155
|
+
grad_z: float,
|
|
156
|
+
step_size: float,
|
|
157
|
+
) -> tuple[float, float, float, float, float, float]:
|
|
158
|
+
"""
|
|
159
|
+
Single Euler integration step for ray equation on GPU.
|
|
160
|
+
|
|
161
|
+
The ray equation in gradient media is:
|
|
162
|
+
dr/ds = d̂
|
|
163
|
+
dd̂/ds = (∇n - (d̂·∇n)d̂) / n
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
x, y, z : float
|
|
168
|
+
Current position
|
|
169
|
+
dx, dy, dz : float
|
|
170
|
+
Current direction (unit vector)
|
|
171
|
+
n : float
|
|
172
|
+
Refractive index at current position
|
|
173
|
+
grad_x, grad_y, grad_z : float
|
|
174
|
+
Gradient of n at current position
|
|
175
|
+
step_size : float
|
|
176
|
+
Step size ds
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
tuple
|
|
181
|
+
(new_x, new_y, new_z, new_dx, new_dy, new_dz)
|
|
182
|
+
"""
|
|
183
|
+
# Curvature: κ = (∇n - (d̂·∇n)d̂) / n
|
|
184
|
+
dot = dx * grad_x + dy * grad_y + dz * grad_z
|
|
185
|
+
kappa_x = (grad_x - dot * dx) / n
|
|
186
|
+
kappa_y = (grad_y - dot * dy) / n
|
|
187
|
+
kappa_z = (grad_z - dot * dz) / n
|
|
188
|
+
|
|
189
|
+
# Update position: r_new = r + d̂ * ds
|
|
190
|
+
new_x = x + dx * step_size
|
|
191
|
+
new_y = y + dy * step_size
|
|
192
|
+
new_z = z + dz * step_size
|
|
193
|
+
|
|
194
|
+
# Update direction: d̂_new = d̂ + κ * ds
|
|
195
|
+
new_dx = dx + kappa_x * step_size
|
|
196
|
+
new_dy = dy + kappa_y * step_size
|
|
197
|
+
new_dz = dz + kappa_z * step_size
|
|
198
|
+
|
|
199
|
+
# Renormalize
|
|
200
|
+
norm = math.sqrt(new_dx * new_dx + new_dy * new_dy + new_dz * new_dz)
|
|
201
|
+
if norm > 1e-12:
|
|
202
|
+
new_dx /= norm
|
|
203
|
+
new_dy /= norm
|
|
204
|
+
new_dz /= norm
|
|
205
|
+
|
|
206
|
+
return new_x, new_y, new_z, new_dx, new_dy, new_dz
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@cuda.jit(device=True)
|
|
210
|
+
def device_rk4_step(
|
|
211
|
+
x: float,
|
|
212
|
+
y: float,
|
|
213
|
+
z: float,
|
|
214
|
+
dx: float,
|
|
215
|
+
dy: float,
|
|
216
|
+
dz: float,
|
|
217
|
+
step_size: float,
|
|
218
|
+
n_func,
|
|
219
|
+
grad_func,
|
|
220
|
+
*material_params,
|
|
221
|
+
) -> tuple[float, float, float, float, float, float, float]:
|
|
222
|
+
"""
|
|
223
|
+
RK4 integration step for ray equation on GPU.
|
|
224
|
+
|
|
225
|
+
This is a generic RK4 step that takes material evaluation functions
|
|
226
|
+
as parameters. For material-specific implementations with better
|
|
227
|
+
performance, materials should define their own device functions.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
x, y, z : float
|
|
232
|
+
Current position
|
|
233
|
+
dx, dy, dz : float
|
|
234
|
+
Current direction (unit vector)
|
|
235
|
+
step_size : float
|
|
236
|
+
Integration step size
|
|
237
|
+
n_func : device function
|
|
238
|
+
Function to evaluate n(x, y, z, *material_params)
|
|
239
|
+
grad_func : device function
|
|
240
|
+
Function to evaluate ∇n(x, y, z, *material_params) -> (gx, gy, gz)
|
|
241
|
+
material_params : tuple
|
|
242
|
+
Material-specific parameters passed to n_func and grad_func
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
tuple of float
|
|
247
|
+
(new_x, new_y, new_z, new_dx, new_dy, new_dz, n_avg)
|
|
248
|
+
|
|
249
|
+
Notes
|
|
250
|
+
-----
|
|
251
|
+
For best performance, materials should implement their own specialized
|
|
252
|
+
RK4 device functions that inline the n and gradient calculations.
|
|
253
|
+
This generic version has function call overhead.
|
|
254
|
+
"""
|
|
255
|
+
h = step_size
|
|
256
|
+
h2 = h / 2.0
|
|
257
|
+
|
|
258
|
+
def _normalize(vx, vy, vz):
|
|
259
|
+
norm = math.sqrt(vx * vx + vy * vy + vz * vz)
|
|
260
|
+
if norm > 1e-12:
|
|
261
|
+
return vx / norm, vy / norm, vz / norm
|
|
262
|
+
return vx, vy, vz
|
|
263
|
+
|
|
264
|
+
def _curvature(px, py, pz, dirx, diry, dirz):
|
|
265
|
+
n = n_func(px, py, pz, *material_params)
|
|
266
|
+
gx, gy, gz = grad_func(px, py, pz, *material_params)
|
|
267
|
+
dot = dirx * gx + diry * gy + dirz * gz
|
|
268
|
+
kx = (gx - dot * dirx) / n
|
|
269
|
+
ky = (gy - dot * diry) / n
|
|
270
|
+
kz = (gz - dot * dirz) / n
|
|
271
|
+
return n, kx, ky, kz
|
|
272
|
+
|
|
273
|
+
# k1 at current point
|
|
274
|
+
n0, k1_dx, k1_dy, k1_dz = _curvature(x, y, z, dx, dy, dz)
|
|
275
|
+
k1_rx, k1_ry, k1_rz = dx, dy, dz
|
|
276
|
+
|
|
277
|
+
# Intermediate point 1
|
|
278
|
+
x1 = x + h2 * k1_rx
|
|
279
|
+
y1 = y + h2 * k1_ry
|
|
280
|
+
z1 = z + h2 * k1_rz
|
|
281
|
+
dx1, dy1, dz1 = _normalize(dx + h2 * k1_dx, dy + h2 * k1_dy, dz + h2 * k1_dz)
|
|
282
|
+
|
|
283
|
+
# k2
|
|
284
|
+
n1, k2_dx, k2_dy, k2_dz = _curvature(x1, y1, z1, dx1, dy1, dz1)
|
|
285
|
+
k2_rx, k2_ry, k2_rz = dx1, dy1, dz1
|
|
286
|
+
|
|
287
|
+
# Intermediate point 2
|
|
288
|
+
x2 = x + h2 * k2_rx
|
|
289
|
+
y2 = y + h2 * k2_ry
|
|
290
|
+
z2 = z + h2 * k2_rz
|
|
291
|
+
dx2, dy2, dz2 = _normalize(dx + h2 * k2_dx, dy + h2 * k2_dy, dz + h2 * k2_dz)
|
|
292
|
+
|
|
293
|
+
# k3
|
|
294
|
+
n2, k3_dx, k3_dy, k3_dz = _curvature(x2, y2, z2, dx2, dy2, dz2)
|
|
295
|
+
k3_rx, k3_ry, k3_rz = dx2, dy2, dz2
|
|
296
|
+
|
|
297
|
+
# End point
|
|
298
|
+
x3 = x + h * k3_rx
|
|
299
|
+
y3 = y + h * k3_ry
|
|
300
|
+
z3 = z + h * k3_rz
|
|
301
|
+
dx3, dy3, dz3 = _normalize(dx + h * k3_dx, dy + h * k3_dy, dz + h * k3_dz)
|
|
302
|
+
|
|
303
|
+
# k4
|
|
304
|
+
n3, k4_dx, k4_dy, k4_dz = _curvature(x3, y3, z3, dx3, dy3, dz3)
|
|
305
|
+
k4_rx, k4_ry, k4_rz = dx3, dy3, dz3
|
|
306
|
+
|
|
307
|
+
# Final RK4 combination
|
|
308
|
+
new_x = x + (h / 6.0) * (k1_rx + 2 * k2_rx + 2 * k3_rx + k4_rx)
|
|
309
|
+
new_y = y + (h / 6.0) * (k1_ry + 2 * k2_ry + 2 * k3_ry + k4_ry)
|
|
310
|
+
new_z = z + (h / 6.0) * (k1_rz + 2 * k2_rz + 2 * k3_rz + k4_rz)
|
|
311
|
+
|
|
312
|
+
new_dx = dx + (h / 6.0) * (k1_dx + 2 * k2_dx + 2 * k3_dx + k4_dx)
|
|
313
|
+
new_dy = dy + (h / 6.0) * (k1_dy + 2 * k2_dy + 2 * k3_dy + k4_dy)
|
|
314
|
+
new_dz = dz + (h / 6.0) * (k1_dz + 2 * k2_dz + 2 * k3_dz + k4_dz)
|
|
315
|
+
|
|
316
|
+
new_dx, new_dy, new_dz = _normalize(new_dx, new_dy, new_dz)
|
|
317
|
+
|
|
318
|
+
# Simpson's rule for average n
|
|
319
|
+
n_avg = (n0 + 4 * n1 + n2) / 6.0
|
|
320
|
+
|
|
321
|
+
return new_x, new_y, new_z, new_dx, new_dy, new_dz, n_avg
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# =============================================================================
|
|
325
|
+
# CUDA Device Functions for Dispersion Models
|
|
326
|
+
# =============================================================================
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@cuda.jit(device=True)
|
|
330
|
+
def device_sellmeier_equation(
|
|
331
|
+
wl_um: float,
|
|
332
|
+
B1: float,
|
|
333
|
+
B2: float,
|
|
334
|
+
B3: float,
|
|
335
|
+
C1: float,
|
|
336
|
+
C2: float,
|
|
337
|
+
C3: float,
|
|
338
|
+
) -> float:
|
|
339
|
+
"""
|
|
340
|
+
GPU-compatible Sellmeier equation.
|
|
341
|
+
|
|
342
|
+
Computes refractive index from wavelength and Sellmeier coefficients.
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
wl_um : float
|
|
347
|
+
Wavelength in micrometers.
|
|
348
|
+
B1, B2, B3 : float
|
|
349
|
+
Sellmeier B coefficients.
|
|
350
|
+
C1, C2, C3 : float
|
|
351
|
+
Sellmeier C coefficients in μm².
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
n : float
|
|
356
|
+
Refractive index.
|
|
357
|
+
"""
|
|
358
|
+
wl2 = wl_um * wl_um
|
|
359
|
+
n2_minus_1 = B1 * wl2 / (wl2 - C1) + B2 * wl2 / (wl2 - C2) + B3 * wl2 / (wl2 - C3)
|
|
360
|
+
return math.sqrt(1.0 + n2_minus_1)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@cuda.jit(device=True)
|
|
364
|
+
def device_cauchy_equation(
|
|
365
|
+
wl_um: float,
|
|
366
|
+
A: float,
|
|
367
|
+
B: float,
|
|
368
|
+
C: float,
|
|
369
|
+
) -> float:
|
|
370
|
+
"""
|
|
371
|
+
GPU-compatible Cauchy equation.
|
|
372
|
+
|
|
373
|
+
Computes refractive index from wavelength and Cauchy coefficients.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
wl_um : float
|
|
378
|
+
Wavelength in micrometers.
|
|
379
|
+
A : float
|
|
380
|
+
Constant term.
|
|
381
|
+
B : float
|
|
382
|
+
First-order dispersion coefficient in μm².
|
|
383
|
+
C : float
|
|
384
|
+
Second-order dispersion coefficient in μm⁴.
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
n : float
|
|
389
|
+
Refractive index.
|
|
390
|
+
"""
|
|
391
|
+
wl2 = wl_um * wl_um
|
|
392
|
+
return A + B / wl2 + C / (wl2 * wl2)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# =============================================================================
|
|
396
|
+
# Pure Python Step Functions (for CPU propagation)
|
|
397
|
+
# =============================================================================
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def euler_step(
|
|
401
|
+
x: float,
|
|
402
|
+
y: float,
|
|
403
|
+
z: float,
|
|
404
|
+
dx: float,
|
|
405
|
+
dy: float,
|
|
406
|
+
dz: float,
|
|
407
|
+
n: float,
|
|
408
|
+
grad_x: float,
|
|
409
|
+
grad_y: float,
|
|
410
|
+
grad_z: float,
|
|
411
|
+
step_size: float,
|
|
412
|
+
) -> tuple[float, float, float, float, float, float]:
|
|
413
|
+
"""
|
|
414
|
+
Single Euler integration step for ray equation.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
x, y, z : float
|
|
419
|
+
Current position
|
|
420
|
+
dx, dy, dz : float
|
|
421
|
+
Current direction (unit vector)
|
|
422
|
+
n : float
|
|
423
|
+
Refractive index at current position
|
|
424
|
+
grad_x, grad_y, grad_z : float
|
|
425
|
+
Gradient of n at current position
|
|
426
|
+
step_size : float
|
|
427
|
+
Step size ds
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
tuple
|
|
432
|
+
(new_x, new_y, new_z, new_dx, new_dy, new_dz)
|
|
433
|
+
"""
|
|
434
|
+
# Curvature: κ = (∇n - (d̂·∇n)d̂) / n
|
|
435
|
+
dot = dx * grad_x + dy * grad_y + dz * grad_z
|
|
436
|
+
kappa_x = (grad_x - dot * dx) / n
|
|
437
|
+
kappa_y = (grad_y - dot * dy) / n
|
|
438
|
+
kappa_z = (grad_z - dot * dz) / n
|
|
439
|
+
|
|
440
|
+
# Update position: r_new = r + d̂ * ds
|
|
441
|
+
new_x = x + dx * step_size
|
|
442
|
+
new_y = y + dy * step_size
|
|
443
|
+
new_z = z + dz * step_size
|
|
444
|
+
|
|
445
|
+
# Update direction: d̂_new = d̂ + κ * ds
|
|
446
|
+
new_dx = dx + kappa_x * step_size
|
|
447
|
+
new_dy = dy + kappa_y * step_size
|
|
448
|
+
new_dz = dz + kappa_z * step_size
|
|
449
|
+
|
|
450
|
+
# Renormalize
|
|
451
|
+
norm = math.sqrt(new_dx**2 + new_dy**2 + new_dz**2)
|
|
452
|
+
if norm > 1e-12:
|
|
453
|
+
new_dx /= norm
|
|
454
|
+
new_dy /= norm
|
|
455
|
+
new_dz /= norm
|
|
456
|
+
|
|
457
|
+
return new_x, new_y, new_z, new_dx, new_dy, new_dz
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def rk4_step(
|
|
461
|
+
x: float,
|
|
462
|
+
y: float,
|
|
463
|
+
z: float,
|
|
464
|
+
dx: float,
|
|
465
|
+
dy: float,
|
|
466
|
+
dz: float,
|
|
467
|
+
n_func: Callable[[float, float, float, float], float],
|
|
468
|
+
grad_func: Callable[[float, float, float, float], tuple[float, float, float]],
|
|
469
|
+
wavelength: float,
|
|
470
|
+
step_size: float,
|
|
471
|
+
) -> tuple[float, float, float, float, float, float, float]:
|
|
472
|
+
"""
|
|
473
|
+
RK4 integration step for ray equation in gradient medium.
|
|
474
|
+
|
|
475
|
+
Solves the coupled ODEs:
|
|
476
|
+
dr/ds = d̂
|
|
477
|
+
dd̂/ds = (∇n - (d̂·∇n)d̂) / n
|
|
478
|
+
|
|
479
|
+
Parameters
|
|
480
|
+
----------
|
|
481
|
+
x, y, z : float
|
|
482
|
+
Current position
|
|
483
|
+
dx, dy, dz : float
|
|
484
|
+
Current direction (unit vector)
|
|
485
|
+
n_func : callable
|
|
486
|
+
Function to evaluate n(x, y, z, wavelength)
|
|
487
|
+
grad_func : callable
|
|
488
|
+
Function to evaluate ∇n(x, y, z, wavelength) -> (gx, gy, gz)
|
|
489
|
+
wavelength : float
|
|
490
|
+
Wavelength
|
|
491
|
+
step_size : float
|
|
492
|
+
Integration step size
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
tuple of float
|
|
497
|
+
(new_x, new_y, new_z, new_dx, new_dy, new_dz, optical_path_increment)
|
|
498
|
+
"""
|
|
499
|
+
h = step_size
|
|
500
|
+
h2 = h / 2.0
|
|
501
|
+
|
|
502
|
+
def _normalize(vx, vy, vz):
|
|
503
|
+
norm = math.sqrt(vx**2 + vy**2 + vz**2)
|
|
504
|
+
if norm > 1e-12:
|
|
505
|
+
return vx / norm, vy / norm, vz / norm
|
|
506
|
+
return vx, vy, vz
|
|
507
|
+
|
|
508
|
+
def _curvature(px, py, pz, dirx, diry, dirz):
|
|
509
|
+
n = n_func(px, py, pz, wavelength)
|
|
510
|
+
gx, gy, gz = grad_func(px, py, pz, wavelength)
|
|
511
|
+
dot = dirx * gx + diry * gy + dirz * gz
|
|
512
|
+
kx = (gx - dot * dirx) / n
|
|
513
|
+
ky = (gy - dot * diry) / n
|
|
514
|
+
kz = (gz - dot * dirz) / n
|
|
515
|
+
return n, kx, ky, kz
|
|
516
|
+
|
|
517
|
+
# k1 evaluation at current point
|
|
518
|
+
n0, k1_dx, k1_dy, k1_dz = _curvature(x, y, z, dx, dy, dz)
|
|
519
|
+
k1_rx, k1_ry, k1_rz = dx, dy, dz
|
|
520
|
+
|
|
521
|
+
# Intermediate point 1
|
|
522
|
+
x1 = x + h2 * k1_rx
|
|
523
|
+
y1 = y + h2 * k1_ry
|
|
524
|
+
z1 = z + h2 * k1_rz
|
|
525
|
+
dx1, dy1, dz1 = _normalize(dx + h2 * k1_dx, dy + h2 * k1_dy, dz + h2 * k1_dz)
|
|
526
|
+
|
|
527
|
+
# k2 evaluation
|
|
528
|
+
n1, k2_dx, k2_dy, k2_dz = _curvature(x1, y1, z1, dx1, dy1, dz1)
|
|
529
|
+
k2_rx, k2_ry, k2_rz = dx1, dy1, dz1
|
|
530
|
+
|
|
531
|
+
# Intermediate point 2
|
|
532
|
+
x2 = x + h2 * k2_rx
|
|
533
|
+
y2 = y + h2 * k2_ry
|
|
534
|
+
z2 = z + h2 * k2_rz
|
|
535
|
+
dx2, dy2, dz2 = _normalize(dx + h2 * k2_dx, dy + h2 * k2_dy, dz + h2 * k2_dz)
|
|
536
|
+
|
|
537
|
+
# k3 evaluation
|
|
538
|
+
n2, k3_dx, k3_dy, k3_dz = _curvature(x2, y2, z2, dx2, dy2, dz2)
|
|
539
|
+
k3_rx, k3_ry, k3_rz = dx2, dy2, dz2
|
|
540
|
+
|
|
541
|
+
# End point
|
|
542
|
+
x3 = x + h * k3_rx
|
|
543
|
+
y3 = y + h * k3_ry
|
|
544
|
+
z3 = z + h * k3_rz
|
|
545
|
+
dx3, dy3, dz3 = _normalize(dx + h * k3_dx, dy + h * k3_dy, dz + h * k3_dz)
|
|
546
|
+
|
|
547
|
+
# k4 evaluation
|
|
548
|
+
n3, k4_dx, k4_dy, k4_dz = _curvature(x3, y3, z3, dx3, dy3, dz3)
|
|
549
|
+
k4_rx, k4_ry, k4_rz = dx3, dy3, dz3
|
|
550
|
+
|
|
551
|
+
# Final RK4 combination
|
|
552
|
+
new_x = x + (h / 6.0) * (k1_rx + 2 * k2_rx + 2 * k3_rx + k4_rx)
|
|
553
|
+
new_y = y + (h / 6.0) * (k1_ry + 2 * k2_ry + 2 * k3_ry + k4_ry)
|
|
554
|
+
new_z = z + (h / 6.0) * (k1_rz + 2 * k2_rz + 2 * k3_rz + k4_rz)
|
|
555
|
+
|
|
556
|
+
new_dx = dx + (h / 6.0) * (k1_dx + 2 * k2_dx + 2 * k3_dx + k4_dx)
|
|
557
|
+
new_dy = dy + (h / 6.0) * (k1_dy + 2 * k2_dy + 2 * k3_dy + k4_dy)
|
|
558
|
+
new_dz = dz + (h / 6.0) * (k1_dz + 2 * k2_dz + 2 * k3_dz + k4_dz)
|
|
559
|
+
|
|
560
|
+
new_dx, new_dy, new_dz = _normalize(new_dx, new_dy, new_dz)
|
|
561
|
+
|
|
562
|
+
# Optical path: Simpson's rule approximation
|
|
563
|
+
n_avg = (n0 + 4 * n1 + n2) / 6.0
|
|
564
|
+
optical_increment = n_avg * h
|
|
565
|
+
|
|
566
|
+
return new_x, new_y, new_z, new_dx, new_dy, new_dz, optical_increment
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def compute_adaptive_step_size(
|
|
570
|
+
gradient_magnitude: float,
|
|
571
|
+
refractive_index: float,
|
|
572
|
+
wavelength: float,
|
|
573
|
+
min_step: float,
|
|
574
|
+
max_step: float,
|
|
575
|
+
) -> float:
|
|
576
|
+
"""
|
|
577
|
+
Compute adaptive step size based on local gradient.
|
|
578
|
+
|
|
579
|
+
Parameters
|
|
580
|
+
----------
|
|
581
|
+
gradient_magnitude : float
|
|
582
|
+
|∇n| at current position
|
|
583
|
+
refractive_index : float
|
|
584
|
+
n at current position
|
|
585
|
+
wavelength : float
|
|
586
|
+
Wavelength in meters
|
|
587
|
+
min_step : float
|
|
588
|
+
Minimum allowed step size
|
|
589
|
+
max_step : float
|
|
590
|
+
Maximum allowed step size
|
|
591
|
+
|
|
592
|
+
Returns
|
|
593
|
+
-------
|
|
594
|
+
float
|
|
595
|
+
Recommended step size
|
|
596
|
+
|
|
597
|
+
Notes
|
|
598
|
+
-----
|
|
599
|
+
Step size is chosen to resolve curvature: Δs ≤ R_c / 10
|
|
600
|
+
where R_c = n / |∇n| is the radius of curvature.
|
|
601
|
+
"""
|
|
602
|
+
# Radius of curvature
|
|
603
|
+
if gradient_magnitude > 1e-12:
|
|
604
|
+
radius_curvature = refractive_index / gradient_magnitude
|
|
605
|
+
step_from_curvature = radius_curvature / 10.0
|
|
606
|
+
else:
|
|
607
|
+
step_from_curvature = max_step
|
|
608
|
+
|
|
609
|
+
# Wavelength in medium
|
|
610
|
+
wavelength_medium = wavelength / refractive_index
|
|
611
|
+
|
|
612
|
+
# Take minimum
|
|
613
|
+
step = min(step_from_curvature, wavelength_medium)
|
|
614
|
+
|
|
615
|
+
# Clamp to limits
|
|
616
|
+
return max(min_step, min(step, max_step))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# =============================================================================
|
|
620
|
+
# Vectorized NumPy Operations (for batch CPU propagation)
|
|
621
|
+
# =============================================================================
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def normalize_directions(
|
|
625
|
+
directions: npt.NDArray[np.float32],
|
|
626
|
+
) -> npt.NDArray[np.float32]:
|
|
627
|
+
"""Normalize direction vectors to unit length."""
|
|
628
|
+
norms = np.linalg.norm(directions, axis=1, keepdims=True)
|
|
629
|
+
norms = np.maximum(norms, 1e-12) # Avoid division by zero
|
|
630
|
+
return directions / norms
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def euler_step_batch(
|
|
634
|
+
positions: npt.NDArray[np.float32],
|
|
635
|
+
directions: npt.NDArray[np.float32],
|
|
636
|
+
active_mask: npt.NDArray[np.bool_],
|
|
637
|
+
n: npt.NDArray[np.float32],
|
|
638
|
+
grad_n: npt.NDArray[np.float32], # (N, 3)
|
|
639
|
+
step_size: float,
|
|
640
|
+
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
|
|
641
|
+
"""
|
|
642
|
+
Vectorized Euler step for batch of rays.
|
|
643
|
+
|
|
644
|
+
Parameters
|
|
645
|
+
----------
|
|
646
|
+
positions : ndarray of shape (N, 3)
|
|
647
|
+
Ray positions
|
|
648
|
+
directions : ndarray of shape (N, 3)
|
|
649
|
+
Ray directions (unit vectors)
|
|
650
|
+
active_mask : ndarray of shape (N,)
|
|
651
|
+
Boolean mask for active rays
|
|
652
|
+
n : ndarray of shape (N,)
|
|
653
|
+
Refractive index at each position
|
|
654
|
+
grad_n : ndarray of shape (N, 3)
|
|
655
|
+
Gradient of n at each position
|
|
656
|
+
step_size : float
|
|
657
|
+
Step size
|
|
658
|
+
|
|
659
|
+
Returns
|
|
660
|
+
-------
|
|
661
|
+
new_positions : ndarray of shape (N, 3)
|
|
662
|
+
new_directions : ndarray of shape (N, 3)
|
|
663
|
+
"""
|
|
664
|
+
# Only process active rays
|
|
665
|
+
new_positions = positions.copy()
|
|
666
|
+
new_directions = directions.copy()
|
|
667
|
+
|
|
668
|
+
# d̂ · ∇n
|
|
669
|
+
dot = np.sum(directions[active_mask] * grad_n[active_mask], axis=1, keepdims=True)
|
|
670
|
+
|
|
671
|
+
# κ = (∇n - (d̂·∇n)d̂) / n
|
|
672
|
+
n_active = n[active_mask][:, np.newaxis]
|
|
673
|
+
kappa = (grad_n[active_mask] - dot * directions[active_mask]) / n_active
|
|
674
|
+
|
|
675
|
+
# Update position and direction
|
|
676
|
+
new_positions[active_mask] += directions[active_mask] * step_size
|
|
677
|
+
new_directions[active_mask] += kappa * step_size
|
|
678
|
+
|
|
679
|
+
# Renormalize
|
|
680
|
+
new_directions[active_mask] = normalize_directions(new_directions[active_mask])
|
|
681
|
+
|
|
682
|
+
return new_positions, new_directions
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def rk4_step_batch(
|
|
686
|
+
positions: npt.NDArray[np.float32],
|
|
687
|
+
directions: npt.NDArray[np.float32],
|
|
688
|
+
active_mask: npt.NDArray[np.bool_],
|
|
689
|
+
material, # MaterialFieldProtocol
|
|
690
|
+
step_size: float,
|
|
691
|
+
wavelength: float,
|
|
692
|
+
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32], npt.NDArray[np.float32]]:
|
|
693
|
+
"""
|
|
694
|
+
Vectorized RK4 step for batch of rays.
|
|
695
|
+
|
|
696
|
+
Parameters
|
|
697
|
+
----------
|
|
698
|
+
positions : ndarray of shape (N, 3)
|
|
699
|
+
Ray positions
|
|
700
|
+
directions : ndarray of shape (N, 3)
|
|
701
|
+
Ray directions (unit vectors)
|
|
702
|
+
active_mask : ndarray of shape (N,)
|
|
703
|
+
Boolean mask for active rays
|
|
704
|
+
material : MaterialFieldProtocol
|
|
705
|
+
Material providing n and ∇n evaluation
|
|
706
|
+
step_size : float
|
|
707
|
+
Step size
|
|
708
|
+
wavelength : float
|
|
709
|
+
Wavelength
|
|
710
|
+
|
|
711
|
+
Returns
|
|
712
|
+
-------
|
|
713
|
+
new_positions : ndarray of shape (N, 3)
|
|
714
|
+
new_directions : ndarray of shape (N, 3)
|
|
715
|
+
n_avg : ndarray of shape (N,)
|
|
716
|
+
Average refractive index over step (for optical path)
|
|
717
|
+
"""
|
|
718
|
+
h = step_size
|
|
719
|
+
h2 = h / 2.0
|
|
720
|
+
num_rays = len(positions)
|
|
721
|
+
|
|
722
|
+
# Initialize outputs
|
|
723
|
+
new_positions = positions.copy()
|
|
724
|
+
new_directions = directions.copy()
|
|
725
|
+
n_avg = np.ones(num_rays, dtype=np.float32)
|
|
726
|
+
|
|
727
|
+
if not np.any(active_mask):
|
|
728
|
+
return new_positions, new_directions, n_avg
|
|
729
|
+
|
|
730
|
+
# Get active rays
|
|
731
|
+
pos = positions[active_mask]
|
|
732
|
+
dirs = directions[active_mask]
|
|
733
|
+
|
|
734
|
+
def get_n_and_grad(p):
|
|
735
|
+
x, y, z = p[:, 0], p[:, 1], p[:, 2]
|
|
736
|
+
n = material.get_refractive_index(x, y, z, wavelength).astype(np.float32)
|
|
737
|
+
gx, gy, gz = material.get_refractive_index_gradient(x, y, z, wavelength)
|
|
738
|
+
grad = np.stack([gx, gy, gz], axis=1).astype(np.float32)
|
|
739
|
+
return n, grad
|
|
740
|
+
|
|
741
|
+
def compute_curvature(n, grad, d):
|
|
742
|
+
dot = np.sum(d * grad, axis=1, keepdims=True)
|
|
743
|
+
kappa = (grad - dot * d) / n[:, np.newaxis]
|
|
744
|
+
return kappa
|
|
745
|
+
|
|
746
|
+
# k1
|
|
747
|
+
n0, grad0 = get_n_and_grad(pos)
|
|
748
|
+
k1_r = dirs
|
|
749
|
+
k1_d = compute_curvature(n0, grad0, dirs)
|
|
750
|
+
|
|
751
|
+
# Intermediate 1
|
|
752
|
+
pos1 = pos + h2 * k1_r
|
|
753
|
+
dirs1 = normalize_directions(dirs + h2 * k1_d)
|
|
754
|
+
|
|
755
|
+
# k2
|
|
756
|
+
n1, grad1 = get_n_and_grad(pos1)
|
|
757
|
+
k2_r = dirs1
|
|
758
|
+
k2_d = compute_curvature(n1, grad1, dirs1)
|
|
759
|
+
|
|
760
|
+
# Intermediate 2
|
|
761
|
+
pos2 = pos + h2 * k2_r
|
|
762
|
+
dirs2 = normalize_directions(dirs + h2 * k2_d)
|
|
763
|
+
|
|
764
|
+
# k3
|
|
765
|
+
n2, grad2 = get_n_and_grad(pos2)
|
|
766
|
+
k3_r = dirs2
|
|
767
|
+
k3_d = compute_curvature(n2, grad2, dirs2)
|
|
768
|
+
|
|
769
|
+
# End
|
|
770
|
+
pos3 = pos + h * k3_r
|
|
771
|
+
dirs3 = normalize_directions(dirs + h * k3_d)
|
|
772
|
+
|
|
773
|
+
# k4
|
|
774
|
+
n3, grad3 = get_n_and_grad(pos3)
|
|
775
|
+
k4_r = dirs3
|
|
776
|
+
k4_d = compute_curvature(n3, grad3, dirs3)
|
|
777
|
+
|
|
778
|
+
# RK4 combination
|
|
779
|
+
new_pos = pos + (h / 6.0) * (k1_r + 2 * k2_r + 2 * k3_r + k4_r)
|
|
780
|
+
new_dir = dirs + (h / 6.0) * (k1_d + 2 * k2_d + 2 * k3_d + k4_d)
|
|
781
|
+
new_dir = normalize_directions(new_dir)
|
|
782
|
+
|
|
783
|
+
# Store results
|
|
784
|
+
new_positions[active_mask] = new_pos
|
|
785
|
+
new_directions[active_mask] = new_dir
|
|
786
|
+
|
|
787
|
+
# Simpson's rule for average n
|
|
788
|
+
n_avg[active_mask] = (n0 + 4 * n1 + n2) / 6.0
|
|
789
|
+
|
|
790
|
+
return new_positions, new_directions, n_avg
|