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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. 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
+ }