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,266 @@
|
|
|
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
|
+
Detectors Module - Ray Collection and Measurement
|
|
36
|
+
|
|
37
|
+
This module provides detector classes for measuring ray arrival times,
|
|
38
|
+
positions, angles, and intensities. Useful for simulating experimental
|
|
39
|
+
measurements.
|
|
40
|
+
|
|
41
|
+
Module Structure
|
|
42
|
+
----------------
|
|
43
|
+
small/
|
|
44
|
+
Point detectors (SphericalDetector, PlanarDetector, DirectionalDetector)
|
|
45
|
+
extended/
|
|
46
|
+
Surface detectors (RecordingSphereDetector, LocalRecordingSphereDetector)
|
|
47
|
+
|
|
48
|
+
Primary Classes
|
|
49
|
+
---------------
|
|
50
|
+
DetectorResult : dataclass
|
|
51
|
+
Unified bulk numpy-based result container. Primary return type for
|
|
52
|
+
all detectors and simulations.
|
|
53
|
+
DetectorProtocol : Protocol
|
|
54
|
+
Protocol defining the detector interface.
|
|
55
|
+
|
|
56
|
+
Available Detectors
|
|
57
|
+
-------------------
|
|
58
|
+
SphericalDetector
|
|
59
|
+
Spherical detector for omnidirectional collection.
|
|
60
|
+
PlanarDetector
|
|
61
|
+
Rectangular planar detector for imaging.
|
|
62
|
+
DirectionalDetector
|
|
63
|
+
Detector with angular acceptance cone.
|
|
64
|
+
RecordingSphereDetector
|
|
65
|
+
Spherical surface at altitude for Earth-scale simulations.
|
|
66
|
+
LocalRecordingSphereDetector
|
|
67
|
+
Spherical surface centered at origin for local simulations.
|
|
68
|
+
|
|
69
|
+
Analysis Functions
|
|
70
|
+
------------------
|
|
71
|
+
compute_angular_distribution
|
|
72
|
+
Histogram of ray arrival angles.
|
|
73
|
+
compute_time_distribution
|
|
74
|
+
Histogram of ray arrival times.
|
|
75
|
+
compute_intensity_distribution
|
|
76
|
+
Histogram of detected intensities.
|
|
77
|
+
compute_wavelength_distribution
|
|
78
|
+
Histogram of detected wavelengths.
|
|
79
|
+
compute_statistics
|
|
80
|
+
Summary statistics for detection events.
|
|
81
|
+
|
|
82
|
+
Interface
|
|
83
|
+
---------
|
|
84
|
+
All detectors implement the detect() method:
|
|
85
|
+
|
|
86
|
+
>>> result = detector.detect(rays)
|
|
87
|
+
|
|
88
|
+
Returns a DetectorResult object with bulk numpy arrays.
|
|
89
|
+
For backward compatibility, result.to_detection_events() returns
|
|
90
|
+
a list of DetectionEvent objects.
|
|
91
|
+
|
|
92
|
+
Examples
|
|
93
|
+
--------
|
|
94
|
+
>>> from lsurf.detectors import SphericalDetector, DetectorResult
|
|
95
|
+
>>>
|
|
96
|
+
>>> # Create a spherical detector at 100m altitude
|
|
97
|
+
>>> sphere = SphericalDetector(
|
|
98
|
+
... center=(0, 0, 100),
|
|
99
|
+
... radius=10.0,
|
|
100
|
+
... name="Far-field detector"
|
|
101
|
+
... )
|
|
102
|
+
>>> result = sphere.detect(reflected_rays)
|
|
103
|
+
>>> print(f"Detected {result.num_rays} rays")
|
|
104
|
+
>>> print(f"Total intensity: {result.total_intensity:.3e}")
|
|
105
|
+
>>>
|
|
106
|
+
>>> # For backward compatibility
|
|
107
|
+
>>> events = result.to_detection_events()
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# GPU functions are loaded lazily to avoid circular import issues
|
|
111
|
+
_detect_multi_spherical_gpu = None
|
|
112
|
+
_gpu_loaded = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_gpu_functions():
|
|
116
|
+
"""Lazily load GPU functions to avoid circular import issues."""
|
|
117
|
+
global _detect_multi_spherical_gpu, _gpu_loaded
|
|
118
|
+
if _gpu_loaded:
|
|
119
|
+
return
|
|
120
|
+
_gpu_loaded = True
|
|
121
|
+
try:
|
|
122
|
+
from ..propagation.detector_gpu import (
|
|
123
|
+
detect_multi_spherical_gpu,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
_detect_multi_spherical_gpu = detect_multi_spherical_gpu
|
|
127
|
+
except ImportError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Import primary result class
|
|
132
|
+
from .results import DetectorResult
|
|
133
|
+
|
|
134
|
+
# Import protocol
|
|
135
|
+
from .protocol import (
|
|
136
|
+
DetectorProtocol,
|
|
137
|
+
AccumulatingDetectorProtocol,
|
|
138
|
+
ExtendedDetectorProtocol,
|
|
139
|
+
AnyDetector,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Import base classes (for backward compatibility)
|
|
143
|
+
from .base import DetectionEvent, Detector
|
|
144
|
+
|
|
145
|
+
# Import analysis functions
|
|
146
|
+
from .analysis import (
|
|
147
|
+
compute_angular_distribution,
|
|
148
|
+
compute_intensity_distribution,
|
|
149
|
+
compute_statistics,
|
|
150
|
+
compute_time_distribution,
|
|
151
|
+
compute_wavelength_distribution,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Import small detectors
|
|
155
|
+
from .small import (
|
|
156
|
+
SphericalDetector,
|
|
157
|
+
PlanarDetector,
|
|
158
|
+
DirectionalDetector,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Import extended detectors
|
|
162
|
+
from .extended import (
|
|
163
|
+
RecordingSphereDetector,
|
|
164
|
+
LocalRecordingSphereDetector,
|
|
165
|
+
RecordingSphere,
|
|
166
|
+
LocalRecordingSphere,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Import constant-size detector rings
|
|
170
|
+
from .constant_size_rings import (
|
|
171
|
+
ConstantSizeDetectorRings,
|
|
172
|
+
create_default_detector_rings,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Keep backward compatibility imports from old locations
|
|
176
|
+
# These are aliases pointing to the new locations
|
|
177
|
+
RecordingSphereBase = RecordingSphereDetector # Base class no longer needed
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def detect_multi_position_gpu(
|
|
181
|
+
rays, detector_centers, detector_radius, threads_per_block=256
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
GPU-accelerated detection across multiple spherical detectors.
|
|
185
|
+
|
|
186
|
+
Wrapper function that accepts a RayBatch object for backward compatibility.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
rays : RayBatch
|
|
191
|
+
Ray batch to detect.
|
|
192
|
+
detector_centers : ndarray, shape (M, 3)
|
|
193
|
+
Detector center positions.
|
|
194
|
+
detector_radius : float
|
|
195
|
+
Detector radius (same for all).
|
|
196
|
+
threads_per_block : int, optional
|
|
197
|
+
CUDA threads per block. Default is 256.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
hit_counts : ndarray, shape (M,)
|
|
202
|
+
Number of rays hitting each detector.
|
|
203
|
+
hit_intensities : ndarray, shape (M,)
|
|
204
|
+
Sum of intensities for rays hitting each detector.
|
|
205
|
+
"""
|
|
206
|
+
import numpy as np
|
|
207
|
+
|
|
208
|
+
_load_gpu_functions()
|
|
209
|
+
if _detect_multi_spherical_gpu is None:
|
|
210
|
+
raise ImportError("GPU detection functions not available")
|
|
211
|
+
|
|
212
|
+
return _detect_multi_spherical_gpu(
|
|
213
|
+
ray_positions=rays.positions.astype(np.float32),
|
|
214
|
+
ray_directions=rays.directions.astype(np.float32),
|
|
215
|
+
ray_active=rays.active,
|
|
216
|
+
ray_times=rays.accumulated_time.astype(np.float32),
|
|
217
|
+
ray_intensities=rays.intensities.astype(np.float32),
|
|
218
|
+
detector_centers=detector_centers.astype(np.float32),
|
|
219
|
+
detector_radius=float(detector_radius),
|
|
220
|
+
threads_per_block=threads_per_block,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def detect_multi_spherical_gpu(*args, **kwargs):
|
|
225
|
+
"""GPU-accelerated multi-detector detection. See detect_multi_position_gpu."""
|
|
226
|
+
_load_gpu_functions()
|
|
227
|
+
if _detect_multi_spherical_gpu is None:
|
|
228
|
+
raise ImportError("GPU detection functions not available")
|
|
229
|
+
return _detect_multi_spherical_gpu(*args, **kwargs)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
__all__ = [
|
|
233
|
+
# Primary result class
|
|
234
|
+
"DetectorResult",
|
|
235
|
+
# Protocol
|
|
236
|
+
"DetectorProtocol",
|
|
237
|
+
"AccumulatingDetectorProtocol",
|
|
238
|
+
"ExtendedDetectorProtocol",
|
|
239
|
+
"AnyDetector",
|
|
240
|
+
# Base classes (backward compatibility)
|
|
241
|
+
"Detector",
|
|
242
|
+
"DetectionEvent",
|
|
243
|
+
# Small detectors
|
|
244
|
+
"SphericalDetector",
|
|
245
|
+
"PlanarDetector",
|
|
246
|
+
"DirectionalDetector",
|
|
247
|
+
# Extended detectors
|
|
248
|
+
"RecordingSphereDetector",
|
|
249
|
+
"LocalRecordingSphereDetector",
|
|
250
|
+
# Backward compatibility aliases
|
|
251
|
+
"RecordingSphereBase",
|
|
252
|
+
"RecordingSphere",
|
|
253
|
+
"LocalRecordingSphere",
|
|
254
|
+
# Analysis functions
|
|
255
|
+
"compute_angular_distribution",
|
|
256
|
+
"compute_time_distribution",
|
|
257
|
+
"compute_intensity_distribution",
|
|
258
|
+
"compute_wavelength_distribution",
|
|
259
|
+
"compute_statistics",
|
|
260
|
+
# GPU functions
|
|
261
|
+
"detect_multi_spherical_gpu",
|
|
262
|
+
"detect_multi_position_gpu",
|
|
263
|
+
# Constant-size detector rings
|
|
264
|
+
"ConstantSizeDetectorRings",
|
|
265
|
+
"create_default_detector_rings",
|
|
266
|
+
]
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
Detector Analysis Utilities
|
|
36
|
+
|
|
37
|
+
Provides utility functions for analyzing detection events, including
|
|
38
|
+
angular and temporal distribution histograms.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from surface_roughness.detectors import (
|
|
43
|
+
... compute_angular_distribution,
|
|
44
|
+
... compute_time_distribution
|
|
45
|
+
... )
|
|
46
|
+
>>>
|
|
47
|
+
>>> angles, counts = compute_angular_distribution(
|
|
48
|
+
... detector.events,
|
|
49
|
+
... reference_direction=np.array([0, 0, 1])
|
|
50
|
+
... )
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import numpy as np
|
|
54
|
+
from numpy.typing import NDArray
|
|
55
|
+
|
|
56
|
+
from .base import DetectionEvent
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compute_angular_distribution(
|
|
60
|
+
events: list[DetectionEvent],
|
|
61
|
+
reference_direction: NDArray[np.float32],
|
|
62
|
+
num_bins: int = 50,
|
|
63
|
+
) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
|
|
64
|
+
"""
|
|
65
|
+
Compute angular distribution histogram.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
events : list of DetectionEvent
|
|
70
|
+
Detection events to analyze.
|
|
71
|
+
reference_direction : ndarray, shape (3,)
|
|
72
|
+
Reference direction for angle calculation.
|
|
73
|
+
num_bins : int, optional
|
|
74
|
+
Number of histogram bins. Default is 50.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
bin_centers : ndarray, shape (num_bins,)
|
|
79
|
+
Bin centers in degrees.
|
|
80
|
+
counts : ndarray, shape (num_bins,)
|
|
81
|
+
Number of events in each bin.
|
|
82
|
+
|
|
83
|
+
Examples
|
|
84
|
+
--------
|
|
85
|
+
>>> angles, counts = compute_angular_distribution(
|
|
86
|
+
... detector.events,
|
|
87
|
+
... reference_direction=np.array([0, 0, 1]),
|
|
88
|
+
... num_bins=90 # 2-degree bins
|
|
89
|
+
... )
|
|
90
|
+
>>> import matplotlib.pyplot as plt
|
|
91
|
+
>>> plt.bar(angles, counts, width=2)
|
|
92
|
+
>>> plt.xlabel('Angle (degrees)')
|
|
93
|
+
>>> plt.ylabel('Count')
|
|
94
|
+
"""
|
|
95
|
+
if len(events) == 0:
|
|
96
|
+
return np.array([], dtype=np.float64), np.array([], dtype=np.int64)
|
|
97
|
+
|
|
98
|
+
ref = reference_direction / np.linalg.norm(reference_direction)
|
|
99
|
+
angles = []
|
|
100
|
+
|
|
101
|
+
for event in events:
|
|
102
|
+
dir_norm = event.direction / np.linalg.norm(event.direction)
|
|
103
|
+
cos_angle = np.dot(dir_norm, ref)
|
|
104
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
105
|
+
angles.append(np.degrees(np.arccos(cos_angle)))
|
|
106
|
+
|
|
107
|
+
angles = np.array(angles)
|
|
108
|
+
counts, bin_edges = np.histogram(angles, bins=num_bins, range=(0, 180))
|
|
109
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
110
|
+
|
|
111
|
+
return bin_centers, counts
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compute_time_distribution(
|
|
115
|
+
events: list[DetectionEvent],
|
|
116
|
+
num_bins: int = 50,
|
|
117
|
+
time_range: tuple[float, float] | None = None,
|
|
118
|
+
) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
|
|
119
|
+
"""
|
|
120
|
+
Compute arrival time distribution histogram.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
events : list of DetectionEvent
|
|
125
|
+
Detection events to analyze.
|
|
126
|
+
num_bins : int, optional
|
|
127
|
+
Number of histogram bins. Default is 50.
|
|
128
|
+
time_range : tuple of float, optional
|
|
129
|
+
(min, max) time range for histogram. If None, uses data range.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
bin_centers : ndarray, shape (N,)
|
|
134
|
+
Bin centers in seconds.
|
|
135
|
+
counts : ndarray, shape (N,)
|
|
136
|
+
Number of events in each bin.
|
|
137
|
+
|
|
138
|
+
Notes
|
|
139
|
+
-----
|
|
140
|
+
Automatically handles cases where all times are very similar by
|
|
141
|
+
reducing the number of bins to avoid empty histograms.
|
|
142
|
+
|
|
143
|
+
Examples
|
|
144
|
+
--------
|
|
145
|
+
>>> times, counts = compute_time_distribution(
|
|
146
|
+
... detector.events,
|
|
147
|
+
... num_bins=100
|
|
148
|
+
... )
|
|
149
|
+
>>> print(f"Time spread: {times.max() - times.min():.3e} s")
|
|
150
|
+
"""
|
|
151
|
+
if len(events) == 0:
|
|
152
|
+
return np.array([], dtype=np.float64), np.array([], dtype=np.int64)
|
|
153
|
+
|
|
154
|
+
times = np.array([e.time for e in events])
|
|
155
|
+
|
|
156
|
+
if time_range is None:
|
|
157
|
+
time_range = (times.min(), times.max())
|
|
158
|
+
|
|
159
|
+
# Handle case where all times are very similar
|
|
160
|
+
time_span = time_range[1] - time_range[0]
|
|
161
|
+
if time_span < 1e-12: # Less than 1 picosecond spread
|
|
162
|
+
return np.array([times.mean()]), np.array([len(times)])
|
|
163
|
+
|
|
164
|
+
# Adjust bins if time span is very small
|
|
165
|
+
effective_bins = min(num_bins, max(1, int(time_span * 1e12)))
|
|
166
|
+
|
|
167
|
+
counts, bin_edges = np.histogram(times, bins=effective_bins, range=time_range)
|
|
168
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
169
|
+
|
|
170
|
+
return bin_centers, counts
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def compute_intensity_distribution(
|
|
174
|
+
events: list[DetectionEvent],
|
|
175
|
+
num_bins: int = 50,
|
|
176
|
+
) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
|
|
177
|
+
"""
|
|
178
|
+
Compute intensity distribution histogram.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
events : list of DetectionEvent
|
|
183
|
+
Detection events to analyze.
|
|
184
|
+
num_bins : int, optional
|
|
185
|
+
Number of histogram bins. Default is 50.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
bin_centers : ndarray, shape (num_bins,)
|
|
190
|
+
Bin centers (intensity values).
|
|
191
|
+
counts : ndarray, shape (num_bins,)
|
|
192
|
+
Number of events in each bin.
|
|
193
|
+
"""
|
|
194
|
+
if len(events) == 0:
|
|
195
|
+
return np.array([], dtype=np.float64), np.array([], dtype=np.int64)
|
|
196
|
+
|
|
197
|
+
intensities = np.array([e.intensity for e in events])
|
|
198
|
+
counts, bin_edges = np.histogram(intensities, bins=num_bins)
|
|
199
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
200
|
+
|
|
201
|
+
return bin_centers, counts
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def compute_wavelength_distribution(
|
|
205
|
+
events: list[DetectionEvent],
|
|
206
|
+
num_bins: int = 50,
|
|
207
|
+
) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
|
|
208
|
+
"""
|
|
209
|
+
Compute wavelength distribution histogram.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
events : list of DetectionEvent
|
|
214
|
+
Detection events to analyze.
|
|
215
|
+
num_bins : int, optional
|
|
216
|
+
Number of histogram bins. Default is 50.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
bin_centers : ndarray, shape (num_bins,)
|
|
221
|
+
Bin centers in meters.
|
|
222
|
+
counts : ndarray, shape (num_bins,)
|
|
223
|
+
Number of events in each bin.
|
|
224
|
+
"""
|
|
225
|
+
if len(events) == 0:
|
|
226
|
+
return np.array([], dtype=np.float64), np.array([], dtype=np.int64)
|
|
227
|
+
|
|
228
|
+
wavelengths = np.array([e.wavelength for e in events])
|
|
229
|
+
counts, bin_edges = np.histogram(wavelengths, bins=num_bins)
|
|
230
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
231
|
+
|
|
232
|
+
return bin_centers, counts
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def compute_statistics(events: list[DetectionEvent]) -> dict:
|
|
236
|
+
"""
|
|
237
|
+
Compute summary statistics for detection events.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
events : list of DetectionEvent
|
|
242
|
+
Detection events to analyze.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
stats : dict
|
|
247
|
+
Dictionary containing:
|
|
248
|
+
- count: number of events
|
|
249
|
+
- total_intensity: sum of intensities
|
|
250
|
+
- mean_time: average arrival time
|
|
251
|
+
- std_time: arrival time standard deviation
|
|
252
|
+
- min_time: earliest arrival
|
|
253
|
+
- max_time: latest arrival
|
|
254
|
+
- mean_wavelength: average wavelength
|
|
255
|
+
- time_spread: max_time - min_time
|
|
256
|
+
|
|
257
|
+
Examples
|
|
258
|
+
--------
|
|
259
|
+
>>> stats = compute_statistics(detector.events)
|
|
260
|
+
>>> print(f"Detected {stats['count']} rays")
|
|
261
|
+
>>> print(f"Total intensity: {stats['total_intensity']:.3e}")
|
|
262
|
+
>>> print(f"Time spread: {stats['time_spread']:.3e} s")
|
|
263
|
+
"""
|
|
264
|
+
if len(events) == 0:
|
|
265
|
+
return {
|
|
266
|
+
"count": 0,
|
|
267
|
+
"total_intensity": 0.0,
|
|
268
|
+
"mean_time": 0.0,
|
|
269
|
+
"std_time": 0.0,
|
|
270
|
+
"min_time": 0.0,
|
|
271
|
+
"max_time": 0.0,
|
|
272
|
+
"mean_wavelength": 0.0,
|
|
273
|
+
"time_spread": 0.0,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
times = np.array([e.time for e in events])
|
|
277
|
+
intensities = np.array([e.intensity for e in events])
|
|
278
|
+
wavelengths = np.array([e.wavelength for e in events])
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"count": len(events),
|
|
282
|
+
"total_intensity": float(np.sum(intensities)),
|
|
283
|
+
"mean_time": float(np.mean(times)),
|
|
284
|
+
"std_time": float(np.std(times)),
|
|
285
|
+
"min_time": float(np.min(times)),
|
|
286
|
+
"max_time": float(np.max(times)),
|
|
287
|
+
"mean_wavelength": float(np.mean(wavelengths)),
|
|
288
|
+
"time_spread": float(np.max(times) - np.min(times)),
|
|
289
|
+
}
|