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,368 @@
|
|
|
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
|
+
Recording Sphere Detector for Earth-Scale Simulations
|
|
36
|
+
|
|
37
|
+
Provides a spherical detection surface at a specified altitude above Earth
|
|
38
|
+
for capturing rays in global-scale atmospheric simulations.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from lsurf.detectors.extended import RecordingSphereDetector
|
|
43
|
+
>>>
|
|
44
|
+
>>> # Create detector at 33 km altitude
|
|
45
|
+
>>> detector = RecordingSphereDetector(altitude=33000.0)
|
|
46
|
+
>>> result = detector.detect(rays)
|
|
47
|
+
>>> print(f"Detected {result.num_rays} rays")
|
|
48
|
+
>>>
|
|
49
|
+
>>> # Compute angular distribution
|
|
50
|
+
>>> coords = result.compute_angular_coordinates()
|
|
51
|
+
>>> elevation = np.degrees(coords['elevation'])
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
from numpy.typing import NDArray
|
|
56
|
+
|
|
57
|
+
from ...surfaces import EARTH_RADIUS
|
|
58
|
+
from ...utilities.ray_data import RayBatch
|
|
59
|
+
from ..base import DetectionEvent
|
|
60
|
+
from ..results import DetectorResult
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RecordingSphereDetector:
|
|
64
|
+
"""
|
|
65
|
+
Spherical detection surface at a specified altitude above Earth.
|
|
66
|
+
|
|
67
|
+
Records all rays that intersect the sphere, capturing full ray state
|
|
68
|
+
for later analysis. Used for Earth-scale simulations where the sphere
|
|
69
|
+
is centered on Earth's center.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
altitude : float
|
|
74
|
+
Altitude above Earth's surface in meters (default 33 km)
|
|
75
|
+
earth_center : tuple of float
|
|
76
|
+
Center of Earth, default (0, 0, -EARTH_RADIUS)
|
|
77
|
+
earth_radius : float
|
|
78
|
+
Earth radius in meters
|
|
79
|
+
name : str
|
|
80
|
+
Detector name for identification
|
|
81
|
+
|
|
82
|
+
Attributes
|
|
83
|
+
----------
|
|
84
|
+
altitude : float
|
|
85
|
+
Altitude above Earth's surface in meters.
|
|
86
|
+
sphere_radius : float
|
|
87
|
+
Radius of detection sphere (earth_radius + altitude).
|
|
88
|
+
center : ndarray, shape (3,)
|
|
89
|
+
Center position (Earth's center).
|
|
90
|
+
name : str
|
|
91
|
+
Detector name.
|
|
92
|
+
accumulated_result : DetectorResult
|
|
93
|
+
All accumulated detections since last clear().
|
|
94
|
+
|
|
95
|
+
Notes
|
|
96
|
+
-----
|
|
97
|
+
The recording sphere has radius = earth_radius + altitude, centered
|
|
98
|
+
at earth_center. This creates a sphere that surrounds Earth at the
|
|
99
|
+
specified altitude.
|
|
100
|
+
|
|
101
|
+
Examples
|
|
102
|
+
--------
|
|
103
|
+
>>> # Detector at 33 km altitude
|
|
104
|
+
>>> detector = RecordingSphereDetector(altitude=33000.0)
|
|
105
|
+
>>> result = detector.detect(rays)
|
|
106
|
+
>>>
|
|
107
|
+
>>> # Access angular coordinates
|
|
108
|
+
>>> coords = result.compute_angular_coordinates()
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
altitude: float = 33000.0, # 33 km default
|
|
114
|
+
earth_center: tuple[float, float, float] = (0, 0, -EARTH_RADIUS),
|
|
115
|
+
earth_radius: float = EARTH_RADIUS,
|
|
116
|
+
name: str = "Recording Sphere",
|
|
117
|
+
):
|
|
118
|
+
"""
|
|
119
|
+
Initialize recording sphere detector.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
altitude : float
|
|
124
|
+
Altitude above Earth's surface in meters.
|
|
125
|
+
earth_center : tuple of float
|
|
126
|
+
Center of Earth.
|
|
127
|
+
earth_radius : float
|
|
128
|
+
Earth radius in meters.
|
|
129
|
+
name : str
|
|
130
|
+
Detector name.
|
|
131
|
+
"""
|
|
132
|
+
self.name = name
|
|
133
|
+
self.altitude = altitude
|
|
134
|
+
self._earth_center = np.array(earth_center, dtype=np.float64)
|
|
135
|
+
self.earth_radius = earth_radius
|
|
136
|
+
self._sphere_radius = earth_radius + altitude
|
|
137
|
+
|
|
138
|
+
self._accumulated_result = DetectorResult.empty(name)
|
|
139
|
+
self._events: list[DetectionEvent] = []
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def sphere_radius(self) -> float:
|
|
143
|
+
"""Radius of the detection sphere in meters."""
|
|
144
|
+
return self._sphere_radius
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def center(self) -> NDArray[np.float64]:
|
|
148
|
+
"""Center position of the sphere (Earth's center)."""
|
|
149
|
+
return self._earth_center
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def accumulated_result(self) -> DetectorResult:
|
|
153
|
+
"""All accumulated detections since last clear()."""
|
|
154
|
+
return self._accumulated_result
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def events(self) -> list[DetectionEvent]:
|
|
158
|
+
"""
|
|
159
|
+
Backward compatibility: list of DetectionEvent objects.
|
|
160
|
+
|
|
161
|
+
For new code, use accumulated_result instead.
|
|
162
|
+
"""
|
|
163
|
+
return self._events
|
|
164
|
+
|
|
165
|
+
def detect(
|
|
166
|
+
self,
|
|
167
|
+
rays: RayBatch,
|
|
168
|
+
current_time: float = 0.0,
|
|
169
|
+
accumulate: bool = True,
|
|
170
|
+
compute_travel_time: bool = True,
|
|
171
|
+
speed_of_light: float = 299792458.0,
|
|
172
|
+
max_propagation_distance: float | None = None,
|
|
173
|
+
) -> DetectorResult:
|
|
174
|
+
"""
|
|
175
|
+
Detect rays intersecting the recording sphere.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
rays : RayBatch
|
|
180
|
+
Rays to detect
|
|
181
|
+
current_time : float
|
|
182
|
+
Current simulation time (unused, for interface compatibility)
|
|
183
|
+
accumulate : bool
|
|
184
|
+
Whether to accumulate results. Default is True.
|
|
185
|
+
compute_travel_time : bool
|
|
186
|
+
If True, add travel time to intersection to ray's accumulated time
|
|
187
|
+
speed_of_light : float
|
|
188
|
+
Speed of light for time computation
|
|
189
|
+
max_propagation_distance : float, optional
|
|
190
|
+
Maximum distance rays can propagate before detection (meters).
|
|
191
|
+
If None, no limit is applied.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
DetectorResult
|
|
196
|
+
Detection results for all intersecting rays
|
|
197
|
+
"""
|
|
198
|
+
active_mask = rays.active
|
|
199
|
+
if not np.any(active_mask):
|
|
200
|
+
return DetectorResult.empty(self.name)
|
|
201
|
+
|
|
202
|
+
origins = rays.positions[active_mask].astype(np.float64)
|
|
203
|
+
directions = rays.directions[active_mask].astype(np.float64)
|
|
204
|
+
active_indices = np.where(active_mask)[0]
|
|
205
|
+
|
|
206
|
+
# Ray-sphere intersection
|
|
207
|
+
# Ray: P = O + t*D
|
|
208
|
+
# Sphere: |P - C|^2 = R^2
|
|
209
|
+
oc = origins - self._earth_center
|
|
210
|
+
a = np.sum(directions**2, axis=1)
|
|
211
|
+
b = 2 * np.sum(oc * directions, axis=1)
|
|
212
|
+
c = np.sum(oc**2, axis=1) - self._sphere_radius**2
|
|
213
|
+
|
|
214
|
+
discriminant = b**2 - 4 * a * c
|
|
215
|
+
hit_mask = discriminant >= 0
|
|
216
|
+
|
|
217
|
+
if not np.any(hit_mask):
|
|
218
|
+
return DetectorResult.empty(self.name)
|
|
219
|
+
|
|
220
|
+
sqrt_disc = np.sqrt(discriminant[hit_mask])
|
|
221
|
+
t1 = (-b[hit_mask] - sqrt_disc) / (2 * a[hit_mask])
|
|
222
|
+
t2 = (-b[hit_mask] + sqrt_disc) / (2 * a[hit_mask])
|
|
223
|
+
|
|
224
|
+
# Take the positive intersection (rays going outward)
|
|
225
|
+
t = np.where(t1 > 0, t1, t2)
|
|
226
|
+
valid = t > 1e-6
|
|
227
|
+
|
|
228
|
+
# Apply max propagation distance if specified
|
|
229
|
+
if max_propagation_distance is not None:
|
|
230
|
+
valid = valid & (t < max_propagation_distance)
|
|
231
|
+
|
|
232
|
+
if not np.any(valid):
|
|
233
|
+
return DetectorResult.empty(self.name)
|
|
234
|
+
|
|
235
|
+
# Get indices into original rays
|
|
236
|
+
hit_indices = active_indices[hit_mask]
|
|
237
|
+
valid_indices = hit_indices[valid]
|
|
238
|
+
|
|
239
|
+
# Compute intersection positions
|
|
240
|
+
t_valid = t[valid]
|
|
241
|
+
hit_origins = origins[hit_mask][valid]
|
|
242
|
+
hit_directions = directions[hit_mask][valid]
|
|
243
|
+
intersection_positions = hit_origins + t_valid[:, np.newaxis] * hit_directions
|
|
244
|
+
|
|
245
|
+
# Compute times
|
|
246
|
+
if compute_travel_time:
|
|
247
|
+
times = rays.accumulated_time[valid_indices] + t_valid / speed_of_light
|
|
248
|
+
else:
|
|
249
|
+
times = rays.accumulated_time[valid_indices].copy()
|
|
250
|
+
|
|
251
|
+
# Get polarization vectors if available
|
|
252
|
+
polarization_vectors = None
|
|
253
|
+
if rays.polarization_vector is not None:
|
|
254
|
+
polarization_vectors = rays.polarization_vector[valid_indices].astype(
|
|
255
|
+
np.float32
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
result = DetectorResult(
|
|
259
|
+
positions=intersection_positions.astype(np.float32),
|
|
260
|
+
directions=hit_directions.astype(np.float32),
|
|
261
|
+
times=times.astype(np.float32),
|
|
262
|
+
intensities=rays.intensities[valid_indices].astype(np.float32),
|
|
263
|
+
wavelengths=rays.wavelengths[valid_indices].astype(np.float32),
|
|
264
|
+
ray_indices=valid_indices.astype(np.int32),
|
|
265
|
+
generations=rays.generations[valid_indices].astype(np.int32),
|
|
266
|
+
polarization_vectors=polarization_vectors,
|
|
267
|
+
detector_name=self.name,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if accumulate:
|
|
271
|
+
self._accumulated_result = DetectorResult.merge(
|
|
272
|
+
[self._accumulated_result, result]
|
|
273
|
+
)
|
|
274
|
+
self._events.extend(result.to_detection_events())
|
|
275
|
+
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
def detect_rays(
|
|
279
|
+
self,
|
|
280
|
+
rays: RayBatch,
|
|
281
|
+
compute_travel_time: bool = True,
|
|
282
|
+
speed_of_light: float = 299792458.0,
|
|
283
|
+
max_propagation_distance: float | None = None,
|
|
284
|
+
) -> DetectorResult:
|
|
285
|
+
"""
|
|
286
|
+
Detect rays (alias for detect with accumulate=False).
|
|
287
|
+
|
|
288
|
+
This method is provided for backward compatibility with code
|
|
289
|
+
that used the detect_rays() method.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
rays : RayBatch
|
|
294
|
+
Rays to detect
|
|
295
|
+
compute_travel_time : bool
|
|
296
|
+
If True, add travel time to intersection
|
|
297
|
+
speed_of_light : float
|
|
298
|
+
Speed of light for time computation
|
|
299
|
+
max_propagation_distance : float, optional
|
|
300
|
+
Maximum distance rays can propagate
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
DetectorResult
|
|
305
|
+
Detection results
|
|
306
|
+
"""
|
|
307
|
+
return self.detect(
|
|
308
|
+
rays,
|
|
309
|
+
accumulate=False,
|
|
310
|
+
compute_travel_time=compute_travel_time,
|
|
311
|
+
speed_of_light=speed_of_light,
|
|
312
|
+
max_propagation_distance=max_propagation_distance,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def clear(self) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Clear all recorded detections.
|
|
318
|
+
|
|
319
|
+
Resets the detector to its initial state with no recorded events.
|
|
320
|
+
"""
|
|
321
|
+
self._accumulated_result = DetectorResult.empty(self.name)
|
|
322
|
+
self._events = []
|
|
323
|
+
|
|
324
|
+
def __repr__(self) -> str:
|
|
325
|
+
"""Return string representation."""
|
|
326
|
+
return (
|
|
327
|
+
f"RecordingSphereDetector(altitude={self.altitude/1000:.1f}km, "
|
|
328
|
+
f"rays={self._accumulated_result.num_rays})"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def __len__(self) -> int:
|
|
332
|
+
"""Return number of detected rays."""
|
|
333
|
+
return self._accumulated_result.num_rays
|
|
334
|
+
|
|
335
|
+
# Backward compatibility methods from old Detector base class
|
|
336
|
+
def get_arrival_times(self) -> NDArray[np.float64]:
|
|
337
|
+
"""Get array of all arrival times."""
|
|
338
|
+
return self._accumulated_result.times.astype(np.float64)
|
|
339
|
+
|
|
340
|
+
def get_arrival_angles(
|
|
341
|
+
self, reference_direction: NDArray[np.float32]
|
|
342
|
+
) -> NDArray[np.float64]:
|
|
343
|
+
"""Get angles between ray directions and reference direction."""
|
|
344
|
+
if self._accumulated_result.is_empty:
|
|
345
|
+
return np.array([], dtype=np.float64)
|
|
346
|
+
ref = reference_direction / np.linalg.norm(reference_direction)
|
|
347
|
+
dir_norms = self._accumulated_result.directions / np.linalg.norm(
|
|
348
|
+
self._accumulated_result.directions, axis=1, keepdims=True
|
|
349
|
+
)
|
|
350
|
+
cos_angles = np.dot(dir_norms, ref)
|
|
351
|
+
cos_angles = np.clip(cos_angles, -1.0, 1.0)
|
|
352
|
+
return np.arccos(cos_angles).astype(np.float64)
|
|
353
|
+
|
|
354
|
+
def get_intensities(self) -> NDArray[np.float64]:
|
|
355
|
+
"""Get array of all detected intensities."""
|
|
356
|
+
return self._accumulated_result.intensities.astype(np.float64)
|
|
357
|
+
|
|
358
|
+
def get_wavelengths(self) -> NDArray[np.float64]:
|
|
359
|
+
"""Get array of all detected wavelengths."""
|
|
360
|
+
return self._accumulated_result.wavelengths.astype(np.float64)
|
|
361
|
+
|
|
362
|
+
def get_positions(self) -> NDArray[np.float32]:
|
|
363
|
+
"""Get array of all detection positions."""
|
|
364
|
+
return self._accumulated_result.positions.astype(np.float32)
|
|
365
|
+
|
|
366
|
+
def get_total_intensity(self) -> float:
|
|
367
|
+
"""Get sum of all detected intensities."""
|
|
368
|
+
return self._accumulated_result.total_intensity
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
Planar Detector Implementation (Backward Compatibility)
|
|
36
|
+
|
|
37
|
+
This module re-exports PlanarDetector from the new location
|
|
38
|
+
for backward compatibility. New code should import from:
|
|
39
|
+
lsurf.detectors or lsurf.detectors.small
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Re-export from new location for backward compatibility
|
|
43
|
+
from .small.planar import PlanarDetector
|
|
44
|
+
|
|
45
|
+
__all__ = ["PlanarDetector"]
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
DetectorProtocol - Protocol definition for detector implementations.
|
|
36
|
+
|
|
37
|
+
This module defines the DetectorProtocol that all detector classes should implement,
|
|
38
|
+
ensuring a consistent interface across small (point) detectors and extended
|
|
39
|
+
(surface) detectors.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> from lsurf.detectors.protocol import DetectorProtocol
|
|
44
|
+
>>>
|
|
45
|
+
>>> def process_detector(detector: DetectorProtocol, rays: RayBatch):
|
|
46
|
+
... result = detector.detect(rays)
|
|
47
|
+
... print(f"Detected {result.num_rays} rays")
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from numpy.typing import NDArray
|
|
56
|
+
|
|
57
|
+
from ..utilities.ray_data import RayBatch
|
|
58
|
+
from .results import DetectorResult
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class DetectorProtocol(Protocol):
|
|
63
|
+
"""
|
|
64
|
+
Protocol defining the interface for all detector implementations.
|
|
65
|
+
|
|
66
|
+
All detectors (small/point detectors and extended/surface detectors)
|
|
67
|
+
should implement this protocol for consistent behavior.
|
|
68
|
+
|
|
69
|
+
Attributes
|
|
70
|
+
----------
|
|
71
|
+
name : str
|
|
72
|
+
Human-readable detector name for identification
|
|
73
|
+
|
|
74
|
+
Methods
|
|
75
|
+
-------
|
|
76
|
+
detect(rays) -> DetectorResult
|
|
77
|
+
Detect rays and return results
|
|
78
|
+
clear()
|
|
79
|
+
Clear accumulated detection data
|
|
80
|
+
|
|
81
|
+
Examples
|
|
82
|
+
--------
|
|
83
|
+
>>> class MyDetector:
|
|
84
|
+
... def __init__(self, name: str = "My Detector"):
|
|
85
|
+
... self.name = name
|
|
86
|
+
... self._result = DetectorResult.empty(name)
|
|
87
|
+
...
|
|
88
|
+
... def detect(self, rays: RayBatch) -> DetectorResult:
|
|
89
|
+
... # Detection logic here
|
|
90
|
+
... return self._result
|
|
91
|
+
...
|
|
92
|
+
... def clear(self) -> None:
|
|
93
|
+
... self._result = DetectorResult.empty(self.name)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: str
|
|
97
|
+
|
|
98
|
+
def detect(self, rays: "RayBatch") -> "DetectorResult":
|
|
99
|
+
"""
|
|
100
|
+
Detect rays and return detection results.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
rays : RayBatch
|
|
105
|
+
Ray batch to test for detection
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
DetectorResult
|
|
110
|
+
Detection results containing all rays that hit this detector
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def clear(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Clear accumulated detection data.
|
|
117
|
+
|
|
118
|
+
Resets the detector to its initial state with no recorded detections.
|
|
119
|
+
"""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@runtime_checkable
|
|
124
|
+
class AccumulatingDetectorProtocol(DetectorProtocol, Protocol):
|
|
125
|
+
"""
|
|
126
|
+
Protocol for detectors that accumulate results over multiple detect() calls.
|
|
127
|
+
|
|
128
|
+
These detectors maintain internal state and can return cumulative results.
|
|
129
|
+
|
|
130
|
+
Attributes
|
|
131
|
+
----------
|
|
132
|
+
name : str
|
|
133
|
+
Human-readable detector name
|
|
134
|
+
accumulated_result : DetectorResult
|
|
135
|
+
All accumulated detections since last clear()
|
|
136
|
+
|
|
137
|
+
Examples
|
|
138
|
+
--------
|
|
139
|
+
>>> detector = SphericalDetector(center=(0, 0, 100), radius=10)
|
|
140
|
+
>>> result1 = detector.detect(rays1)
|
|
141
|
+
>>> result2 = detector.detect(rays2)
|
|
142
|
+
>>> total = detector.accumulated_result # Contains both result1 and result2
|
|
143
|
+
>>> detector.clear() # Reset accumulation
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
accumulated_result: "DetectorResult"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@runtime_checkable
|
|
150
|
+
class ExtendedDetectorProtocol(DetectorProtocol, Protocol):
|
|
151
|
+
"""
|
|
152
|
+
Protocol for extended (surface) detectors with additional geometric properties.
|
|
153
|
+
|
|
154
|
+
Extended detectors have a defined geometric surface and can provide
|
|
155
|
+
additional information about the detection geometry.
|
|
156
|
+
|
|
157
|
+
Attributes
|
|
158
|
+
----------
|
|
159
|
+
name : str
|
|
160
|
+
Human-readable detector name
|
|
161
|
+
sphere_radius : float
|
|
162
|
+
Radius of the detection sphere (for spherical detectors)
|
|
163
|
+
center : ndarray, shape (3,)
|
|
164
|
+
Center position of the detector
|
|
165
|
+
|
|
166
|
+
Examples
|
|
167
|
+
--------
|
|
168
|
+
>>> from lsurf.detectors import RecordingSphereDetector
|
|
169
|
+
>>> detector = RecordingSphereDetector(altitude=33000.0)
|
|
170
|
+
>>> print(detector.sphere_radius) # Earth radius + altitude
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def sphere_radius(self) -> float:
|
|
175
|
+
"""Radius of the detection sphere in meters."""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def center(self) -> "NDArray":
|
|
180
|
+
"""Center position of the detector."""
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Type alias for any detector
|
|
185
|
+
AnyDetector = Union[
|
|
186
|
+
DetectorProtocol, AccumulatingDetectorProtocol, ExtendedDetectorProtocol
|
|
187
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
Recording Sphere Detectors (Backward Compatibility)
|
|
36
|
+
|
|
37
|
+
This module re-exports recording sphere detectors from the new location
|
|
38
|
+
for backward compatibility. New code should import from:
|
|
39
|
+
lsurf.detectors or lsurf.detectors.extended
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Re-export from new locations for backward compatibility
|
|
43
|
+
from .extended.recording_sphere import RecordingSphereDetector
|
|
44
|
+
from .extended.local_sphere import LocalRecordingSphereDetector
|
|
45
|
+
|
|
46
|
+
# Keep RecordedRays import for backward compatibility
|
|
47
|
+
from ..utilities.recording_sphere import RecordedRays
|
|
48
|
+
|
|
49
|
+
# Backward compatibility base class (now just an alias)
|
|
50
|
+
RecordingSphereBase = RecordingSphereDetector
|
|
51
|
+
|
|
52
|
+
# Backward compatibility aliases
|
|
53
|
+
RecordingSphere = RecordingSphereDetector
|
|
54
|
+
LocalRecordingSphere = LocalRecordingSphereDetector
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"RecordingSphereBase",
|
|
58
|
+
"RecordingSphereDetector",
|
|
59
|
+
"LocalRecordingSphereDetector",
|
|
60
|
+
"RecordingSphere",
|
|
61
|
+
"LocalRecordingSphere",
|
|
62
|
+
"RecordedRays",
|
|
63
|
+
]
|