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,1280 @@
|
|
|
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
|
+
Sphere Visualization - Mollweide Projections and Time Analysis
|
|
36
|
+
|
|
37
|
+
Functions for visualizing ray patterns on spherical detection surfaces
|
|
38
|
+
using Mollweide projections and time distribution analysis.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import matplotlib.gridspec as gridspec
|
|
42
|
+
import matplotlib.pyplot as plt
|
|
43
|
+
import numpy as np
|
|
44
|
+
from matplotlib.colors import LogNorm
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from astropy import units as u
|
|
48
|
+
from astropy_healpix import HEALPix
|
|
49
|
+
|
|
50
|
+
HAS_HEALPIX = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
HAS_HEALPIX = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def plot_mollweide_intensity(
|
|
56
|
+
healpix_data,
|
|
57
|
+
output_path: str | None = None,
|
|
58
|
+
log_scale: bool = True,
|
|
59
|
+
cmap: str = "hot",
|
|
60
|
+
title: str | None = None,
|
|
61
|
+
show_aggregated: bool = False,
|
|
62
|
+
dpi: int = 150,
|
|
63
|
+
) -> plt.Figure:
|
|
64
|
+
"""
|
|
65
|
+
Plot intensity distribution on Mollweide projection.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
healpix_data : HEALPixData
|
|
70
|
+
HEALPix-mapped ray data
|
|
71
|
+
output_path : str, optional
|
|
72
|
+
If provided, save figure to this path
|
|
73
|
+
log_scale : bool, optional
|
|
74
|
+
Use logarithmic color scale (default: True)
|
|
75
|
+
cmap : str, optional
|
|
76
|
+
Matplotlib colormap (default: 'hot')
|
|
77
|
+
title : str, optional
|
|
78
|
+
Figure title
|
|
79
|
+
show_aggregated : bool, optional
|
|
80
|
+
If True and aggregated data exists, show per-pixel aggregated intensities
|
|
81
|
+
If False, show individual ray scatter plot (default: False)
|
|
82
|
+
dpi : int, optional
|
|
83
|
+
Figure resolution (default: 150)
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
Figure
|
|
88
|
+
Matplotlib figure object
|
|
89
|
+
"""
|
|
90
|
+
fig = plt.figure(figsize=(14, 7))
|
|
91
|
+
ax = fig.add_subplot(111, projection="mollweide")
|
|
92
|
+
|
|
93
|
+
if show_aggregated and healpix_data.aggregated is not None:
|
|
94
|
+
# Plot aggregated per-pixel data
|
|
95
|
+
pixel_ids = healpix_data.aggregated["pixel_ids"]
|
|
96
|
+
intensity_sum = healpix_data.aggregated["intensity_sum"]
|
|
97
|
+
|
|
98
|
+
# Get pixel centers
|
|
99
|
+
hp = HEALPix(nside=healpix_data.nside, order="ring", frame=None)
|
|
100
|
+
lon, lat = hp.healpix_to_lonlat(pixel_ids)
|
|
101
|
+
|
|
102
|
+
# Convert to radians and wrap longitude
|
|
103
|
+
lon_rad = lon.to(u.rad).value
|
|
104
|
+
lat_rad = lat.to(u.rad).value
|
|
105
|
+
|
|
106
|
+
# Wrap longitude to [-π, π] for Mollweide
|
|
107
|
+
lon_rad = np.where(lon_rad > np.pi, lon_rad - 2 * np.pi, lon_rad)
|
|
108
|
+
|
|
109
|
+
# Plot
|
|
110
|
+
if log_scale and np.any(intensity_sum > 0):
|
|
111
|
+
norm = LogNorm(
|
|
112
|
+
vmin=intensity_sum[intensity_sum > 0].min(), vmax=intensity_sum.max()
|
|
113
|
+
)
|
|
114
|
+
scatter = ax.scatter(
|
|
115
|
+
lon_rad, lat_rad, c=intensity_sum, cmap=cmap, s=5, alpha=0.8, norm=norm
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
scatter = ax.scatter(
|
|
119
|
+
lon_rad, lat_rad, c=intensity_sum, cmap=cmap, s=5, alpha=0.8
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
cbar_label = "Summed Intensity per Pixel"
|
|
123
|
+
|
|
124
|
+
else:
|
|
125
|
+
# Plot individual rays
|
|
126
|
+
lon_rad = healpix_data.lon.copy()
|
|
127
|
+
lat_rad = healpix_data.lat.copy()
|
|
128
|
+
intensities = healpix_data.intensities
|
|
129
|
+
|
|
130
|
+
# Wrap longitude to [-π, π]
|
|
131
|
+
lon_rad = np.where(lon_rad > np.pi, lon_rad - 2 * np.pi, lon_rad)
|
|
132
|
+
|
|
133
|
+
# Plot
|
|
134
|
+
if log_scale and np.any(intensities > 0):
|
|
135
|
+
norm = LogNorm(
|
|
136
|
+
vmin=intensities[intensities > 0].min(), vmax=intensities.max()
|
|
137
|
+
)
|
|
138
|
+
scatter = ax.scatter(
|
|
139
|
+
lon_rad, lat_rad, c=intensities, cmap=cmap, s=2, alpha=0.6, norm=norm
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
scatter = ax.scatter(
|
|
143
|
+
lon_rad, lat_rad, c=intensities, cmap=cmap, s=2, alpha=0.6
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
cbar_label = "Ray Intensity"
|
|
147
|
+
|
|
148
|
+
# Colorbar
|
|
149
|
+
cbar = plt.colorbar(scatter, ax=ax, pad=0.1, shrink=0.8)
|
|
150
|
+
if log_scale:
|
|
151
|
+
cbar_label += " (log scale)"
|
|
152
|
+
cbar.set_label(cbar_label, fontsize=12)
|
|
153
|
+
|
|
154
|
+
# Labels
|
|
155
|
+
ax.set_xlabel("Azimuth (longitude)", fontsize=12)
|
|
156
|
+
ax.set_ylabel("Elevation (latitude)", fontsize=12)
|
|
157
|
+
|
|
158
|
+
if title is None:
|
|
159
|
+
title = f"Intensity Distribution on Detection Sphere (N={healpix_data.num_rays:,} rays)"
|
|
160
|
+
ax.set_title(title, fontsize=14, pad=20)
|
|
161
|
+
|
|
162
|
+
ax.grid(True, alpha=0.3)
|
|
163
|
+
|
|
164
|
+
plt.tight_layout()
|
|
165
|
+
|
|
166
|
+
if output_path:
|
|
167
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
168
|
+
print(f"Saved: {output_path}")
|
|
169
|
+
|
|
170
|
+
return fig
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def plot_mollweide_time(
|
|
174
|
+
healpix_data,
|
|
175
|
+
output_path: str | None = None,
|
|
176
|
+
cmap: str = "viridis",
|
|
177
|
+
title: str | None = None,
|
|
178
|
+
show_aggregated: bool = False,
|
|
179
|
+
time_units: str = "us",
|
|
180
|
+
dpi: int = 150,
|
|
181
|
+
) -> plt.Figure:
|
|
182
|
+
"""
|
|
183
|
+
Plot arrival time distribution on Mollweide projection.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
healpix_data : HEALPixData
|
|
188
|
+
HEALPix-mapped ray data
|
|
189
|
+
output_path : str, optional
|
|
190
|
+
If provided, save figure to this path
|
|
191
|
+
cmap : str, optional
|
|
192
|
+
Matplotlib colormap (default: 'viridis')
|
|
193
|
+
title : str, optional
|
|
194
|
+
Figure title
|
|
195
|
+
show_aggregated : bool, optional
|
|
196
|
+
If True, show per-pixel weighted mean time (default: False)
|
|
197
|
+
time_units : str, optional
|
|
198
|
+
Time units for display: 'us', 'ns', 's' (default: 'us')
|
|
199
|
+
dpi : int, optional
|
|
200
|
+
Figure resolution (default: 150)
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
Figure
|
|
205
|
+
Matplotlib figure object
|
|
206
|
+
"""
|
|
207
|
+
# Convert time units
|
|
208
|
+
unit_scales = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
209
|
+
unit_labels = {"s": "s", "ms": "ms", "us": "μs", "ns": "ns"}
|
|
210
|
+
scale = unit_scales.get(time_units, 1e6)
|
|
211
|
+
label = unit_labels.get(time_units, "μs")
|
|
212
|
+
|
|
213
|
+
fig = plt.figure(figsize=(14, 7))
|
|
214
|
+
ax = fig.add_subplot(111, projection="mollweide")
|
|
215
|
+
|
|
216
|
+
if show_aggregated and healpix_data.aggregated is not None:
|
|
217
|
+
# Plot aggregated weighted mean time
|
|
218
|
+
pixel_ids = healpix_data.aggregated["pixel_ids"]
|
|
219
|
+
times = healpix_data.aggregated["time_weighted_mean"] * scale
|
|
220
|
+
|
|
221
|
+
# Get pixel centers
|
|
222
|
+
hp = HEALPix(nside=healpix_data.nside, order="ring", frame=None)
|
|
223
|
+
lon, lat = hp.healpix_to_lonlat(pixel_ids)
|
|
224
|
+
|
|
225
|
+
lon_rad = lon.to(u.rad).value
|
|
226
|
+
lat_rad = lat.to(u.rad).value
|
|
227
|
+
lon_rad = np.where(lon_rad > np.pi, lon_rad - 2 * np.pi, lon_rad)
|
|
228
|
+
|
|
229
|
+
scatter = ax.scatter(lon_rad, lat_rad, c=times, cmap=cmap, s=5, alpha=0.8)
|
|
230
|
+
cbar_label = f"Intensity-weighted Mean Time ({label})"
|
|
231
|
+
|
|
232
|
+
else:
|
|
233
|
+
# Plot individual ray times
|
|
234
|
+
lon_rad = healpix_data.lon.copy()
|
|
235
|
+
lat_rad = healpix_data.lat.copy()
|
|
236
|
+
times = healpix_data.times * scale
|
|
237
|
+
|
|
238
|
+
lon_rad = np.where(lon_rad > np.pi, lon_rad - 2 * np.pi, lon_rad)
|
|
239
|
+
|
|
240
|
+
scatter = ax.scatter(lon_rad, lat_rad, c=times, cmap=cmap, s=2, alpha=0.6)
|
|
241
|
+
cbar_label = f"Arrival Time ({label})"
|
|
242
|
+
|
|
243
|
+
# Colorbar
|
|
244
|
+
cbar = plt.colorbar(scatter, ax=ax, pad=0.1, shrink=0.8)
|
|
245
|
+
cbar.set_label(cbar_label, fontsize=12)
|
|
246
|
+
|
|
247
|
+
# Labels
|
|
248
|
+
ax.set_xlabel("Azimuth (longitude)", fontsize=12)
|
|
249
|
+
ax.set_ylabel("Elevation (latitude)", fontsize=12)
|
|
250
|
+
|
|
251
|
+
if title is None:
|
|
252
|
+
title = "Arrival Time Distribution on Detection Sphere"
|
|
253
|
+
ax.set_title(title, fontsize=14, pad=20)
|
|
254
|
+
|
|
255
|
+
ax.grid(True, alpha=0.3)
|
|
256
|
+
|
|
257
|
+
plt.tight_layout()
|
|
258
|
+
|
|
259
|
+
if output_path:
|
|
260
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
261
|
+
print(f"Saved: {output_path}")
|
|
262
|
+
|
|
263
|
+
return fig
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def plot_time_distributions(
|
|
267
|
+
healpix_data,
|
|
268
|
+
peak_mask: np.ndarray | None = None,
|
|
269
|
+
time_stats: dict | None = None,
|
|
270
|
+
peak_stats: dict | None = None,
|
|
271
|
+
output_path: str | None = None,
|
|
272
|
+
time_units: str = "us",
|
|
273
|
+
bins: int = 100,
|
|
274
|
+
dpi: int = 150,
|
|
275
|
+
) -> plt.Figure:
|
|
276
|
+
"""
|
|
277
|
+
Plot arrival time distributions comparing all rays vs peak region.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
healpix_data : HEALPixData
|
|
282
|
+
HEALPix-mapped ray data
|
|
283
|
+
peak_mask : ndarray, optional
|
|
284
|
+
Boolean mask for rays in peak region
|
|
285
|
+
time_stats : dict, optional
|
|
286
|
+
Statistics for all rays
|
|
287
|
+
peak_stats : dict, optional
|
|
288
|
+
Statistics for peak region rays
|
|
289
|
+
output_path : str, optional
|
|
290
|
+
If provided, save figure to this path
|
|
291
|
+
time_units : str, optional
|
|
292
|
+
Time units: 'us', 'ns', 's' (default: 'us')
|
|
293
|
+
bins : int, optional
|
|
294
|
+
Number of histogram bins (default: 100)
|
|
295
|
+
dpi : int, optional
|
|
296
|
+
Figure resolution (default: 150)
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
Figure
|
|
301
|
+
Matplotlib figure object
|
|
302
|
+
"""
|
|
303
|
+
unit_scales = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
304
|
+
unit_labels = {"s": "s", "ms": "ms", "us": "μs", "ns": "ns"}
|
|
305
|
+
scale = unit_scales.get(time_units, 1e6)
|
|
306
|
+
label = unit_labels.get(time_units, "μs")
|
|
307
|
+
|
|
308
|
+
fig = plt.figure(figsize=(16, 10))
|
|
309
|
+
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.3, wspace=0.3)
|
|
310
|
+
|
|
311
|
+
# All times
|
|
312
|
+
all_times = healpix_data.times * scale
|
|
313
|
+
all_intensities = healpix_data.intensities
|
|
314
|
+
|
|
315
|
+
# Peak times (if mask provided)
|
|
316
|
+
if peak_mask is not None and np.any(peak_mask):
|
|
317
|
+
peak_times = healpix_data.times[peak_mask] * scale
|
|
318
|
+
peak_intensities = healpix_data.intensities[peak_mask]
|
|
319
|
+
has_peak = True
|
|
320
|
+
else:
|
|
321
|
+
has_peak = False
|
|
322
|
+
|
|
323
|
+
# =========================================================================
|
|
324
|
+
# 1. All rays histogram (intensity-weighted)
|
|
325
|
+
# =========================================================================
|
|
326
|
+
ax1 = fig.add_subplot(gs[0, 0])
|
|
327
|
+
ax1.hist(
|
|
328
|
+
all_times,
|
|
329
|
+
bins=bins,
|
|
330
|
+
weights=all_intensities,
|
|
331
|
+
color="steelblue",
|
|
332
|
+
alpha=0.7,
|
|
333
|
+
edgecolor="black",
|
|
334
|
+
)
|
|
335
|
+
ax1.set_xlabel(f"Arrival Time ({label})", fontsize=11)
|
|
336
|
+
ax1.set_ylabel("Intensity-weighted Count", fontsize=11)
|
|
337
|
+
ax1.set_title("All Rays - Time Distribution", fontsize=12)
|
|
338
|
+
ax1.grid(True, alpha=0.3)
|
|
339
|
+
|
|
340
|
+
if time_stats:
|
|
341
|
+
stats_text = f"Mean: {time_stats['mean_time']*scale:.2f} {label}\n"
|
|
342
|
+
stats_text += f"Std: {time_stats['std_time']*scale:.2f} {label}\n"
|
|
343
|
+
stats_text += f"FWHM: {time_stats['fwhm_time']*scale:.2f} {label}"
|
|
344
|
+
ax1.text(
|
|
345
|
+
0.02,
|
|
346
|
+
0.98,
|
|
347
|
+
stats_text,
|
|
348
|
+
transform=ax1.transAxes,
|
|
349
|
+
verticalalignment="top",
|
|
350
|
+
fontsize=9,
|
|
351
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# =========================================================================
|
|
355
|
+
# 2. Peak region histogram (if available)
|
|
356
|
+
# =========================================================================
|
|
357
|
+
ax2 = fig.add_subplot(gs[0, 1])
|
|
358
|
+
if has_peak:
|
|
359
|
+
ax2.hist(
|
|
360
|
+
peak_times,
|
|
361
|
+
bins=bins,
|
|
362
|
+
weights=peak_intensities,
|
|
363
|
+
color="coral",
|
|
364
|
+
alpha=0.7,
|
|
365
|
+
edgecolor="black",
|
|
366
|
+
)
|
|
367
|
+
ax2.set_xlabel(f"Arrival Time ({label})", fontsize=11)
|
|
368
|
+
ax2.set_ylabel("Intensity-weighted Count", fontsize=11)
|
|
369
|
+
ax2.set_title("Peak Region - Time Distribution", fontsize=12)
|
|
370
|
+
ax2.grid(True, alpha=0.3)
|
|
371
|
+
|
|
372
|
+
if peak_stats:
|
|
373
|
+
stats_text = f"Mean: {peak_stats['mean_time']*scale:.2f} {label}\n"
|
|
374
|
+
stats_text += f"Std: {peak_stats['std_time']*scale:.2f} {label}\n"
|
|
375
|
+
stats_text += f"FWHM: {peak_stats['fwhm_time']*scale:.2f} {label}"
|
|
376
|
+
ax2.text(
|
|
377
|
+
0.02,
|
|
378
|
+
0.98,
|
|
379
|
+
stats_text,
|
|
380
|
+
transform=ax2.transAxes,
|
|
381
|
+
verticalalignment="top",
|
|
382
|
+
fontsize=9,
|
|
383
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
ax2.text(
|
|
387
|
+
0.5,
|
|
388
|
+
0.5,
|
|
389
|
+
"No peak region defined",
|
|
390
|
+
transform=ax2.transAxes,
|
|
391
|
+
ha="center",
|
|
392
|
+
va="center",
|
|
393
|
+
fontsize=12,
|
|
394
|
+
)
|
|
395
|
+
ax2.set_title("Peak Region - Time Distribution", fontsize=12)
|
|
396
|
+
|
|
397
|
+
# =========================================================================
|
|
398
|
+
# 3. Comparison overlay
|
|
399
|
+
# =========================================================================
|
|
400
|
+
ax3 = fig.add_subplot(gs[1, 0])
|
|
401
|
+
ax3.hist(
|
|
402
|
+
all_times,
|
|
403
|
+
bins=bins,
|
|
404
|
+
weights=all_intensities,
|
|
405
|
+
color="steelblue",
|
|
406
|
+
alpha=0.5,
|
|
407
|
+
label="All rays",
|
|
408
|
+
edgecolor="black",
|
|
409
|
+
)
|
|
410
|
+
if has_peak:
|
|
411
|
+
ax3.hist(
|
|
412
|
+
peak_times,
|
|
413
|
+
bins=bins,
|
|
414
|
+
weights=peak_intensities,
|
|
415
|
+
color="coral",
|
|
416
|
+
alpha=0.5,
|
|
417
|
+
label="Peak region",
|
|
418
|
+
edgecolor="black",
|
|
419
|
+
)
|
|
420
|
+
ax3.set_xlabel(f"Arrival Time ({label})", fontsize=11)
|
|
421
|
+
ax3.set_ylabel("Intensity-weighted Count", fontsize=11)
|
|
422
|
+
ax3.set_title("Comparison: All vs Peak Region", fontsize=12)
|
|
423
|
+
ax3.legend(fontsize=10)
|
|
424
|
+
ax3.grid(True, alpha=0.3)
|
|
425
|
+
|
|
426
|
+
# =========================================================================
|
|
427
|
+
# 4. Statistics summary
|
|
428
|
+
# =========================================================================
|
|
429
|
+
ax4 = fig.add_subplot(gs[1, 1])
|
|
430
|
+
ax4.axis("off")
|
|
431
|
+
|
|
432
|
+
summary_text = "Time Distribution Statistics\n"
|
|
433
|
+
summary_text += "=" * 50 + "\n\n"
|
|
434
|
+
|
|
435
|
+
if time_stats:
|
|
436
|
+
summary_text += f"ALL RAYS (N={time_stats['num_rays']:,})\n"
|
|
437
|
+
summary_text += "-" * 50 + "\n"
|
|
438
|
+
summary_text += (
|
|
439
|
+
f"Mean time: {time_stats['mean_time']*scale:>10.2f} {label}\n"
|
|
440
|
+
)
|
|
441
|
+
summary_text += (
|
|
442
|
+
f"Median time: {time_stats['median_time']*scale:>10.2f} {label}\n"
|
|
443
|
+
)
|
|
444
|
+
summary_text += (
|
|
445
|
+
f"Std deviation: {time_stats['std_time']*scale:>10.2f} {label}\n"
|
|
446
|
+
)
|
|
447
|
+
summary_text += (
|
|
448
|
+
f"FWHM: {time_stats['fwhm_time']*scale:>10.2f} {label}\n"
|
|
449
|
+
)
|
|
450
|
+
summary_text += (
|
|
451
|
+
f"Time span: {time_stats['time_span']*scale:>10.2f} {label}\n"
|
|
452
|
+
)
|
|
453
|
+
summary_text += f"Weighted mean: {time_stats['weighted_mean_time']*scale:>10.2f} {label}\n\n"
|
|
454
|
+
|
|
455
|
+
if peak_stats and has_peak:
|
|
456
|
+
summary_text += f"PEAK REGION (N={peak_stats['num_rays']:,})\n"
|
|
457
|
+
summary_text += "-" * 50 + "\n"
|
|
458
|
+
summary_text += (
|
|
459
|
+
f"Mean time: {peak_stats['mean_time']*scale:>10.2f} {label}\n"
|
|
460
|
+
)
|
|
461
|
+
summary_text += (
|
|
462
|
+
f"Median time: {peak_stats['median_time']*scale:>10.2f} {label}\n"
|
|
463
|
+
)
|
|
464
|
+
summary_text += (
|
|
465
|
+
f"Std deviation: {peak_stats['std_time']*scale:>10.2f} {label}\n"
|
|
466
|
+
)
|
|
467
|
+
summary_text += (
|
|
468
|
+
f"FWHM: {peak_stats['fwhm_time']*scale:>10.2f} {label}\n"
|
|
469
|
+
)
|
|
470
|
+
summary_text += (
|
|
471
|
+
f"Time span: {peak_stats['time_span']*scale:>10.2f} {label}\n"
|
|
472
|
+
)
|
|
473
|
+
summary_text += (
|
|
474
|
+
f"Weighted mean: {peak_stats['weighted_mean_time']*scale:>10.2f} {label}\n"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
ax4.text(
|
|
478
|
+
0.1,
|
|
479
|
+
0.9,
|
|
480
|
+
summary_text,
|
|
481
|
+
transform=ax4.transAxes,
|
|
482
|
+
verticalalignment="top",
|
|
483
|
+
fontsize=10,
|
|
484
|
+
fontfamily="monospace",
|
|
485
|
+
bbox={"boxstyle": "round", "facecolor": "lightblue", "alpha": 0.3},
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
plt.suptitle("Arrival Time Analysis", fontsize=14, y=0.98)
|
|
489
|
+
|
|
490
|
+
if output_path:
|
|
491
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
492
|
+
print(f"Saved: {output_path}")
|
|
493
|
+
|
|
494
|
+
return fig
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def plot_combined_analysis(
|
|
498
|
+
healpix_data,
|
|
499
|
+
peak_mask: np.ndarray | None = None,
|
|
500
|
+
time_stats: dict | None = None,
|
|
501
|
+
peak_stats: dict | None = None,
|
|
502
|
+
output_path: str | None = None,
|
|
503
|
+
log_scale: bool = True,
|
|
504
|
+
time_units: str = "us",
|
|
505
|
+
dpi: int = 150,
|
|
506
|
+
) -> plt.Figure:
|
|
507
|
+
"""
|
|
508
|
+
Create comprehensive multi-panel analysis figure.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
healpix_data : HEALPixData
|
|
513
|
+
HEALPix-mapped ray data
|
|
514
|
+
peak_mask : ndarray, optional
|
|
515
|
+
Boolean mask for peak region rays
|
|
516
|
+
time_stats : dict, optional
|
|
517
|
+
Statistics for all rays
|
|
518
|
+
peak_stats : dict, optional
|
|
519
|
+
Statistics for peak region
|
|
520
|
+
output_path : str, optional
|
|
521
|
+
If provided, save figure to this path
|
|
522
|
+
log_scale : bool, optional
|
|
523
|
+
Use log scale for intensity (default: True)
|
|
524
|
+
time_units : str, optional
|
|
525
|
+
Time units (default: 'us')
|
|
526
|
+
dpi : int, optional
|
|
527
|
+
Figure resolution (default: 150)
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
Figure
|
|
532
|
+
Matplotlib figure object
|
|
533
|
+
"""
|
|
534
|
+
unit_scales = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
535
|
+
unit_labels = {"s": "s", "ms": "ms", "us": "μs", "ns": "ns"}
|
|
536
|
+
scale = unit_scales.get(time_units, 1e6)
|
|
537
|
+
label = unit_labels.get(time_units, "μs")
|
|
538
|
+
|
|
539
|
+
fig = plt.figure(figsize=(18, 12))
|
|
540
|
+
gs = gridspec.GridSpec(
|
|
541
|
+
3, 2, figure=fig, hspace=0.35, wspace=0.25, height_ratios=[1.2, 1, 1]
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# =========================================================================
|
|
545
|
+
# 1. Intensity map (Mollweide)
|
|
546
|
+
# =========================================================================
|
|
547
|
+
ax1 = fig.add_subplot(gs[0, :], projection="mollweide")
|
|
548
|
+
|
|
549
|
+
lon_rad = healpix_data.lon.copy()
|
|
550
|
+
lat_rad = healpix_data.lat.copy()
|
|
551
|
+
intensities = healpix_data.intensities
|
|
552
|
+
|
|
553
|
+
lon_rad = np.where(lon_rad > np.pi, lon_rad - 2 * np.pi, lon_rad)
|
|
554
|
+
|
|
555
|
+
if log_scale and np.any(intensities > 0):
|
|
556
|
+
norm = LogNorm(vmin=intensities[intensities > 0].min(), vmax=intensities.max())
|
|
557
|
+
scatter1 = ax1.scatter(
|
|
558
|
+
lon_rad, lat_rad, c=intensities, cmap="hot", s=2, alpha=0.6, norm=norm
|
|
559
|
+
)
|
|
560
|
+
cbar_label = "Ray Intensity (log scale)"
|
|
561
|
+
else:
|
|
562
|
+
scatter1 = ax1.scatter(
|
|
563
|
+
lon_rad, lat_rad, c=intensities, cmap="hot", s=2, alpha=0.6
|
|
564
|
+
)
|
|
565
|
+
cbar_label = "Ray Intensity"
|
|
566
|
+
|
|
567
|
+
cbar1 = plt.colorbar(scatter1, ax=ax1, pad=0.05, shrink=0.6, aspect=20)
|
|
568
|
+
cbar1.set_label(cbar_label, fontsize=10)
|
|
569
|
+
|
|
570
|
+
ax1.set_xlabel("Azimuth", fontsize=11)
|
|
571
|
+
ax1.set_ylabel("Elevation", fontsize=11)
|
|
572
|
+
ax1.set_title(
|
|
573
|
+
f"Intensity Distribution (N={healpix_data.num_rays:,} rays)",
|
|
574
|
+
fontsize=12,
|
|
575
|
+
pad=15,
|
|
576
|
+
)
|
|
577
|
+
ax1.grid(True, alpha=0.3)
|
|
578
|
+
|
|
579
|
+
# =========================================================================
|
|
580
|
+
# 2. Time map (Mollweide)
|
|
581
|
+
# =========================================================================
|
|
582
|
+
ax2 = fig.add_subplot(gs[1, :], projection="mollweide")
|
|
583
|
+
|
|
584
|
+
times = healpix_data.times * scale
|
|
585
|
+
scatter2 = ax2.scatter(lon_rad, lat_rad, c=times, cmap="viridis", s=2, alpha=0.6)
|
|
586
|
+
|
|
587
|
+
cbar2 = plt.colorbar(scatter2, ax=ax2, pad=0.05, shrink=0.6, aspect=20)
|
|
588
|
+
cbar2.set_label(f"Arrival Time ({label})", fontsize=10)
|
|
589
|
+
|
|
590
|
+
ax2.set_xlabel("Azimuth", fontsize=11)
|
|
591
|
+
ax2.set_ylabel("Elevation", fontsize=11)
|
|
592
|
+
ax2.set_title("Arrival Time Distribution", fontsize=12, pad=15)
|
|
593
|
+
ax2.grid(True, alpha=0.3)
|
|
594
|
+
|
|
595
|
+
# =========================================================================
|
|
596
|
+
# 3. Time histogram - All rays
|
|
597
|
+
# =========================================================================
|
|
598
|
+
ax3 = fig.add_subplot(gs[2, 0])
|
|
599
|
+
|
|
600
|
+
all_times = healpix_data.times * scale
|
|
601
|
+
all_intensities = healpix_data.intensities
|
|
602
|
+
|
|
603
|
+
ax3.hist(
|
|
604
|
+
all_times,
|
|
605
|
+
bins=80,
|
|
606
|
+
weights=all_intensities,
|
|
607
|
+
color="steelblue",
|
|
608
|
+
alpha=0.7,
|
|
609
|
+
edgecolor="black",
|
|
610
|
+
)
|
|
611
|
+
ax3.set_xlabel(f"Arrival Time ({label})", fontsize=10)
|
|
612
|
+
ax3.set_ylabel("Intensity-weighted Count", fontsize=10)
|
|
613
|
+
ax3.set_title("All Rays - Time Distribution", fontsize=11)
|
|
614
|
+
ax3.grid(True, alpha=0.3)
|
|
615
|
+
|
|
616
|
+
# =========================================================================
|
|
617
|
+
# 4. Time histogram - Peak region
|
|
618
|
+
# =========================================================================
|
|
619
|
+
ax4 = fig.add_subplot(gs[2, 1])
|
|
620
|
+
|
|
621
|
+
if peak_mask is not None and np.any(peak_mask):
|
|
622
|
+
peak_times = healpix_data.times[peak_mask] * scale
|
|
623
|
+
peak_intensities = healpix_data.intensities[peak_mask]
|
|
624
|
+
|
|
625
|
+
ax4.hist(
|
|
626
|
+
peak_times,
|
|
627
|
+
bins=80,
|
|
628
|
+
weights=peak_intensities,
|
|
629
|
+
color="coral",
|
|
630
|
+
alpha=0.7,
|
|
631
|
+
edgecolor="black",
|
|
632
|
+
)
|
|
633
|
+
ax4.set_xlabel(f"Arrival Time ({label})", fontsize=10)
|
|
634
|
+
ax4.set_ylabel("Intensity-weighted Count", fontsize=10)
|
|
635
|
+
ax4.set_title("Peak Region - Time Distribution", fontsize=11)
|
|
636
|
+
ax4.grid(True, alpha=0.3)
|
|
637
|
+
|
|
638
|
+
if peak_stats:
|
|
639
|
+
stats_text = f"N={peak_stats['num_rays']:,}\n"
|
|
640
|
+
stats_text += f"Mean: {peak_stats['mean_time']*scale:.2f} {label}\n"
|
|
641
|
+
stats_text += f"FWHM: {peak_stats['fwhm_time']*scale:.2f} {label}"
|
|
642
|
+
ax4.text(
|
|
643
|
+
0.98,
|
|
644
|
+
0.98,
|
|
645
|
+
stats_text,
|
|
646
|
+
transform=ax4.transAxes,
|
|
647
|
+
verticalalignment="top",
|
|
648
|
+
horizontalalignment="right",
|
|
649
|
+
fontsize=9,
|
|
650
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
ax4.text(
|
|
654
|
+
0.5,
|
|
655
|
+
0.5,
|
|
656
|
+
"No peak region defined",
|
|
657
|
+
transform=ax4.transAxes,
|
|
658
|
+
ha="center",
|
|
659
|
+
va="center",
|
|
660
|
+
fontsize=11,
|
|
661
|
+
)
|
|
662
|
+
ax4.set_title("Peak Region - Time Distribution", fontsize=11)
|
|
663
|
+
|
|
664
|
+
plt.suptitle("Sphere Intersection Pattern Analysis", fontsize=16, y=0.995)
|
|
665
|
+
|
|
666
|
+
if output_path:
|
|
667
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
668
|
+
print(f"Saved: {output_path}")
|
|
669
|
+
|
|
670
|
+
return fig
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def plot_2d_intensity(
|
|
674
|
+
healpix_data,
|
|
675
|
+
output_path: str | None = None,
|
|
676
|
+
log_scale: bool = True,
|
|
677
|
+
cmap: str = "hot",
|
|
678
|
+
title: str | None = None,
|
|
679
|
+
plot_type: str = "profile",
|
|
680
|
+
gridsize: int = 50,
|
|
681
|
+
dpi: int = 150,
|
|
682
|
+
) -> plt.Figure:
|
|
683
|
+
"""
|
|
684
|
+
Plot intensity distribution vs elevation angle (1D profile).
|
|
685
|
+
|
|
686
|
+
Shows intensity as a function of elevation angle, suitable for
|
|
687
|
+
tightly clustered ray patterns.
|
|
688
|
+
|
|
689
|
+
Parameters
|
|
690
|
+
----------
|
|
691
|
+
healpix_data : HEALPixData
|
|
692
|
+
HEALPix-mapped ray data
|
|
693
|
+
output_path : str, optional
|
|
694
|
+
If provided, save figure to this path
|
|
695
|
+
log_scale : bool, optional
|
|
696
|
+
Use logarithmic y-axis scale (default: True)
|
|
697
|
+
cmap : str, optional
|
|
698
|
+
Matplotlib colormap (default: 'hot')
|
|
699
|
+
title : str, optional
|
|
700
|
+
Figure title
|
|
701
|
+
plot_type : str, optional
|
|
702
|
+
'profile' for elevation profile, 'hexbin' for 2D, 'scatter' for 2D scatter (default: 'profile')
|
|
703
|
+
gridsize : int, optional
|
|
704
|
+
Number of bins for elevation profile (default: 50)
|
|
705
|
+
dpi : int, optional
|
|
706
|
+
Figure resolution (default: 150)
|
|
707
|
+
|
|
708
|
+
Returns
|
|
709
|
+
-------
|
|
710
|
+
Figure
|
|
711
|
+
Matplotlib figure object
|
|
712
|
+
"""
|
|
713
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
|
714
|
+
|
|
715
|
+
# Convert to degrees for easier reading
|
|
716
|
+
elevation_deg = np.degrees(healpix_data.lat)
|
|
717
|
+
intensities = healpix_data.intensities
|
|
718
|
+
|
|
719
|
+
if plot_type == "profile":
|
|
720
|
+
# 1D profile: elevation angle vs intensity
|
|
721
|
+
# Bin by elevation and sum intensities
|
|
722
|
+
hist, bin_edges = np.histogram(
|
|
723
|
+
elevation_deg, bins=gridsize, weights=intensities
|
|
724
|
+
)
|
|
725
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
726
|
+
|
|
727
|
+
ax.plot(bin_centers, hist, "o-", color="steelblue", linewidth=2, markersize=4)
|
|
728
|
+
ax.fill_between(bin_centers, 0, hist, alpha=0.3, color="steelblue")
|
|
729
|
+
|
|
730
|
+
ax.set_xlabel("Elevation (degrees)", fontsize=12)
|
|
731
|
+
ax.set_ylabel("Intensity", fontsize=12)
|
|
732
|
+
|
|
733
|
+
if log_scale and np.any(hist > 0):
|
|
734
|
+
ax.set_yscale("log")
|
|
735
|
+
|
|
736
|
+
ax.grid(True, alpha=0.3)
|
|
737
|
+
|
|
738
|
+
else:
|
|
739
|
+
# Fall back to 2D plots
|
|
740
|
+
azimuth_deg = np.degrees(healpix_data.lon)
|
|
741
|
+
|
|
742
|
+
if plot_type == "hexbin":
|
|
743
|
+
# Hexagonal binning
|
|
744
|
+
if log_scale and np.any(intensities > 0):
|
|
745
|
+
hb = ax.hexbin(
|
|
746
|
+
azimuth_deg,
|
|
747
|
+
elevation_deg,
|
|
748
|
+
C=intensities,
|
|
749
|
+
gridsize=gridsize,
|
|
750
|
+
cmap=cmap,
|
|
751
|
+
reduce_C_function=np.sum,
|
|
752
|
+
norm=LogNorm(
|
|
753
|
+
vmin=intensities[intensities > 0].min(),
|
|
754
|
+
vmax=intensities.max(),
|
|
755
|
+
),
|
|
756
|
+
mincnt=1,
|
|
757
|
+
)
|
|
758
|
+
else:
|
|
759
|
+
hb = ax.hexbin(
|
|
760
|
+
azimuth_deg,
|
|
761
|
+
elevation_deg,
|
|
762
|
+
C=intensities,
|
|
763
|
+
gridsize=gridsize,
|
|
764
|
+
cmap=cmap,
|
|
765
|
+
reduce_C_function=np.sum,
|
|
766
|
+
mincnt=1,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
cbar = plt.colorbar(hb, ax=ax)
|
|
770
|
+
cbar_label = "Summed Intensity per Bin"
|
|
771
|
+
if log_scale:
|
|
772
|
+
cbar_label += " (log scale)"
|
|
773
|
+
cbar.set_label(cbar_label, fontsize=12)
|
|
774
|
+
|
|
775
|
+
ax.set_xlabel("Azimuth (degrees)", fontsize=12)
|
|
776
|
+
ax.set_ylabel("Elevation (degrees)", fontsize=12)
|
|
777
|
+
|
|
778
|
+
else:
|
|
779
|
+
# Scatter plot
|
|
780
|
+
if log_scale and np.any(intensities > 0):
|
|
781
|
+
norm = LogNorm(
|
|
782
|
+
vmin=intensities[intensities > 0].min(), vmax=intensities.max()
|
|
783
|
+
)
|
|
784
|
+
scatter = ax.scatter(
|
|
785
|
+
azimuth_deg,
|
|
786
|
+
elevation_deg,
|
|
787
|
+
c=intensities,
|
|
788
|
+
cmap=cmap,
|
|
789
|
+
s=5,
|
|
790
|
+
alpha=0.6,
|
|
791
|
+
norm=norm,
|
|
792
|
+
)
|
|
793
|
+
else:
|
|
794
|
+
scatter = ax.scatter(
|
|
795
|
+
azimuth_deg,
|
|
796
|
+
elevation_deg,
|
|
797
|
+
c=intensities,
|
|
798
|
+
cmap=cmap,
|
|
799
|
+
s=5,
|
|
800
|
+
alpha=0.6,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
cbar = plt.colorbar(scatter, ax=ax)
|
|
804
|
+
cbar_label = "Ray Intensity"
|
|
805
|
+
if log_scale:
|
|
806
|
+
cbar_label += " (log scale)"
|
|
807
|
+
cbar.set_label(cbar_label, fontsize=12)
|
|
808
|
+
|
|
809
|
+
ax.set_xlabel("Azimuth (degrees)", fontsize=12)
|
|
810
|
+
ax.set_ylabel("Elevation (degrees)", fontsize=12)
|
|
811
|
+
|
|
812
|
+
if title is None:
|
|
813
|
+
title = f"Intensity Distribution (N={healpix_data.num_rays:,} rays)"
|
|
814
|
+
ax.set_title(title, fontsize=14)
|
|
815
|
+
|
|
816
|
+
plt.tight_layout()
|
|
817
|
+
|
|
818
|
+
if output_path:
|
|
819
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
820
|
+
print(f"Saved: {output_path}")
|
|
821
|
+
|
|
822
|
+
return fig
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def plot_viewing_angle_intensity(
|
|
826
|
+
healpix_data,
|
|
827
|
+
output_path: str | None = None,
|
|
828
|
+
log_scale: bool = True,
|
|
829
|
+
title: str | None = None,
|
|
830
|
+
gridsize: int = 50,
|
|
831
|
+
dpi: int = 150,
|
|
832
|
+
) -> plt.Figure:
|
|
833
|
+
"""
|
|
834
|
+
Plot intensity vs viewing angle from horizontal at (0,0,0).
|
|
835
|
+
|
|
836
|
+
Shows intensity as a function of viewing angle from horizontal plane
|
|
837
|
+
at the origin (Earth surface reference point). This is the geometric
|
|
838
|
+
angle to the intersection point, not the ray direction angle.
|
|
839
|
+
|
|
840
|
+
Parameters
|
|
841
|
+
----------
|
|
842
|
+
healpix_data : HEALPixData
|
|
843
|
+
HEALPix-mapped ray data with viewing_angle attribute
|
|
844
|
+
output_path : str, optional
|
|
845
|
+
If provided, save figure to this path
|
|
846
|
+
log_scale : bool, optional
|
|
847
|
+
Use logarithmic y-axis scale (default: True)
|
|
848
|
+
title : str, optional
|
|
849
|
+
Figure title
|
|
850
|
+
gridsize : int, optional
|
|
851
|
+
Number of bins for angle profile (default: 50)
|
|
852
|
+
dpi : int, optional
|
|
853
|
+
Figure resolution (default: 150)
|
|
854
|
+
|
|
855
|
+
Returns
|
|
856
|
+
-------
|
|
857
|
+
Figure
|
|
858
|
+
Matplotlib figure object
|
|
859
|
+
"""
|
|
860
|
+
if healpix_data.viewing_angle is None:
|
|
861
|
+
raise ValueError("HEALPixData does not have viewing_angle computed")
|
|
862
|
+
|
|
863
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
|
864
|
+
|
|
865
|
+
# Convert to degrees for easier reading
|
|
866
|
+
viewing_angle_deg = np.degrees(healpix_data.viewing_angle)
|
|
867
|
+
intensities = healpix_data.intensities
|
|
868
|
+
|
|
869
|
+
# Bin by viewing angle and sum intensities
|
|
870
|
+
hist, bin_edges = np.histogram(
|
|
871
|
+
viewing_angle_deg, bins=gridsize, weights=intensities
|
|
872
|
+
)
|
|
873
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
874
|
+
|
|
875
|
+
ax.plot(bin_centers, hist, "o-", color="steelblue", linewidth=2, markersize=4)
|
|
876
|
+
ax.fill_between(bin_centers, 0, hist, alpha=0.3, color="steelblue")
|
|
877
|
+
|
|
878
|
+
ax.set_xlabel("Viewing Angle from Horizontal (degrees)", fontsize=12)
|
|
879
|
+
ax.set_ylabel("Intensity (W)", fontsize=12)
|
|
880
|
+
|
|
881
|
+
if log_scale and np.any(hist > 0):
|
|
882
|
+
ax.set_yscale("log")
|
|
883
|
+
|
|
884
|
+
ax.grid(True, alpha=0.3)
|
|
885
|
+
|
|
886
|
+
if title is None:
|
|
887
|
+
title = f"Intensity vs Viewing Angle (N={healpix_data.num_rays:,} rays)"
|
|
888
|
+
ax.set_title(title, fontsize=14)
|
|
889
|
+
|
|
890
|
+
plt.tight_layout()
|
|
891
|
+
|
|
892
|
+
if output_path:
|
|
893
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
894
|
+
print(f"Saved: {output_path}")
|
|
895
|
+
|
|
896
|
+
return fig
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def plot_ray_direction_intensity(
|
|
900
|
+
healpix_data,
|
|
901
|
+
output_path: str | None = None,
|
|
902
|
+
log_scale: bool = True,
|
|
903
|
+
title: str | None = None,
|
|
904
|
+
gridsize: int = 50,
|
|
905
|
+
dpi: int = 150,
|
|
906
|
+
) -> plt.Figure:
|
|
907
|
+
"""
|
|
908
|
+
Plot intensity vs ray direction elevation angle.
|
|
909
|
+
|
|
910
|
+
Shows intensity as a function of the elevation angle of the ray
|
|
911
|
+
direction vectors themselves (angle above horizontal).
|
|
912
|
+
|
|
913
|
+
Parameters
|
|
914
|
+
----------
|
|
915
|
+
healpix_data : HEALPixData
|
|
916
|
+
HEALPix-mapped ray data with ray_elevation attribute
|
|
917
|
+
output_path : str, optional
|
|
918
|
+
If provided, save figure to this path
|
|
919
|
+
log_scale : bool, optional
|
|
920
|
+
Use logarithmic y-axis scale (default: True)
|
|
921
|
+
title : str, optional
|
|
922
|
+
Figure title
|
|
923
|
+
gridsize : int, optional
|
|
924
|
+
Number of bins for angle profile (default: 50)
|
|
925
|
+
dpi : int, optional
|
|
926
|
+
Figure resolution (default: 150)
|
|
927
|
+
|
|
928
|
+
Returns
|
|
929
|
+
-------
|
|
930
|
+
Figure
|
|
931
|
+
Matplotlib figure object
|
|
932
|
+
"""
|
|
933
|
+
if healpix_data.ray_elevation is None:
|
|
934
|
+
raise ValueError("HEALPixData does not have ray_elevation computed")
|
|
935
|
+
|
|
936
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
|
937
|
+
|
|
938
|
+
# Convert to degrees for easier reading
|
|
939
|
+
ray_elevation_deg = np.degrees(healpix_data.ray_elevation)
|
|
940
|
+
intensities = healpix_data.intensities
|
|
941
|
+
|
|
942
|
+
# Bin by ray direction elevation and sum intensities
|
|
943
|
+
hist, bin_edges = np.histogram(
|
|
944
|
+
ray_elevation_deg, bins=gridsize, weights=intensities
|
|
945
|
+
)
|
|
946
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
947
|
+
|
|
948
|
+
ax.plot(bin_centers, hist, "o-", color="darkgreen", linewidth=2, markersize=4)
|
|
949
|
+
ax.fill_between(bin_centers, 0, hist, alpha=0.3, color="darkgreen")
|
|
950
|
+
|
|
951
|
+
ax.set_xlabel("Ray Direction Elevation from Horizontal (degrees)", fontsize=12)
|
|
952
|
+
ax.set_ylabel("Intensity (W)", fontsize=12)
|
|
953
|
+
|
|
954
|
+
if log_scale and np.any(hist > 0):
|
|
955
|
+
ax.set_yscale("log")
|
|
956
|
+
|
|
957
|
+
ax.grid(True, alpha=0.3)
|
|
958
|
+
|
|
959
|
+
if title is None:
|
|
960
|
+
title = f"Intensity vs Ray Direction Angle (N={healpix_data.num_rays:,} rays)"
|
|
961
|
+
ax.set_title(title, fontsize=14)
|
|
962
|
+
|
|
963
|
+
plt.tight_layout()
|
|
964
|
+
|
|
965
|
+
if output_path:
|
|
966
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
967
|
+
print(f"Saved: {output_path}")
|
|
968
|
+
|
|
969
|
+
return fig
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def plot_2d_time(
|
|
973
|
+
healpix_data,
|
|
974
|
+
output_path: str | None = None,
|
|
975
|
+
cmap: str = "viridis",
|
|
976
|
+
title: str | None = None,
|
|
977
|
+
plot_type: str = "hexbin",
|
|
978
|
+
gridsize: int = 50,
|
|
979
|
+
time_units: str = "us",
|
|
980
|
+
dpi: int = 150,
|
|
981
|
+
) -> plt.Figure:
|
|
982
|
+
"""
|
|
983
|
+
Plot arrival time distribution as simple 2D plot (azimuth vs elevation).
|
|
984
|
+
|
|
985
|
+
Parameters
|
|
986
|
+
----------
|
|
987
|
+
healpix_data : HEALPixData
|
|
988
|
+
HEALPix-mapped ray data
|
|
989
|
+
output_path : str, optional
|
|
990
|
+
If provided, save figure to this path
|
|
991
|
+
cmap : str, optional
|
|
992
|
+
Matplotlib colormap (default: 'viridis')
|
|
993
|
+
title : str, optional
|
|
994
|
+
Figure title
|
|
995
|
+
plot_type : str, optional
|
|
996
|
+
'hexbin' for hexagonal binning, 'scatter' for scatter plot (default: 'hexbin')
|
|
997
|
+
gridsize : int, optional
|
|
998
|
+
Grid size for hexbin (default: 50)
|
|
999
|
+
time_units : str, optional
|
|
1000
|
+
Time units: 'us', 'ns', 's' (default: 'us')
|
|
1001
|
+
dpi : int, optional
|
|
1002
|
+
Figure resolution (default: 150)
|
|
1003
|
+
|
|
1004
|
+
Returns
|
|
1005
|
+
-------
|
|
1006
|
+
Figure
|
|
1007
|
+
Matplotlib figure object
|
|
1008
|
+
"""
|
|
1009
|
+
unit_scales = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
1010
|
+
unit_labels = {"s": "s", "ms": "ms", "us": "μs", "ns": "ns"}
|
|
1011
|
+
scale = unit_scales.get(time_units, 1e6)
|
|
1012
|
+
label = unit_labels.get(time_units, "μs")
|
|
1013
|
+
|
|
1014
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
|
1015
|
+
|
|
1016
|
+
# Convert to degrees
|
|
1017
|
+
azimuth_deg = np.degrees(healpix_data.lon)
|
|
1018
|
+
elevation_deg = np.degrees(healpix_data.lat)
|
|
1019
|
+
times = healpix_data.times * scale
|
|
1020
|
+
|
|
1021
|
+
if plot_type == "hexbin":
|
|
1022
|
+
# Hexagonal binning with intensity-weighted mean time
|
|
1023
|
+
hb = ax.hexbin(
|
|
1024
|
+
azimuth_deg,
|
|
1025
|
+
elevation_deg,
|
|
1026
|
+
C=times,
|
|
1027
|
+
gridsize=gridsize,
|
|
1028
|
+
cmap=cmap,
|
|
1029
|
+
reduce_C_function=np.mean,
|
|
1030
|
+
mincnt=1,
|
|
1031
|
+
)
|
|
1032
|
+
cbar_label = f"Mean Arrival Time ({label})"
|
|
1033
|
+
else:
|
|
1034
|
+
# Scatter plot
|
|
1035
|
+
scatter = ax.scatter(
|
|
1036
|
+
azimuth_deg, elevation_deg, c=times, cmap=cmap, s=5, alpha=0.6
|
|
1037
|
+
)
|
|
1038
|
+
hb = scatter
|
|
1039
|
+
cbar_label = f"Arrival Time ({label})"
|
|
1040
|
+
|
|
1041
|
+
cbar = plt.colorbar(hb, ax=ax)
|
|
1042
|
+
cbar.set_label(cbar_label, fontsize=12)
|
|
1043
|
+
|
|
1044
|
+
ax.set_xlabel("Azimuth (degrees)", fontsize=12)
|
|
1045
|
+
ax.set_ylabel("Elevation (degrees)", fontsize=12)
|
|
1046
|
+
|
|
1047
|
+
if title is None:
|
|
1048
|
+
title = f"Arrival Time Distribution (N={healpix_data.num_rays:,} rays)"
|
|
1049
|
+
ax.set_title(title, fontsize=14)
|
|
1050
|
+
|
|
1051
|
+
ax.grid(True, alpha=0.3)
|
|
1052
|
+
|
|
1053
|
+
plt.tight_layout()
|
|
1054
|
+
|
|
1055
|
+
if output_path:
|
|
1056
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
1057
|
+
print(f"Saved: {output_path}")
|
|
1058
|
+
|
|
1059
|
+
return fig
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def plot_2d_combined(
|
|
1063
|
+
healpix_data,
|
|
1064
|
+
peak_mask: np.ndarray | None = None,
|
|
1065
|
+
time_stats: dict | None = None,
|
|
1066
|
+
peak_stats: dict | None = None,
|
|
1067
|
+
output_path: str | None = None,
|
|
1068
|
+
log_scale: bool = True,
|
|
1069
|
+
time_units: str = "us",
|
|
1070
|
+
plot_type: str = "hexbin",
|
|
1071
|
+
gridsize: int = 50,
|
|
1072
|
+
dpi: int = 150,
|
|
1073
|
+
) -> plt.Figure:
|
|
1074
|
+
"""
|
|
1075
|
+
Create 2D combined analysis figure (simpler than Mollweide for localized patterns).
|
|
1076
|
+
|
|
1077
|
+
Parameters
|
|
1078
|
+
----------
|
|
1079
|
+
healpix_data : HEALPixData
|
|
1080
|
+
HEALPix-mapped ray data
|
|
1081
|
+
peak_mask : ndarray, optional
|
|
1082
|
+
Boolean mask for peak region rays
|
|
1083
|
+
time_stats : dict, optional
|
|
1084
|
+
Statistics for all rays
|
|
1085
|
+
peak_stats : dict, optional
|
|
1086
|
+
Statistics for peak region
|
|
1087
|
+
output_path : str, optional
|
|
1088
|
+
If provided, save figure to this path
|
|
1089
|
+
log_scale : bool, optional
|
|
1090
|
+
Use log scale for intensity (default: True)
|
|
1091
|
+
time_units : str, optional
|
|
1092
|
+
Time units (default: 'us')
|
|
1093
|
+
plot_type : str, optional
|
|
1094
|
+
'hexbin' or 'scatter' (default: 'hexbin')
|
|
1095
|
+
gridsize : int, optional
|
|
1096
|
+
Grid size for hexbin (default: 50)
|
|
1097
|
+
dpi : int, optional
|
|
1098
|
+
Figure resolution (default: 150)
|
|
1099
|
+
|
|
1100
|
+
Returns
|
|
1101
|
+
-------
|
|
1102
|
+
Figure
|
|
1103
|
+
Matplotlib figure object
|
|
1104
|
+
"""
|
|
1105
|
+
unit_scales = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
1106
|
+
unit_labels = {"s": "s", "ms": "ms", "us": "μs", "ns": "ns"}
|
|
1107
|
+
scale = unit_scales.get(time_units, 1e6)
|
|
1108
|
+
label = unit_labels.get(time_units, "μs")
|
|
1109
|
+
|
|
1110
|
+
fig = plt.figure(figsize=(16, 10))
|
|
1111
|
+
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.3, wspace=0.3)
|
|
1112
|
+
|
|
1113
|
+
# Convert to degrees
|
|
1114
|
+
azimuth_deg = np.degrees(healpix_data.lon)
|
|
1115
|
+
elevation_deg = np.degrees(healpix_data.lat)
|
|
1116
|
+
intensities = healpix_data.intensities
|
|
1117
|
+
times = healpix_data.times * scale
|
|
1118
|
+
|
|
1119
|
+
# =========================================================================
|
|
1120
|
+
# 1. Intensity map (2D)
|
|
1121
|
+
# =========================================================================
|
|
1122
|
+
ax1 = fig.add_subplot(gs[0, 0])
|
|
1123
|
+
|
|
1124
|
+
if plot_type == "hexbin":
|
|
1125
|
+
if log_scale and np.any(intensities > 0):
|
|
1126
|
+
hb1 = ax1.hexbin(
|
|
1127
|
+
azimuth_deg,
|
|
1128
|
+
elevation_deg,
|
|
1129
|
+
C=intensities,
|
|
1130
|
+
gridsize=gridsize,
|
|
1131
|
+
cmap="hot",
|
|
1132
|
+
reduce_C_function=np.sum,
|
|
1133
|
+
norm=LogNorm(
|
|
1134
|
+
vmin=intensities[intensities > 0].min(), vmax=intensities.max()
|
|
1135
|
+
),
|
|
1136
|
+
mincnt=1,
|
|
1137
|
+
)
|
|
1138
|
+
else:
|
|
1139
|
+
hb1 = ax1.hexbin(
|
|
1140
|
+
azimuth_deg,
|
|
1141
|
+
elevation_deg,
|
|
1142
|
+
C=intensities,
|
|
1143
|
+
gridsize=gridsize,
|
|
1144
|
+
cmap="hot",
|
|
1145
|
+
reduce_C_function=np.sum,
|
|
1146
|
+
mincnt=1,
|
|
1147
|
+
)
|
|
1148
|
+
cbar1 = plt.colorbar(hb1, ax=ax1)
|
|
1149
|
+
cbar_label = "Summed Intensity"
|
|
1150
|
+
else:
|
|
1151
|
+
if log_scale and np.any(intensities > 0):
|
|
1152
|
+
norm = LogNorm(
|
|
1153
|
+
vmin=intensities[intensities > 0].min(), vmax=intensities.max()
|
|
1154
|
+
)
|
|
1155
|
+
sc1 = ax1.scatter(
|
|
1156
|
+
azimuth_deg,
|
|
1157
|
+
elevation_deg,
|
|
1158
|
+
c=intensities,
|
|
1159
|
+
cmap="hot",
|
|
1160
|
+
s=2,
|
|
1161
|
+
alpha=0.6,
|
|
1162
|
+
norm=norm,
|
|
1163
|
+
)
|
|
1164
|
+
else:
|
|
1165
|
+
sc1 = ax1.scatter(
|
|
1166
|
+
azimuth_deg, elevation_deg, c=intensities, cmap="hot", s=2, alpha=0.6
|
|
1167
|
+
)
|
|
1168
|
+
cbar1 = plt.colorbar(sc1, ax=ax1)
|
|
1169
|
+
cbar_label = "Intensity"
|
|
1170
|
+
|
|
1171
|
+
if log_scale:
|
|
1172
|
+
cbar_label += " (log)"
|
|
1173
|
+
cbar1.set_label(cbar_label, fontsize=10)
|
|
1174
|
+
|
|
1175
|
+
ax1.set_xlabel("Azimuth (°)", fontsize=11)
|
|
1176
|
+
ax1.set_ylabel("Elevation (°)", fontsize=11)
|
|
1177
|
+
ax1.set_title(f"Intensity Distribution (N={healpix_data.num_rays:,})", fontsize=12)
|
|
1178
|
+
ax1.grid(True, alpha=0.3)
|
|
1179
|
+
|
|
1180
|
+
# =========================================================================
|
|
1181
|
+
# 2. Time map (2D)
|
|
1182
|
+
# =========================================================================
|
|
1183
|
+
ax2 = fig.add_subplot(gs[0, 1])
|
|
1184
|
+
|
|
1185
|
+
if plot_type == "hexbin":
|
|
1186
|
+
hb2 = ax2.hexbin(
|
|
1187
|
+
azimuth_deg,
|
|
1188
|
+
elevation_deg,
|
|
1189
|
+
C=times,
|
|
1190
|
+
gridsize=gridsize,
|
|
1191
|
+
cmap="viridis",
|
|
1192
|
+
reduce_C_function=np.mean,
|
|
1193
|
+
mincnt=1,
|
|
1194
|
+
)
|
|
1195
|
+
else:
|
|
1196
|
+
hb2 = ax2.scatter(
|
|
1197
|
+
azimuth_deg, elevation_deg, c=times, cmap="viridis", s=2, alpha=0.6
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
cbar2 = plt.colorbar(hb2, ax=ax2)
|
|
1201
|
+
cbar2.set_label(f"Arrival Time ({label})", fontsize=10)
|
|
1202
|
+
|
|
1203
|
+
ax2.set_xlabel("Azimuth (°)", fontsize=11)
|
|
1204
|
+
ax2.set_ylabel("Elevation (°)", fontsize=11)
|
|
1205
|
+
ax2.set_title("Arrival Time Distribution", fontsize=12)
|
|
1206
|
+
ax2.grid(True, alpha=0.3)
|
|
1207
|
+
|
|
1208
|
+
# =========================================================================
|
|
1209
|
+
# 3. Time histogram - All rays
|
|
1210
|
+
# =========================================================================
|
|
1211
|
+
ax3 = fig.add_subplot(gs[1, 0])
|
|
1212
|
+
|
|
1213
|
+
ax3.hist(
|
|
1214
|
+
times,
|
|
1215
|
+
bins=80,
|
|
1216
|
+
weights=intensities,
|
|
1217
|
+
color="steelblue",
|
|
1218
|
+
alpha=0.7,
|
|
1219
|
+
edgecolor="black",
|
|
1220
|
+
)
|
|
1221
|
+
ax3.set_xlabel(f"Arrival Time ({label})", fontsize=10)
|
|
1222
|
+
ax3.set_ylabel("Intensity-weighted Count", fontsize=10)
|
|
1223
|
+
ax3.set_title("All Rays - Time Distribution", fontsize=11)
|
|
1224
|
+
ax3.grid(True, alpha=0.3)
|
|
1225
|
+
|
|
1226
|
+
# =========================================================================
|
|
1227
|
+
# 4. Time histogram - Peak region
|
|
1228
|
+
# =========================================================================
|
|
1229
|
+
ax4 = fig.add_subplot(gs[1, 1])
|
|
1230
|
+
|
|
1231
|
+
if peak_mask is not None and np.any(peak_mask):
|
|
1232
|
+
peak_times = healpix_data.times[peak_mask] * scale
|
|
1233
|
+
peak_intensities = healpix_data.intensities[peak_mask]
|
|
1234
|
+
|
|
1235
|
+
ax4.hist(
|
|
1236
|
+
peak_times,
|
|
1237
|
+
bins=80,
|
|
1238
|
+
weights=peak_intensities,
|
|
1239
|
+
color="coral",
|
|
1240
|
+
alpha=0.7,
|
|
1241
|
+
edgecolor="black",
|
|
1242
|
+
)
|
|
1243
|
+
ax4.set_xlabel(f"Arrival Time ({label})", fontsize=10)
|
|
1244
|
+
ax4.set_ylabel("Intensity-weighted Count", fontsize=10)
|
|
1245
|
+
ax4.set_title("Peak Region - Time Distribution", fontsize=11)
|
|
1246
|
+
ax4.grid(True, alpha=0.3)
|
|
1247
|
+
|
|
1248
|
+
if peak_stats:
|
|
1249
|
+
stats_text = f"N={peak_stats['num_rays']:,}\n"
|
|
1250
|
+
stats_text += f"Mean: {peak_stats['mean_time']*scale:.2f} {label}\n"
|
|
1251
|
+
stats_text += f"FWHM: {peak_stats['fwhm_time']*scale:.2f} {label}"
|
|
1252
|
+
ax4.text(
|
|
1253
|
+
0.98,
|
|
1254
|
+
0.98,
|
|
1255
|
+
stats_text,
|
|
1256
|
+
transform=ax4.transAxes,
|
|
1257
|
+
verticalalignment="top",
|
|
1258
|
+
horizontalalignment="right",
|
|
1259
|
+
fontsize=9,
|
|
1260
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
|
|
1261
|
+
)
|
|
1262
|
+
else:
|
|
1263
|
+
ax4.text(
|
|
1264
|
+
0.5,
|
|
1265
|
+
0.5,
|
|
1266
|
+
"No peak region defined",
|
|
1267
|
+
transform=ax4.transAxes,
|
|
1268
|
+
ha="center",
|
|
1269
|
+
va="center",
|
|
1270
|
+
fontsize=11,
|
|
1271
|
+
)
|
|
1272
|
+
ax4.set_title("Peak Region - Time Distribution", fontsize=11)
|
|
1273
|
+
|
|
1274
|
+
plt.suptitle("Sphere Intersection Pattern - 2D Analysis", fontsize=14, y=0.995)
|
|
1275
|
+
|
|
1276
|
+
if output_path:
|
|
1277
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
1278
|
+
print(f"Saved: {output_path}")
|
|
1279
|
+
|
|
1280
|
+
return fig
|