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,418 @@
|
|
|
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
|
+
HEALPix Utilities for Sphere Pattern Analysis
|
|
36
|
+
|
|
37
|
+
Functions for converting recorded ray data to HEALPix representation
|
|
38
|
+
and aggregating statistics per pixel.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import numpy as np
|
|
42
|
+
from numpy.typing import NDArray
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from astropy import units as u
|
|
46
|
+
from astropy_healpix import HEALPix
|
|
47
|
+
|
|
48
|
+
HAS_HEALPIX = True
|
|
49
|
+
except ImportError:
|
|
50
|
+
HAS_HEALPIX = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HEALPixData:
|
|
54
|
+
"""
|
|
55
|
+
Container for HEALPix-mapped ray data.
|
|
56
|
+
|
|
57
|
+
Attributes
|
|
58
|
+
----------
|
|
59
|
+
nside : int
|
|
60
|
+
HEALPix resolution parameter
|
|
61
|
+
npix : int
|
|
62
|
+
Total number of HEALPix pixels
|
|
63
|
+
pixel_indices : ndarray
|
|
64
|
+
HEALPix pixel index for each ray
|
|
65
|
+
lon : ndarray
|
|
66
|
+
Longitude (azimuth) for each ray in radians
|
|
67
|
+
lat : ndarray
|
|
68
|
+
Latitude (elevation) for each ray in radians
|
|
69
|
+
intensities : ndarray
|
|
70
|
+
Intensity for each ray
|
|
71
|
+
times : ndarray
|
|
72
|
+
Arrival time for each ray (seconds)
|
|
73
|
+
generations : ndarray
|
|
74
|
+
Generation (bounce count) for each ray
|
|
75
|
+
viewing_angle : ndarray or None
|
|
76
|
+
Viewing angle from horizontal at (0,0,0) in radians
|
|
77
|
+
ray_elevation : ndarray or None
|
|
78
|
+
Ray direction elevation angle from horizontal in radians
|
|
79
|
+
ray_azimuth : ndarray or None
|
|
80
|
+
Ray direction azimuth angle in radians
|
|
81
|
+
aggregated : dict or None
|
|
82
|
+
Aggregated statistics per pixel (if computed)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
nside: int,
|
|
88
|
+
pixel_indices: NDArray[np.int64],
|
|
89
|
+
lon: NDArray[np.float64],
|
|
90
|
+
lat: NDArray[np.float64],
|
|
91
|
+
intensities: NDArray[np.float32],
|
|
92
|
+
times: NDArray[np.float32],
|
|
93
|
+
generations: NDArray[np.int32],
|
|
94
|
+
viewing_angle: NDArray[np.float32] | None = None,
|
|
95
|
+
ray_elevation: NDArray[np.float32] | None = None,
|
|
96
|
+
ray_azimuth: NDArray[np.float32] | None = None,
|
|
97
|
+
):
|
|
98
|
+
self.nside = nside
|
|
99
|
+
self.npix = 12 * nside**2
|
|
100
|
+
self.pixel_indices = pixel_indices
|
|
101
|
+
self.lon = lon
|
|
102
|
+
self.lat = lat
|
|
103
|
+
self.intensities = intensities
|
|
104
|
+
self.times = times
|
|
105
|
+
self.generations = generations
|
|
106
|
+
self.viewing_angle = viewing_angle
|
|
107
|
+
self.ray_elevation = ray_elevation
|
|
108
|
+
self.ray_azimuth = ray_azimuth
|
|
109
|
+
self.aggregated = None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def num_rays(self) -> int:
|
|
113
|
+
"""Number of recorded rays."""
|
|
114
|
+
return len(self.pixel_indices)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def rays_to_healpix(
|
|
118
|
+
recorded_rays,
|
|
119
|
+
nside: int = 128,
|
|
120
|
+
) -> HEALPixData:
|
|
121
|
+
"""
|
|
122
|
+
Convert recorded rays to HEALPix pixel representation.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
recorded_rays : RecordedRays
|
|
127
|
+
Recorded ray data from detection sphere
|
|
128
|
+
nside : int, optional
|
|
129
|
+
HEALPix resolution parameter (default: 128)
|
|
130
|
+
Higher values give finer resolution
|
|
131
|
+
nside=64 → ~3,400 pixels
|
|
132
|
+
nside=128 → ~13,000 pixels
|
|
133
|
+
nside=256 → ~50,000 pixels
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
HEALPixData
|
|
138
|
+
HEALPix-mapped ray data
|
|
139
|
+
|
|
140
|
+
Raises
|
|
141
|
+
------
|
|
142
|
+
ImportError
|
|
143
|
+
If astropy-healpix is not installed
|
|
144
|
+
"""
|
|
145
|
+
if not HAS_HEALPIX:
|
|
146
|
+
raise ImportError(
|
|
147
|
+
"astropy-healpix is required. Install with: pip install astropy-healpix"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Compute angular coordinates
|
|
151
|
+
angular = recorded_rays.compute_angular_coordinates()
|
|
152
|
+
|
|
153
|
+
# Extract azimuth (longitude) and elevation (latitude)
|
|
154
|
+
# azimuth: 0 to 2π
|
|
155
|
+
# elevation: -π/2 to π/2 (horizon at 0)
|
|
156
|
+
azimuth = angular["azimuth"] # radians
|
|
157
|
+
elevation = angular["elevation"] # radians
|
|
158
|
+
|
|
159
|
+
# Create HEALPix object
|
|
160
|
+
hp = HEALPix(nside=nside, order="ring", frame=None)
|
|
161
|
+
|
|
162
|
+
# Convert to astropy units
|
|
163
|
+
lon = azimuth * u.rad
|
|
164
|
+
lat = elevation * u.rad
|
|
165
|
+
|
|
166
|
+
# Get HEALPix pixel indices
|
|
167
|
+
pixel_indices = hp.lonlat_to_healpix(lon, lat)
|
|
168
|
+
|
|
169
|
+
# Compute viewing angle from horizontal at (0,0,0)
|
|
170
|
+
viewing_angle = recorded_rays.compute_viewing_angle_from_origin()
|
|
171
|
+
|
|
172
|
+
# Compute ray direction angles
|
|
173
|
+
ray_dir_angles = recorded_rays.compute_ray_direction_angles()
|
|
174
|
+
|
|
175
|
+
return HEALPixData(
|
|
176
|
+
nside=nside,
|
|
177
|
+
pixel_indices=pixel_indices,
|
|
178
|
+
lon=azimuth,
|
|
179
|
+
lat=elevation,
|
|
180
|
+
intensities=recorded_rays.intensities,
|
|
181
|
+
times=recorded_rays.times,
|
|
182
|
+
generations=recorded_rays.generations,
|
|
183
|
+
viewing_angle=viewing_angle,
|
|
184
|
+
ray_elevation=ray_dir_angles["elevation"],
|
|
185
|
+
ray_azimuth=ray_dir_angles["azimuth"],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def aggregate_healpix_data(
|
|
190
|
+
healpix_data: HEALPixData,
|
|
191
|
+
) -> dict[str, NDArray]:
|
|
192
|
+
"""
|
|
193
|
+
Aggregate ray properties per HEALPix pixel.
|
|
194
|
+
|
|
195
|
+
Computes per-pixel statistics:
|
|
196
|
+
- Total intensity (sum)
|
|
197
|
+
- Mean arrival time (intensity-weighted)
|
|
198
|
+
- Ray count
|
|
199
|
+
- Mean generation
|
|
200
|
+
- Intensity standard deviation
|
|
201
|
+
- Time standard deviation
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
healpix_data : HEALPixData
|
|
206
|
+
HEALPix-mapped ray data
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
dict
|
|
211
|
+
Dictionary with aggregated arrays for each occupied pixel:
|
|
212
|
+
- 'pixel_ids': Array of pixel indices with data
|
|
213
|
+
- 'intensity_sum': Total intensity per pixel
|
|
214
|
+
- 'intensity_mean': Mean intensity per pixel
|
|
215
|
+
- 'time_weighted_mean': Intensity-weighted mean time
|
|
216
|
+
- 'time_mean': Arithmetic mean time
|
|
217
|
+
- 'time_std': Standard deviation of arrival time
|
|
218
|
+
- 'ray_count': Number of rays per pixel
|
|
219
|
+
- 'generation_mean': Mean generation per pixel
|
|
220
|
+
"""
|
|
221
|
+
# Get unique pixel indices
|
|
222
|
+
unique_pixels = np.unique(healpix_data.pixel_indices)
|
|
223
|
+
n_pixels = len(unique_pixels)
|
|
224
|
+
|
|
225
|
+
# Initialize aggregated arrays
|
|
226
|
+
intensity_sum = np.zeros(n_pixels, dtype=np.float64)
|
|
227
|
+
intensity_mean = np.zeros(n_pixels, dtype=np.float64)
|
|
228
|
+
time_weighted_mean = np.zeros(n_pixels, dtype=np.float64)
|
|
229
|
+
time_mean = np.zeros(n_pixels, dtype=np.float64)
|
|
230
|
+
time_std = np.zeros(n_pixels, dtype=np.float64)
|
|
231
|
+
ray_count = np.zeros(n_pixels, dtype=np.int32)
|
|
232
|
+
generation_mean = np.zeros(n_pixels, dtype=np.float64)
|
|
233
|
+
|
|
234
|
+
# Aggregate per pixel
|
|
235
|
+
for i, pixel_id in enumerate(unique_pixels):
|
|
236
|
+
mask = healpix_data.pixel_indices == pixel_id
|
|
237
|
+
|
|
238
|
+
# Intensities in this pixel
|
|
239
|
+
pixel_intensities = healpix_data.intensities[mask]
|
|
240
|
+
pixel_times = healpix_data.times[mask]
|
|
241
|
+
pixel_generations = healpix_data.generations[mask]
|
|
242
|
+
|
|
243
|
+
# Sum and count
|
|
244
|
+
intensity_sum[i] = np.sum(pixel_intensities)
|
|
245
|
+
ray_count[i] = np.sum(mask)
|
|
246
|
+
intensity_mean[i] = np.mean(pixel_intensities)
|
|
247
|
+
|
|
248
|
+
# Intensity-weighted mean time
|
|
249
|
+
total_intensity = intensity_sum[i]
|
|
250
|
+
if total_intensity > 0:
|
|
251
|
+
time_weighted_mean[i] = (
|
|
252
|
+
np.sum(pixel_times * pixel_intensities) / total_intensity
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
time_weighted_mean[i] = np.mean(pixel_times)
|
|
256
|
+
|
|
257
|
+
# Arithmetic mean time and std
|
|
258
|
+
time_mean[i] = np.mean(pixel_times)
|
|
259
|
+
time_std[i] = np.std(pixel_times) if len(pixel_times) > 1 else 0.0
|
|
260
|
+
|
|
261
|
+
# Mean generation
|
|
262
|
+
generation_mean[i] = np.mean(pixel_generations)
|
|
263
|
+
|
|
264
|
+
aggregated = {
|
|
265
|
+
"pixel_ids": unique_pixels,
|
|
266
|
+
"intensity_sum": intensity_sum,
|
|
267
|
+
"intensity_mean": intensity_mean,
|
|
268
|
+
"time_weighted_mean": time_weighted_mean,
|
|
269
|
+
"time_mean": time_mean,
|
|
270
|
+
"time_std": time_std,
|
|
271
|
+
"ray_count": ray_count,
|
|
272
|
+
"generation_mean": generation_mean,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Store in HEALPixData object
|
|
276
|
+
healpix_data.aggregated = aggregated
|
|
277
|
+
|
|
278
|
+
return aggregated
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def identify_peak_region(
|
|
282
|
+
healpix_data: HEALPixData,
|
|
283
|
+
threshold_percentile: float = 90.0,
|
|
284
|
+
) -> tuple[NDArray[np.int64], NDArray[np.bool_]]:
|
|
285
|
+
"""
|
|
286
|
+
Identify pixels in the peak intensity region.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
healpix_data : HEALPixData
|
|
291
|
+
HEALPix data with aggregated statistics
|
|
292
|
+
threshold_percentile : float, optional
|
|
293
|
+
Percentile threshold for peak region (default: 90.0)
|
|
294
|
+
90.0 means top 10% of pixels by intensity
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
peak_pixel_ids : ndarray
|
|
299
|
+
Pixel IDs in the peak region
|
|
300
|
+
peak_mask : ndarray
|
|
301
|
+
Boolean mask for rays in peak region
|
|
302
|
+
|
|
303
|
+
Raises
|
|
304
|
+
------
|
|
305
|
+
ValueError
|
|
306
|
+
If aggregated data not computed
|
|
307
|
+
"""
|
|
308
|
+
if healpix_data.aggregated is None:
|
|
309
|
+
raise ValueError("Must call aggregate_healpix_data() first")
|
|
310
|
+
|
|
311
|
+
# Get intensity sum per pixel
|
|
312
|
+
intensity_sum = healpix_data.aggregated["intensity_sum"]
|
|
313
|
+
pixel_ids = healpix_data.aggregated["pixel_ids"]
|
|
314
|
+
|
|
315
|
+
# Find threshold
|
|
316
|
+
threshold = np.percentile(intensity_sum, threshold_percentile)
|
|
317
|
+
|
|
318
|
+
# Identify peak pixels
|
|
319
|
+
peak_pixel_mask = intensity_sum >= threshold
|
|
320
|
+
peak_pixel_ids = pixel_ids[peak_pixel_mask]
|
|
321
|
+
|
|
322
|
+
# Create mask for individual rays in peak region
|
|
323
|
+
peak_ray_mask = np.isin(healpix_data.pixel_indices, peak_pixel_ids)
|
|
324
|
+
|
|
325
|
+
return peak_pixel_ids, peak_ray_mask
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def compute_time_statistics(
|
|
329
|
+
healpix_data: HEALPixData,
|
|
330
|
+
peak_mask: NDArray[np.bool_] | None = None,
|
|
331
|
+
) -> dict[str, float]:
|
|
332
|
+
"""
|
|
333
|
+
Compute arrival time statistics.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
healpix_data : HEALPixData
|
|
338
|
+
HEALPix-mapped ray data
|
|
339
|
+
peak_mask : ndarray, optional
|
|
340
|
+
Boolean mask to select rays in peak region
|
|
341
|
+
If None, uses all rays
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
dict
|
|
346
|
+
Statistics including:
|
|
347
|
+
- 'mean_time': Mean arrival time (s)
|
|
348
|
+
- 'median_time': Median arrival time (s)
|
|
349
|
+
- 'std_time': Standard deviation (s)
|
|
350
|
+
- 'min_time': Minimum time (s)
|
|
351
|
+
- 'max_time': Maximum time (s)
|
|
352
|
+
- 'time_span': max - min (s)
|
|
353
|
+
- 'weighted_mean_time': Intensity-weighted mean (s)
|
|
354
|
+
- 'fwhm_time': Full width at half maximum (s)
|
|
355
|
+
- 'num_rays': Number of rays included
|
|
356
|
+
"""
|
|
357
|
+
if peak_mask is None:
|
|
358
|
+
times = healpix_data.times
|
|
359
|
+
intensities = healpix_data.intensities
|
|
360
|
+
else:
|
|
361
|
+
times = healpix_data.times[peak_mask]
|
|
362
|
+
intensities = healpix_data.intensities[peak_mask]
|
|
363
|
+
|
|
364
|
+
if len(times) == 0:
|
|
365
|
+
return {
|
|
366
|
+
"mean_time": 0.0,
|
|
367
|
+
"median_time": 0.0,
|
|
368
|
+
"std_time": 0.0,
|
|
369
|
+
"min_time": 0.0,
|
|
370
|
+
"max_time": 0.0,
|
|
371
|
+
"time_span": 0.0,
|
|
372
|
+
"weighted_mean_time": 0.0,
|
|
373
|
+
"fwhm_time": 0.0,
|
|
374
|
+
"num_rays": 0,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Basic statistics
|
|
378
|
+
mean_time = np.mean(times)
|
|
379
|
+
median_time = np.median(times)
|
|
380
|
+
std_time = np.std(times)
|
|
381
|
+
min_time = np.min(times)
|
|
382
|
+
max_time = np.max(times)
|
|
383
|
+
time_span = max_time - min_time
|
|
384
|
+
|
|
385
|
+
# Intensity-weighted mean
|
|
386
|
+
total_intensity = np.sum(intensities)
|
|
387
|
+
if total_intensity > 0:
|
|
388
|
+
weighted_mean_time = np.sum(times * intensities) / total_intensity
|
|
389
|
+
else:
|
|
390
|
+
weighted_mean_time = mean_time
|
|
391
|
+
|
|
392
|
+
# Estimate FWHM from histogram
|
|
393
|
+
hist, bin_edges = np.histogram(times, bins=100, weights=intensities)
|
|
394
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
395
|
+
|
|
396
|
+
peak_idx = np.argmax(hist)
|
|
397
|
+
peak_value = hist[peak_idx]
|
|
398
|
+
half_max = peak_value / 2
|
|
399
|
+
|
|
400
|
+
# Find indices where histogram > half max
|
|
401
|
+
above_half = hist >= half_max
|
|
402
|
+
if np.any(above_half):
|
|
403
|
+
indices = np.where(above_half)[0]
|
|
404
|
+
fwhm_time = bin_centers[indices[-1]] - bin_centers[indices[0]]
|
|
405
|
+
else:
|
|
406
|
+
fwhm_time = std_time * 2.355 # Gaussian approximation
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
"mean_time": float(mean_time),
|
|
410
|
+
"median_time": float(median_time),
|
|
411
|
+
"std_time": float(std_time),
|
|
412
|
+
"min_time": float(min_time),
|
|
413
|
+
"max_time": float(max_time),
|
|
414
|
+
"time_span": float(time_span),
|
|
415
|
+
"weighted_mean_time": float(weighted_mean_time),
|
|
416
|
+
"fwhm_time": float(fwhm_time),
|
|
417
|
+
"num_rays": int(len(times)),
|
|
418
|
+
}
|