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,79 @@
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
+ Small (Point) Detectors Submodule
36
+
37
+ This submodule contains point detectors that detect rays passing through
38
+ a localized region in space.
39
+
40
+ Available Detectors
41
+ -------------------
42
+ SphericalDetector
43
+ Spherical detector for omnidirectional collection.
44
+ PlanarDetector
45
+ Rectangular planar detector for imaging.
46
+ DirectionalDetector
47
+ Detector with angular acceptance cone.
48
+
49
+ Examples
50
+ --------
51
+ >>> from lsurf.detectors.small import SphericalDetector, PlanarDetector
52
+ >>>
53
+ >>> # Create a spherical detector at 100m altitude
54
+ >>> sphere = SphericalDetector(
55
+ ... center=(0, 0, 100),
56
+ ... radius=10.0,
57
+ ... name="Far-field detector"
58
+ ... )
59
+ >>> result = sphere.detect(rays)
60
+ >>>
61
+ >>> # Create a planar imaging detector
62
+ >>> plane = PlanarDetector(
63
+ ... center=(0, 0, 50),
64
+ ... normal=(0, 0, -1),
65
+ ... width=0.1,
66
+ ... height=0.1
67
+ ... )
68
+ >>> result = plane.detect(rays)
69
+ """
70
+
71
+ from .spherical import SphericalDetector
72
+ from .planar import PlanarDetector
73
+ from .directional import DirectionalDetector
74
+
75
+ __all__ = [
76
+ "SphericalDetector",
77
+ "PlanarDetector",
78
+ "DirectionalDetector",
79
+ ]
@@ -0,0 +1,330 @@
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
+ Directional Detector Implementation
36
+
37
+ Provides a directional detector with limited angular acceptance.
38
+ Useful for modeling detectors with finite field of view.
39
+
40
+ Examples
41
+ --------
42
+ >>> from lsurf.detectors.small import DirectionalDetector
43
+ >>>
44
+ >>> detector = DirectionalDetector(
45
+ ... position=(0, 0, 100),
46
+ ... direction=(0, 0, -1), # Looking back toward source
47
+ ... acceptance_angle=np.radians(10), # 10 degree acceptance cone
48
+ ... radius=5.0,
49
+ ... name="Telescope detector"
50
+ ... )
51
+ >>> result = detector.detect(rays)
52
+ """
53
+
54
+ import numpy as np
55
+
56
+ from ...utilities.ray_data import RayBatch
57
+ from ..base import DetectionEvent
58
+ from ..results import DetectorResult
59
+
60
+
61
+ class DirectionalDetector:
62
+ """
63
+ Detector with angular acceptance cone.
64
+
65
+ Detects rays that pass within a specified radius AND arrive within
66
+ a specified angular acceptance cone. Useful for modeling detectors
67
+ with limited field of view such as telescopes or fiber couplers.
68
+
69
+ Parameters
70
+ ----------
71
+ position : tuple of float
72
+ Detector position (x, y, z) in meters.
73
+ direction : tuple of float
74
+ Direction detector is pointing (acceptance cone axis).
75
+ acceptance_angle : float
76
+ Half-angle of acceptance cone in radians.
77
+ radius : float
78
+ Detection radius at position in meters.
79
+ name : str, optional
80
+ Detector name. Default is "Directional Detector".
81
+
82
+ Attributes
83
+ ----------
84
+ position : ndarray, shape (3,)
85
+ Detector position.
86
+ direction : ndarray, shape (3,)
87
+ Unit vector pointing in detector viewing direction.
88
+ acceptance_angle : float
89
+ Acceptance cone half-angle in radians.
90
+ radius : float
91
+ Detection radius.
92
+ name : str
93
+ Detector name.
94
+ accumulated_result : DetectorResult
95
+ All accumulated detections since last clear().
96
+
97
+ Notes
98
+ -----
99
+ A ray is detected if:
100
+ 1. Its closest approach to the detector position is within radius
101
+ 2. The angle between the ray direction and the detector direction
102
+ (considering the ray as incoming) is within acceptance_angle
103
+
104
+ Examples
105
+ --------
106
+ >>> # 10-degree acceptance cone detector
107
+ >>> detector = DirectionalDetector(
108
+ ... position=(0, 0, 100),
109
+ ... direction=(0, 0, -1),
110
+ ... acceptance_angle=np.radians(10),
111
+ ... radius=5.0
112
+ ... )
113
+ >>> result = detector.detect(rays)
114
+ >>> print(f"Detected {result.num_rays} rays within acceptance cone")
115
+ """
116
+
117
+ def __init__(
118
+ self,
119
+ position: tuple[float, float, float],
120
+ direction: tuple[float, float, float],
121
+ acceptance_angle: float,
122
+ radius: float,
123
+ name: str = "Directional Detector",
124
+ ):
125
+ """
126
+ Initialize directional detector.
127
+
128
+ Parameters
129
+ ----------
130
+ position : tuple of float
131
+ Detector position in meters.
132
+ direction : tuple of float
133
+ Direction detector is pointing.
134
+ acceptance_angle : float
135
+ Acceptance cone half-angle in radians.
136
+ radius : float
137
+ Detection radius in meters.
138
+ name : str, optional
139
+ Detector name.
140
+ """
141
+ self.name = name
142
+ self.position = np.array(position, dtype=np.float32)
143
+ self.direction = np.array(direction, dtype=np.float32)
144
+ self.direction = self.direction / np.linalg.norm(self.direction)
145
+ self.acceptance_angle = acceptance_angle
146
+ self.radius = radius
147
+
148
+ self._accumulated_result = DetectorResult.empty(name)
149
+ self._events: list[DetectionEvent] = []
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, rays: RayBatch, current_time: float = 0.0, accumulate: bool = True
167
+ ) -> DetectorResult:
168
+ """
169
+ Detect rays within acceptance cone and detection radius.
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
+ c = 299792458.0 # Speed of light in m/s
189
+ n = 1.0 # Refractive index of air
190
+
191
+ # Vectorized computation
192
+ active_mask = rays.active
193
+ if not np.any(active_mask):
194
+ return DetectorResult.empty(self.name)
195
+
196
+ origins = rays.positions[active_mask]
197
+ directions = rays.directions[active_mask]
198
+ active_indices = np.where(active_mask)[0]
199
+
200
+ # Find closest approach to detector position
201
+ oc = self.position - origins
202
+ dir_norms = directions / np.linalg.norm(directions, axis=1, keepdims=True)
203
+ t_closest = np.sum(oc * dir_norms, axis=1)
204
+
205
+ # Only consider forward propagation
206
+ forward_mask = t_closest > 0
207
+
208
+ if not np.any(forward_mask):
209
+ return DetectorResult.empty(self.name)
210
+
211
+ # Get closest points
212
+ closest_points = (
213
+ origins[forward_mask]
214
+ + t_closest[forward_mask, np.newaxis] * dir_norms[forward_mask]
215
+ )
216
+ dists = np.linalg.norm(closest_points - self.position, axis=1)
217
+
218
+ # Check if within radius
219
+ in_radius = dists <= self.radius
220
+
221
+ if not np.any(in_radius):
222
+ return DetectorResult.empty(self.name)
223
+
224
+ # Update indices for rays within radius
225
+ radius_indices = active_indices[forward_mask][in_radius]
226
+ radius_dir_norms = dir_norms[forward_mask][in_radius]
227
+ radius_t_closest = t_closest[forward_mask][in_radius]
228
+ radius_closest_points = closest_points[in_radius]
229
+
230
+ # Check if ray direction is within acceptance cone
231
+ # Negative because ray is incoming (traveling toward detector)
232
+ cos_angles = np.dot(-radius_dir_norms, self.direction)
233
+ cos_angles = np.clip(cos_angles, -1.0, 1.0)
234
+ angles = np.arccos(cos_angles)
235
+
236
+ in_cone = angles <= self.acceptance_angle
237
+
238
+ if not np.any(in_cone):
239
+ return DetectorResult.empty(self.name)
240
+
241
+ # Get final results
242
+ final_indices = radius_indices[in_cone]
243
+ final_closest_points = radius_closest_points[in_cone]
244
+ final_t_closest = radius_t_closest[in_cone]
245
+
246
+ # Compute arrival times
247
+ additional_times = final_t_closest * n / c
248
+ arrival_times = rays.accumulated_time[final_indices] + additional_times
249
+
250
+ result = DetectorResult(
251
+ positions=final_closest_points.astype(np.float32),
252
+ directions=rays.directions[final_indices].astype(np.float32),
253
+ times=arrival_times.astype(np.float32),
254
+ intensities=rays.intensities[final_indices].astype(np.float32),
255
+ wavelengths=rays.wavelengths[final_indices].astype(np.float32),
256
+ ray_indices=final_indices.astype(np.int32),
257
+ generations=(
258
+ rays.generations[final_indices].astype(np.int32)
259
+ if rays.generations is not None
260
+ else None
261
+ ),
262
+ polarization_vectors=(
263
+ rays.polarization_vector[final_indices].astype(np.float32)
264
+ if rays.polarization_vector is not None
265
+ else None
266
+ ),
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 clear(self) -> None:
279
+ """
280
+ Clear all recorded detections.
281
+
282
+ Resets the detector to its initial state with no recorded events.
283
+ """
284
+ self._accumulated_result = DetectorResult.empty(self.name)
285
+ self._events = []
286
+
287
+ def __repr__(self) -> str:
288
+ """Return string representation."""
289
+ return (
290
+ f"DirectionalDetector(position={self.position.tolist()}, "
291
+ f"acceptance_angle={np.degrees(self.acceptance_angle):.1f} deg, "
292
+ f"rays={self._accumulated_result.num_rays})"
293
+ )
294
+
295
+ def __len__(self) -> int:
296
+ """Return number of detected rays."""
297
+ return self._accumulated_result.num_rays
298
+
299
+ # Backward compatibility methods from old Detector base class
300
+ def get_arrival_times(self) -> np.ndarray:
301
+ """Get array of all arrival times."""
302
+ return self._accumulated_result.times.astype(np.float64)
303
+
304
+ def get_arrival_angles(self, reference_direction: np.ndarray) -> np.ndarray:
305
+ """Get angles between ray directions and reference direction."""
306
+ if self._accumulated_result.is_empty:
307
+ return np.array([], dtype=np.float64)
308
+ ref = reference_direction / np.linalg.norm(reference_direction)
309
+ dir_norms = self._accumulated_result.directions / np.linalg.norm(
310
+ self._accumulated_result.directions, axis=1, keepdims=True
311
+ )
312
+ cos_angles = np.dot(dir_norms, ref)
313
+ cos_angles = np.clip(cos_angles, -1.0, 1.0)
314
+ return np.arccos(cos_angles).astype(np.float64)
315
+
316
+ def get_intensities(self) -> np.ndarray:
317
+ """Get array of all detected intensities."""
318
+ return self._accumulated_result.intensities.astype(np.float64)
319
+
320
+ def get_wavelengths(self) -> np.ndarray:
321
+ """Get array of all detected wavelengths."""
322
+ return self._accumulated_result.wavelengths.astype(np.float64)
323
+
324
+ def get_positions(self) -> np.ndarray:
325
+ """Get array of all detection positions."""
326
+ return self._accumulated_result.positions.astype(np.float32)
327
+
328
+ def get_total_intensity(self) -> float:
329
+ """Get sum of all detected intensities."""
330
+ return self._accumulated_result.total_intensity