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