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