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,401 @@
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
36
+
37
+ Provides a finite rectangular planar detector for imaging applications.
38
+
39
+ Examples
40
+ --------
41
+ >>> from lsurf.detectors.small import PlanarDetector
42
+ >>>
43
+ >>> detector = PlanarDetector(
44
+ ... center=(0, 0, 100),
45
+ ... normal=(0, 0, -1), # Facing back toward source
46
+ ... width=0.1, # 10 cm wide
47
+ ... height=0.1, # 10 cm tall
48
+ ... name="Imaging detector"
49
+ ... )
50
+ >>> result = detector.detect(rays)
51
+ """
52
+
53
+ import numpy as np
54
+ from numpy.typing import NDArray
55
+
56
+ from ...utilities.ray_data import RayBatch
57
+ from ..base import DetectionEvent
58
+ from ..results import DetectorResult
59
+
60
+
61
+ class PlanarDetector:
62
+ """
63
+ Planar detector with finite rectangular size.
64
+
65
+ Detects rays that intersect a rectangular plane at a specific position
66
+ and orientation. Useful for imaging applications and beam profiling.
67
+
68
+ Parameters
69
+ ----------
70
+ center : tuple of float
71
+ Center position of detector plane (x, y, z) in meters.
72
+ normal : tuple of float
73
+ Normal vector (defines which direction detector faces).
74
+ width : float
75
+ Width of detector (in local u direction) in meters.
76
+ height : float
77
+ Height of detector (in local v direction) in meters.
78
+ name : str, optional
79
+ Detector name. Default is "Planar Detector".
80
+
81
+ Attributes
82
+ ----------
83
+ center : ndarray, shape (3,)
84
+ Detector center position.
85
+ normal : ndarray, shape (3,)
86
+ Unit normal vector.
87
+ width : float
88
+ Detector width.
89
+ height : float
90
+ Detector height.
91
+ u : ndarray, shape (3,)
92
+ Local x-axis direction (width direction).
93
+ v : ndarray, shape (3,)
94
+ Local y-axis direction (height direction).
95
+ name : str
96
+ Detector name.
97
+ accumulated_result : DetectorResult
98
+ All accumulated detections since last clear().
99
+
100
+ Notes
101
+ -----
102
+ The local coordinate system is constructed with:
103
+ - `normal`: direction the detector faces
104
+ - `u`: perpendicular to normal (width direction)
105
+ - `v`: perpendicular to both normal and u (height direction)
106
+
107
+ Only rays traveling toward the front face of the detector (opposite
108
+ to the normal direction) are detected.
109
+
110
+ Examples
111
+ --------
112
+ >>> detector = PlanarDetector(
113
+ ... center=(0, 0, 50),
114
+ ... normal=(0, 0, -1), # Facing -z direction
115
+ ... width=0.1,
116
+ ... height=0.1
117
+ ... )
118
+ >>> result = detector.detect(rays)
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ center: tuple[float, float, float],
124
+ normal: tuple[float, float, float],
125
+ width: float,
126
+ height: float,
127
+ name: str = "Planar Detector",
128
+ ):
129
+ """
130
+ Initialize planar detector.
131
+
132
+ Parameters
133
+ ----------
134
+ center : tuple of float
135
+ Center position of detector plane.
136
+ normal : tuple of float
137
+ Normal vector (detector faces in this direction).
138
+ width : float
139
+ Detector width in meters.
140
+ height : float
141
+ Detector height in meters.
142
+ name : str, optional
143
+ Detector name.
144
+ """
145
+ self.name = name
146
+ self.center = np.array(center, dtype=np.float32)
147
+ self.normal = np.array(normal, dtype=np.float32)
148
+ self.normal = self.normal / np.linalg.norm(self.normal)
149
+ self.width = width
150
+ self.height = height
151
+
152
+ # Create local coordinate system
153
+ if abs(self.normal[2]) < 0.9:
154
+ self.u = np.cross(self.normal, np.array([0, 0, 1], dtype=np.float32))
155
+ else:
156
+ self.u = np.cross(self.normal, np.array([1, 0, 0], dtype=np.float32))
157
+ self.u = self.u / np.linalg.norm(self.u)
158
+ self.v = np.cross(self.normal, self.u)
159
+ self.v = self.v / np.linalg.norm(self.v)
160
+
161
+ self._accumulated_result = DetectorResult.empty(name)
162
+ self._events: list[DetectionEvent] = []
163
+
164
+ @property
165
+ def accumulated_result(self) -> DetectorResult:
166
+ """All accumulated detections since last clear()."""
167
+ return self._accumulated_result
168
+
169
+ @property
170
+ def events(self) -> list[DetectionEvent]:
171
+ """
172
+ Backward compatibility: list of DetectionEvent objects.
173
+
174
+ For new code, use accumulated_result instead.
175
+ """
176
+ return self._events
177
+
178
+ def detect(
179
+ self, rays: RayBatch, current_time: float = 0.0, accumulate: bool = True
180
+ ) -> DetectorResult:
181
+ """
182
+ Detect rays that intersect the detector plane within bounds.
183
+
184
+ Parameters
185
+ ----------
186
+ rays : RayBatch
187
+ Ray batch to test.
188
+ current_time : float, optional
189
+ Current simulation time. Default is 0.0.
190
+ accumulate : bool, optional
191
+ Whether to accumulate results. Default is True.
192
+
193
+ Returns
194
+ -------
195
+ DetectorResult
196
+ Newly detected rays.
197
+
198
+ Notes
199
+ -----
200
+ Only detects rays that:
201
+ - Are traveling toward the front face (opposite to normal)
202
+ - Intersect the plane in the forward direction
203
+ - Hit within the width x height bounds
204
+ """
205
+ if rays is None or rays.num_rays == 0:
206
+ return DetectorResult.empty(self.name)
207
+
208
+ c = 299792458.0 # Speed of light in m/s
209
+ n = 1.0 # Refractive index of air
210
+
211
+ # Vectorized computation
212
+ active_mask = rays.active
213
+ if not np.any(active_mask):
214
+ return DetectorResult.empty(self.name)
215
+
216
+ origins = rays.positions[active_mask]
217
+ directions = rays.directions[active_mask]
218
+ active_indices = np.where(active_mask)[0]
219
+
220
+ # Check if rays intersect plane
221
+ denoms = np.dot(directions, self.normal)
222
+
223
+ # Parallel to plane or pointing away?
224
+ valid_mask = denoms < -1e-10
225
+
226
+ if not np.any(valid_mask):
227
+ return DetectorResult.empty(self.name)
228
+
229
+ # Find intersection parameter
230
+ to_center = self.center - origins[valid_mask]
231
+ t = np.dot(to_center, self.normal) / denoms[valid_mask]
232
+
233
+ # Only forward propagation
234
+ forward_mask = t > 0
235
+
236
+ if not np.any(forward_mask):
237
+ return DetectorResult.empty(self.name)
238
+
239
+ # Update indices
240
+ valid_active_indices = active_indices[valid_mask][forward_mask]
241
+ valid_origins = origins[valid_mask][forward_mask]
242
+ valid_directions = directions[valid_mask][forward_mask]
243
+ valid_t = t[forward_mask]
244
+
245
+ # Intersection points
246
+ intersections = valid_origins + valid_t[:, np.newaxis] * valid_directions
247
+
248
+ # Check if within detector bounds
249
+ offsets = intersections - self.center
250
+ local_x = np.dot(offsets, self.u)
251
+ local_y = np.dot(offsets, self.v)
252
+
253
+ in_bounds = (np.abs(local_x) <= self.width / 2) & (
254
+ np.abs(local_y) <= self.height / 2
255
+ )
256
+
257
+ if not np.any(in_bounds):
258
+ return DetectorResult.empty(self.name)
259
+
260
+ # Get final results
261
+ final_indices = valid_active_indices[in_bounds]
262
+ final_intersections = intersections[in_bounds]
263
+ final_t = valid_t[in_bounds]
264
+
265
+ # Compute arrival times
266
+ additional_times = final_t * n / c
267
+ arrival_times = rays.accumulated_time[final_indices] + additional_times
268
+
269
+ result = DetectorResult(
270
+ positions=final_intersections.astype(np.float32),
271
+ directions=rays.directions[final_indices].astype(np.float32),
272
+ times=arrival_times.astype(np.float32),
273
+ intensities=rays.intensities[final_indices].astype(np.float32),
274
+ wavelengths=rays.wavelengths[final_indices].astype(np.float32),
275
+ ray_indices=final_indices.astype(np.int32),
276
+ generations=(
277
+ rays.generations[final_indices].astype(np.int32)
278
+ if rays.generations is not None
279
+ else None
280
+ ),
281
+ polarization_vectors=(
282
+ rays.polarization_vector[final_indices].astype(np.float32)
283
+ if rays.polarization_vector is not None
284
+ else None
285
+ ),
286
+ detector_name=self.name,
287
+ )
288
+
289
+ if accumulate:
290
+ self._accumulated_result = DetectorResult.merge(
291
+ [self._accumulated_result, result]
292
+ )
293
+ self._events.extend(result.to_detection_events())
294
+
295
+ return result
296
+
297
+ def clear(self) -> None:
298
+ """
299
+ Clear all recorded detections.
300
+
301
+ Resets the detector to its initial state with no recorded events.
302
+ """
303
+ self._accumulated_result = DetectorResult.empty(self.name)
304
+ self._events = []
305
+
306
+ def get_image(
307
+ self, bins_u: int = 100, bins_v: int = 100
308
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
309
+ """
310
+ Generate intensity image from detection events.
311
+
312
+ Parameters
313
+ ----------
314
+ bins_u : int, optional
315
+ Number of bins in u direction. Default is 100.
316
+ bins_v : int, optional
317
+ Number of bins in v direction. Default is 100.
318
+
319
+ Returns
320
+ -------
321
+ u_centers : ndarray, shape (bins_u,)
322
+ Bin centers in u direction (meters).
323
+ v_centers : ndarray, shape (bins_v,)
324
+ Bin centers in v direction (meters).
325
+ image : ndarray, shape (bins_v, bins_u)
326
+ Intensity image (sum of intensities per bin).
327
+ """
328
+ if self._accumulated_result.is_empty:
329
+ u_centers = np.linspace(-self.width / 2, self.width / 2, bins_u)
330
+ v_centers = np.linspace(-self.height / 2, self.height / 2, bins_v)
331
+ return u_centers, v_centers, np.zeros((bins_v, bins_u))
332
+
333
+ # Extract local coordinates
334
+ positions = self._accumulated_result.positions
335
+ offsets = positions - self.center
336
+ local_u = np.dot(offsets, self.u)
337
+ local_v = np.dot(offsets, self.v)
338
+ intensities = self._accumulated_result.intensities
339
+
340
+ # Create 2D histogram weighted by intensity
341
+ image, u_edges, v_edges = np.histogram2d(
342
+ local_u,
343
+ local_v,
344
+ bins=[bins_u, bins_v],
345
+ range=[
346
+ [-self.width / 2, self.width / 2],
347
+ [-self.height / 2, self.height / 2],
348
+ ],
349
+ weights=intensities,
350
+ )
351
+
352
+ u_centers = (u_edges[:-1] + u_edges[1:]) / 2
353
+ v_centers = (v_edges[:-1] + v_edges[1:]) / 2
354
+
355
+ return u_centers, v_centers, image.T
356
+
357
+ def __repr__(self) -> str:
358
+ """Return string representation."""
359
+ return (
360
+ f"PlanarDetector(center={self.center.tolist()}, "
361
+ f"size=({self.width}, {self.height}), rays={self._accumulated_result.num_rays})"
362
+ )
363
+
364
+ def __len__(self) -> int:
365
+ """Return number of detected rays."""
366
+ return self._accumulated_result.num_rays
367
+
368
+ # Backward compatibility methods from old Detector base class
369
+ def get_arrival_times(self) -> NDArray[np.float64]:
370
+ """Get array of all arrival times."""
371
+ return self._accumulated_result.times.astype(np.float64)
372
+
373
+ def get_arrival_angles(
374
+ self, reference_direction: NDArray[np.float32]
375
+ ) -> NDArray[np.float64]:
376
+ """Get angles between ray directions and reference direction."""
377
+ if self._accumulated_result.is_empty:
378
+ return np.array([], dtype=np.float64)
379
+ ref = reference_direction / np.linalg.norm(reference_direction)
380
+ dir_norms = self._accumulated_result.directions / np.linalg.norm(
381
+ self._accumulated_result.directions, axis=1, keepdims=True
382
+ )
383
+ cos_angles = np.dot(dir_norms, ref)
384
+ cos_angles = np.clip(cos_angles, -1.0, 1.0)
385
+ return np.arccos(cos_angles).astype(np.float64)
386
+
387
+ def get_intensities(self) -> NDArray[np.float64]:
388
+ """Get array of all detected intensities."""
389
+ return self._accumulated_result.intensities.astype(np.float64)
390
+
391
+ def get_wavelengths(self) -> NDArray[np.float64]:
392
+ """Get array of all detected wavelengths."""
393
+ return self._accumulated_result.wavelengths.astype(np.float64)
394
+
395
+ def get_positions(self) -> NDArray[np.float32]:
396
+ """Get array of all detection positions."""
397
+ return self._accumulated_result.positions.astype(np.float32)
398
+
399
+ def get_total_intensity(self) -> float:
400
+ """Get sum of all detected intensities."""
401
+ return self._accumulated_result.total_intensity