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,745 @@
|
|
|
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
|
+
Recording Sphere for Ray Detection and Data Storage
|
|
36
|
+
|
|
37
|
+
This module implements a spherical detection surface for capturing rays
|
|
38
|
+
at a specified altitude above Earth's surface, along with HDF5/numpy
|
|
39
|
+
data storage for ray information.
|
|
40
|
+
|
|
41
|
+
The recording sphere captures:
|
|
42
|
+
- Position (x, y, z)
|
|
43
|
+
- Direction (dx, dy, dz)
|
|
44
|
+
- Time of arrival
|
|
45
|
+
- Intensity
|
|
46
|
+
- Wavelength
|
|
47
|
+
- Generation number
|
|
48
|
+
- All geometric information needed to fully reconstruct rays
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from datetime import datetime
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
import numpy as np
|
|
57
|
+
from numpy.typing import NDArray
|
|
58
|
+
|
|
59
|
+
# Optional h5py import
|
|
60
|
+
try:
|
|
61
|
+
import h5py
|
|
62
|
+
|
|
63
|
+
HAS_H5PY = True
|
|
64
|
+
except ImportError:
|
|
65
|
+
HAS_H5PY = False
|
|
66
|
+
|
|
67
|
+
from ..surfaces import EARTH_RADIUS
|
|
68
|
+
from .ray_data import RayBatch
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class RecordedRays:
|
|
73
|
+
"""
|
|
74
|
+
Container for recorded ray data at the detection sphere.
|
|
75
|
+
|
|
76
|
+
All arrays have shape (N,) or (N, 3) where N is the number of recorded rays.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
positions : ndarray, shape (N, 3)
|
|
81
|
+
Intersection positions on the recording sphere (meters)
|
|
82
|
+
directions : ndarray, shape (N, 3)
|
|
83
|
+
Ray directions at intersection (unit vectors)
|
|
84
|
+
times : ndarray, shape (N,)
|
|
85
|
+
Time of arrival at recording sphere (seconds)
|
|
86
|
+
intensities : ndarray, shape (N,)
|
|
87
|
+
Ray intensity at recording sphere
|
|
88
|
+
wavelengths : ndarray, shape (N,)
|
|
89
|
+
Ray wavelength (meters)
|
|
90
|
+
generations : ndarray, shape (N,)
|
|
91
|
+
Ray generation (number of surface interactions)
|
|
92
|
+
polarization_vectors : ndarray, shape (N, 3), optional
|
|
93
|
+
3D polarization vectors (electric field direction) at intersection.
|
|
94
|
+
Unit vectors perpendicular to ray direction representing E-field orientation.
|
|
95
|
+
|
|
96
|
+
Derived quantities (computed on demand or at save time):
|
|
97
|
+
|
|
98
|
+
elevation_angles : ndarray, shape (N,)
|
|
99
|
+
Elevation angle above local horizon (radians)
|
|
100
|
+
azimuth_angles : ndarray, shape (N,)
|
|
101
|
+
Azimuth angle in local tangent plane (radians)
|
|
102
|
+
zenith_angles : ndarray, shape (N,)
|
|
103
|
+
Angle from local zenith (radians)
|
|
104
|
+
local_positions : ndarray, shape (N, 2)
|
|
105
|
+
Position in local tangent coordinates (meters)
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
positions: NDArray[np.float32]
|
|
109
|
+
directions: NDArray[np.float32]
|
|
110
|
+
times: NDArray[np.float32]
|
|
111
|
+
intensities: NDArray[np.float32]
|
|
112
|
+
wavelengths: NDArray[np.float32]
|
|
113
|
+
generations: NDArray[np.int32]
|
|
114
|
+
polarization_vectors: NDArray[np.float32] | None = None
|
|
115
|
+
ray_indices: NDArray[np.int32] | None = None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def num_rays(self) -> int:
|
|
119
|
+
"""Number of recorded rays."""
|
|
120
|
+
return len(self.positions)
|
|
121
|
+
|
|
122
|
+
def compute_angular_coordinates(
|
|
123
|
+
self,
|
|
124
|
+
earth_center: NDArray[np.float64] = None,
|
|
125
|
+
) -> dict[str, NDArray[np.float32]]:
|
|
126
|
+
"""
|
|
127
|
+
Compute angular coordinates for all recorded ray intersection points.
|
|
128
|
+
|
|
129
|
+
Computes spherical coordinates (latitude/longitude) of intersection points
|
|
130
|
+
on the detection sphere relative to Earth's center.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
earth_center : ndarray, optional
|
|
135
|
+
Earth center position, default (0, 0, -EARTH_RADIUS)
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
dict
|
|
140
|
+
Dictionary with:
|
|
141
|
+
- 'elevation': Latitude angle above equator (radians, -π/2 to π/2)
|
|
142
|
+
- 'azimuth': Longitude angle (radians, -π to π)
|
|
143
|
+
- 'zenith': Zenith angle from north pole (radians, 0 to π)
|
|
144
|
+
- 'incidence': Angle between ray direction and outward radial (radians)
|
|
145
|
+
"""
|
|
146
|
+
if earth_center is None:
|
|
147
|
+
earth_center = np.array([0, 0, -EARTH_RADIUS], dtype=np.float64)
|
|
148
|
+
|
|
149
|
+
# Vector from Earth center to intersection point
|
|
150
|
+
to_pos = self.positions.astype(np.float64) - earth_center
|
|
151
|
+
r = np.linalg.norm(to_pos, axis=1, keepdims=True)
|
|
152
|
+
|
|
153
|
+
# Spherical coordinates of the intersection point
|
|
154
|
+
# Elevation (latitude): angle above the equatorial plane (XY plane through Earth center)
|
|
155
|
+
elevation = np.arcsin(to_pos[:, 2] / r.squeeze())
|
|
156
|
+
|
|
157
|
+
# Azimuth (longitude): angle in the XY plane
|
|
158
|
+
azimuth = np.arctan2(to_pos[:, 1], to_pos[:, 0])
|
|
159
|
+
|
|
160
|
+
# Zenith angle: angle from +Z axis (north pole)
|
|
161
|
+
np.arccos(to_pos[:, 2] / r.squeeze())
|
|
162
|
+
|
|
163
|
+
# Incidence angle: angle between ray direction and outward radial (for reference)
|
|
164
|
+
radial = to_pos / r
|
|
165
|
+
cos_incidence = np.sum(self.directions * radial, axis=1)
|
|
166
|
+
incidence = np.arccos(np.clip(cos_incidence, -1.0, 1.0))
|
|
167
|
+
|
|
168
|
+
# For azimuth, project direction onto local tangent plane
|
|
169
|
+
# Create local basis
|
|
170
|
+
global_z = np.array([0, 0, 1], dtype=np.float64)
|
|
171
|
+
tangent_x = np.cross(global_z, radial)
|
|
172
|
+
tangent_x_norm = np.linalg.norm(tangent_x, axis=1, keepdims=True)
|
|
173
|
+
tangent_x = tangent_x / np.maximum(tangent_x_norm, 1e-10)
|
|
174
|
+
tangent_y = np.cross(radial, tangent_x)
|
|
175
|
+
|
|
176
|
+
# Project direction onto tangent plane
|
|
177
|
+
dir_x = np.sum(self.directions * tangent_x, axis=1)
|
|
178
|
+
dir_y = np.sum(self.directions * tangent_y, axis=1)
|
|
179
|
+
azimuth = np.arctan2(dir_y, dir_x)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"elevation": elevation.astype(np.float32),
|
|
183
|
+
"azimuth": azimuth.astype(np.float32),
|
|
184
|
+
"zenith": incidence.astype(np.float32),
|
|
185
|
+
"incidence": incidence.astype(np.float32),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def compute_viewing_angle_from_origin(
|
|
189
|
+
self,
|
|
190
|
+
origin: NDArray[np.float64] = None,
|
|
191
|
+
) -> NDArray[np.float32]:
|
|
192
|
+
"""
|
|
193
|
+
Compute viewing angle from horizontal at specified origin.
|
|
194
|
+
|
|
195
|
+
Calculates the angle above the horizontal plane (XY plane) when
|
|
196
|
+
viewing each intersection point from the origin position.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
origin : ndarray, optional
|
|
201
|
+
Observer position, default (0, 0, 0) - Earth surface at reference point
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
ndarray
|
|
206
|
+
Viewing angle from horizontal in radians (-π/2 to π/2)
|
|
207
|
+
Positive angles are above horizontal, negative below
|
|
208
|
+
"""
|
|
209
|
+
if origin is None:
|
|
210
|
+
origin = np.array([0, 0, 0], dtype=np.float64)
|
|
211
|
+
|
|
212
|
+
# Vector from origin to intersection point
|
|
213
|
+
to_point = self.positions.astype(np.float64) - origin
|
|
214
|
+
|
|
215
|
+
# Horizontal distance (in XY plane)
|
|
216
|
+
horiz_dist = np.sqrt(to_point[:, 0] ** 2 + to_point[:, 1] ** 2)
|
|
217
|
+
|
|
218
|
+
# Vertical distance (Z component)
|
|
219
|
+
vert_dist = to_point[:, 2]
|
|
220
|
+
|
|
221
|
+
# Angle from horizontal: arctan(z / sqrt(x^2 + y^2))
|
|
222
|
+
viewing_angle = np.arctan2(vert_dist, horiz_dist)
|
|
223
|
+
|
|
224
|
+
return viewing_angle.astype(np.float32)
|
|
225
|
+
|
|
226
|
+
def compute_ray_direction_angles(self) -> dict[str, NDArray[np.float32]]:
|
|
227
|
+
"""
|
|
228
|
+
Compute elevation and azimuth angles of ray directions.
|
|
229
|
+
|
|
230
|
+
Calculates the angles of the ray direction vectors themselves,
|
|
231
|
+
not the position coordinates. Useful for understanding the
|
|
232
|
+
angular distribution of ray propagation.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
dict
|
|
237
|
+
Dictionary with:
|
|
238
|
+
- 'elevation': Angle above horizontal plane in radians (-π/2 to π/2)
|
|
239
|
+
0 = horizontal, π/2 = straight up, -π/2 = straight down
|
|
240
|
+
- 'azimuth': Azimuth angle in horizontal plane in radians (-π to π)
|
|
241
|
+
0 = +X direction, π/2 = +Y direction
|
|
242
|
+
"""
|
|
243
|
+
# Elevation: angle from horizontal (XY) plane
|
|
244
|
+
# elevation = arcsin(z_component)
|
|
245
|
+
elevation = np.arcsin(np.clip(self.directions[:, 2], -1.0, 1.0))
|
|
246
|
+
|
|
247
|
+
# Azimuth: angle in XY plane
|
|
248
|
+
# azimuth = arctan2(y, x)
|
|
249
|
+
azimuth = np.arctan2(self.directions[:, 1], self.directions[:, 0])
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"elevation": elevation.astype(np.float32),
|
|
253
|
+
"azimuth": azimuth.astype(np.float32),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class RecordingSphere:
|
|
258
|
+
"""
|
|
259
|
+
Spherical detection surface at a specified altitude above Earth.
|
|
260
|
+
|
|
261
|
+
Records all rays that intersect the sphere, capturing full ray state
|
|
262
|
+
for later analysis.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
altitude : float
|
|
267
|
+
Altitude above Earth's surface in meters (default 33 km)
|
|
268
|
+
earth_center : Tuple[float, float, float]
|
|
269
|
+
Center of Earth, default (0, 0, -EARTH_RADIUS)
|
|
270
|
+
earth_radius : float
|
|
271
|
+
Earth radius in meters
|
|
272
|
+
|
|
273
|
+
Notes
|
|
274
|
+
-----
|
|
275
|
+
The recording sphere has radius = earth_radius + altitude, centered
|
|
276
|
+
at earth_center.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
altitude: float = 33000.0, # 33 km default
|
|
282
|
+
earth_center: tuple[float, float, float] = (0, 0, -EARTH_RADIUS),
|
|
283
|
+
earth_radius: float = EARTH_RADIUS,
|
|
284
|
+
):
|
|
285
|
+
self.altitude = altitude
|
|
286
|
+
self.earth_center = np.array(earth_center, dtype=np.float64)
|
|
287
|
+
self.earth_radius = earth_radius
|
|
288
|
+
self.sphere_radius = earth_radius + altitude
|
|
289
|
+
|
|
290
|
+
def detect(
|
|
291
|
+
self,
|
|
292
|
+
rays: RayBatch,
|
|
293
|
+
compute_travel_time: bool = True,
|
|
294
|
+
speed_of_light: float = 299792458.0,
|
|
295
|
+
max_propagation_distance: float | None = None,
|
|
296
|
+
) -> RecordedRays:
|
|
297
|
+
"""
|
|
298
|
+
Detect rays intersecting the recording sphere.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
rays : RayBatch
|
|
303
|
+
Rays to detect
|
|
304
|
+
compute_travel_time : bool
|
|
305
|
+
If True, add travel time to intersection to ray's accumulated time
|
|
306
|
+
speed_of_light : float
|
|
307
|
+
Speed of light for time computation
|
|
308
|
+
max_propagation_distance : float, optional
|
|
309
|
+
Maximum distance rays can propagate before detection (meters).
|
|
310
|
+
If None, no limit is applied. Use this to prevent detecting rays
|
|
311
|
+
that would hit the sphere from the opposite hemisphere.
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
RecordedRays
|
|
316
|
+
Recorded ray data for all intersecting rays
|
|
317
|
+
"""
|
|
318
|
+
active_mask = rays.active
|
|
319
|
+
if not np.any(active_mask):
|
|
320
|
+
return RecordedRays(
|
|
321
|
+
positions=np.zeros((0, 3), dtype=np.float32),
|
|
322
|
+
directions=np.zeros((0, 3), dtype=np.float32),
|
|
323
|
+
times=np.zeros(0, dtype=np.float32),
|
|
324
|
+
intensities=np.zeros(0, dtype=np.float32),
|
|
325
|
+
wavelengths=np.zeros(0, dtype=np.float32),
|
|
326
|
+
generations=np.zeros(0, dtype=np.int32),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
origins = rays.positions[active_mask].astype(np.float64)
|
|
330
|
+
directions = rays.directions[active_mask].astype(np.float64)
|
|
331
|
+
|
|
332
|
+
# Ray-sphere intersection using numerically stable formulation
|
|
333
|
+
# Ray: P = O + t*D
|
|
334
|
+
# Sphere: |P - C|² = R²
|
|
335
|
+
# Using half_b = b/2 to reduce magnitude and improve numerical stability
|
|
336
|
+
oc = origins - self.earth_center
|
|
337
|
+
a = np.sum(directions * directions, axis=1)
|
|
338
|
+
half_b = np.sum(directions * oc, axis=1) # This is b/2
|
|
339
|
+
c = np.sum(oc * oc, axis=1) - self.sphere_radius**2
|
|
340
|
+
|
|
341
|
+
# Discriminant using half_b: (b/2)² - ac instead of b² - 4ac
|
|
342
|
+
discriminant = half_b**2 - a * c
|
|
343
|
+
has_hit = discriminant >= 0
|
|
344
|
+
|
|
345
|
+
# Get intersection distance using stable formula
|
|
346
|
+
sqrt_disc = np.sqrt(np.maximum(discriminant, 0))
|
|
347
|
+
# t = (-b ± sqrt(b²-4ac)) / 2a = (-half_b ± sqrt(half_b² - ac)) / a
|
|
348
|
+
t1 = (-half_b - sqrt_disc) / (a + 1e-20)
|
|
349
|
+
t2 = (-half_b + sqrt_disc) / (a + 1e-20)
|
|
350
|
+
|
|
351
|
+
# For rays inside the sphere going outward, use t2 (far intersection)
|
|
352
|
+
# For rays outside going inward, use t1 (near intersection)
|
|
353
|
+
# Check if origin is inside or outside sphere
|
|
354
|
+
dist_from_center = np.linalg.norm(oc, axis=1)
|
|
355
|
+
inside = dist_from_center < self.sphere_radius
|
|
356
|
+
|
|
357
|
+
t = np.where(inside, t2, t1)
|
|
358
|
+
t = np.where(t > 0, t, t2) # If first choice was negative, try second
|
|
359
|
+
|
|
360
|
+
# Valid hits: positive t, discriminant >= 0, and within max distance
|
|
361
|
+
valid_hits = has_hit & (t > 1e-6)
|
|
362
|
+
if max_propagation_distance is not None:
|
|
363
|
+
valid_hits = valid_hits & (t < max_propagation_distance)
|
|
364
|
+
|
|
365
|
+
if not np.any(valid_hits):
|
|
366
|
+
return RecordedRays(
|
|
367
|
+
positions=np.zeros((0, 3), dtype=np.float32),
|
|
368
|
+
directions=np.zeros((0, 3), dtype=np.float32),
|
|
369
|
+
times=np.zeros(0, dtype=np.float32),
|
|
370
|
+
intensities=np.zeros(0, dtype=np.float32),
|
|
371
|
+
wavelengths=np.zeros(0, dtype=np.float32),
|
|
372
|
+
generations=np.zeros(0, dtype=np.int32),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Compute intersection positions
|
|
376
|
+
hit_positions = (
|
|
377
|
+
origins[valid_hits] + t[valid_hits, np.newaxis] * directions[valid_hits]
|
|
378
|
+
)
|
|
379
|
+
hit_directions = directions[valid_hits]
|
|
380
|
+
hit_distances = t[valid_hits]
|
|
381
|
+
|
|
382
|
+
# Get other ray properties
|
|
383
|
+
active_indices = np.where(active_mask)[0]
|
|
384
|
+
hit_indices = active_indices[valid_hits]
|
|
385
|
+
|
|
386
|
+
hit_intensities = rays.intensities[hit_indices]
|
|
387
|
+
hit_wavelengths = rays.wavelengths[hit_indices]
|
|
388
|
+
hit_generations = rays.generations[hit_indices]
|
|
389
|
+
hit_times = rays.accumulated_time[hit_indices]
|
|
390
|
+
|
|
391
|
+
# Add travel time
|
|
392
|
+
if compute_travel_time:
|
|
393
|
+
travel_time = hit_distances / speed_of_light
|
|
394
|
+
hit_times = hit_times + travel_time.astype(np.float32)
|
|
395
|
+
|
|
396
|
+
# Get polarization vectors if available
|
|
397
|
+
hit_polarization_vectors = None
|
|
398
|
+
if rays.polarization_vector is not None:
|
|
399
|
+
hit_polarization_vectors = rays.polarization_vector[hit_indices]
|
|
400
|
+
|
|
401
|
+
return RecordedRays(
|
|
402
|
+
positions=hit_positions.astype(np.float32),
|
|
403
|
+
directions=hit_directions.astype(np.float32),
|
|
404
|
+
times=hit_times,
|
|
405
|
+
intensities=hit_intensities,
|
|
406
|
+
wavelengths=hit_wavelengths,
|
|
407
|
+
generations=hit_generations,
|
|
408
|
+
polarization_vectors=hit_polarization_vectors,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def save_recorded_rays_hdf5(
|
|
413
|
+
recorded_rays: RecordedRays,
|
|
414
|
+
filepath: str,
|
|
415
|
+
metadata: dict[str, Any] | None = None,
|
|
416
|
+
compression: str = "gzip",
|
|
417
|
+
) -> None:
|
|
418
|
+
"""
|
|
419
|
+
Save recorded rays to HDF5 file.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
recorded_rays : RecordedRays
|
|
424
|
+
Ray data to save
|
|
425
|
+
filepath : str
|
|
426
|
+
Output file path
|
|
427
|
+
metadata : dict, optional
|
|
428
|
+
Additional metadata to store (simulation parameters, etc.)
|
|
429
|
+
compression : str
|
|
430
|
+
Compression algorithm ('gzip', 'lzf', or None)
|
|
431
|
+
"""
|
|
432
|
+
if not HAS_H5PY:
|
|
433
|
+
raise ImportError(
|
|
434
|
+
"h5py is required for HDF5 support. Install with: pip install h5py"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
filepath = Path(filepath)
|
|
438
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
439
|
+
|
|
440
|
+
with h5py.File(filepath, "w") as f:
|
|
441
|
+
# Create rays group
|
|
442
|
+
rays_grp = f.create_group("rays")
|
|
443
|
+
|
|
444
|
+
# Store ray data with compression
|
|
445
|
+
rays_grp.create_dataset(
|
|
446
|
+
"positions", data=recorded_rays.positions, compression=compression
|
|
447
|
+
)
|
|
448
|
+
rays_grp.create_dataset(
|
|
449
|
+
"directions", data=recorded_rays.directions, compression=compression
|
|
450
|
+
)
|
|
451
|
+
rays_grp.create_dataset(
|
|
452
|
+
"times", data=recorded_rays.times, compression=compression
|
|
453
|
+
)
|
|
454
|
+
rays_grp.create_dataset(
|
|
455
|
+
"intensities", data=recorded_rays.intensities, compression=compression
|
|
456
|
+
)
|
|
457
|
+
rays_grp.create_dataset(
|
|
458
|
+
"wavelengths", data=recorded_rays.wavelengths, compression=compression
|
|
459
|
+
)
|
|
460
|
+
rays_grp.create_dataset(
|
|
461
|
+
"generations", data=recorded_rays.generations, compression=compression
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Save polarization vectors if available
|
|
465
|
+
if recorded_rays.polarization_vectors is not None:
|
|
466
|
+
rays_grp.create_dataset(
|
|
467
|
+
"polarization_vectors",
|
|
468
|
+
data=recorded_rays.polarization_vectors,
|
|
469
|
+
compression=compression,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# Compute and store angular coordinates
|
|
473
|
+
angular = recorded_rays.compute_angular_coordinates()
|
|
474
|
+
angular_grp = f.create_group("angular")
|
|
475
|
+
for key, value in angular.items():
|
|
476
|
+
angular_grp.create_dataset(key, data=value, compression=compression)
|
|
477
|
+
|
|
478
|
+
# Store metadata
|
|
479
|
+
meta_grp = f.create_group("metadata")
|
|
480
|
+
meta_grp.attrs["num_rays"] = recorded_rays.num_rays
|
|
481
|
+
meta_grp.attrs["save_time"] = datetime.now().isoformat()
|
|
482
|
+
|
|
483
|
+
if metadata is not None:
|
|
484
|
+
for key, value in metadata.items():
|
|
485
|
+
if isinstance(value, (int, float, str, bool)):
|
|
486
|
+
meta_grp.attrs[key] = value
|
|
487
|
+
elif isinstance(value, np.ndarray):
|
|
488
|
+
meta_grp.create_dataset(key, data=value)
|
|
489
|
+
elif isinstance(value, (list, tuple)):
|
|
490
|
+
meta_grp.create_dataset(key, data=np.array(value))
|
|
491
|
+
else:
|
|
492
|
+
meta_grp.attrs[key] = str(value)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def load_recorded_rays_hdf5(filepath: str) -> tuple[RecordedRays, dict[str, Any]]:
|
|
496
|
+
"""
|
|
497
|
+
Load recorded rays from HDF5 file.
|
|
498
|
+
|
|
499
|
+
Parameters
|
|
500
|
+
----------
|
|
501
|
+
filepath : str
|
|
502
|
+
Input file path
|
|
503
|
+
|
|
504
|
+
Returns
|
|
505
|
+
-------
|
|
506
|
+
recorded_rays : RecordedRays
|
|
507
|
+
Loaded ray data
|
|
508
|
+
metadata : dict
|
|
509
|
+
Loaded metadata
|
|
510
|
+
"""
|
|
511
|
+
if not HAS_H5PY:
|
|
512
|
+
raise ImportError(
|
|
513
|
+
"h5py is required for HDF5 support. Install with: pip install h5py"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
with h5py.File(filepath, "r") as f:
|
|
517
|
+
rays_grp = f["rays"]
|
|
518
|
+
|
|
519
|
+
# Load polarization vectors if available
|
|
520
|
+
polarization_vectors = None
|
|
521
|
+
if "polarization_vectors" in rays_grp:
|
|
522
|
+
polarization_vectors = rays_grp["polarization_vectors"][...]
|
|
523
|
+
|
|
524
|
+
recorded_rays = RecordedRays(
|
|
525
|
+
positions=rays_grp["positions"][...],
|
|
526
|
+
directions=rays_grp["directions"][...],
|
|
527
|
+
times=rays_grp["times"][...],
|
|
528
|
+
intensities=rays_grp["intensities"][...],
|
|
529
|
+
wavelengths=rays_grp["wavelengths"][...],
|
|
530
|
+
generations=rays_grp["generations"][...],
|
|
531
|
+
polarization_vectors=polarization_vectors,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
metadata = {}
|
|
535
|
+
if "metadata" in f:
|
|
536
|
+
meta_grp = f["metadata"]
|
|
537
|
+
for key, value in meta_grp.attrs.items():
|
|
538
|
+
metadata[key] = value
|
|
539
|
+
for key in meta_grp.keys():
|
|
540
|
+
metadata[key] = meta_grp[key][...]
|
|
541
|
+
|
|
542
|
+
return recorded_rays, metadata
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def save_recorded_rays_numpy(
|
|
546
|
+
recorded_rays: RecordedRays,
|
|
547
|
+
filepath: str,
|
|
548
|
+
metadata: dict[str, Any] | None = None,
|
|
549
|
+
) -> None:
|
|
550
|
+
"""
|
|
551
|
+
Save recorded rays to numpy .npz file.
|
|
552
|
+
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
recorded_rays : RecordedRays
|
|
556
|
+
Ray data to save
|
|
557
|
+
filepath : str
|
|
558
|
+
Output file path
|
|
559
|
+
metadata : dict, optional
|
|
560
|
+
Additional metadata to store
|
|
561
|
+
"""
|
|
562
|
+
filepath = Path(filepath)
|
|
563
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
564
|
+
|
|
565
|
+
# Compute angular coordinates
|
|
566
|
+
angular = recorded_rays.compute_angular_coordinates()
|
|
567
|
+
|
|
568
|
+
# Prepare save dict
|
|
569
|
+
save_dict = {
|
|
570
|
+
"positions": recorded_rays.positions,
|
|
571
|
+
"directions": recorded_rays.directions,
|
|
572
|
+
"times": recorded_rays.times,
|
|
573
|
+
"intensities": recorded_rays.intensities,
|
|
574
|
+
"wavelengths": recorded_rays.wavelengths,
|
|
575
|
+
"generations": recorded_rays.generations,
|
|
576
|
+
"elevation": angular["elevation"],
|
|
577
|
+
"azimuth": angular["azimuth"],
|
|
578
|
+
"zenith": angular["zenith"],
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# Save polarization vectors if available
|
|
582
|
+
if recorded_rays.polarization_vectors is not None:
|
|
583
|
+
save_dict["polarization_vectors"] = recorded_rays.polarization_vectors
|
|
584
|
+
|
|
585
|
+
# Add metadata as arrays or scalars
|
|
586
|
+
if metadata is not None:
|
|
587
|
+
for key, value in metadata.items():
|
|
588
|
+
save_dict[f"meta_{key}"] = np.array(value)
|
|
589
|
+
|
|
590
|
+
np.savez_compressed(filepath, **save_dict)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def load_recorded_rays_numpy(filepath: str) -> tuple[RecordedRays, dict[str, Any]]:
|
|
594
|
+
"""
|
|
595
|
+
Load recorded rays from numpy .npz file.
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
filepath : str
|
|
600
|
+
Input file path
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
recorded_rays : RecordedRays
|
|
605
|
+
Loaded ray data
|
|
606
|
+
metadata : dict
|
|
607
|
+
Loaded metadata
|
|
608
|
+
"""
|
|
609
|
+
data = np.load(filepath)
|
|
610
|
+
|
|
611
|
+
# Load polarization vectors if available
|
|
612
|
+
polarization_vectors = None
|
|
613
|
+
if "polarization_vectors" in data.files:
|
|
614
|
+
polarization_vectors = data["polarization_vectors"]
|
|
615
|
+
|
|
616
|
+
recorded_rays = RecordedRays(
|
|
617
|
+
positions=data["positions"],
|
|
618
|
+
directions=data["directions"],
|
|
619
|
+
times=data["times"],
|
|
620
|
+
intensities=data["intensities"],
|
|
621
|
+
wavelengths=data["wavelengths"],
|
|
622
|
+
generations=data["generations"],
|
|
623
|
+
polarization_vectors=polarization_vectors,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
metadata = {}
|
|
627
|
+
for key in data.files:
|
|
628
|
+
if key.startswith("meta_"):
|
|
629
|
+
metadata[key[5:]] = data[key]
|
|
630
|
+
|
|
631
|
+
return recorded_rays, metadata
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class LocalRecordingSphere:
|
|
635
|
+
"""
|
|
636
|
+
Simple spherical detection surface centered at origin.
|
|
637
|
+
|
|
638
|
+
Records all rays that intersect the sphere from inside, useful for
|
|
639
|
+
local-scale simulations without Earth curvature.
|
|
640
|
+
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
radius : float
|
|
644
|
+
Sphere radius in meters (default 33 km)
|
|
645
|
+
center : tuple
|
|
646
|
+
Center position (default (0, 0, 0))
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
def __init__(self, radius: float = 33000.0, center=(0, 0, 0)):
|
|
650
|
+
self.radius = radius
|
|
651
|
+
self.center = np.array(center, dtype=np.float64)
|
|
652
|
+
self.sphere_radius = radius # For compatibility with RecordingSphere
|
|
653
|
+
|
|
654
|
+
def record_rays(self, rays: RayBatch) -> RecordedRays:
|
|
655
|
+
"""
|
|
656
|
+
Record rays that intersect the sphere.
|
|
657
|
+
|
|
658
|
+
Parameters
|
|
659
|
+
----------
|
|
660
|
+
rays : RayBatch
|
|
661
|
+
Rays to check for intersections
|
|
662
|
+
|
|
663
|
+
Returns
|
|
664
|
+
-------
|
|
665
|
+
RecordedRays
|
|
666
|
+
Recorded ray data
|
|
667
|
+
"""
|
|
668
|
+
# Vector from ray origins to sphere center
|
|
669
|
+
oc = rays.positions - self.center
|
|
670
|
+
|
|
671
|
+
# Quadratic equation coefficients for ray-sphere intersection
|
|
672
|
+
# (origin + t*direction - center)^2 = radius^2
|
|
673
|
+
a = np.sum(rays.directions**2, axis=1)
|
|
674
|
+
b = 2 * np.sum(oc * rays.directions, axis=1)
|
|
675
|
+
c = np.sum(oc**2, axis=1) - self.radius**2
|
|
676
|
+
|
|
677
|
+
discriminant = b**2 - 4 * a * c
|
|
678
|
+
|
|
679
|
+
# Find rays that hit the sphere
|
|
680
|
+
hit_mask = discriminant >= 0
|
|
681
|
+
|
|
682
|
+
if np.sum(hit_mask) == 0:
|
|
683
|
+
# No intersections
|
|
684
|
+
return RecordedRays(
|
|
685
|
+
positions=np.empty((0, 3), dtype=np.float32),
|
|
686
|
+
directions=np.empty((0, 3), dtype=np.float32),
|
|
687
|
+
times=np.empty(0, dtype=np.float32),
|
|
688
|
+
intensities=np.empty(0, dtype=np.float32),
|
|
689
|
+
wavelengths=np.empty(0, dtype=np.float32),
|
|
690
|
+
generations=np.empty(0, dtype=np.int32),
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# Calculate intersection distances
|
|
694
|
+
sqrt_disc = np.sqrt(discriminant[hit_mask])
|
|
695
|
+
t1 = (-b[hit_mask] - sqrt_disc) / (2 * a[hit_mask])
|
|
696
|
+
t2 = (-b[hit_mask] + sqrt_disc) / (2 * a[hit_mask])
|
|
697
|
+
|
|
698
|
+
# Take the positive intersection (forward along ray)
|
|
699
|
+
# For rays inside the sphere, take t2 (exit point)
|
|
700
|
+
t = np.where(t1 > 0, t1, t2)
|
|
701
|
+
valid = t > 0
|
|
702
|
+
|
|
703
|
+
if np.sum(valid) == 0:
|
|
704
|
+
return RecordedRays(
|
|
705
|
+
positions=np.empty((0, 3), dtype=np.float32),
|
|
706
|
+
directions=np.empty((0, 3), dtype=np.float32),
|
|
707
|
+
times=np.empty(0, dtype=np.float32),
|
|
708
|
+
intensities=np.empty(0, dtype=np.float32),
|
|
709
|
+
wavelengths=np.empty(0, dtype=np.float32),
|
|
710
|
+
generations=np.empty(0, dtype=np.int32),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Extract hit rays
|
|
714
|
+
final_mask = np.zeros(len(hit_mask), dtype=bool)
|
|
715
|
+
final_mask[np.where(hit_mask)[0][valid]] = True
|
|
716
|
+
|
|
717
|
+
hit_positions = rays.positions[final_mask]
|
|
718
|
+
hit_directions = rays.directions[final_mask]
|
|
719
|
+
hit_intensities = rays.intensities[final_mask]
|
|
720
|
+
hit_times = rays.accumulated_time[final_mask]
|
|
721
|
+
hit_wavelengths = rays.wavelengths[final_mask]
|
|
722
|
+
hit_generations = rays.generations[final_mask]
|
|
723
|
+
t_final = t[valid]
|
|
724
|
+
|
|
725
|
+
# Calculate intersection positions
|
|
726
|
+
intersection_positions = hit_positions + t_final[:, np.newaxis] * hit_directions
|
|
727
|
+
|
|
728
|
+
# Update times (distance / speed of light)
|
|
729
|
+
c_light = 299792458.0 # m/s
|
|
730
|
+
arrival_times = hit_times + (t_final / c_light).astype(np.float32)
|
|
731
|
+
|
|
732
|
+
# Get polarization vectors if available
|
|
733
|
+
hit_polarization_vectors = None
|
|
734
|
+
if rays.polarization_vector is not None:
|
|
735
|
+
hit_polarization_vectors = rays.polarization_vector[final_mask]
|
|
736
|
+
|
|
737
|
+
return RecordedRays(
|
|
738
|
+
positions=intersection_positions.astype(np.float32),
|
|
739
|
+
directions=hit_directions.astype(np.float32),
|
|
740
|
+
times=arrival_times,
|
|
741
|
+
intensities=hit_intensities,
|
|
742
|
+
wavelengths=hit_wavelengths,
|
|
743
|
+
generations=hit_generations,
|
|
744
|
+
polarization_vectors=hit_polarization_vectors,
|
|
745
|
+
)
|