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,485 @@
|
|
|
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
|
+
Constant-Size Detector Rings with No Shadowing.
|
|
36
|
+
|
|
37
|
+
This module provides a detector ring geometry where each ring has constant
|
|
38
|
+
physical size (radial width) regardless of distance from origin. Adjacent rings
|
|
39
|
+
touch exactly (no shadowing) to provide complete angular coverage.
|
|
40
|
+
|
|
41
|
+
The detector centers lie on a sphere at fixed altitude above Earth's surface,
|
|
42
|
+
with normals pointing toward the origin (0,0,0).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from dataclasses import dataclass
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
from scipy.optimize import brentq
|
|
49
|
+
|
|
50
|
+
from ..surfaces import EARTH_RADIUS
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ConstantSizeDetectorRings:
|
|
55
|
+
"""
|
|
56
|
+
Constant-size detector ring geometry with no shadowing.
|
|
57
|
+
|
|
58
|
+
Creates a set of annular detector rings centered on a sphere at fixed
|
|
59
|
+
altitude above Earth's surface. Each ring has constant physical radial
|
|
60
|
+
width, with angular size varying based on distance from origin.
|
|
61
|
+
|
|
62
|
+
Adjacent rings touch exactly (no overlap, no gaps) when viewed from origin,
|
|
63
|
+
ensuring complete angular coverage with no shadowing.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
detector_radial_size : float
|
|
68
|
+
Physical radial width of each detector in meters (default: 10 km)
|
|
69
|
+
detector_altitude : float
|
|
70
|
+
Altitude of detector sphere above Earth's surface in meters (default: 33 km)
|
|
71
|
+
max_elevation_deg : float
|
|
72
|
+
Maximum elevation angle from horizontal (90° = zenith) (default: 90°)
|
|
73
|
+
min_elevation_deg : float
|
|
74
|
+
Minimum elevation angle, where to stop generating rings (default: -2°)
|
|
75
|
+
earth_radius : float
|
|
76
|
+
Earth radius in meters (default: EARTH_RADIUS constant)
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
ring_boundaries_deg : ndarray
|
|
81
|
+
Elevation angles of ring boundaries (N+1 values for N rings)
|
|
82
|
+
ring_centers_deg : ndarray
|
|
83
|
+
Elevation angles of ring centers (N values)
|
|
84
|
+
ring_distances : ndarray
|
|
85
|
+
Distances from origin to ring centers in meters (N values)
|
|
86
|
+
n_rings : int
|
|
87
|
+
Number of detector rings
|
|
88
|
+
detector_sphere_radius : float
|
|
89
|
+
Radius of detector sphere from Earth center in meters
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
>>> rings = ConstantSizeDetectorRings(
|
|
94
|
+
... detector_radial_size=10000.0, # 10 km
|
|
95
|
+
... detector_altitude=33000.0, # 33 km
|
|
96
|
+
... )
|
|
97
|
+
>>> print(f"Created {rings.n_rings} rings")
|
|
98
|
+
Created 17 rings
|
|
99
|
+
>>> print(f"Coverage: {rings.ring_boundaries_deg[-1]:.1f}° to {rings.ring_boundaries_deg[0]:.1f}°")
|
|
100
|
+
Coverage: -2.3° to 90.0°
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
detector_radial_size: float = 10000.0 # 10 km
|
|
104
|
+
detector_altitude: float = 33000.0 # 33 km
|
|
105
|
+
max_elevation_deg: float = 90.0 # Zenith
|
|
106
|
+
min_elevation_deg: float = -2.0 # Stop 2 deg below horizontal
|
|
107
|
+
earth_radius: float = EARTH_RADIUS
|
|
108
|
+
|
|
109
|
+
# Computed attributes (set in __post_init__)
|
|
110
|
+
ring_boundaries_deg: np.ndarray = None
|
|
111
|
+
ring_centers_deg: np.ndarray = None
|
|
112
|
+
ring_distances: np.ndarray = None
|
|
113
|
+
n_rings: int = 0
|
|
114
|
+
detector_sphere_radius: float = 0.0
|
|
115
|
+
|
|
116
|
+
def __post_init__(self):
|
|
117
|
+
"""Compute ring geometry after initialization."""
|
|
118
|
+
self.detector_sphere_radius = self.earth_radius + self.detector_altitude
|
|
119
|
+
self._compute_ring_geometry()
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def detector_half_width(self) -> float:
|
|
123
|
+
"""Physical half-width of each detector in meters."""
|
|
124
|
+
return self.detector_radial_size / 2
|
|
125
|
+
|
|
126
|
+
def distance_at_elevation(self, elev_deg: float) -> float:
|
|
127
|
+
"""
|
|
128
|
+
Compute distance from origin (0,0,0) to detector sphere at given elevation.
|
|
129
|
+
|
|
130
|
+
Uses the formula:
|
|
131
|
+
d(θ) = -sin(θ)·R_E + √(R_d² - R_E²·cos²(θ))
|
|
132
|
+
|
|
133
|
+
where R_E = Earth radius, R_d = detector sphere radius, θ = elevation angle.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
elev_deg : float
|
|
138
|
+
Elevation angle from horizontal in degrees (90° = zenith)
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
float
|
|
143
|
+
Distance from origin to intersection point (meters)
|
|
144
|
+
|
|
145
|
+
Raises
|
|
146
|
+
------
|
|
147
|
+
ValueError
|
|
148
|
+
If no intersection exists at the given elevation
|
|
149
|
+
"""
|
|
150
|
+
elev_rad = np.radians(elev_deg)
|
|
151
|
+
cos_e, sin_e = np.cos(elev_rad), np.sin(elev_rad)
|
|
152
|
+
discriminant = self.detector_sphere_radius**2 - self.earth_radius**2 * cos_e**2
|
|
153
|
+
if discriminant < 0:
|
|
154
|
+
raise ValueError(f"No intersection at elevation {elev_deg}°")
|
|
155
|
+
return -sin_e * self.earth_radius + np.sqrt(discriminant)
|
|
156
|
+
|
|
157
|
+
def point_at_elevation(
|
|
158
|
+
self, elev_deg: float, azimuth_deg: float = 0.0
|
|
159
|
+
) -> np.ndarray:
|
|
160
|
+
"""
|
|
161
|
+
Compute 3D point on detector sphere at given elevation and azimuth.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
elev_deg : float
|
|
166
|
+
Elevation angle from horizontal in degrees
|
|
167
|
+
azimuth_deg : float
|
|
168
|
+
Azimuth angle in degrees (0 = +x direction)
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
ndarray
|
|
173
|
+
(x, y, z) position in meters
|
|
174
|
+
"""
|
|
175
|
+
dist = self.distance_at_elevation(elev_deg)
|
|
176
|
+
elev_rad = np.radians(elev_deg)
|
|
177
|
+
az_rad = np.radians(azimuth_deg)
|
|
178
|
+
x = dist * np.cos(elev_rad) * np.cos(az_rad)
|
|
179
|
+
y = dist * np.cos(elev_rad) * np.sin(az_rad)
|
|
180
|
+
z = dist * np.sin(elev_rad)
|
|
181
|
+
return np.array([x, y, z])
|
|
182
|
+
|
|
183
|
+
def find_detector_center(self, theta_top_deg: float) -> float:
|
|
184
|
+
"""
|
|
185
|
+
Find detector center elevation such that top edge is at theta_top.
|
|
186
|
+
|
|
187
|
+
For a detector with physical half-width w and center at elevation θ_c,
|
|
188
|
+
the angular half-width as seen from origin is:
|
|
189
|
+
α = arctan(w / d(θ_c))
|
|
190
|
+
|
|
191
|
+
The top edge elevation is θ_top = θ_c + α.
|
|
192
|
+
|
|
193
|
+
This function solves for θ_c given θ_top using brentq root finding.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
theta_top_deg : float
|
|
198
|
+
Desired elevation angle of top edge (degrees)
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
float
|
|
203
|
+
Center elevation angle (degrees)
|
|
204
|
+
"""
|
|
205
|
+
hw = self.detector_half_width
|
|
206
|
+
|
|
207
|
+
def residual(theta_c_deg):
|
|
208
|
+
dist = self.distance_at_elevation(theta_c_deg)
|
|
209
|
+
alpha_deg = np.degrees(np.arctan(hw / dist))
|
|
210
|
+
return theta_top_deg - (theta_c_deg + alpha_deg)
|
|
211
|
+
|
|
212
|
+
# Search bounds: center must be below top edge
|
|
213
|
+
lower_bound = max(theta_top_deg - 45.0, -10.0)
|
|
214
|
+
upper_bound = theta_top_deg - 0.001
|
|
215
|
+
|
|
216
|
+
return brentq(residual, lower_bound, upper_bound)
|
|
217
|
+
|
|
218
|
+
def angular_width_at_ring(self, ring_index: int) -> float:
|
|
219
|
+
"""
|
|
220
|
+
Compute angular width of a ring as seen from origin.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
ring_index : int
|
|
225
|
+
Index of the ring (0 = nearest to zenith)
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
float
|
|
230
|
+
Angular width in degrees
|
|
231
|
+
"""
|
|
232
|
+
if ring_index < 0 or ring_index >= self.n_rings:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Ring index {ring_index} out of range [0, {self.n_rings})"
|
|
235
|
+
)
|
|
236
|
+
dist = self.ring_distances[ring_index]
|
|
237
|
+
return 2 * np.degrees(np.arctan(self.detector_half_width / dist))
|
|
238
|
+
|
|
239
|
+
def horizontal_distance_at_elevation(self, elev_deg: float) -> float:
|
|
240
|
+
"""
|
|
241
|
+
Compute horizontal distance from origin to detector at given elevation.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
elev_deg : float
|
|
246
|
+
Elevation angle in degrees
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
float
|
|
251
|
+
Horizontal distance in meters
|
|
252
|
+
"""
|
|
253
|
+
dist = self.distance_at_elevation(elev_deg)
|
|
254
|
+
return dist * np.cos(np.radians(elev_deg))
|
|
255
|
+
|
|
256
|
+
def get_ring_horizontal_distances(self) -> np.ndarray:
|
|
257
|
+
"""
|
|
258
|
+
Get horizontal distances for all ring boundaries.
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
ndarray
|
|
263
|
+
Horizontal distances in meters for each ring boundary
|
|
264
|
+
"""
|
|
265
|
+
return np.array(
|
|
266
|
+
[self.horizontal_distance_at_elevation(e) for e in self.ring_boundaries_deg]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _compute_ring_geometry(self):
|
|
270
|
+
"""
|
|
271
|
+
Build ring boundaries with constant physical size (no shadowing).
|
|
272
|
+
|
|
273
|
+
Algorithm:
|
|
274
|
+
1. Start at θ_boundary[0] = max_elevation_deg (zenith)
|
|
275
|
+
2. Find center θ_c such that top edge is at θ_boundary[i]
|
|
276
|
+
3. Compute bottom edge: θ_bottom = θ_c - α = 2·θ_c - θ_top
|
|
277
|
+
4. Set θ_boundary[i+1] = θ_bottom
|
|
278
|
+
5. Repeat until θ_boundary < min_elevation_deg
|
|
279
|
+
"""
|
|
280
|
+
boundaries = [self.max_elevation_deg]
|
|
281
|
+
centers = []
|
|
282
|
+
distances = []
|
|
283
|
+
|
|
284
|
+
current_top = self.max_elevation_deg
|
|
285
|
+
while current_top > self.min_elevation_deg:
|
|
286
|
+
# Find center such that top edge is at current_top
|
|
287
|
+
theta_c = self.find_detector_center(current_top)
|
|
288
|
+
dist_c = self.distance_at_elevation(theta_c)
|
|
289
|
+
|
|
290
|
+
# Angular half-width at this distance
|
|
291
|
+
alpha_deg = np.degrees(np.arctan(self.detector_half_width / dist_c))
|
|
292
|
+
|
|
293
|
+
# Bottom edge (no-shadowing: adjacent ring's top = this ring's bottom)
|
|
294
|
+
theta_bottom = theta_c - alpha_deg
|
|
295
|
+
|
|
296
|
+
centers.append(theta_c)
|
|
297
|
+
distances.append(dist_c)
|
|
298
|
+
boundaries.append(theta_bottom)
|
|
299
|
+
|
|
300
|
+
current_top = theta_bottom
|
|
301
|
+
|
|
302
|
+
self.ring_boundaries_deg = np.array(boundaries)
|
|
303
|
+
self.ring_centers_deg = np.array(centers)
|
|
304
|
+
self.ring_distances = np.array(distances)
|
|
305
|
+
self.n_rings = len(centers)
|
|
306
|
+
|
|
307
|
+
def summary(self) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Return a summary string of the detector ring configuration.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
str
|
|
314
|
+
Multi-line summary of configuration
|
|
315
|
+
"""
|
|
316
|
+
lines = [
|
|
317
|
+
"Constant-Size Detector Ring Configuration:",
|
|
318
|
+
f" Detector altitude: {self.detector_altitude/1000:.1f} km",
|
|
319
|
+
f" Sphere radius: {self.detector_sphere_radius/1000:.1f} km",
|
|
320
|
+
f" Detector radial size: {self.detector_radial_size/1000:.1f} km",
|
|
321
|
+
f" Elevation range: {self.ring_boundaries_deg[-1]:.2f}° to {self.ring_boundaries_deg[0]:.2f}°",
|
|
322
|
+
f" Number of rings: {self.n_rings}",
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
if self.n_rings > 0:
|
|
326
|
+
aw0 = self.angular_width_at_ring(0)
|
|
327
|
+
aw_last = self.angular_width_at_ring(self.n_rings - 1)
|
|
328
|
+
lines.extend(
|
|
329
|
+
[
|
|
330
|
+
f" Ring 0 (zenith): {self.ring_distances[0]/1000:.1f} km, {aw0:.2f}° angular width",
|
|
331
|
+
f" Ring {self.n_rings-1} (edge): {self.ring_distances[-1]/1000:.1f} km, {aw_last:.2f}° angular width",
|
|
332
|
+
]
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return "\n".join(lines)
|
|
336
|
+
|
|
337
|
+
def get_ring_info(self, ring_index: int) -> dict:
|
|
338
|
+
"""
|
|
339
|
+
Get detailed information about a specific ring.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
ring_index : int
|
|
344
|
+
Index of the ring
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
dict
|
|
349
|
+
Dictionary with ring properties
|
|
350
|
+
"""
|
|
351
|
+
if ring_index < 0 or ring_index >= self.n_rings:
|
|
352
|
+
raise ValueError(
|
|
353
|
+
f"Ring index {ring_index} out of range [0, {self.n_rings})"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"ring_index": ring_index,
|
|
358
|
+
"center_elevation_deg": self.ring_centers_deg[ring_index],
|
|
359
|
+
"top_elevation_deg": self.ring_boundaries_deg[ring_index],
|
|
360
|
+
"bottom_elevation_deg": self.ring_boundaries_deg[ring_index + 1],
|
|
361
|
+
"distance_m": self.ring_distances[ring_index],
|
|
362
|
+
"distance_km": self.ring_distances[ring_index] / 1000,
|
|
363
|
+
"angular_width_deg": self.angular_width_at_ring(ring_index),
|
|
364
|
+
"horizontal_distance_m": self.horizontal_distance_at_elevation(
|
|
365
|
+
self.ring_centers_deg[ring_index]
|
|
366
|
+
),
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def azimuth_bins_for_ring(
|
|
370
|
+
self, ring_index: int, az_bin_size_m: float, az_range_deg: float = 10.0
|
|
371
|
+
) -> tuple[int, np.ndarray, np.ndarray]:
|
|
372
|
+
"""
|
|
373
|
+
Compute azimuthal bins of constant physical size for a ring.
|
|
374
|
+
|
|
375
|
+
At each ring distance, computes how many bins of the given physical
|
|
376
|
+
width fit within the ±az_range azimuth range.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
ring_index : int
|
|
381
|
+
Index of the ring
|
|
382
|
+
az_bin_size_m : float
|
|
383
|
+
Physical azimuthal bin size in meters
|
|
384
|
+
az_range_deg : float
|
|
385
|
+
Azimuth range in degrees (±this value from beam direction)
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
n_bins : int
|
|
390
|
+
Number of azimuthal bins for this ring
|
|
391
|
+
az_edges_deg : ndarray
|
|
392
|
+
Azimuth bin edges in degrees (n_bins + 1 values)
|
|
393
|
+
az_centers_deg : ndarray
|
|
394
|
+
Azimuth bin centers in degrees (n_bins values)
|
|
395
|
+
"""
|
|
396
|
+
if ring_index < 0 or ring_index >= self.n_rings:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"Ring index {ring_index} out of range [0, {self.n_rings})"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
dist = self.ring_distances[ring_index]
|
|
402
|
+
|
|
403
|
+
# Arc length for full azimuth range at this distance
|
|
404
|
+
# Arc = distance * angle_radians (for small angles on a sphere from origin)
|
|
405
|
+
total_arc_m = dist * np.radians(2 * az_range_deg)
|
|
406
|
+
|
|
407
|
+
# Number of bins that fit
|
|
408
|
+
n_bins = max(1, int(np.round(total_arc_m / az_bin_size_m)))
|
|
409
|
+
|
|
410
|
+
# Compute bin edges in degrees
|
|
411
|
+
az_edges_deg = np.linspace(-az_range_deg, az_range_deg, n_bins + 1)
|
|
412
|
+
az_centers_deg = (az_edges_deg[:-1] + az_edges_deg[1:]) / 2
|
|
413
|
+
|
|
414
|
+
return n_bins, az_edges_deg, az_centers_deg
|
|
415
|
+
|
|
416
|
+
def get_constant_size_grid(
|
|
417
|
+
self, az_bin_size_m: float, az_range_deg: float = 10.0
|
|
418
|
+
) -> list[dict]:
|
|
419
|
+
"""
|
|
420
|
+
Get a grid of constant-size bins across all rings.
|
|
421
|
+
|
|
422
|
+
Each bin has approximately constant physical size:
|
|
423
|
+
- Radial size: detector_radial_size (same for all rings)
|
|
424
|
+
- Azimuthal size: az_bin_size_m (variable number of bins per ring)
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
az_bin_size_m : float
|
|
429
|
+
Physical azimuthal bin size in meters
|
|
430
|
+
az_range_deg : float
|
|
431
|
+
Azimuth range in degrees (±this value)
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
list of dict
|
|
436
|
+
List of bin specifications with keys:
|
|
437
|
+
- ring_idx: int
|
|
438
|
+
- az_bin_idx: int
|
|
439
|
+
- n_az_bins: int (total azimuth bins for this ring)
|
|
440
|
+
- az_lo_deg, az_hi_deg: float
|
|
441
|
+
- az_center_deg: float
|
|
442
|
+
- distance_m: float
|
|
443
|
+
- bin_area_m2: float (approximate)
|
|
444
|
+
"""
|
|
445
|
+
grid = []
|
|
446
|
+
|
|
447
|
+
for ring_idx in range(self.n_rings):
|
|
448
|
+
n_az_bins, az_edges, az_centers = self.azimuth_bins_for_ring(
|
|
449
|
+
ring_idx, az_bin_size_m, az_range_deg
|
|
450
|
+
)
|
|
451
|
+
dist = self.ring_distances[ring_idx]
|
|
452
|
+
|
|
453
|
+
# Approximate bin area (radial_size * azimuthal_arc)
|
|
454
|
+
az_width_rad = np.radians(az_edges[1] - az_edges[0])
|
|
455
|
+
az_arc_m = dist * az_width_rad
|
|
456
|
+
bin_area = self.detector_radial_size * az_arc_m
|
|
457
|
+
|
|
458
|
+
for az_bin_idx in range(n_az_bins):
|
|
459
|
+
grid.append(
|
|
460
|
+
{
|
|
461
|
+
"ring_idx": ring_idx,
|
|
462
|
+
"az_bin_idx": az_bin_idx,
|
|
463
|
+
"n_az_bins": n_az_bins,
|
|
464
|
+
"az_lo_deg": az_edges[az_bin_idx],
|
|
465
|
+
"az_hi_deg": az_edges[az_bin_idx + 1],
|
|
466
|
+
"az_center_deg": az_centers[az_bin_idx],
|
|
467
|
+
"distance_m": dist,
|
|
468
|
+
"elev_center_deg": self.ring_centers_deg[ring_idx],
|
|
469
|
+
"bin_area_m2": bin_area,
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return grid
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def create_default_detector_rings() -> ConstantSizeDetectorRings:
|
|
477
|
+
"""
|
|
478
|
+
Create detector rings with default parameters (10 km size, 33 km altitude).
|
|
479
|
+
|
|
480
|
+
Returns
|
|
481
|
+
-------
|
|
482
|
+
ConstantSizeDetectorRings
|
|
483
|
+
Detector ring configuration with default parameters
|
|
484
|
+
"""
|
|
485
|
+
return ConstantSizeDetectorRings()
|
|
@@ -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
|
+
Directional Detector Implementation (Backward Compatibility)
|
|
36
|
+
|
|
37
|
+
This module re-exports DirectionalDetector 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.directional import DirectionalDetector
|
|
44
|
+
|
|
45
|
+
__all__ = ["DirectionalDetector"]
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
Extended (Surface) Detectors Submodule
|
|
36
|
+
|
|
37
|
+
This submodule contains extended detectors that are large detection surfaces
|
|
38
|
+
(e.g., recording spheres) for capturing rays at altitude or across large areas.
|
|
39
|
+
|
|
40
|
+
Available Detectors
|
|
41
|
+
-------------------
|
|
42
|
+
RecordingSphereDetector
|
|
43
|
+
Spherical detection surface at altitude above Earth for global simulations.
|
|
44
|
+
LocalRecordingSphereDetector
|
|
45
|
+
Spherical detection surface centered at a point for local simulations.
|
|
46
|
+
|
|
47
|
+
Examples
|
|
48
|
+
--------
|
|
49
|
+
>>> from lsurf.detectors.extended import RecordingSphereDetector, LocalRecordingSphereDetector
|
|
50
|
+
>>>
|
|
51
|
+
>>> # Earth-scale: satellite at 33 km altitude
|
|
52
|
+
>>> earth_detector = RecordingSphereDetector(altitude=33000.0)
|
|
53
|
+
>>> result = earth_detector.detect(rays)
|
|
54
|
+
>>>
|
|
55
|
+
>>> # Local-scale: 33 km radius sphere at origin
|
|
56
|
+
>>> local_detector = LocalRecordingSphereDetector(radius=33000.0)
|
|
57
|
+
>>> result = local_detector.detect(rays)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from .recording_sphere import RecordingSphereDetector
|
|
61
|
+
from .local_sphere import LocalRecordingSphereDetector
|
|
62
|
+
|
|
63
|
+
# Backwards compatibility aliases
|
|
64
|
+
RecordingSphere = RecordingSphereDetector
|
|
65
|
+
LocalRecordingSphere = LocalRecordingSphereDetector
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"RecordingSphereDetector",
|
|
69
|
+
"LocalRecordingSphereDetector",
|
|
70
|
+
# Backwards compatibility
|
|
71
|
+
"RecordingSphere",
|
|
72
|
+
"LocalRecordingSphere",
|
|
73
|
+
]
|