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.
- lsurf/__init__.py +471 -0
- lsurf/analysis/__init__.py +107 -0
- lsurf/analysis/healpix_utils.py +418 -0
- lsurf/analysis/sphere_viz.py +1280 -0
- lsurf/cli/__init__.py +48 -0
- lsurf/cli/build.py +398 -0
- lsurf/cli/config_schema.py +318 -0
- lsurf/cli/gui_cmd.py +76 -0
- lsurf/cli/interactive.py +850 -0
- lsurf/cli/main.py +81 -0
- lsurf/cli/run.py +806 -0
- lsurf/detectors/__init__.py +266 -0
- lsurf/detectors/analysis.py +289 -0
- lsurf/detectors/base.py +284 -0
- lsurf/detectors/constant_size_rings.py +485 -0
- lsurf/detectors/directional.py +45 -0
- lsurf/detectors/extended/__init__.py +73 -0
- lsurf/detectors/extended/local_sphere.py +353 -0
- lsurf/detectors/extended/recording_sphere.py +368 -0
- lsurf/detectors/planar.py +45 -0
- lsurf/detectors/protocol.py +187 -0
- lsurf/detectors/recording_spheres.py +63 -0
- lsurf/detectors/results.py +1140 -0
- lsurf/detectors/small/__init__.py +79 -0
- lsurf/detectors/small/directional.py +330 -0
- lsurf/detectors/small/planar.py +401 -0
- lsurf/detectors/small/spherical.py +450 -0
- lsurf/detectors/spherical.py +45 -0
- lsurf/geometry/__init__.py +199 -0
- lsurf/geometry/builder.py +478 -0
- lsurf/geometry/cell.py +228 -0
- lsurf/geometry/cell_geometry.py +247 -0
- lsurf/geometry/detector_arrays.py +1785 -0
- lsurf/geometry/geometry.py +222 -0
- lsurf/geometry/surface_analysis.py +375 -0
- lsurf/geometry/validation.py +91 -0
- lsurf/gui/__init__.py +51 -0
- lsurf/gui/app.py +903 -0
- lsurf/gui/core/__init__.py +39 -0
- lsurf/gui/core/scene.py +343 -0
- lsurf/gui/core/simulation.py +264 -0
- lsurf/gui/renderers/__init__.py +40 -0
- lsurf/gui/renderers/ray_renderer.py +353 -0
- lsurf/gui/renderers/source_renderer.py +505 -0
- lsurf/gui/renderers/surface_renderer.py +477 -0
- lsurf/gui/views/__init__.py +48 -0
- lsurf/gui/views/config_editor.py +3199 -0
- lsurf/gui/views/properties.py +257 -0
- lsurf/gui/views/results.py +291 -0
- lsurf/gui/views/scene_tree.py +180 -0
- lsurf/gui/views/viewport_3d.py +555 -0
- lsurf/gui/views/visualizations.py +712 -0
- lsurf/materials/__init__.py +169 -0
- lsurf/materials/base/__init__.py +64 -0
- lsurf/materials/base/full_inhomogeneous.py +208 -0
- lsurf/materials/base/grid_inhomogeneous.py +319 -0
- lsurf/materials/base/homogeneous.py +342 -0
- lsurf/materials/base/material_field.py +527 -0
- lsurf/materials/base/simple_inhomogeneous.py +418 -0
- lsurf/materials/base/spectral_inhomogeneous.py +497 -0
- lsurf/materials/implementations/__init__.py +120 -0
- lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
- lsurf/materials/implementations/duct_atmosphere.py +390 -0
- lsurf/materials/implementations/exponential_atmosphere.py +435 -0
- lsurf/materials/implementations/gaussian_lens.py +120 -0
- lsurf/materials/implementations/interpolated_data.py +123 -0
- lsurf/materials/implementations/layered_atmosphere.py +134 -0
- lsurf/materials/implementations/linear_gradient.py +109 -0
- lsurf/materials/implementations/linsley_atmosphere.py +764 -0
- lsurf/materials/implementations/standard_materials.py +126 -0
- lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
- lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
- lsurf/materials/utils/__init__.py +77 -0
- lsurf/materials/utils/constants.py +45 -0
- lsurf/materials/utils/device_functions.py +117 -0
- lsurf/materials/utils/dispersion.py +160 -0
- lsurf/materials/utils/factories.py +142 -0
- lsurf/propagation/__init__.py +91 -0
- lsurf/propagation/detector_gpu.py +67 -0
- lsurf/propagation/gpu_device_rays.py +294 -0
- lsurf/propagation/kernels/__init__.py +175 -0
- lsurf/propagation/kernels/absorption/__init__.py +61 -0
- lsurf/propagation/kernels/absorption/grid.py +240 -0
- lsurf/propagation/kernels/absorption/simple.py +232 -0
- lsurf/propagation/kernels/absorption/spectral.py +410 -0
- lsurf/propagation/kernels/detection/__init__.py +64 -0
- lsurf/propagation/kernels/detection/protocol.py +102 -0
- lsurf/propagation/kernels/detection/spherical.py +255 -0
- lsurf/propagation/kernels/device_functions.py +790 -0
- lsurf/propagation/kernels/fresnel/__init__.py +64 -0
- lsurf/propagation/kernels/fresnel/protocol.py +97 -0
- lsurf/propagation/kernels/fresnel/standard.py +258 -0
- lsurf/propagation/kernels/intersection/__init__.py +79 -0
- lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
- lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
- lsurf/propagation/kernels/intersection/plane.py +166 -0
- lsurf/propagation/kernels/intersection/protocol.py +95 -0
- lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
- lsurf/propagation/kernels/intersection/sphere.py +190 -0
- lsurf/propagation/kernels/propagation/__init__.py +85 -0
- lsurf/propagation/kernels/propagation/grid.py +527 -0
- lsurf/propagation/kernels/propagation/protocol.py +105 -0
- lsurf/propagation/kernels/propagation/simple.py +460 -0
- lsurf/propagation/kernels/propagation/spectral.py +875 -0
- lsurf/propagation/kernels/registry.py +331 -0
- lsurf/propagation/kernels/surface/__init__.py +72 -0
- lsurf/propagation/kernels/surface/bisection.py +232 -0
- lsurf/propagation/kernels/surface/detection.py +402 -0
- lsurf/propagation/kernels/surface/reduction.py +166 -0
- lsurf/propagation/propagator_protocol.py +222 -0
- lsurf/propagation/propagators/__init__.py +101 -0
- lsurf/propagation/propagators/detector_handler.py +354 -0
- lsurf/propagation/propagators/factory.py +200 -0
- lsurf/propagation/propagators/fresnel_handler.py +305 -0
- lsurf/propagation/propagators/gpu_gradient.py +566 -0
- lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
- lsurf/propagation/propagators/gradient.py +429 -0
- lsurf/propagation/propagators/intersection_handler.py +327 -0
- lsurf/propagation/propagators/material_propagator.py +398 -0
- lsurf/propagation/propagators/signed_distance_handler.py +522 -0
- lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
- lsurf/propagation/propagators/surface_interaction.py +616 -0
- lsurf/propagation/propagators/surface_propagator.py +719 -0
- lsurf/py.typed +1 -0
- lsurf/simulation/__init__.py +70 -0
- lsurf/simulation/config.py +164 -0
- lsurf/simulation/orchestrator.py +462 -0
- lsurf/simulation/result.py +299 -0
- lsurf/simulation/simulation.py +262 -0
- lsurf/sources/__init__.py +128 -0
- lsurf/sources/base.py +264 -0
- lsurf/sources/collimated.py +252 -0
- lsurf/sources/custom.py +409 -0
- lsurf/sources/diverging.py +228 -0
- lsurf/sources/gaussian.py +272 -0
- lsurf/sources/parallel_from_positions.py +197 -0
- lsurf/sources/point.py +172 -0
- lsurf/sources/uniform_diverging.py +258 -0
- lsurf/surfaces/__init__.py +184 -0
- lsurf/surfaces/cpu/__init__.py +50 -0
- lsurf/surfaces/cpu/curved_wave.py +463 -0
- lsurf/surfaces/cpu/gerstner_wave.py +381 -0
- lsurf/surfaces/cpu/wave_params.py +118 -0
- lsurf/surfaces/gpu/__init__.py +72 -0
- lsurf/surfaces/gpu/annular_plane.py +453 -0
- lsurf/surfaces/gpu/bounded_plane.py +390 -0
- lsurf/surfaces/gpu/curved_wave.py +483 -0
- lsurf/surfaces/gpu/gerstner_wave.py +377 -0
- lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
- lsurf/surfaces/gpu/plane.py +299 -0
- lsurf/surfaces/gpu/recording_sphere.py +587 -0
- lsurf/surfaces/gpu/sphere.py +311 -0
- lsurf/surfaces/protocol.py +336 -0
- lsurf/surfaces/registry.py +373 -0
- lsurf/utilities/__init__.py +175 -0
- lsurf/utilities/detector_analysis.py +814 -0
- lsurf/utilities/fresnel.py +628 -0
- lsurf/utilities/interactions.py +1215 -0
- lsurf/utilities/propagation.py +602 -0
- lsurf/utilities/ray_data.py +532 -0
- lsurf/utilities/recording_sphere.py +745 -0
- lsurf/utilities/time_spread.py +463 -0
- lsurf/visualization/__init__.py +329 -0
- lsurf/visualization/absorption_plots.py +334 -0
- lsurf/visualization/atmospheric_plots.py +754 -0
- lsurf/visualization/common.py +348 -0
- lsurf/visualization/detector_plots.py +1350 -0
- lsurf/visualization/detector_sphere_plots.py +1173 -0
- lsurf/visualization/fresnel_plots.py +1061 -0
- lsurf/visualization/ocean_simulation_plots.py +999 -0
- lsurf/visualization/polarization_plots.py +916 -0
- lsurf/visualization/raytracing_plots.py +1521 -0
- lsurf/visualization/ring_detector_plots.py +1867 -0
- lsurf/visualization/time_spread_plots.py +531 -0
- lsurf-1.0.0.dist-info/METADATA +381 -0
- lsurf-1.0.0.dist-info/RECORD +180 -0
- lsurf-1.0.0.dist-info/WHEEL +5 -0
- lsurf-1.0.0.dist-info/entry_points.txt +2 -0
- lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
- 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
|