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,463 @@
|
|
|
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
|
+
Time Spread Estimation Utilities
|
|
36
|
+
|
|
37
|
+
Provides geometric estimation of time spread for diverging beams reflecting
|
|
38
|
+
off surfaces and reaching a detector. Useful for computing upper/lower bounds
|
|
39
|
+
on arrival time distributions without full ray tracing.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> from lsurf.utilities.time_spread import estimate_time_spread
|
|
44
|
+
>>> from lsurf.surfaces import GerstnerWaveSurface, GerstnerWaveParams
|
|
45
|
+
>>>
|
|
46
|
+
>>> # Create a wave surface
|
|
47
|
+
>>> wave = GerstnerWaveParams(amplitude=1.0, wavelength=50.0)
|
|
48
|
+
>>> surface = GerstnerWaveSurface(wave_params=[wave])
|
|
49
|
+
>>>
|
|
50
|
+
>>> result = estimate_time_spread(
|
|
51
|
+
... source_position=(0, 0, 500),
|
|
52
|
+
... beam_direction=(0.98, 0, -0.17),
|
|
53
|
+
... divergence_angle=np.radians(1.0),
|
|
54
|
+
... detector_position=(32000, 0, 5700),
|
|
55
|
+
... surface=surface,
|
|
56
|
+
... )
|
|
57
|
+
>>> print(f"Time spread: {result.time_spread_ns:.2f} ns")
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
from dataclasses import dataclass
|
|
63
|
+
from typing import TYPE_CHECKING
|
|
64
|
+
|
|
65
|
+
import numpy as np
|
|
66
|
+
from numpy.typing import NDArray
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from ..surfaces import Surface
|
|
70
|
+
|
|
71
|
+
# Speed of light in m/s
|
|
72
|
+
SPEED_OF_LIGHT = 299792458.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class TimeSpreadResult:
|
|
77
|
+
"""
|
|
78
|
+
Results from time spread estimation.
|
|
79
|
+
|
|
80
|
+
Attributes
|
|
81
|
+
----------
|
|
82
|
+
min_path : float
|
|
83
|
+
Shortest path length in meters
|
|
84
|
+
max_path : float
|
|
85
|
+
Longest path length in meters
|
|
86
|
+
path_spread : float
|
|
87
|
+
Difference between max and min path in meters
|
|
88
|
+
time_spread_s : float
|
|
89
|
+
Time spread in seconds
|
|
90
|
+
time_spread_ns : float
|
|
91
|
+
Time spread in nanoseconds
|
|
92
|
+
min_path_point : ndarray
|
|
93
|
+
Surface point with shortest path
|
|
94
|
+
max_path_point : ndarray
|
|
95
|
+
Surface point with longest path
|
|
96
|
+
edge_points : ndarray
|
|
97
|
+
All beam edge intersection points on surface
|
|
98
|
+
path_lengths : ndarray
|
|
99
|
+
Path lengths for all edge points
|
|
100
|
+
center_point : ndarray
|
|
101
|
+
Center ray intersection point
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
min_path: float
|
|
105
|
+
max_path: float
|
|
106
|
+
path_spread: float
|
|
107
|
+
time_spread_s: float
|
|
108
|
+
time_spread_ns: float
|
|
109
|
+
min_path_point: NDArray[np.float64]
|
|
110
|
+
max_path_point: NDArray[np.float64]
|
|
111
|
+
edge_points: NDArray[np.float64]
|
|
112
|
+
path_lengths: NDArray[np.float64]
|
|
113
|
+
center_point: NDArray[np.float64]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def compute_beam_footprint(
|
|
117
|
+
source_position: tuple[float, float, float],
|
|
118
|
+
beam_direction: tuple[float, float, float],
|
|
119
|
+
divergence_angle: float,
|
|
120
|
+
n_edge_points: int = 100,
|
|
121
|
+
surface: Surface | None = None,
|
|
122
|
+
) -> dict[str, float | NDArray[np.float64]]:
|
|
123
|
+
"""
|
|
124
|
+
Compute where the edges of a diverging beam hit a surface.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
source_position : tuple
|
|
129
|
+
(x, y, z) position of the source in meters
|
|
130
|
+
beam_direction : tuple
|
|
131
|
+
Unit vector of beam center direction
|
|
132
|
+
divergence_angle : float
|
|
133
|
+
Half-angle divergence of the beam in radians
|
|
134
|
+
n_edge_points : int
|
|
135
|
+
Number of points around the beam edge cone
|
|
136
|
+
surface : Surface, optional
|
|
137
|
+
Surface object to intersect with (e.g., GerstnerWaveSurface,
|
|
138
|
+
CurvedWaveSurface, PlanarSurface). If None, uses flat surface at z=0.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
dict
|
|
143
|
+
Dictionary containing:
|
|
144
|
+
- edge_points : ndarray, shape (N, 3) - Surface intersection points
|
|
145
|
+
- center_point : ndarray, shape (3,) - Center ray intersection
|
|
146
|
+
- edge_distances : ndarray - Distances from source to each edge point
|
|
147
|
+
- center_distance : float - Distance from source to center point
|
|
148
|
+
"""
|
|
149
|
+
src = np.array(source_position, dtype=np.float64)
|
|
150
|
+
beam_dir = np.array(beam_direction, dtype=np.float64)
|
|
151
|
+
beam_dir = beam_dir / np.linalg.norm(beam_dir)
|
|
152
|
+
|
|
153
|
+
# Create orthonormal basis around beam direction
|
|
154
|
+
if abs(beam_dir[2]) < 0.9:
|
|
155
|
+
up = np.array([0.0, 0.0, 1.0])
|
|
156
|
+
else:
|
|
157
|
+
up = np.array([1.0, 0.0, 0.0])
|
|
158
|
+
|
|
159
|
+
v1 = np.cross(beam_dir, up)
|
|
160
|
+
v1 = v1 / np.linalg.norm(v1)
|
|
161
|
+
v2 = np.cross(beam_dir, v1)
|
|
162
|
+
v2 = v2 / np.linalg.norm(v2)
|
|
163
|
+
|
|
164
|
+
# Generate all edge ray directions
|
|
165
|
+
phi_angles = np.linspace(0, 2 * np.pi, n_edge_points, endpoint=False)
|
|
166
|
+
|
|
167
|
+
edge_directions = []
|
|
168
|
+
for phi in phi_angles:
|
|
169
|
+
edge_dir = np.cos(divergence_angle) * beam_dir + np.sin(divergence_angle) * (
|
|
170
|
+
np.cos(phi) * v1 + np.sin(phi) * v2
|
|
171
|
+
)
|
|
172
|
+
edge_dir = edge_dir / np.linalg.norm(edge_dir)
|
|
173
|
+
edge_directions.append(edge_dir)
|
|
174
|
+
|
|
175
|
+
edge_directions = np.array(edge_directions, dtype=np.float32)
|
|
176
|
+
origins = np.tile(src.astype(np.float32), (n_edge_points, 1))
|
|
177
|
+
|
|
178
|
+
# Find intersections
|
|
179
|
+
edge_points = []
|
|
180
|
+
edge_distances = []
|
|
181
|
+
|
|
182
|
+
if surface is not None:
|
|
183
|
+
# Use surface.intersect() for batch intersection
|
|
184
|
+
distances, hit_mask = surface.intersect(origins, edge_directions)
|
|
185
|
+
|
|
186
|
+
for i in range(n_edge_points):
|
|
187
|
+
if hit_mask[i] and distances[i] > 0:
|
|
188
|
+
t = float(distances[i])
|
|
189
|
+
hit_point = src + t * edge_directions[i].astype(np.float64)
|
|
190
|
+
edge_points.append(hit_point)
|
|
191
|
+
edge_distances.append(t)
|
|
192
|
+
else:
|
|
193
|
+
# Flat surface at z=0
|
|
194
|
+
for i in range(n_edge_points):
|
|
195
|
+
edge_dir = edge_directions[i].astype(np.float64)
|
|
196
|
+
if abs(edge_dir[2]) > 1e-10:
|
|
197
|
+
t = -src[2] / edge_dir[2]
|
|
198
|
+
if t > 0:
|
|
199
|
+
hit_point = src + t * edge_dir
|
|
200
|
+
edge_points.append(hit_point)
|
|
201
|
+
edge_distances.append(t)
|
|
202
|
+
|
|
203
|
+
edge_points = np.array(edge_points) if edge_points else np.empty((0, 3))
|
|
204
|
+
edge_distances = np.array(edge_distances)
|
|
205
|
+
|
|
206
|
+
# Center ray intersection
|
|
207
|
+
center_origins = src.astype(np.float32).reshape(1, 3)
|
|
208
|
+
center_directions = beam_dir.astype(np.float32).reshape(1, 3)
|
|
209
|
+
|
|
210
|
+
if surface is not None:
|
|
211
|
+
distances, hit_mask = surface.intersect(center_origins, center_directions)
|
|
212
|
+
if hit_mask[0] and distances[0] > 0:
|
|
213
|
+
t_center = float(distances[0])
|
|
214
|
+
center_point = src + t_center * beam_dir
|
|
215
|
+
else:
|
|
216
|
+
# Fallback to flat surface
|
|
217
|
+
t_center = -src[2] / beam_dir[2]
|
|
218
|
+
center_point = src + t_center * beam_dir
|
|
219
|
+
else:
|
|
220
|
+
t_center = -src[2] / beam_dir[2]
|
|
221
|
+
center_point = src + t_center * beam_dir
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"edge_points": edge_points,
|
|
225
|
+
"center_point": center_point,
|
|
226
|
+
"edge_distances": edge_distances,
|
|
227
|
+
"center_distance": t_center,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def estimate_time_spread(
|
|
232
|
+
source_position: tuple[float, float, float],
|
|
233
|
+
beam_direction: tuple[float, float, float],
|
|
234
|
+
divergence_angle: float,
|
|
235
|
+
detector_position: tuple[float, float, float],
|
|
236
|
+
surface: Surface | None = None,
|
|
237
|
+
n_edge_points: int = 100,
|
|
238
|
+
speed_of_light: float = SPEED_OF_LIGHT,
|
|
239
|
+
) -> TimeSpreadResult:
|
|
240
|
+
"""
|
|
241
|
+
Estimate time spread for a diverging beam reflecting to a detector.
|
|
242
|
+
|
|
243
|
+
Computes geometric path lengths from source through surface edge points
|
|
244
|
+
to detector, giving an upper bound estimate on arrival time spread.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
source_position : tuple
|
|
249
|
+
(x, y, z) source position in meters
|
|
250
|
+
beam_direction : tuple
|
|
251
|
+
Beam center direction (will be normalized)
|
|
252
|
+
divergence_angle : float
|
|
253
|
+
Beam half-angle divergence in radians
|
|
254
|
+
detector_position : tuple
|
|
255
|
+
(x, y, z) detector position in meters
|
|
256
|
+
surface : Surface, optional
|
|
257
|
+
Surface object to intersect with (e.g., GerstnerWaveSurface,
|
|
258
|
+
CurvedWaveSurface, PlanarSurface). If None, uses flat surface at z=0.
|
|
259
|
+
n_edge_points : int
|
|
260
|
+
Number of points around beam edge for sampling
|
|
261
|
+
speed_of_light : float
|
|
262
|
+
Speed of light in m/s
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
TimeSpreadResult
|
|
267
|
+
Dataclass containing path lengths, time spread, and geometry info
|
|
268
|
+
|
|
269
|
+
Examples
|
|
270
|
+
--------
|
|
271
|
+
>>> # Flat surface estimate
|
|
272
|
+
>>> result = estimate_time_spread(
|
|
273
|
+
... source_position=(-2800, 0, 500),
|
|
274
|
+
... beam_direction=(0.98, 0, -0.17),
|
|
275
|
+
... divergence_angle=np.radians(1.0),
|
|
276
|
+
... detector_position=(32000, 0, 5700),
|
|
277
|
+
... )
|
|
278
|
+
>>> print(f"Flat surface: {result.time_spread_ns:.2f} ns")
|
|
279
|
+
|
|
280
|
+
>>> # Wavy surface estimate with GerstnerWaveSurface
|
|
281
|
+
>>> from lsurf.surfaces import GerstnerWaveSurface, GerstnerWaveParams
|
|
282
|
+
>>> wave = GerstnerWaveParams(amplitude=1.0, wavelength=50.0, direction=(0, 1))
|
|
283
|
+
>>> surface = GerstnerWaveSurface(wave_params=[wave])
|
|
284
|
+
>>> result = estimate_time_spread(
|
|
285
|
+
... source_position=(-2800, 0, 500),
|
|
286
|
+
... beam_direction=(0.98, 0, -0.17),
|
|
287
|
+
... divergence_angle=np.radians(1.0),
|
|
288
|
+
... detector_position=(32000, 0, 5700),
|
|
289
|
+
... surface=surface,
|
|
290
|
+
... )
|
|
291
|
+
>>> print(f"Wavy surface: {result.time_spread_ns:.2f} ns")
|
|
292
|
+
"""
|
|
293
|
+
src = np.array(source_position, dtype=np.float64)
|
|
294
|
+
det = np.array(detector_position, dtype=np.float64)
|
|
295
|
+
|
|
296
|
+
# Get beam footprint on surface
|
|
297
|
+
footprint = compute_beam_footprint(
|
|
298
|
+
source_position,
|
|
299
|
+
beam_direction,
|
|
300
|
+
divergence_angle,
|
|
301
|
+
n_edge_points=n_edge_points,
|
|
302
|
+
surface=surface,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
edge_points = footprint["edge_points"]
|
|
306
|
+
center_point = footprint["center_point"]
|
|
307
|
+
|
|
308
|
+
if len(edge_points) == 0:
|
|
309
|
+
# No intersections found
|
|
310
|
+
return TimeSpreadResult(
|
|
311
|
+
min_path=0.0,
|
|
312
|
+
max_path=0.0,
|
|
313
|
+
path_spread=0.0,
|
|
314
|
+
time_spread_s=0.0,
|
|
315
|
+
time_spread_ns=0.0,
|
|
316
|
+
min_path_point=center_point,
|
|
317
|
+
max_path_point=center_point,
|
|
318
|
+
edge_points=edge_points,
|
|
319
|
+
path_lengths=np.array([]),
|
|
320
|
+
center_point=center_point,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Compute total path lengths: source -> surface -> detector
|
|
324
|
+
path_lengths = []
|
|
325
|
+
for pt in edge_points:
|
|
326
|
+
d_src_to_surface = np.linalg.norm(pt - src)
|
|
327
|
+
d_surface_to_det = np.linalg.norm(det - pt)
|
|
328
|
+
total_path = d_src_to_surface + d_surface_to_det
|
|
329
|
+
path_lengths.append(total_path)
|
|
330
|
+
|
|
331
|
+
path_lengths = np.array(path_lengths)
|
|
332
|
+
|
|
333
|
+
min_path = np.min(path_lengths)
|
|
334
|
+
max_path = np.max(path_lengths)
|
|
335
|
+
path_spread = max_path - min_path
|
|
336
|
+
|
|
337
|
+
time_spread_s = path_spread / speed_of_light
|
|
338
|
+
time_spread_ns = time_spread_s * 1e9
|
|
339
|
+
|
|
340
|
+
min_idx = np.argmin(path_lengths)
|
|
341
|
+
max_idx = np.argmax(path_lengths)
|
|
342
|
+
|
|
343
|
+
return TimeSpreadResult(
|
|
344
|
+
min_path=min_path,
|
|
345
|
+
max_path=max_path,
|
|
346
|
+
path_spread=path_spread,
|
|
347
|
+
time_spread_s=time_spread_s,
|
|
348
|
+
time_spread_ns=time_spread_ns,
|
|
349
|
+
min_path_point=edge_points[min_idx],
|
|
350
|
+
max_path_point=edge_points[max_idx],
|
|
351
|
+
edge_points=edge_points,
|
|
352
|
+
path_lengths=path_lengths,
|
|
353
|
+
center_point=center_point,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def estimate_time_spread_bounds(
|
|
358
|
+
source_position: tuple[float, float, float],
|
|
359
|
+
beam_direction: tuple[float, float, float],
|
|
360
|
+
divergence_angle: float,
|
|
361
|
+
detector_positions: NDArray[np.float64],
|
|
362
|
+
surface: Surface | None = None,
|
|
363
|
+
n_edge_points: int = 100,
|
|
364
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
365
|
+
"""
|
|
366
|
+
Estimate time spread bounds for multiple detector positions.
|
|
367
|
+
|
|
368
|
+
Useful for computing time spread maps across a detector surface.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
source_position : tuple
|
|
373
|
+
(x, y, z) source position in meters
|
|
374
|
+
beam_direction : tuple
|
|
375
|
+
Beam center direction
|
|
376
|
+
divergence_angle : float
|
|
377
|
+
Beam half-angle divergence in radians
|
|
378
|
+
detector_positions : ndarray, shape (N, 3)
|
|
379
|
+
Array of detector positions to evaluate
|
|
380
|
+
surface : Surface, optional
|
|
381
|
+
Surface object to intersect with. If None, uses flat surface at z=0.
|
|
382
|
+
n_edge_points : int
|
|
383
|
+
Number of edge points for beam footprint
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
dict
|
|
388
|
+
Dictionary containing:
|
|
389
|
+
- time_spread_ns : ndarray, shape (N,) - Time spread at each position
|
|
390
|
+
- path_spread : ndarray, shape (N,) - Path spread at each position
|
|
391
|
+
- min_path : ndarray, shape (N,) - Minimum path at each position
|
|
392
|
+
- max_path : ndarray, shape (N,) - Maximum path at each position
|
|
393
|
+
|
|
394
|
+
Examples
|
|
395
|
+
--------
|
|
396
|
+
>>> # Compute time spread for grid of detector positions
|
|
397
|
+
>>> lon = np.linspace(-5, 5, 20)
|
|
398
|
+
>>> lat = np.linspace(5, 15, 20)
|
|
399
|
+
>>> lon_grid, lat_grid = np.meshgrid(lon, lat)
|
|
400
|
+
>>> r = 33000 # 33 km
|
|
401
|
+
>>> x = r * np.cos(np.radians(lat_grid)) * np.cos(np.radians(lon_grid))
|
|
402
|
+
>>> y = r * np.cos(np.radians(lat_grid)) * np.sin(np.radians(lon_grid))
|
|
403
|
+
>>> z = r * np.sin(np.radians(lat_grid))
|
|
404
|
+
>>> positions = np.stack([x.ravel(), y.ravel(), z.ravel()], axis=1)
|
|
405
|
+
>>>
|
|
406
|
+
>>> bounds = estimate_time_spread_bounds(
|
|
407
|
+
... source_position=(-2800, 0, 500),
|
|
408
|
+
... beam_direction=(0.98, 0, -0.17),
|
|
409
|
+
... divergence_angle=np.radians(1.0),
|
|
410
|
+
... detector_positions=positions,
|
|
411
|
+
... )
|
|
412
|
+
>>> time_spread_map = bounds['time_spread_ns'].reshape(lat_grid.shape)
|
|
413
|
+
"""
|
|
414
|
+
n_detectors = len(detector_positions)
|
|
415
|
+
|
|
416
|
+
time_spreads = np.zeros(n_detectors)
|
|
417
|
+
path_spreads = np.zeros(n_detectors)
|
|
418
|
+
min_paths = np.zeros(n_detectors)
|
|
419
|
+
max_paths = np.zeros(n_detectors)
|
|
420
|
+
|
|
421
|
+
# Compute footprint once (same for all detectors)
|
|
422
|
+
footprint = compute_beam_footprint(
|
|
423
|
+
source_position,
|
|
424
|
+
beam_direction,
|
|
425
|
+
divergence_angle,
|
|
426
|
+
n_edge_points=n_edge_points,
|
|
427
|
+
surface=surface,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
src = np.array(source_position, dtype=np.float64)
|
|
431
|
+
edge_points = footprint["edge_points"]
|
|
432
|
+
|
|
433
|
+
if len(edge_points) == 0:
|
|
434
|
+
return {
|
|
435
|
+
"time_spread_ns": time_spreads,
|
|
436
|
+
"path_spread": path_spreads,
|
|
437
|
+
"min_path": min_paths,
|
|
438
|
+
"max_path": max_paths,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# Pre-compute source-to-surface distances (same for all detectors)
|
|
442
|
+
src_to_surface = np.linalg.norm(edge_points - src, axis=1)
|
|
443
|
+
|
|
444
|
+
for i, det_pos in enumerate(detector_positions):
|
|
445
|
+
det = np.array(det_pos, dtype=np.float64)
|
|
446
|
+
|
|
447
|
+
# Surface-to-detector distances
|
|
448
|
+
surface_to_det = np.linalg.norm(edge_points - det, axis=1)
|
|
449
|
+
|
|
450
|
+
# Total paths
|
|
451
|
+
total_paths = src_to_surface + surface_to_det
|
|
452
|
+
|
|
453
|
+
min_paths[i] = np.min(total_paths)
|
|
454
|
+
max_paths[i] = np.max(total_paths)
|
|
455
|
+
path_spreads[i] = max_paths[i] - min_paths[i]
|
|
456
|
+
time_spreads[i] = path_spreads[i] / SPEED_OF_LIGHT * 1e9
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
"time_spread_ns": time_spreads,
|
|
460
|
+
"path_spread": path_spreads,
|
|
461
|
+
"min_path": min_paths,
|
|
462
|
+
"max_path": max_paths,
|
|
463
|
+
}
|