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,222 @@
|
|
|
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
|
+
Protocol definitions for ray propagators and GPU materials.
|
|
36
|
+
|
|
37
|
+
This module defines:
|
|
38
|
+
- RayPropagatorProtocol: Common interface for CPU/GPU propagators
|
|
39
|
+
- MaterialFieldProtocol: Interface for materials providing refractive index
|
|
40
|
+
- GPUMaterialProtocol: Interface for GPU-accelerated materials
|
|
41
|
+
- GPUMaterialID: Enum for GPU material types
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from enum import IntEnum
|
|
45
|
+
from typing import Protocol, runtime_checkable
|
|
46
|
+
|
|
47
|
+
import numpy.typing as npt
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# GPU Material Registry
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GPUMaterialID(IntEnum):
|
|
56
|
+
"""
|
|
57
|
+
Registry of GPU-compatible material types.
|
|
58
|
+
|
|
59
|
+
Each material type has specialized device functions compiled into the
|
|
60
|
+
CUDA kernels. This enum identifies which material's functions to use.
|
|
61
|
+
|
|
62
|
+
Tiers:
|
|
63
|
+
- EXPONENTIAL_ATMOSPHERE (1): Legacy specialized kernels
|
|
64
|
+
- SIMPLE_INHOMOGENEOUS (2): 1D radial profile via LUT interpolation
|
|
65
|
+
- GRID_INHOMOGENEOUS (3): 3D grid data via trilinear interpolation
|
|
66
|
+
- SPECTRAL_INHOMOGENEOUS (4): 2D LUT-based (altitude × wavelength)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
EXPONENTIAL_ATMOSPHERE = 1 # Legacy: specialized hard-coded kernels
|
|
70
|
+
SIMPLE_INHOMOGENEOUS = 2 # Tier 1: 1D LUT-based (SimpleInhomogeneousModel)
|
|
71
|
+
GRID_INHOMOGENEOUS = 3 # Tier 2: 3D grid interpolation (GridInhomogeneousModel)
|
|
72
|
+
SPECTRAL_INHOMOGENEOUS = 4 # 2D LUT-based (SpectralInhomogeneousModel)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@runtime_checkable
|
|
76
|
+
class RayPropagatorProtocol(Protocol):
|
|
77
|
+
"""
|
|
78
|
+
Protocol for ray propagators in inhomogeneous media.
|
|
79
|
+
|
|
80
|
+
All propagators (CPU and GPU) must implement this interface to ensure
|
|
81
|
+
they are interchangeable drop-in replacements.
|
|
82
|
+
|
|
83
|
+
The propagator operates on RayBatch objects and modifies them in-place.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def propagate_step(
|
|
87
|
+
self,
|
|
88
|
+
rays, # RayBatch - avoiding import to prevent circular dependency
|
|
89
|
+
step_size: float,
|
|
90
|
+
wavelength: float = 532e-9,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Propagate rays by a single integration step.
|
|
94
|
+
|
|
95
|
+
This is the primary interface method that all propagators must implement.
|
|
96
|
+
It takes a RayBatch object and advances all active rays by one step.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
rays : RayBatch
|
|
101
|
+
Ray batch containing positions, directions, and other ray properties.
|
|
102
|
+
Modified in-place.
|
|
103
|
+
step_size : float
|
|
104
|
+
Integration step size in meters. For gradient-based propagators,
|
|
105
|
+
this is the nominal step size that may be adaptively adjusted.
|
|
106
|
+
wavelength : float, optional
|
|
107
|
+
Wavelength in meters, default 532 nm (green light).
|
|
108
|
+
Used for wavelength-dependent effects like dispersion.
|
|
109
|
+
|
|
110
|
+
Notes
|
|
111
|
+
-----
|
|
112
|
+
- Modifies `rays.positions`, `rays.directions` in-place
|
|
113
|
+
- Updates path length accumulators if present
|
|
114
|
+
- Only propagates rays where `rays.active` is True
|
|
115
|
+
- Implementations may use different integration schemes (Euler, RK4, etc.)
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
def propagate(
|
|
120
|
+
self,
|
|
121
|
+
rays, # RayBatch
|
|
122
|
+
total_distance: float,
|
|
123
|
+
step_size: float,
|
|
124
|
+
wavelength: float = 532e-9,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Propagate rays through a total distance.
|
|
128
|
+
|
|
129
|
+
This is a convenience method that calls propagate_step repeatedly.
|
|
130
|
+
Implementations may optimize this for batch processing.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
rays : RayBatch
|
|
135
|
+
Ray batch to propagate (modified in-place)
|
|
136
|
+
total_distance : float
|
|
137
|
+
Total distance to propagate in meters
|
|
138
|
+
step_size : float
|
|
139
|
+
Integration step size in meters
|
|
140
|
+
wavelength : float, optional
|
|
141
|
+
Wavelength in meters, default 532 nm
|
|
142
|
+
"""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@runtime_checkable
|
|
147
|
+
class MaterialFieldProtocol(Protocol):
|
|
148
|
+
"""
|
|
149
|
+
Protocol for material fields that provide refractive index.
|
|
150
|
+
|
|
151
|
+
Materials must implement these methods to work with gradient propagators.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def get_refractive_index(
|
|
155
|
+
self,
|
|
156
|
+
x: npt.NDArray,
|
|
157
|
+
y: npt.NDArray,
|
|
158
|
+
z: npt.NDArray,
|
|
159
|
+
wavelength: float = 532e-9,
|
|
160
|
+
) -> npt.NDArray:
|
|
161
|
+
"""
|
|
162
|
+
Get refractive index at given positions.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
x, y, z : ndarray
|
|
167
|
+
Positions in meters
|
|
168
|
+
wavelength : float
|
|
169
|
+
Wavelength in meters
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
ndarray
|
|
174
|
+
Refractive index at each position
|
|
175
|
+
"""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
def get_refractive_index_gradient(
|
|
179
|
+
self,
|
|
180
|
+
x: npt.NDArray,
|
|
181
|
+
y: npt.NDArray,
|
|
182
|
+
z: npt.NDArray,
|
|
183
|
+
wavelength: float = 532e-9,
|
|
184
|
+
) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray]:
|
|
185
|
+
"""
|
|
186
|
+
Get gradient of refractive index at given positions.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
x, y, z : ndarray
|
|
191
|
+
Positions in meters
|
|
192
|
+
wavelength : float
|
|
193
|
+
Wavelength in meters
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
tuple of (dn_dx, dn_dy, dn_dz)
|
|
198
|
+
Gradient components in m^-1
|
|
199
|
+
"""
|
|
200
|
+
...
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@runtime_checkable
|
|
204
|
+
class GPUMaterialProtocol(Protocol):
|
|
205
|
+
"""
|
|
206
|
+
Protocol for GPU-accelerated materials.
|
|
207
|
+
|
|
208
|
+
Materials implementing this protocol can be used with GPU propagators.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def gpu_material_id(self) -> GPUMaterialID:
|
|
213
|
+
"""Return GPU material ID enum value."""
|
|
214
|
+
...
|
|
215
|
+
|
|
216
|
+
def get_gpu_kernels(self) -> dict:
|
|
217
|
+
"""Return dict of GPU kernel functions keyed by method name."""
|
|
218
|
+
...
|
|
219
|
+
|
|
220
|
+
def get_gpu_parameters(self) -> tuple:
|
|
221
|
+
"""Return tuple of material-specific parameters for GPU kernels."""
|
|
222
|
+
...
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
Propagators for ray tracing in gradient media.
|
|
36
|
+
|
|
37
|
+
This subpackage contains:
|
|
38
|
+
- GradientPropagator: CPU-based propagator using NumPy vectorization
|
|
39
|
+
- GPUGradientPropagator: GPU-accelerated propagator using CUDA
|
|
40
|
+
- SpectralGPUGradientPropagator: GPU propagator with per-ray wavelengths
|
|
41
|
+
- SurfacePropagator: GPU propagator with multi-surface intersection detection
|
|
42
|
+
- create_propagator: Factory function for creating propagators based on material compatibility
|
|
43
|
+
|
|
44
|
+
New architecture components:
|
|
45
|
+
- MaterialPropagator: Clean wrapper for in-material propagation with surface detection
|
|
46
|
+
- SurfaceInteractionProcessor: Handles Fresnel physics, detection, absorption
|
|
47
|
+
- PropagationResult, SurfaceCrossing, SurfaceInteractionResult: Data structures
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from .gradient import GradientPropagator
|
|
51
|
+
from .gpu_gradient import GPUGradientPropagator
|
|
52
|
+
from .spectral_gpu_gradient import SpectralGPUGradientPropagator
|
|
53
|
+
from .surface_propagator import SurfacePropagator, HitData
|
|
54
|
+
from .gpu_surface_propagator import GPUSurfacePropagator
|
|
55
|
+
from .factory import create_propagator
|
|
56
|
+
from .material_propagator import (
|
|
57
|
+
MaterialPropagator,
|
|
58
|
+
CPUMaterialPropagator,
|
|
59
|
+
GPUMaterialPropagator,
|
|
60
|
+
MaterialPropagatorProtocol,
|
|
61
|
+
PropagationResult,
|
|
62
|
+
)
|
|
63
|
+
from .surface_interaction import (
|
|
64
|
+
SurfaceInteractionProcessor,
|
|
65
|
+
SurfaceCrossing,
|
|
66
|
+
SurfaceInteractionResult,
|
|
67
|
+
)
|
|
68
|
+
from .signed_distance_handler import (
|
|
69
|
+
compute_signed_distance_gpu,
|
|
70
|
+
compute_surface_normal_gpu,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Re-export kernel registry types for convenience
|
|
74
|
+
from ..kernels.registry import PropagationKernelID, PropagatorID
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
# Factory
|
|
78
|
+
"create_propagator",
|
|
79
|
+
# Kernel/propagator IDs
|
|
80
|
+
"PropagationKernelID",
|
|
81
|
+
"PropagatorID",
|
|
82
|
+
# Legacy propagators
|
|
83
|
+
"GradientPropagator",
|
|
84
|
+
"GPUGradientPropagator",
|
|
85
|
+
"SpectralGPUGradientPropagator",
|
|
86
|
+
"SurfacePropagator",
|
|
87
|
+
"GPUSurfacePropagator",
|
|
88
|
+
"HitData",
|
|
89
|
+
# New architecture
|
|
90
|
+
"MaterialPropagator",
|
|
91
|
+
"CPUMaterialPropagator",
|
|
92
|
+
"GPUMaterialPropagator",
|
|
93
|
+
"MaterialPropagatorProtocol",
|
|
94
|
+
"PropagationResult",
|
|
95
|
+
"SurfaceInteractionProcessor",
|
|
96
|
+
"SurfaceCrossing",
|
|
97
|
+
"SurfaceInteractionResult",
|
|
98
|
+
# Signed distance handlers
|
|
99
|
+
"compute_signed_distance_gpu",
|
|
100
|
+
"compute_surface_normal_gpu",
|
|
101
|
+
]
|
|
@@ -0,0 +1,354 @@
|
|
|
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
|
+
Detector Handler
|
|
36
|
+
|
|
37
|
+
GPU memory management for ray detection kernels.
|
|
38
|
+
This module provides high-level interfaces that handle cuda.to_device(),
|
|
39
|
+
copy_to_host(), and kernel launching for the detection kernels.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
from numpy.typing import NDArray
|
|
44
|
+
|
|
45
|
+
from ..kernels.detection import (
|
|
46
|
+
kernel_spherical_detect_single,
|
|
47
|
+
kernel_spherical_detect_multi,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# GPU support is optional
|
|
51
|
+
try:
|
|
52
|
+
from numba import cuda
|
|
53
|
+
|
|
54
|
+
_HAS_CUDA = cuda.is_available()
|
|
55
|
+
except ImportError:
|
|
56
|
+
_HAS_CUDA = False
|
|
57
|
+
|
|
58
|
+
class _FakeCuda:
|
|
59
|
+
@staticmethod
|
|
60
|
+
def is_available():
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
cuda = _FakeCuda() # type: ignore[assignment]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_spherical_gpu(
|
|
67
|
+
ray_positions: NDArray[np.float32],
|
|
68
|
+
ray_directions: NDArray[np.float32],
|
|
69
|
+
ray_active: NDArray[np.bool_],
|
|
70
|
+
ray_times: NDArray[np.float32],
|
|
71
|
+
ray_wavelengths: NDArray[np.float32],
|
|
72
|
+
ray_intensities: NDArray[np.float32],
|
|
73
|
+
detector_center: NDArray[np.float32],
|
|
74
|
+
detector_radius: float,
|
|
75
|
+
threads_per_block: int = 256,
|
|
76
|
+
) -> tuple[NDArray[np.bool_], NDArray[np.float32], NDArray[np.float32]]:
|
|
77
|
+
"""
|
|
78
|
+
GPU-accelerated detection for a single spherical detector.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
ray_positions : ndarray, shape (N, 3)
|
|
83
|
+
Ray starting positions
|
|
84
|
+
ray_directions : ndarray, shape (N, 3)
|
|
85
|
+
Ray directions
|
|
86
|
+
ray_active : ndarray, shape (N,)
|
|
87
|
+
Boolean mask of active rays
|
|
88
|
+
ray_times : ndarray, shape (N,)
|
|
89
|
+
Accumulated ray times
|
|
90
|
+
ray_wavelengths : ndarray, shape (N,)
|
|
91
|
+
Ray wavelengths
|
|
92
|
+
ray_intensities : ndarray, shape (N,)
|
|
93
|
+
Ray intensities
|
|
94
|
+
detector_center : ndarray, shape (3,)
|
|
95
|
+
Detector center position
|
|
96
|
+
detector_radius : float
|
|
97
|
+
Detector radius
|
|
98
|
+
threads_per_block : int
|
|
99
|
+
CUDA threads per block
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
hit_mask : ndarray, shape (N,)
|
|
104
|
+
Boolean mask of rays that hit detector
|
|
105
|
+
hit_distances : ndarray, shape (N,)
|
|
106
|
+
Distance to detector for each ray
|
|
107
|
+
hit_times : ndarray, shape (N,)
|
|
108
|
+
Arrival time at detector for each ray
|
|
109
|
+
"""
|
|
110
|
+
num_rays = len(ray_positions)
|
|
111
|
+
|
|
112
|
+
# Output arrays
|
|
113
|
+
hit_mask = np.zeros(num_rays, dtype=np.bool_)
|
|
114
|
+
hit_distances = np.zeros(num_rays, dtype=np.float32)
|
|
115
|
+
hit_times = np.zeros(num_rays, dtype=np.float32)
|
|
116
|
+
|
|
117
|
+
if _HAS_CUDA:
|
|
118
|
+
# Transfer to GPU
|
|
119
|
+
d_positions = cuda.to_device(ray_positions.astype(np.float32))
|
|
120
|
+
d_directions = cuda.to_device(ray_directions.astype(np.float32))
|
|
121
|
+
d_active = cuda.to_device(ray_active)
|
|
122
|
+
d_times = cuda.to_device(ray_times.astype(np.float32))
|
|
123
|
+
d_wavelengths = cuda.to_device(ray_wavelengths.astype(np.float32))
|
|
124
|
+
d_intensities = cuda.to_device(ray_intensities.astype(np.float32))
|
|
125
|
+
d_center = cuda.to_device(detector_center.astype(np.float32))
|
|
126
|
+
d_hit_mask = cuda.to_device(hit_mask)
|
|
127
|
+
d_hit_distances = cuda.to_device(hit_distances)
|
|
128
|
+
d_hit_times = cuda.to_device(hit_times)
|
|
129
|
+
|
|
130
|
+
# Launch kernel
|
|
131
|
+
blocks = (num_rays + threads_per_block - 1) // threads_per_block
|
|
132
|
+
kernel_spherical_detect_single[blocks, threads_per_block](
|
|
133
|
+
d_positions,
|
|
134
|
+
d_directions,
|
|
135
|
+
d_active,
|
|
136
|
+
d_times,
|
|
137
|
+
d_wavelengths,
|
|
138
|
+
d_intensities,
|
|
139
|
+
d_center,
|
|
140
|
+
np.float32(detector_radius),
|
|
141
|
+
d_hit_mask,
|
|
142
|
+
d_hit_distances,
|
|
143
|
+
d_hit_times,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
cuda.synchronize()
|
|
147
|
+
hit_mask = d_hit_mask.copy_to_host()
|
|
148
|
+
hit_distances = d_hit_distances.copy_to_host()
|
|
149
|
+
hit_times = d_hit_times.copy_to_host()
|
|
150
|
+
else:
|
|
151
|
+
# CPU fallback
|
|
152
|
+
hit_mask, hit_distances, hit_times = _detect_spherical_cpu(
|
|
153
|
+
ray_positions,
|
|
154
|
+
ray_directions,
|
|
155
|
+
ray_active,
|
|
156
|
+
ray_times,
|
|
157
|
+
detector_center,
|
|
158
|
+
detector_radius,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return hit_mask, hit_distances, hit_times
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def detect_multi_spherical_gpu(
|
|
165
|
+
ray_positions: NDArray[np.float32],
|
|
166
|
+
ray_directions: NDArray[np.float32],
|
|
167
|
+
ray_active: NDArray[np.bool_],
|
|
168
|
+
ray_times: NDArray[np.float32],
|
|
169
|
+
ray_intensities: NDArray[np.float32],
|
|
170
|
+
detector_centers: NDArray[np.float32],
|
|
171
|
+
detector_radius: float,
|
|
172
|
+
threads_per_block: int = 256,
|
|
173
|
+
) -> tuple[NDArray[np.int32], NDArray[np.float32]]:
|
|
174
|
+
"""
|
|
175
|
+
GPU-accelerated detection across multiple spherical detectors.
|
|
176
|
+
|
|
177
|
+
This is optimized for detector scans where we want to quickly count
|
|
178
|
+
hits and sum intensities for many detector positions at once.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
ray_positions : ndarray, shape (N, 3)
|
|
183
|
+
Ray starting positions
|
|
184
|
+
ray_directions : ndarray, shape (N, 3)
|
|
185
|
+
Ray directions
|
|
186
|
+
ray_active : ndarray, shape (N,)
|
|
187
|
+
Boolean mask of active rays
|
|
188
|
+
ray_times : ndarray, shape (N,)
|
|
189
|
+
Accumulated ray times
|
|
190
|
+
ray_intensities : ndarray, shape (N,)
|
|
191
|
+
Ray intensities
|
|
192
|
+
detector_centers : ndarray, shape (M, 3)
|
|
193
|
+
Detector center positions
|
|
194
|
+
detector_radius : float
|
|
195
|
+
Detector radius (same for all)
|
|
196
|
+
threads_per_block : int
|
|
197
|
+
CUDA threads per block
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
hit_counts : ndarray, shape (M,)
|
|
202
|
+
Number of rays hitting each detector
|
|
203
|
+
hit_intensities : ndarray, shape (M,)
|
|
204
|
+
Sum of intensities for rays hitting each detector
|
|
205
|
+
"""
|
|
206
|
+
num_rays = len(ray_positions)
|
|
207
|
+
num_detectors = len(detector_centers)
|
|
208
|
+
|
|
209
|
+
# Output arrays
|
|
210
|
+
hit_counts = np.zeros(num_detectors, dtype=np.int32)
|
|
211
|
+
hit_intensities = np.zeros(num_detectors, dtype=np.float32)
|
|
212
|
+
|
|
213
|
+
if _HAS_CUDA:
|
|
214
|
+
# Transfer to GPU
|
|
215
|
+
d_positions = cuda.to_device(ray_positions.astype(np.float32))
|
|
216
|
+
d_directions = cuda.to_device(ray_directions.astype(np.float32))
|
|
217
|
+
d_active = cuda.to_device(ray_active)
|
|
218
|
+
d_times = cuda.to_device(ray_times.astype(np.float32))
|
|
219
|
+
d_intensities = cuda.to_device(ray_intensities.astype(np.float32))
|
|
220
|
+
d_centers = cuda.to_device(detector_centers.astype(np.float32))
|
|
221
|
+
d_hit_counts = cuda.to_device(hit_counts)
|
|
222
|
+
d_hit_intensities = cuda.to_device(hit_intensities)
|
|
223
|
+
|
|
224
|
+
# Launch kernel (1D grid over rays)
|
|
225
|
+
blocks = (num_rays + threads_per_block - 1) // threads_per_block
|
|
226
|
+
kernel_spherical_detect_multi[blocks, threads_per_block](
|
|
227
|
+
d_positions,
|
|
228
|
+
d_directions,
|
|
229
|
+
d_active,
|
|
230
|
+
d_times,
|
|
231
|
+
d_centers,
|
|
232
|
+
np.float32(detector_radius),
|
|
233
|
+
d_hit_counts,
|
|
234
|
+
d_hit_intensities,
|
|
235
|
+
d_intensities,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
cuda.synchronize()
|
|
239
|
+
hit_counts = d_hit_counts.copy_to_host()
|
|
240
|
+
hit_intensities = d_hit_intensities.copy_to_host()
|
|
241
|
+
else:
|
|
242
|
+
# CPU fallback
|
|
243
|
+
hit_counts, hit_intensities = _detect_multi_spherical_cpu(
|
|
244
|
+
ray_positions,
|
|
245
|
+
ray_directions,
|
|
246
|
+
ray_active,
|
|
247
|
+
ray_intensities,
|
|
248
|
+
detector_centers,
|
|
249
|
+
detector_radius,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return hit_counts, hit_intensities
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# =============================================================================
|
|
256
|
+
# CPU Fallback Implementations
|
|
257
|
+
# =============================================================================
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _detect_spherical_cpu(
|
|
261
|
+
ray_positions: NDArray[np.float32],
|
|
262
|
+
ray_directions: NDArray[np.float32],
|
|
263
|
+
ray_active: NDArray[np.bool_],
|
|
264
|
+
ray_times: NDArray[np.float32],
|
|
265
|
+
detector_center: NDArray[np.float32],
|
|
266
|
+
detector_radius: float,
|
|
267
|
+
) -> tuple[NDArray[np.bool_], NDArray[np.float32], NDArray[np.float32]]:
|
|
268
|
+
"""CPU fallback for single detector detection."""
|
|
269
|
+
num_rays = len(ray_positions)
|
|
270
|
+
hit_mask = np.zeros(num_rays, dtype=np.bool_)
|
|
271
|
+
hit_distances = np.zeros(num_rays, dtype=np.float32)
|
|
272
|
+
hit_times = np.zeros(num_rays, dtype=np.float32)
|
|
273
|
+
|
|
274
|
+
c = 299792458.0
|
|
275
|
+
|
|
276
|
+
for i in range(num_rays):
|
|
277
|
+
if not ray_active[i]:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
origin = ray_positions[i]
|
|
281
|
+
direction = ray_directions[i]
|
|
282
|
+
d_len = np.linalg.norm(direction)
|
|
283
|
+
if d_len < 1e-10:
|
|
284
|
+
continue
|
|
285
|
+
direction = direction / d_len
|
|
286
|
+
|
|
287
|
+
oc = detector_center - origin
|
|
288
|
+
t_closest = np.dot(oc, direction)
|
|
289
|
+
|
|
290
|
+
if t_closest < 0:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
closest_point = origin + t_closest * direction
|
|
294
|
+
dist = np.linalg.norm(closest_point - detector_center)
|
|
295
|
+
|
|
296
|
+
if dist <= detector_radius:
|
|
297
|
+
hit_mask[i] = True
|
|
298
|
+
hit_distances[i] = t_closest
|
|
299
|
+
hit_times[i] = ray_times[i] + t_closest / c
|
|
300
|
+
|
|
301
|
+
return hit_mask, hit_distances, hit_times
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _detect_multi_spherical_cpu(
|
|
305
|
+
ray_positions: NDArray[np.float32],
|
|
306
|
+
ray_directions: NDArray[np.float32],
|
|
307
|
+
ray_active: NDArray[np.bool_],
|
|
308
|
+
ray_intensities: NDArray[np.float32],
|
|
309
|
+
detector_centers: NDArray[np.float32],
|
|
310
|
+
detector_radius: float,
|
|
311
|
+
) -> tuple[NDArray[np.int32], NDArray[np.float32]]:
|
|
312
|
+
"""CPU fallback for multi-detector detection."""
|
|
313
|
+
num_rays = len(ray_positions)
|
|
314
|
+
num_detectors = len(detector_centers)
|
|
315
|
+
|
|
316
|
+
hit_counts = np.zeros(num_detectors, dtype=np.int32)
|
|
317
|
+
hit_intensities = np.zeros(num_detectors, dtype=np.float32)
|
|
318
|
+
|
|
319
|
+
radius_sq = detector_radius**2
|
|
320
|
+
|
|
321
|
+
for i in range(num_rays):
|
|
322
|
+
if not ray_active[i]:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
origin = ray_positions[i]
|
|
326
|
+
direction = ray_directions[i]
|
|
327
|
+
d_len = np.linalg.norm(direction)
|
|
328
|
+
if d_len < 1e-10:
|
|
329
|
+
continue
|
|
330
|
+
direction = direction / d_len
|
|
331
|
+
intensity = ray_intensities[i]
|
|
332
|
+
|
|
333
|
+
for det_idx in range(num_detectors):
|
|
334
|
+
center = detector_centers[det_idx]
|
|
335
|
+
oc = center - origin
|
|
336
|
+
t_closest = np.dot(oc, direction)
|
|
337
|
+
|
|
338
|
+
if t_closest < 0:
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
closest_point = origin + t_closest * direction
|
|
342
|
+
dist_sq = np.sum((closest_point - center) ** 2)
|
|
343
|
+
|
|
344
|
+
if dist_sq <= radius_sq:
|
|
345
|
+
hit_counts[det_idx] += 1
|
|
346
|
+
hit_intensities[det_idx] += intensity
|
|
347
|
+
|
|
348
|
+
return hit_counts, hit_intensities
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
__all__ = [
|
|
352
|
+
"detect_spherical_gpu",
|
|
353
|
+
"detect_multi_spherical_gpu",
|
|
354
|
+
]
|