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,450 @@
|
|
|
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
|
+
Spherical Detector Implementation
|
|
36
|
+
|
|
37
|
+
Provides a spherical detector that detects rays passing within a specified
|
|
38
|
+
radius of a center point. Ideal for collecting rays from all directions.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from lsurf.detectors.small import SphericalDetector
|
|
43
|
+
>>>
|
|
44
|
+
>>> detector = SphericalDetector(
|
|
45
|
+
... center=(0, 0, 100),
|
|
46
|
+
... radius=10.0,
|
|
47
|
+
... name="Far-field detector"
|
|
48
|
+
... )
|
|
49
|
+
>>> result = detector.detect(rays)
|
|
50
|
+
>>> print(f"Detected {result.num_rays} rays")
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
from ...utilities.ray_data import RayBatch
|
|
56
|
+
from ..base import DetectionEvent
|
|
57
|
+
from ..results import DetectorResult
|
|
58
|
+
|
|
59
|
+
# Check if GPU is available
|
|
60
|
+
try:
|
|
61
|
+
from numba import cuda
|
|
62
|
+
|
|
63
|
+
CUDA_AVAILABLE = cuda.is_available()
|
|
64
|
+
except ImportError:
|
|
65
|
+
CUDA_AVAILABLE = False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SphericalDetector:
|
|
69
|
+
"""
|
|
70
|
+
Spherical detector centered at a point.
|
|
71
|
+
|
|
72
|
+
Detects all rays that pass within a certain radius of the center point.
|
|
73
|
+
Good for collecting rays from all directions without directional bias.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
center : tuple of float
|
|
78
|
+
Center position (x, y, z) in meters.
|
|
79
|
+
radius : float
|
|
80
|
+
Detection radius in meters.
|
|
81
|
+
name : str, optional
|
|
82
|
+
Detector name. Default is "Spherical Detector".
|
|
83
|
+
use_gpu : bool, optional
|
|
84
|
+
Whether to use GPU acceleration when available. Default is True.
|
|
85
|
+
|
|
86
|
+
Attributes
|
|
87
|
+
----------
|
|
88
|
+
center : ndarray, shape (3,)
|
|
89
|
+
Detector center position.
|
|
90
|
+
radius : float
|
|
91
|
+
Detection radius.
|
|
92
|
+
use_gpu : bool
|
|
93
|
+
GPU acceleration flag.
|
|
94
|
+
name : str
|
|
95
|
+
Detector name.
|
|
96
|
+
accumulated_result : DetectorResult
|
|
97
|
+
All accumulated detections since last clear().
|
|
98
|
+
|
|
99
|
+
Notes
|
|
100
|
+
-----
|
|
101
|
+
Detection is based on the closest approach distance between each ray
|
|
102
|
+
and the center point. A ray is detected if this distance is less than
|
|
103
|
+
or equal to the detection radius.
|
|
104
|
+
|
|
105
|
+
The arrival time is computed assuming the ray travels through air
|
|
106
|
+
(n approx 1.0) from its current position to the detection point.
|
|
107
|
+
|
|
108
|
+
Examples
|
|
109
|
+
--------
|
|
110
|
+
>>> detector = SphericalDetector(
|
|
111
|
+
... center=(0, 0, 100),
|
|
112
|
+
... radius=10.0,
|
|
113
|
+
... use_gpu=True
|
|
114
|
+
... )
|
|
115
|
+
>>> result = detector.detect(reflected_rays)
|
|
116
|
+
>>> print(f"Detected {result.num_rays} rays")
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
center: tuple[float, float, float],
|
|
122
|
+
radius: float,
|
|
123
|
+
name: str = "Spherical Detector",
|
|
124
|
+
use_gpu: bool = True,
|
|
125
|
+
):
|
|
126
|
+
"""
|
|
127
|
+
Initialize spherical detector.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
center : tuple of float
|
|
132
|
+
Center position (x, y, z) in meters.
|
|
133
|
+
radius : float
|
|
134
|
+
Detection radius in meters.
|
|
135
|
+
name : str, optional
|
|
136
|
+
Detector name. Default is "Spherical Detector".
|
|
137
|
+
use_gpu : bool, optional
|
|
138
|
+
Whether to use GPU acceleration. Default is True.
|
|
139
|
+
"""
|
|
140
|
+
self.name = name
|
|
141
|
+
self.center = np.array(center, dtype=np.float32)
|
|
142
|
+
self.radius = radius
|
|
143
|
+
self.use_gpu = use_gpu
|
|
144
|
+
self._accumulated_result = DetectorResult.empty(name)
|
|
145
|
+
# Keep events list for backward compatibility
|
|
146
|
+
self._events: list[DetectionEvent] = []
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def accumulated_result(self) -> DetectorResult:
|
|
150
|
+
"""All accumulated detections since last clear()."""
|
|
151
|
+
return self._accumulated_result
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def events(self) -> list[DetectionEvent]:
|
|
155
|
+
"""
|
|
156
|
+
Backward compatibility: list of DetectionEvent objects.
|
|
157
|
+
|
|
158
|
+
For new code, use accumulated_result instead.
|
|
159
|
+
"""
|
|
160
|
+
return self._events
|
|
161
|
+
|
|
162
|
+
def detect(
|
|
163
|
+
self, rays: RayBatch, current_time: float = 0.0, accumulate: bool = True
|
|
164
|
+
) -> DetectorResult:
|
|
165
|
+
"""
|
|
166
|
+
Detect rays that pass within detection radius.
|
|
167
|
+
|
|
168
|
+
For each ray, find the closest approach to center. If within
|
|
169
|
+
radius, record a detection event.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
rays : RayBatch
|
|
174
|
+
Ray batch to test.
|
|
175
|
+
current_time : float, optional
|
|
176
|
+
Current simulation time. Default is 0.0.
|
|
177
|
+
accumulate : bool, optional
|
|
178
|
+
Whether to accumulate results. Default is True.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
DetectorResult
|
|
183
|
+
Newly detected rays.
|
|
184
|
+
"""
|
|
185
|
+
if rays is None or rays.num_rays == 0:
|
|
186
|
+
return DetectorResult.empty(self.name)
|
|
187
|
+
|
|
188
|
+
# Use GPU only if requested AND available
|
|
189
|
+
if self.use_gpu and CUDA_AVAILABLE:
|
|
190
|
+
result = self._detect_gpu(rays, current_time)
|
|
191
|
+
else:
|
|
192
|
+
result = self._detect_cpu(rays, current_time)
|
|
193
|
+
|
|
194
|
+
if accumulate:
|
|
195
|
+
self._accumulated_result = DetectorResult.merge(
|
|
196
|
+
[self._accumulated_result, result]
|
|
197
|
+
)
|
|
198
|
+
# Update events list for backward compatibility
|
|
199
|
+
self._events.extend(result.to_detection_events())
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def _detect_gpu(self, rays: RayBatch, current_time: float = 0.0) -> DetectorResult:
|
|
204
|
+
"""
|
|
205
|
+
GPU-accelerated detection.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
rays : RayBatch
|
|
210
|
+
Ray batch to test.
|
|
211
|
+
current_time : float
|
|
212
|
+
Current simulation time.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
DetectorResult
|
|
217
|
+
Newly detected rays.
|
|
218
|
+
"""
|
|
219
|
+
from ...propagation.detector_gpu import detect_spherical_gpu
|
|
220
|
+
|
|
221
|
+
hit_mask, hit_distances, hit_times = detect_spherical_gpu(
|
|
222
|
+
rays.positions.astype(np.float32),
|
|
223
|
+
rays.directions.astype(np.float32),
|
|
224
|
+
rays.active,
|
|
225
|
+
rays.accumulated_time.astype(np.float32),
|
|
226
|
+
rays.wavelengths.astype(np.float32),
|
|
227
|
+
rays.intensities.astype(np.float32),
|
|
228
|
+
self.center,
|
|
229
|
+
self.radius,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
hit_indices = np.where(hit_mask)[0]
|
|
233
|
+
if len(hit_indices) == 0:
|
|
234
|
+
return DetectorResult.empty(self.name)
|
|
235
|
+
|
|
236
|
+
# Compute closest points
|
|
237
|
+
dir_norms = rays.directions[hit_indices] / np.linalg.norm(
|
|
238
|
+
rays.directions[hit_indices], axis=1, keepdims=True
|
|
239
|
+
)
|
|
240
|
+
closest_points = (
|
|
241
|
+
rays.positions[hit_indices]
|
|
242
|
+
+ hit_distances[hit_indices, np.newaxis] * dir_norms
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return DetectorResult(
|
|
246
|
+
positions=closest_points.astype(np.float32),
|
|
247
|
+
directions=rays.directions[hit_indices].astype(np.float32),
|
|
248
|
+
times=hit_times[hit_indices].astype(np.float32),
|
|
249
|
+
intensities=rays.intensities[hit_indices].astype(np.float32),
|
|
250
|
+
wavelengths=rays.wavelengths[hit_indices].astype(np.float32),
|
|
251
|
+
ray_indices=hit_indices.astype(np.int32),
|
|
252
|
+
generations=(
|
|
253
|
+
rays.generations[hit_indices].astype(np.int32)
|
|
254
|
+
if rays.generations is not None
|
|
255
|
+
else None
|
|
256
|
+
),
|
|
257
|
+
polarization_vectors=(
|
|
258
|
+
rays.polarization_vector[hit_indices].astype(np.float32)
|
|
259
|
+
if rays.polarization_vector is not None
|
|
260
|
+
else None
|
|
261
|
+
),
|
|
262
|
+
detector_name=self.name,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _detect_cpu(self, rays: RayBatch, current_time: float = 0.0) -> DetectorResult:
|
|
266
|
+
"""
|
|
267
|
+
CPU detection implementation.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
rays : RayBatch
|
|
272
|
+
Ray batch to test.
|
|
273
|
+
current_time : float
|
|
274
|
+
Current simulation time.
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
DetectorResult
|
|
279
|
+
Newly detected rays.
|
|
280
|
+
"""
|
|
281
|
+
c = 299792458.0 # Speed of light in m/s
|
|
282
|
+
n = 1.0 # Refractive index of air
|
|
283
|
+
|
|
284
|
+
# Vectorized computation
|
|
285
|
+
active_mask = rays.active
|
|
286
|
+
if not np.any(active_mask):
|
|
287
|
+
return DetectorResult.empty(self.name)
|
|
288
|
+
|
|
289
|
+
origins = rays.positions[active_mask]
|
|
290
|
+
directions = rays.directions[active_mask]
|
|
291
|
+
active_indices = np.where(active_mask)[0]
|
|
292
|
+
|
|
293
|
+
# Vector from ray origin to sphere center
|
|
294
|
+
oc = self.center - origins
|
|
295
|
+
|
|
296
|
+
# Normalize directions
|
|
297
|
+
dir_norms = directions / np.linalg.norm(directions, axis=1, keepdims=True)
|
|
298
|
+
|
|
299
|
+
# Project onto ray direction
|
|
300
|
+
t_closest = np.sum(oc * dir_norms, axis=1)
|
|
301
|
+
|
|
302
|
+
# Only consider forward propagation
|
|
303
|
+
forward_mask = t_closest > 0
|
|
304
|
+
|
|
305
|
+
if not np.any(forward_mask):
|
|
306
|
+
return DetectorResult.empty(self.name)
|
|
307
|
+
|
|
308
|
+
# Find closest points on rays
|
|
309
|
+
closest_points = (
|
|
310
|
+
origins[forward_mask]
|
|
311
|
+
+ t_closest[forward_mask, np.newaxis] * dir_norms[forward_mask]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Distance to center
|
|
315
|
+
dists = np.linalg.norm(closest_points - self.center, axis=1)
|
|
316
|
+
|
|
317
|
+
# Check if within radius
|
|
318
|
+
hit_mask = dists <= self.radius
|
|
319
|
+
|
|
320
|
+
if not np.any(hit_mask):
|
|
321
|
+
return DetectorResult.empty(self.name)
|
|
322
|
+
|
|
323
|
+
# Get final indices
|
|
324
|
+
final_active_indices = active_indices[forward_mask][hit_mask]
|
|
325
|
+
final_closest_points = closest_points[hit_mask]
|
|
326
|
+
final_t_closest = t_closest[forward_mask][hit_mask]
|
|
327
|
+
|
|
328
|
+
# Compute arrival times
|
|
329
|
+
additional_times = final_t_closest * n / c
|
|
330
|
+
arrival_times = rays.accumulated_time[final_active_indices] + additional_times
|
|
331
|
+
|
|
332
|
+
return DetectorResult(
|
|
333
|
+
positions=final_closest_points.astype(np.float32),
|
|
334
|
+
directions=rays.directions[final_active_indices].astype(np.float32),
|
|
335
|
+
times=arrival_times.astype(np.float32),
|
|
336
|
+
intensities=rays.intensities[final_active_indices].astype(np.float32),
|
|
337
|
+
wavelengths=rays.wavelengths[final_active_indices].astype(np.float32),
|
|
338
|
+
ray_indices=final_active_indices.astype(np.int32),
|
|
339
|
+
generations=(
|
|
340
|
+
rays.generations[final_active_indices].astype(np.int32)
|
|
341
|
+
if rays.generations is not None
|
|
342
|
+
else None
|
|
343
|
+
),
|
|
344
|
+
polarization_vectors=(
|
|
345
|
+
rays.polarization_vector[final_active_indices].astype(np.float32)
|
|
346
|
+
if rays.polarization_vector is not None
|
|
347
|
+
else None
|
|
348
|
+
),
|
|
349
|
+
detector_name=self.name,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def clear(self) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Clear all recorded detections.
|
|
355
|
+
|
|
356
|
+
Resets the detector to its initial state with no recorded events.
|
|
357
|
+
"""
|
|
358
|
+
self._accumulated_result = DetectorResult.empty(self.name)
|
|
359
|
+
self._events = []
|
|
360
|
+
|
|
361
|
+
def __repr__(self) -> str:
|
|
362
|
+
"""Return string representation."""
|
|
363
|
+
return (
|
|
364
|
+
f"SphericalDetector(center={self.center.tolist()}, "
|
|
365
|
+
f"radius={self.radius}, rays={self._accumulated_result.num_rays})"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def __len__(self) -> int:
|
|
369
|
+
"""Return number of detected rays."""
|
|
370
|
+
return self._accumulated_result.num_rays
|
|
371
|
+
|
|
372
|
+
# Backward compatibility methods from old Detector base class
|
|
373
|
+
def get_arrival_times(self) -> np.ndarray:
|
|
374
|
+
"""
|
|
375
|
+
Get array of all arrival times.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
times : ndarray, shape (N,)
|
|
380
|
+
Arrival times in seconds for all detected rays.
|
|
381
|
+
"""
|
|
382
|
+
return self._accumulated_result.times.astype(np.float64)
|
|
383
|
+
|
|
384
|
+
def get_arrival_angles(self, reference_direction: np.ndarray) -> np.ndarray:
|
|
385
|
+
"""
|
|
386
|
+
Get angles between ray directions and reference direction.
|
|
387
|
+
|
|
388
|
+
Parameters
|
|
389
|
+
----------
|
|
390
|
+
reference_direction : ndarray, shape (3,)
|
|
391
|
+
Reference vector for angle calculation.
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
angles : ndarray, shape (N,)
|
|
396
|
+
Angles in radians.
|
|
397
|
+
"""
|
|
398
|
+
if self._accumulated_result.is_empty:
|
|
399
|
+
return np.array([], dtype=np.float64)
|
|
400
|
+
ref = reference_direction / np.linalg.norm(reference_direction)
|
|
401
|
+
dir_norms = self._accumulated_result.directions / np.linalg.norm(
|
|
402
|
+
self._accumulated_result.directions, axis=1, keepdims=True
|
|
403
|
+
)
|
|
404
|
+
cos_angles = np.dot(dir_norms, ref)
|
|
405
|
+
cos_angles = np.clip(cos_angles, -1.0, 1.0)
|
|
406
|
+
return np.arccos(cos_angles).astype(np.float64)
|
|
407
|
+
|
|
408
|
+
def get_intensities(self) -> np.ndarray:
|
|
409
|
+
"""
|
|
410
|
+
Get array of all detected intensities.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
intensities : ndarray, shape (N,)
|
|
415
|
+
Intensity values for all detected rays.
|
|
416
|
+
"""
|
|
417
|
+
return self._accumulated_result.intensities.astype(np.float64)
|
|
418
|
+
|
|
419
|
+
def get_wavelengths(self) -> np.ndarray:
|
|
420
|
+
"""
|
|
421
|
+
Get array of all detected wavelengths.
|
|
422
|
+
|
|
423
|
+
Returns
|
|
424
|
+
-------
|
|
425
|
+
wavelengths : ndarray, shape (N,)
|
|
426
|
+
Wavelengths in meters.
|
|
427
|
+
"""
|
|
428
|
+
return self._accumulated_result.wavelengths.astype(np.float64)
|
|
429
|
+
|
|
430
|
+
def get_positions(self) -> np.ndarray:
|
|
431
|
+
"""
|
|
432
|
+
Get array of all detection positions.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
positions : ndarray, shape (N, 3)
|
|
437
|
+
3D positions where rays were detected.
|
|
438
|
+
"""
|
|
439
|
+
return self._accumulated_result.positions.astype(np.float32)
|
|
440
|
+
|
|
441
|
+
def get_total_intensity(self) -> float:
|
|
442
|
+
"""
|
|
443
|
+
Get sum of all detected intensities.
|
|
444
|
+
|
|
445
|
+
Returns
|
|
446
|
+
-------
|
|
447
|
+
float
|
|
448
|
+
Total detected intensity.
|
|
449
|
+
"""
|
|
450
|
+
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
|
+
Spherical Detector Implementation (Backward Compatibility)
|
|
36
|
+
|
|
37
|
+
This module re-exports SphericalDetector 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.spherical import SphericalDetector
|
|
44
|
+
|
|
45
|
+
__all__ = ["SphericalDetector"]
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
Geometry Module
|
|
36
|
+
|
|
37
|
+
Provides a fluent interface for constructing simulation geometries
|
|
38
|
+
with named media for material consistency.
|
|
39
|
+
|
|
40
|
+
Classes
|
|
41
|
+
-------
|
|
42
|
+
GeometryBuilder
|
|
43
|
+
Fluent builder for constructing geometries with any GPUSurface.
|
|
44
|
+
Geometry
|
|
45
|
+
Immutable container for the built geometry.
|
|
46
|
+
|
|
47
|
+
Functions
|
|
48
|
+
---------
|
|
49
|
+
fibonacci_sphere_points
|
|
50
|
+
Generate uniformly distributed points on unit sphere.
|
|
51
|
+
fibonacci_cone_points
|
|
52
|
+
Generate uniformly distributed points within a cone.
|
|
53
|
+
create_planar_detector_array
|
|
54
|
+
Create array of bounded planar detectors at specified altitude.
|
|
55
|
+
create_grid_detector_array
|
|
56
|
+
Create rectangular grid of planar detectors.
|
|
57
|
+
create_optimized_detector_grid
|
|
58
|
+
Create optimal detector grid based on observed ray positions.
|
|
59
|
+
compute_footprint_statistics
|
|
60
|
+
Compute statistics about the spatial footprint of detected rays.
|
|
61
|
+
create_ring_detector_array
|
|
62
|
+
Create concentric annular ring detectors in a flat plane.
|
|
63
|
+
create_sphere_patch_detectors
|
|
64
|
+
Create detector patches on a sphere centered at origin.
|
|
65
|
+
bin_rays_by_ring_and_azimuth
|
|
66
|
+
Bin detected rays into (ring, azimuth) segments.
|
|
67
|
+
bin_rays_by_patch
|
|
68
|
+
Bin detected rays by which patch detector they hit.
|
|
69
|
+
compute_ring_summary
|
|
70
|
+
Compute summary statistics across all ring segments.
|
|
71
|
+
|
|
72
|
+
Examples
|
|
73
|
+
--------
|
|
74
|
+
>>> from lsurf.geometry import GeometryBuilder
|
|
75
|
+
>>> from lsurf.materials import WATER, ExponentialAtmosphere
|
|
76
|
+
>>> from lsurf.surfaces import SphereSurface, PlaneSurface, SurfaceRole
|
|
77
|
+
>>>
|
|
78
|
+
>>> EARTH_RADIUS = 6.371e6
|
|
79
|
+
>>> atmosphere = ExponentialAtmosphere()
|
|
80
|
+
>>>
|
|
81
|
+
>>> # Create surfaces without specifying materials (assigned via media)
|
|
82
|
+
>>> ocean = SphereSurface(
|
|
83
|
+
... center=(0, 0, -EARTH_RADIUS),
|
|
84
|
+
... radius=EARTH_RADIUS,
|
|
85
|
+
... role=SurfaceRole.OPTICAL,
|
|
86
|
+
... name="ocean",
|
|
87
|
+
... )
|
|
88
|
+
>>> detector = PlaneSurface(
|
|
89
|
+
... point=(0, 0, 35000),
|
|
90
|
+
... normal=(0, 0, 1),
|
|
91
|
+
... role=SurfaceRole.DETECTOR,
|
|
92
|
+
... name="detector_35km",
|
|
93
|
+
... )
|
|
94
|
+
>>>
|
|
95
|
+
>>> # Register media first, then assign to surfaces
|
|
96
|
+
>>> geometry = (
|
|
97
|
+
... GeometryBuilder()
|
|
98
|
+
... .register_medium("atmosphere", atmosphere)
|
|
99
|
+
... .register_medium("ocean", WATER)
|
|
100
|
+
... .set_background("atmosphere")
|
|
101
|
+
... .add_surface(ocean, front="atmosphere", back="ocean")
|
|
102
|
+
... .add_detector(detector)
|
|
103
|
+
... .build()
|
|
104
|
+
... )
|
|
105
|
+
>>>
|
|
106
|
+
>>> # Use with SurfacePropagator
|
|
107
|
+
>>> from lsurf.propagation.propagators import SurfacePropagator
|
|
108
|
+
>>> propagator = SurfacePropagator(
|
|
109
|
+
... material=geometry.background_material,
|
|
110
|
+
... surfaces=geometry.to_surface_list(),
|
|
111
|
+
... )
|
|
112
|
+
|
|
113
|
+
>>> # Create planar detector array
|
|
114
|
+
>>> from lsurf.geometry import create_planar_detector_array
|
|
115
|
+
>>> detectors = create_planar_detector_array(
|
|
116
|
+
... n_detectors=100,
|
|
117
|
+
... altitude=33000.0,
|
|
118
|
+
... edge_length=100.0,
|
|
119
|
+
... cone_half_angle_deg=30.0,
|
|
120
|
+
... )
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
from .builder import GeometryBuilder
|
|
124
|
+
from .geometry import Geometry
|
|
125
|
+
from .cell import Cell, HalfSpace, create_cell, create_half_space
|
|
126
|
+
from .cell_geometry import CellGeometry
|
|
127
|
+
from .validation import GeometryValidationError, IntersectingSurfacesError
|
|
128
|
+
from .surface_analysis import (
|
|
129
|
+
SurfaceRelationship,
|
|
130
|
+
SurfaceAnalysisResult,
|
|
131
|
+
are_planes_parallel,
|
|
132
|
+
are_spheres_concentric,
|
|
133
|
+
analyze_surface_pair,
|
|
134
|
+
)
|
|
135
|
+
from .detector_arrays import (
|
|
136
|
+
fibonacci_sphere_points,
|
|
137
|
+
fibonacci_cone_points,
|
|
138
|
+
create_planar_detector_array,
|
|
139
|
+
create_grid_detector_array,
|
|
140
|
+
create_optimized_detector_grid,
|
|
141
|
+
compute_footprint_statistics,
|
|
142
|
+
# Ring detector arrays
|
|
143
|
+
create_ring_detector_array,
|
|
144
|
+
create_sphere_patch_detectors,
|
|
145
|
+
# Ray binning (flat plane rings)
|
|
146
|
+
RingSegmentStats,
|
|
147
|
+
PatchStats,
|
|
148
|
+
bin_rays_by_ring_and_azimuth,
|
|
149
|
+
bin_rays_by_patch,
|
|
150
|
+
compute_ring_summary,
|
|
151
|
+
# Ray binning (spherical arc rings)
|
|
152
|
+
SphericalRingSegmentStats,
|
|
153
|
+
bin_rays_by_spherical_arc_rings,
|
|
154
|
+
compute_spherical_ring_summary,
|
|
155
|
+
# Ray binning (elevation rings - no shadowing)
|
|
156
|
+
bin_rays_by_elevation_rings,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"GeometryBuilder",
|
|
161
|
+
"Geometry",
|
|
162
|
+
# Cell-based geometry
|
|
163
|
+
"Cell",
|
|
164
|
+
"HalfSpace",
|
|
165
|
+
"CellGeometry",
|
|
166
|
+
"create_cell",
|
|
167
|
+
"create_half_space",
|
|
168
|
+
# Validation
|
|
169
|
+
"GeometryValidationError",
|
|
170
|
+
"IntersectingSurfacesError",
|
|
171
|
+
# Surface analysis
|
|
172
|
+
"SurfaceRelationship",
|
|
173
|
+
"SurfaceAnalysisResult",
|
|
174
|
+
"are_planes_parallel",
|
|
175
|
+
"are_spheres_concentric",
|
|
176
|
+
"analyze_surface_pair",
|
|
177
|
+
# Detector arrays
|
|
178
|
+
"fibonacci_sphere_points",
|
|
179
|
+
"fibonacci_cone_points",
|
|
180
|
+
"create_planar_detector_array",
|
|
181
|
+
"create_grid_detector_array",
|
|
182
|
+
"create_optimized_detector_grid",
|
|
183
|
+
"compute_footprint_statistics",
|
|
184
|
+
# Ring detector arrays
|
|
185
|
+
"create_ring_detector_array",
|
|
186
|
+
"create_sphere_patch_detectors",
|
|
187
|
+
# Ray binning (flat plane rings)
|
|
188
|
+
"RingSegmentStats",
|
|
189
|
+
"PatchStats",
|
|
190
|
+
"bin_rays_by_ring_and_azimuth",
|
|
191
|
+
"bin_rays_by_patch",
|
|
192
|
+
"compute_ring_summary",
|
|
193
|
+
# Ray binning (spherical arc rings)
|
|
194
|
+
"SphericalRingSegmentStats",
|
|
195
|
+
"bin_rays_by_spherical_arc_rings",
|
|
196
|
+
"compute_spherical_ring_summary",
|
|
197
|
+
# Ray binning (elevation rings - no shadowing)
|
|
198
|
+
"bin_rays_by_elevation_rings",
|
|
199
|
+
]
|