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,429 @@
|
|
|
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
|
+
CPU-based Gradient Propagator for inhomogeneous media.
|
|
36
|
+
|
|
37
|
+
This module provides a propagator for materials with spatially-varying
|
|
38
|
+
refractive indices, using vectorized NumPy operations for efficiency.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import numpy as np
|
|
42
|
+
import numpy.typing as npt
|
|
43
|
+
|
|
44
|
+
from ..propagator_protocol import MaterialFieldProtocol
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GradientPropagator:
|
|
48
|
+
"""
|
|
49
|
+
Propagator for inhomogeneous media using ray equation integration.
|
|
50
|
+
|
|
51
|
+
This propagator handles materials with spatially-varying refractive
|
|
52
|
+
indices by numerically integrating the ray equation (eikonal equation).
|
|
53
|
+
Supports both Euler and RK4 integration methods.
|
|
54
|
+
|
|
55
|
+
Attributes
|
|
56
|
+
----------
|
|
57
|
+
use_rk4 : bool
|
|
58
|
+
If True, use RK4 integration; otherwise use Euler
|
|
59
|
+
adaptive_stepping : bool
|
|
60
|
+
If True, adapt step size to local gradient
|
|
61
|
+
min_step_size : float
|
|
62
|
+
Minimum step size in meters
|
|
63
|
+
max_step_size : float
|
|
64
|
+
Maximum step size in meters
|
|
65
|
+
threads_per_block : int
|
|
66
|
+
CUDA threads per block (legacy, not used for CPU)
|
|
67
|
+
|
|
68
|
+
References
|
|
69
|
+
----------
|
|
70
|
+
.. [1] Born, M., & Wolf, E. (1999). Principles of Optics. Section 3.1.
|
|
71
|
+
.. [2] Sharma, A., Kumar, D. V., & Ghatak, A. K. (1982). Tracing rays
|
|
72
|
+
through graded-index media: a new method. Applied Optics, 21(6),
|
|
73
|
+
984-987.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
use_rk4: bool | None = None,
|
|
79
|
+
method: str | None = None,
|
|
80
|
+
adaptive_stepping: bool = True,
|
|
81
|
+
min_step_size: float = 1e-6,
|
|
82
|
+
max_step_size: float = 1e-3,
|
|
83
|
+
threads_per_block: int = 256,
|
|
84
|
+
):
|
|
85
|
+
"""
|
|
86
|
+
Initialize gradient propagator.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
use_rk4 : bool, optional
|
|
91
|
+
Use RK4 instead of Euler (deprecated, use method instead)
|
|
92
|
+
method : str, optional
|
|
93
|
+
Integration method: "euler" or "rk4", default "rk4"
|
|
94
|
+
adaptive_stepping : bool, optional
|
|
95
|
+
Enable adaptive step sizing, default True
|
|
96
|
+
min_step_size : float, optional
|
|
97
|
+
Minimum step size in meters, default 1 μm
|
|
98
|
+
max_step_size : float, optional
|
|
99
|
+
Maximum step size in meters, default 1 mm
|
|
100
|
+
threads_per_block : int, optional
|
|
101
|
+
CUDA threads per block (legacy), default 256
|
|
102
|
+
"""
|
|
103
|
+
# Handle both old (use_rk4) and new (method) parameter styles
|
|
104
|
+
if method is not None:
|
|
105
|
+
if method.lower() == "rk4":
|
|
106
|
+
self.use_rk4 = True
|
|
107
|
+
elif method.lower() == "euler":
|
|
108
|
+
self.use_rk4 = False
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(f"Unknown method '{method}'. Use 'euler' or 'rk4'.")
|
|
111
|
+
elif use_rk4 is not None:
|
|
112
|
+
self.use_rk4 = use_rk4
|
|
113
|
+
else:
|
|
114
|
+
self.use_rk4 = True # Default to RK4
|
|
115
|
+
|
|
116
|
+
self.adaptive_stepping = adaptive_stepping
|
|
117
|
+
self.min_step_size = min_step_size
|
|
118
|
+
self.max_step_size = max_step_size
|
|
119
|
+
self.threads_per_block = threads_per_block
|
|
120
|
+
|
|
121
|
+
def compute_gradient_threshold(self, wavelength: float) -> float:
|
|
122
|
+
"""
|
|
123
|
+
Compute gradient threshold for switching to curved-path mode.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
wavelength : float
|
|
128
|
+
Wavelength in meters
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
float
|
|
133
|
+
Gradient magnitude threshold in m^-1
|
|
134
|
+
|
|
135
|
+
Notes
|
|
136
|
+
-----
|
|
137
|
+
Threshold is chosen such that ray curvature becomes significant
|
|
138
|
+
over a distance of ~wavelength: |∇n|/n > 1/λ
|
|
139
|
+
"""
|
|
140
|
+
return 1.0 / wavelength
|
|
141
|
+
|
|
142
|
+
def propagate_step_cpu(
|
|
143
|
+
self,
|
|
144
|
+
positions: npt.NDArray[np.floating],
|
|
145
|
+
directions: npt.NDArray[np.floating],
|
|
146
|
+
active: npt.NDArray[np.bool_],
|
|
147
|
+
material: MaterialFieldProtocol,
|
|
148
|
+
step_size: float,
|
|
149
|
+
wavelength: float,
|
|
150
|
+
geometric_path_lengths: npt.NDArray[np.floating] | None = None,
|
|
151
|
+
optical_path_lengths: npt.NDArray[np.floating] | None = None,
|
|
152
|
+
accumulated_time: npt.NDArray[np.floating] | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Propagate rays through gradient medium using CPU (vectorized NumPy).
|
|
156
|
+
|
|
157
|
+
This method modifies arrays in-place using Euler or RK4 integration.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
positions : ndarray
|
|
162
|
+
Ray positions, shape (N, 3), modified in-place.
|
|
163
|
+
directions : ndarray
|
|
164
|
+
Ray directions, shape (N, 3), modified in-place.
|
|
165
|
+
active : ndarray
|
|
166
|
+
Active ray mask, shape (N,).
|
|
167
|
+
material : MaterialFieldProtocol
|
|
168
|
+
Material with get_refractive_index and get_refractive_index_gradient.
|
|
169
|
+
step_size : float
|
|
170
|
+
Step size in meters.
|
|
171
|
+
wavelength : float
|
|
172
|
+
Wavelength in meters.
|
|
173
|
+
geometric_path_lengths : ndarray, optional
|
|
174
|
+
Geometric path lengths, shape (N,), updated in-place.
|
|
175
|
+
optical_path_lengths : ndarray, optional
|
|
176
|
+
Optical path lengths, shape (N,), updated in-place.
|
|
177
|
+
accumulated_time : ndarray, optional
|
|
178
|
+
Accumulated time, shape (N,), updated in-place.
|
|
179
|
+
"""
|
|
180
|
+
c = 299792458.0 # Speed of light
|
|
181
|
+
|
|
182
|
+
active_mask = active.astype(bool)
|
|
183
|
+
if not np.any(active_mask):
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
pos = positions[active_mask]
|
|
187
|
+
dirs = directions[active_mask]
|
|
188
|
+
|
|
189
|
+
if self.use_rk4:
|
|
190
|
+
new_pos, new_dirs, n_avg = self._rk4_step_vectorized(
|
|
191
|
+
pos, dirs, material, step_size, wavelength
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
new_pos, new_dirs, n_avg = self._euler_step_vectorized(
|
|
195
|
+
pos, dirs, material, step_size, wavelength
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
positions[active_mask] = new_pos.astype(np.float32)
|
|
199
|
+
directions[active_mask] = new_dirs.astype(np.float32)
|
|
200
|
+
|
|
201
|
+
if geometric_path_lengths is not None:
|
|
202
|
+
geometric_path_lengths[active_mask] += step_size
|
|
203
|
+
|
|
204
|
+
if optical_path_lengths is not None:
|
|
205
|
+
optical_path_lengths[active_mask] += (n_avg * step_size).astype(np.float32)
|
|
206
|
+
|
|
207
|
+
if accumulated_time is not None:
|
|
208
|
+
dt = (n_avg * step_size / c).astype(np.float32)
|
|
209
|
+
accumulated_time[active_mask] += dt
|
|
210
|
+
|
|
211
|
+
def _euler_step_vectorized(
|
|
212
|
+
self,
|
|
213
|
+
positions: npt.NDArray[np.floating],
|
|
214
|
+
directions: npt.NDArray[np.floating],
|
|
215
|
+
material: MaterialFieldProtocol,
|
|
216
|
+
step_size: float,
|
|
217
|
+
wavelength: float,
|
|
218
|
+
) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray]:
|
|
219
|
+
"""
|
|
220
|
+
Vectorized Euler step for gradient medium.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
new_positions : ndarray
|
|
225
|
+
Updated positions.
|
|
226
|
+
new_directions : ndarray
|
|
227
|
+
Updated directions.
|
|
228
|
+
n_values : ndarray
|
|
229
|
+
Refractive index values for optical path calculation.
|
|
230
|
+
"""
|
|
231
|
+
x, y, z = positions[:, 0], positions[:, 1], positions[:, 2]
|
|
232
|
+
dx, dy, dz = directions[:, 0], directions[:, 1], directions[:, 2]
|
|
233
|
+
|
|
234
|
+
n = material.get_refractive_index(x, y, z, wavelength)
|
|
235
|
+
grad = material.get_refractive_index_gradient(x, y, z, wavelength)
|
|
236
|
+
grad_x, grad_y, grad_z = (
|
|
237
|
+
np.asarray(grad[0]),
|
|
238
|
+
np.asarray(grad[1]),
|
|
239
|
+
np.asarray(grad[2]),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
dot_product = dx * grad_x + dy * grad_y + dz * grad_z
|
|
243
|
+
kappa_x = (grad_x - dot_product * dx) / n
|
|
244
|
+
kappa_y = (grad_y - dot_product * dy) / n
|
|
245
|
+
kappa_z = (grad_z - dot_product * dz) / n
|
|
246
|
+
|
|
247
|
+
new_x = x + dx * step_size
|
|
248
|
+
new_y = y + dy * step_size
|
|
249
|
+
new_z = z + dz * step_size
|
|
250
|
+
|
|
251
|
+
new_dx = dx + kappa_x * step_size
|
|
252
|
+
new_dy = dy + kappa_y * step_size
|
|
253
|
+
new_dz = dz + kappa_z * step_size
|
|
254
|
+
|
|
255
|
+
norm = np.sqrt(new_dx**2 + new_dy**2 + new_dz**2)
|
|
256
|
+
norm = np.where(norm < 1e-12, 1.0, norm)
|
|
257
|
+
new_dx /= norm
|
|
258
|
+
new_dy /= norm
|
|
259
|
+
new_dz /= norm
|
|
260
|
+
|
|
261
|
+
new_positions = np.column_stack([new_x, new_y, new_z])
|
|
262
|
+
new_directions = np.column_stack([new_dx, new_dy, new_dz])
|
|
263
|
+
|
|
264
|
+
return new_positions, new_directions, n
|
|
265
|
+
|
|
266
|
+
def _rk4_step_vectorized(
|
|
267
|
+
self,
|
|
268
|
+
positions: npt.NDArray[np.floating],
|
|
269
|
+
directions: npt.NDArray[np.floating],
|
|
270
|
+
material: MaterialFieldProtocol,
|
|
271
|
+
step_size: float,
|
|
272
|
+
wavelength: float,
|
|
273
|
+
) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray]:
|
|
274
|
+
"""
|
|
275
|
+
Vectorized RK4 step for gradient medium.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
new_positions : ndarray
|
|
280
|
+
Updated positions.
|
|
281
|
+
new_directions : ndarray
|
|
282
|
+
Updated directions.
|
|
283
|
+
n_avg : ndarray
|
|
284
|
+
Average refractive index for optical path calculation.
|
|
285
|
+
"""
|
|
286
|
+
h = step_size
|
|
287
|
+
h2 = h / 2.0
|
|
288
|
+
|
|
289
|
+
def compute_derivatives(pos, dirs):
|
|
290
|
+
x, y, z = pos[:, 0], pos[:, 1], pos[:, 2]
|
|
291
|
+
dx, dy, dz = dirs[:, 0], dirs[:, 1], dirs[:, 2]
|
|
292
|
+
|
|
293
|
+
n = material.get_refractive_index(x, y, z, wavelength)
|
|
294
|
+
grad = material.get_refractive_index_gradient(x, y, z, wavelength)
|
|
295
|
+
grad_x, grad_y, grad_z = (
|
|
296
|
+
np.asarray(grad[0]),
|
|
297
|
+
np.asarray(grad[1]),
|
|
298
|
+
np.asarray(grad[2]),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
dot_product = dx * grad_x + dy * grad_y + dz * grad_z
|
|
302
|
+
kappa_x = (grad_x - dot_product * dx) / n
|
|
303
|
+
kappa_y = (grad_y - dot_product * dy) / n
|
|
304
|
+
kappa_z = (grad_z - dot_product * dz) / n
|
|
305
|
+
|
|
306
|
+
dr_ds = dirs.copy()
|
|
307
|
+
dd_ds = np.column_stack([kappa_x, kappa_y, kappa_z])
|
|
308
|
+
|
|
309
|
+
return dr_ds, dd_ds, n
|
|
310
|
+
|
|
311
|
+
def normalize_directions(dirs):
|
|
312
|
+
norm = np.linalg.norm(dirs, axis=1, keepdims=True)
|
|
313
|
+
norm = np.where(norm < 1e-12, 1.0, norm)
|
|
314
|
+
return dirs / norm
|
|
315
|
+
|
|
316
|
+
# k1
|
|
317
|
+
k1_r, k1_d, n0 = compute_derivatives(positions, directions)
|
|
318
|
+
|
|
319
|
+
# k2
|
|
320
|
+
pos1 = positions + h2 * k1_r
|
|
321
|
+
dirs1 = normalize_directions(directions + h2 * k1_d)
|
|
322
|
+
k2_r, k2_d, n1 = compute_derivatives(pos1, dirs1)
|
|
323
|
+
|
|
324
|
+
# k3
|
|
325
|
+
pos2 = positions + h2 * k2_r
|
|
326
|
+
dirs2 = normalize_directions(directions + h2 * k2_d)
|
|
327
|
+
k3_r, k3_d, n2 = compute_derivatives(pos2, dirs2)
|
|
328
|
+
|
|
329
|
+
# k4
|
|
330
|
+
pos3 = positions + h * k3_r
|
|
331
|
+
dirs3 = normalize_directions(directions + h * k3_d)
|
|
332
|
+
k4_r, k4_d, n3 = compute_derivatives(pos3, dirs3)
|
|
333
|
+
|
|
334
|
+
# Final RK4 combination
|
|
335
|
+
new_positions = positions + (h / 6.0) * (k1_r + 2 * k2_r + 2 * k3_r + k4_r)
|
|
336
|
+
new_directions = directions + (h / 6.0) * (k1_d + 2 * k2_d + 2 * k3_d + k4_d)
|
|
337
|
+
new_directions = normalize_directions(new_directions)
|
|
338
|
+
|
|
339
|
+
# Simpson's rule for average n
|
|
340
|
+
n_avg = (n0 + 4 * n1 + n2) / 6.0
|
|
341
|
+
|
|
342
|
+
return new_positions, new_directions, n_avg
|
|
343
|
+
|
|
344
|
+
# ========================================================================
|
|
345
|
+
# Unified Interface Methods (RayPropagatorProtocol)
|
|
346
|
+
# ========================================================================
|
|
347
|
+
|
|
348
|
+
def propagate_step(
|
|
349
|
+
self,
|
|
350
|
+
rays, # RayBatch
|
|
351
|
+
step_size: float,
|
|
352
|
+
wavelength: float = 532e-9,
|
|
353
|
+
material: MaterialFieldProtocol | None = None,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Propagate rays by a single integration step.
|
|
357
|
+
|
|
358
|
+
This is the unified interface method compatible with GPU propagators.
|
|
359
|
+
Delegates to propagate_step_cpu internally.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
rays : RayBatch
|
|
364
|
+
Ray batch containing positions, directions, and other ray properties.
|
|
365
|
+
Modified in-place.
|
|
366
|
+
step_size : float
|
|
367
|
+
Integration step size in meters.
|
|
368
|
+
wavelength : float, optional
|
|
369
|
+
Wavelength in meters, default 532 nm.
|
|
370
|
+
material : MaterialFieldProtocol, optional
|
|
371
|
+
Material to propagate through. If not provided, must be set via
|
|
372
|
+
a separate mechanism (e.g., stored during initialization).
|
|
373
|
+
"""
|
|
374
|
+
if material is None:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
"material must be provided. CPU propagator requires explicit material."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
self.propagate_step_cpu(
|
|
380
|
+
positions=rays.positions,
|
|
381
|
+
directions=rays.directions,
|
|
382
|
+
active=rays.active,
|
|
383
|
+
material=material,
|
|
384
|
+
step_size=step_size,
|
|
385
|
+
wavelength=wavelength,
|
|
386
|
+
geometric_path_lengths=rays.geometric_path_lengths,
|
|
387
|
+
optical_path_lengths=rays.optical_path_lengths,
|
|
388
|
+
accumulated_time=rays.accumulated_time,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def propagate(
|
|
392
|
+
self,
|
|
393
|
+
rays, # RayBatch
|
|
394
|
+
total_distance: float,
|
|
395
|
+
step_size: float,
|
|
396
|
+
wavelength: float = 532e-9,
|
|
397
|
+
material: MaterialFieldProtocol | None = None,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""
|
|
400
|
+
Propagate rays through a total distance.
|
|
401
|
+
|
|
402
|
+
This is the unified interface method compatible with GPU propagators.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
rays : RayBatch
|
|
407
|
+
Ray batch to propagate (modified in-place)
|
|
408
|
+
total_distance : float
|
|
409
|
+
Total distance to propagate in meters
|
|
410
|
+
step_size : float
|
|
411
|
+
Integration step size in meters
|
|
412
|
+
wavelength : float, optional
|
|
413
|
+
Wavelength in meters, default 532 nm
|
|
414
|
+
material : MaterialFieldProtocol, optional
|
|
415
|
+
Material to propagate through
|
|
416
|
+
"""
|
|
417
|
+
if material is None:
|
|
418
|
+
raise ValueError(
|
|
419
|
+
"material must be provided. CPU propagator requires explicit material."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
num_steps = int(total_distance / step_size)
|
|
423
|
+
for _ in range(num_steps):
|
|
424
|
+
self.propagate_step(
|
|
425
|
+
rays=rays,
|
|
426
|
+
step_size=step_size,
|
|
427
|
+
wavelength=wavelength,
|
|
428
|
+
material=material,
|
|
429
|
+
)
|