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,1350 @@
|
|
|
1
|
+
# The Clear BSD License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Tobias Heibges
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted (subject to the limitations in the disclaimer
|
|
8
|
+
# below) provided that the following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
18
|
+
# contributors may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
22
|
+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
24
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
27
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
28
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
29
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
30
|
+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
Detector Visualization - Individual Axis Functions
|
|
36
|
+
|
|
37
|
+
Functions for plotting detector-related data: beam profiles, wavelength distributions,
|
|
38
|
+
detection counts, arrival times, and scan results.
|
|
39
|
+
Each function draws on a single axis, enabling flexible composition.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from typing import TYPE_CHECKING, Any
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from ..utilities.ray_data import RayBatch, RayStatistics
|
|
46
|
+
from ..surfaces import Surface
|
|
47
|
+
|
|
48
|
+
import matplotlib.pyplot as plt
|
|
49
|
+
import numpy as np
|
|
50
|
+
from matplotlib.axes import Axes
|
|
51
|
+
from matplotlib.figure import Figure
|
|
52
|
+
|
|
53
|
+
from .common import (
|
|
54
|
+
add_colorbar,
|
|
55
|
+
save_figure,
|
|
56
|
+
setup_axis_grid,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Beam Profile Functions
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def plot_beam_slice(
|
|
65
|
+
ax: Axes,
|
|
66
|
+
rays: "RayBatch",
|
|
67
|
+
axis: str = "z",
|
|
68
|
+
slice_value: float = 0.0,
|
|
69
|
+
slice_width: float = 0.1,
|
|
70
|
+
color_by: str = "intensity",
|
|
71
|
+
point_size: float = 20,
|
|
72
|
+
alpha: float = 0.6,
|
|
73
|
+
show_colorbar: bool = True,
|
|
74
|
+
) -> Any | None:
|
|
75
|
+
"""
|
|
76
|
+
Plot beam profile at a specific slice along propagation axis.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
ax : Axes
|
|
81
|
+
Matplotlib axes.
|
|
82
|
+
rays : RayBatch
|
|
83
|
+
Ray batch.
|
|
84
|
+
axis : str
|
|
85
|
+
Propagation axis: 'x', 'y', or 'z'.
|
|
86
|
+
slice_value : float
|
|
87
|
+
Position along axis to slice.
|
|
88
|
+
slice_width : float
|
|
89
|
+
Width of slice.
|
|
90
|
+
color_by : str
|
|
91
|
+
Color by: 'intensity', 'wavelength'.
|
|
92
|
+
point_size : float
|
|
93
|
+
Scatter point size.
|
|
94
|
+
alpha : float
|
|
95
|
+
Transparency.
|
|
96
|
+
show_colorbar : bool
|
|
97
|
+
Whether to add colorbar.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
scatter or None
|
|
102
|
+
ScalarMappable for external colorbar.
|
|
103
|
+
"""
|
|
104
|
+
active_mask = rays.active
|
|
105
|
+
positions = rays.positions[active_mask]
|
|
106
|
+
intensities = rays.intensities[active_mask]
|
|
107
|
+
wavelengths = rays.wavelengths[active_mask] * 1e9
|
|
108
|
+
|
|
109
|
+
# Determine axis indices
|
|
110
|
+
axis_map = {"x": 0, "y": 1, "z": 2}
|
|
111
|
+
if axis.lower() not in axis_map:
|
|
112
|
+
raise ValueError(f"Invalid axis: {axis}. Use 'x', 'y', or 'z'")
|
|
113
|
+
|
|
114
|
+
axis_idx = axis_map[axis.lower()]
|
|
115
|
+
perp_idx1 = (axis_idx + 1) % 3
|
|
116
|
+
perp_idx2 = (axis_idx + 2) % 3
|
|
117
|
+
axis_labels = ["X", "Y", "Z"]
|
|
118
|
+
|
|
119
|
+
# Select rays in slice
|
|
120
|
+
axis_pos = positions[:, axis_idx]
|
|
121
|
+
mask = np.abs(axis_pos - slice_value) <= slice_width / 2
|
|
122
|
+
|
|
123
|
+
if np.sum(mask) == 0:
|
|
124
|
+
ax.text(
|
|
125
|
+
0.5,
|
|
126
|
+
0.5,
|
|
127
|
+
"No rays in slice",
|
|
128
|
+
ha="center",
|
|
129
|
+
va="center",
|
|
130
|
+
transform=ax.transAxes,
|
|
131
|
+
)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
x = positions[mask, perp_idx1]
|
|
135
|
+
y = positions[mask, perp_idx2]
|
|
136
|
+
|
|
137
|
+
if color_by == "intensity":
|
|
138
|
+
c = intensities[mask]
|
|
139
|
+
cmap = "hot"
|
|
140
|
+
clabel = "Intensity"
|
|
141
|
+
else:
|
|
142
|
+
c = wavelengths[mask]
|
|
143
|
+
cmap = "rainbow"
|
|
144
|
+
clabel = "Wavelength (nm)"
|
|
145
|
+
|
|
146
|
+
scatter = ax.scatter(x, y, c=c, s=point_size, cmap=cmap, alpha=alpha)
|
|
147
|
+
ax.set_xlabel(f"{axis_labels[perp_idx1]} (m)")
|
|
148
|
+
ax.set_ylabel(f"{axis_labels[perp_idx2]} (m)")
|
|
149
|
+
ax.set_title(f"{axis.upper()}={slice_value:.3f} m")
|
|
150
|
+
ax.set_aspect("equal", adjustable="box")
|
|
151
|
+
ax.grid(True, alpha=0.3)
|
|
152
|
+
|
|
153
|
+
if show_colorbar:
|
|
154
|
+
add_colorbar(ax, scatter, clabel)
|
|
155
|
+
|
|
156
|
+
return scatter
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# =============================================================================
|
|
160
|
+
# Wavelength Distribution Functions
|
|
161
|
+
# =============================================================================
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def plot_wavelength_histogram(
|
|
165
|
+
ax: Axes,
|
|
166
|
+
rays: "RayBatch",
|
|
167
|
+
bins: int = 50,
|
|
168
|
+
alpha: float = 0.7,
|
|
169
|
+
color: str = "steelblue",
|
|
170
|
+
edgecolor: str = "black",
|
|
171
|
+
label: str | None = None,
|
|
172
|
+
weight_by_intensity: bool = False,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Plot histogram of ray wavelengths.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
ax : Axes
|
|
180
|
+
Matplotlib axes.
|
|
181
|
+
rays : RayBatch
|
|
182
|
+
Ray batch.
|
|
183
|
+
bins : int
|
|
184
|
+
Number of histogram bins.
|
|
185
|
+
alpha : float
|
|
186
|
+
Bar transparency.
|
|
187
|
+
color : str
|
|
188
|
+
Bar color.
|
|
189
|
+
edgecolor : str
|
|
190
|
+
Edge color.
|
|
191
|
+
label : str, optional
|
|
192
|
+
Legend label.
|
|
193
|
+
weight_by_intensity : bool
|
|
194
|
+
If True, weight histogram by ray intensities.
|
|
195
|
+
"""
|
|
196
|
+
active_mask = rays.active
|
|
197
|
+
wavelengths = rays.wavelengths[active_mask] * 1e9 # nm
|
|
198
|
+
|
|
199
|
+
weights = rays.intensities[active_mask] if weight_by_intensity else None
|
|
200
|
+
ylabel = "Total Intensity" if weight_by_intensity else "Count"
|
|
201
|
+
title = (
|
|
202
|
+
"Intensity per Wavelength" if weight_by_intensity else "Wavelength Distribution"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
ax.hist(
|
|
206
|
+
wavelengths,
|
|
207
|
+
bins=bins,
|
|
208
|
+
weights=weights,
|
|
209
|
+
alpha=alpha,
|
|
210
|
+
color=color,
|
|
211
|
+
edgecolor=edgecolor,
|
|
212
|
+
label=label,
|
|
213
|
+
)
|
|
214
|
+
setup_axis_grid(ax, "Wavelength (nm)", ylabel, title)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def plot_wavelength_intensity_histogram(
|
|
218
|
+
ax: Axes,
|
|
219
|
+
rays: "RayBatch",
|
|
220
|
+
bins: int = 50,
|
|
221
|
+
alpha: float = 0.7,
|
|
222
|
+
color: str = "orange",
|
|
223
|
+
edgecolor: str = "black",
|
|
224
|
+
label: str | None = None,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Plot intensity-weighted histogram of ray wavelengths.
|
|
228
|
+
|
|
229
|
+
Deprecated: Use plot_wavelength_histogram(weight_by_intensity=True) instead.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
ax : Axes
|
|
234
|
+
Matplotlib axes.
|
|
235
|
+
rays : RayBatch
|
|
236
|
+
Ray batch.
|
|
237
|
+
bins : int
|
|
238
|
+
Number of histogram bins.
|
|
239
|
+
alpha : float
|
|
240
|
+
Bar transparency.
|
|
241
|
+
color : str
|
|
242
|
+
Bar color.
|
|
243
|
+
edgecolor : str
|
|
244
|
+
Edge color.
|
|
245
|
+
label : str, optional
|
|
246
|
+
Legend label.
|
|
247
|
+
"""
|
|
248
|
+
return plot_wavelength_histogram(
|
|
249
|
+
ax,
|
|
250
|
+
rays,
|
|
251
|
+
bins=bins,
|
|
252
|
+
alpha=alpha,
|
|
253
|
+
color=color,
|
|
254
|
+
edgecolor=edgecolor,
|
|
255
|
+
label=label,
|
|
256
|
+
weight_by_intensity=True,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# =============================================================================
|
|
261
|
+
# Detection Count and Efficiency Functions
|
|
262
|
+
# =============================================================================
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def plot_detection_counts(
|
|
266
|
+
ax: Axes,
|
|
267
|
+
detector_angles_deg: np.ndarray,
|
|
268
|
+
detection_counts: np.ndarray,
|
|
269
|
+
color: str = "blue",
|
|
270
|
+
marker: str = "o",
|
|
271
|
+
linewidth: float = 2,
|
|
272
|
+
markersize: float = 8,
|
|
273
|
+
label: str | None = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Plot detection counts vs detector angle.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
ax : Axes
|
|
281
|
+
Matplotlib axes.
|
|
282
|
+
detector_angles_deg : ndarray
|
|
283
|
+
Detector angles in degrees.
|
|
284
|
+
detection_counts : ndarray
|
|
285
|
+
Number of rays detected.
|
|
286
|
+
color : str
|
|
287
|
+
Line color.
|
|
288
|
+
marker : str
|
|
289
|
+
Marker style.
|
|
290
|
+
linewidth : float
|
|
291
|
+
Line width.
|
|
292
|
+
markersize : float
|
|
293
|
+
Marker size.
|
|
294
|
+
label : str, optional
|
|
295
|
+
Legend label.
|
|
296
|
+
"""
|
|
297
|
+
ax.plot(
|
|
298
|
+
detector_angles_deg,
|
|
299
|
+
detection_counts,
|
|
300
|
+
f"{color[0]}{marker}-",
|
|
301
|
+
linewidth=linewidth,
|
|
302
|
+
markersize=markersize,
|
|
303
|
+
label=label,
|
|
304
|
+
)
|
|
305
|
+
ax.axhline(0, color="k", linestyle="-", linewidth=0.5)
|
|
306
|
+
setup_axis_grid(ax, "Detector Angle (degrees)", "Detected Rays", "Detection Count")
|
|
307
|
+
ax.set_xlim(0, 90)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def plot_detection_efficiency(
|
|
311
|
+
ax: Axes,
|
|
312
|
+
detector_angles_deg: np.ndarray,
|
|
313
|
+
detected_intensities: np.ndarray,
|
|
314
|
+
total_source_intensity: float,
|
|
315
|
+
color: str = "magenta",
|
|
316
|
+
marker: str = "o",
|
|
317
|
+
linewidth: float = 2,
|
|
318
|
+
markersize: float = 8,
|
|
319
|
+
label: str | None = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Plot detection efficiency vs detector angle.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
ax : Axes
|
|
327
|
+
Matplotlib axes.
|
|
328
|
+
detector_angles_deg : ndarray
|
|
329
|
+
Detector angles in degrees.
|
|
330
|
+
detected_intensities : ndarray
|
|
331
|
+
Total intensity detected.
|
|
332
|
+
total_source_intensity : float
|
|
333
|
+
Total source intensity.
|
|
334
|
+
color : str
|
|
335
|
+
Line color.
|
|
336
|
+
marker : str
|
|
337
|
+
Marker style.
|
|
338
|
+
linewidth : float
|
|
339
|
+
Line width.
|
|
340
|
+
markersize : float
|
|
341
|
+
Marker size.
|
|
342
|
+
label : str, optional
|
|
343
|
+
Legend label.
|
|
344
|
+
"""
|
|
345
|
+
efficiency = detected_intensities / total_source_intensity * 100
|
|
346
|
+
|
|
347
|
+
ax.plot(
|
|
348
|
+
detector_angles_deg,
|
|
349
|
+
efficiency,
|
|
350
|
+
f"{color[0]}{marker}-",
|
|
351
|
+
linewidth=linewidth,
|
|
352
|
+
markersize=markersize,
|
|
353
|
+
label=label,
|
|
354
|
+
)
|
|
355
|
+
ax.axhline(0, color="k", linestyle="-", linewidth=0.5)
|
|
356
|
+
setup_axis_grid(
|
|
357
|
+
ax, "Detector Angle (degrees)", "Efficiency (%)", "Detection Efficiency"
|
|
358
|
+
)
|
|
359
|
+
ax.set_xlim(0, 90)
|
|
360
|
+
ax.set_ylim(0, max(efficiency) * 1.1 if max(efficiency) > 0 else 1)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# =============================================================================
|
|
364
|
+
# Arrival Time Functions
|
|
365
|
+
# =============================================================================
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def plot_mean_arrival_time(
|
|
369
|
+
ax: Axes,
|
|
370
|
+
detector_angles_deg: np.ndarray,
|
|
371
|
+
mean_times: np.ndarray,
|
|
372
|
+
std_times: np.ndarray | None = None,
|
|
373
|
+
detection_counts: np.ndarray | None = None,
|
|
374
|
+
color: str = "cyan",
|
|
375
|
+
marker: str = "o",
|
|
376
|
+
linewidth: float = 2,
|
|
377
|
+
markersize: float = 8,
|
|
378
|
+
label: str | None = None,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""
|
|
381
|
+
Plot mean arrival time vs detector angle.
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
ax : Axes
|
|
386
|
+
Matplotlib axes.
|
|
387
|
+
detector_angles_deg : ndarray
|
|
388
|
+
Detector angles in degrees.
|
|
389
|
+
mean_times : ndarray
|
|
390
|
+
Mean arrival times in seconds.
|
|
391
|
+
std_times : ndarray, optional
|
|
392
|
+
Standard deviation of arrival times.
|
|
393
|
+
detection_counts : ndarray, optional
|
|
394
|
+
For masking invalid data.
|
|
395
|
+
color : str
|
|
396
|
+
Line color.
|
|
397
|
+
marker : str
|
|
398
|
+
Marker style.
|
|
399
|
+
linewidth : float
|
|
400
|
+
Line width.
|
|
401
|
+
markersize : float
|
|
402
|
+
Marker size.
|
|
403
|
+
label : str, optional
|
|
404
|
+
Legend label.
|
|
405
|
+
"""
|
|
406
|
+
if detection_counts is not None:
|
|
407
|
+
valid_mask = detection_counts > 0
|
|
408
|
+
angles = detector_angles_deg[valid_mask]
|
|
409
|
+
times = mean_times[valid_mask] * 1e6 # to microseconds
|
|
410
|
+
yerr = std_times[valid_mask] * 1e6 if std_times is not None else None
|
|
411
|
+
else:
|
|
412
|
+
angles = detector_angles_deg
|
|
413
|
+
times = mean_times * 1e6
|
|
414
|
+
yerr = std_times * 1e6 if std_times is not None else None
|
|
415
|
+
|
|
416
|
+
ax.errorbar(
|
|
417
|
+
angles,
|
|
418
|
+
times,
|
|
419
|
+
yerr=yerr,
|
|
420
|
+
fmt=f"{color[0]}{marker}-",
|
|
421
|
+
linewidth=linewidth,
|
|
422
|
+
markersize=markersize,
|
|
423
|
+
capsize=5,
|
|
424
|
+
alpha=0.7,
|
|
425
|
+
label=label,
|
|
426
|
+
)
|
|
427
|
+
setup_axis_grid(
|
|
428
|
+
ax, "Detector Angle (degrees)", "Mean Arrival Time (μs)", "Arrival Time"
|
|
429
|
+
)
|
|
430
|
+
ax.set_xlim(0, 90)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def plot_timing_distribution(
|
|
434
|
+
ax: Axes,
|
|
435
|
+
all_time_distributions: list[tuple],
|
|
436
|
+
detector_angles_deg: np.ndarray,
|
|
437
|
+
log_scale: bool = True,
|
|
438
|
+
show_legend: bool = True,
|
|
439
|
+
max_curves: int = 10,
|
|
440
|
+
) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Plot arrival time distributions for multiple detector positions.
|
|
443
|
+
|
|
444
|
+
Parameters
|
|
445
|
+
----------
|
|
446
|
+
ax : Axes
|
|
447
|
+
Matplotlib axes.
|
|
448
|
+
all_time_distributions : list
|
|
449
|
+
List of (times, intensities, angles) tuples for each detector.
|
|
450
|
+
detector_angles_deg : ndarray
|
|
451
|
+
Detector angles in degrees.
|
|
452
|
+
log_scale : bool
|
|
453
|
+
Whether to use log scales.
|
|
454
|
+
show_legend : bool
|
|
455
|
+
Whether to show legend.
|
|
456
|
+
max_curves : int
|
|
457
|
+
Maximum number of curves to plot.
|
|
458
|
+
"""
|
|
459
|
+
# Find first arrival time
|
|
460
|
+
all_times = []
|
|
461
|
+
for time_data in all_time_distributions:
|
|
462
|
+
if len(time_data) == 3:
|
|
463
|
+
times, _, _ = time_data
|
|
464
|
+
if len(times) > 0:
|
|
465
|
+
all_times.extend(times)
|
|
466
|
+
|
|
467
|
+
if len(all_times) == 0:
|
|
468
|
+
ax.text(
|
|
469
|
+
0.5, 0.5, "No timing data", ha="center", va="center", transform=ax.transAxes
|
|
470
|
+
)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
first_arrival = np.min(all_times)
|
|
474
|
+
log_bin_edges = np.logspace(-9, -3, 50) # seconds
|
|
475
|
+
|
|
476
|
+
# Collect positions with data
|
|
477
|
+
positions_with_data = []
|
|
478
|
+
for i, angle_deg in enumerate(detector_angles_deg):
|
|
479
|
+
time_data = all_time_distributions[i]
|
|
480
|
+
if len(time_data) == 3:
|
|
481
|
+
times, intensities, _ = time_data
|
|
482
|
+
if len(times) > 0:
|
|
483
|
+
times_rel = times - first_arrival
|
|
484
|
+
counts, _ = np.histogram(
|
|
485
|
+
times_rel, bins=log_bin_edges, weights=intensities
|
|
486
|
+
)
|
|
487
|
+
if counts.sum() > 0:
|
|
488
|
+
positions_with_data.append((angle_deg, counts))
|
|
489
|
+
|
|
490
|
+
if len(positions_with_data) == 0:
|
|
491
|
+
ax.text(
|
|
492
|
+
0.5, 0.5, "No timing data", ha="center", va="center", transform=ax.transAxes
|
|
493
|
+
)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
# Sample if too many curves
|
|
497
|
+
if len(positions_with_data) > max_curves:
|
|
498
|
+
indices = np.linspace(0, len(positions_with_data) - 1, max_curves, dtype=int)
|
|
499
|
+
positions_with_data = [positions_with_data[i] for i in indices]
|
|
500
|
+
|
|
501
|
+
colors = plt.cm.turbo(np.linspace(0, 1, len(positions_with_data)))
|
|
502
|
+
bin_centers = (log_bin_edges[:-1] + log_bin_edges[1:]) / 2
|
|
503
|
+
|
|
504
|
+
for idx, (angle_deg, counts) in enumerate(positions_with_data):
|
|
505
|
+
label = f"{angle_deg:.0f}°" if show_legend else ""
|
|
506
|
+
ax.plot(
|
|
507
|
+
bin_centers * 1e9,
|
|
508
|
+
counts,
|
|
509
|
+
color=colors[idx],
|
|
510
|
+
linewidth=1.5,
|
|
511
|
+
label=label,
|
|
512
|
+
alpha=0.7,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
setup_axis_grid(
|
|
516
|
+
ax, "Relative Arrival Time (ns)", "Intensity", "Timing Distribution"
|
|
517
|
+
)
|
|
518
|
+
if log_scale:
|
|
519
|
+
ax.set_xscale("log")
|
|
520
|
+
ax.set_yscale("log")
|
|
521
|
+
if show_legend:
|
|
522
|
+
ax.legend(loc="upper right", fontsize=8, ncol=2)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# =============================================================================
|
|
526
|
+
# Angular Distribution Functions
|
|
527
|
+
# =============================================================================
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def plot_arrival_angle_distribution(
|
|
531
|
+
ax: Axes,
|
|
532
|
+
detector_angles_deg: np.ndarray,
|
|
533
|
+
mean_angles: np.ndarray,
|
|
534
|
+
std_angles: np.ndarray | None = None,
|
|
535
|
+
detection_counts: np.ndarray | None = None,
|
|
536
|
+
color: str = "magenta",
|
|
537
|
+
marker: str = "o",
|
|
538
|
+
linewidth: float = 2,
|
|
539
|
+
markersize: float = 8,
|
|
540
|
+
) -> None:
|
|
541
|
+
"""
|
|
542
|
+
Plot mean arrival angle vs detector position.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
ax : Axes
|
|
547
|
+
Matplotlib axes.
|
|
548
|
+
detector_angles_deg : ndarray
|
|
549
|
+
Detector position angles.
|
|
550
|
+
mean_angles : ndarray
|
|
551
|
+
Mean arrival angles to normal.
|
|
552
|
+
std_angles : ndarray, optional
|
|
553
|
+
Standard deviation.
|
|
554
|
+
detection_counts : ndarray, optional
|
|
555
|
+
For masking invalid data.
|
|
556
|
+
color : str
|
|
557
|
+
Line color.
|
|
558
|
+
marker : str
|
|
559
|
+
Marker style.
|
|
560
|
+
linewidth : float
|
|
561
|
+
Line width.
|
|
562
|
+
markersize : float
|
|
563
|
+
Marker size.
|
|
564
|
+
"""
|
|
565
|
+
if detection_counts is not None:
|
|
566
|
+
valid_mask = detection_counts > 0
|
|
567
|
+
angles = detector_angles_deg[valid_mask]
|
|
568
|
+
means = mean_angles[valid_mask]
|
|
569
|
+
yerr = std_angles[valid_mask] if std_angles is not None else None
|
|
570
|
+
else:
|
|
571
|
+
angles = detector_angles_deg
|
|
572
|
+
means = mean_angles
|
|
573
|
+
yerr = std_angles
|
|
574
|
+
|
|
575
|
+
ax.errorbar(
|
|
576
|
+
angles,
|
|
577
|
+
means,
|
|
578
|
+
yerr=yerr,
|
|
579
|
+
fmt=f"{color[0]}{marker}-",
|
|
580
|
+
linewidth=linewidth,
|
|
581
|
+
markersize=markersize,
|
|
582
|
+
capsize=5,
|
|
583
|
+
alpha=0.7,
|
|
584
|
+
)
|
|
585
|
+
setup_axis_grid(
|
|
586
|
+
ax,
|
|
587
|
+
"Detector Angle (degrees)",
|
|
588
|
+
"Mean Angle to Normal (degrees)",
|
|
589
|
+
"Arrival Angles",
|
|
590
|
+
)
|
|
591
|
+
ax.set_xlim(0, 90)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def plot_angular_histogram(
|
|
595
|
+
ax: Axes,
|
|
596
|
+
all_time_distributions: list[tuple],
|
|
597
|
+
detection_counts: np.ndarray,
|
|
598
|
+
bins: int = 50,
|
|
599
|
+
color: str = "purple",
|
|
600
|
+
alpha: float = 0.7,
|
|
601
|
+
) -> None:
|
|
602
|
+
"""
|
|
603
|
+
Plot histogram of all arrival angles.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
ax : Axes
|
|
608
|
+
Matplotlib axes.
|
|
609
|
+
all_time_distributions : list
|
|
610
|
+
List of (times, intensities, angles) tuples.
|
|
611
|
+
detection_counts : ndarray
|
|
612
|
+
Detection counts per position.
|
|
613
|
+
bins : int
|
|
614
|
+
Number of histogram bins.
|
|
615
|
+
color : str
|
|
616
|
+
Bar color.
|
|
617
|
+
alpha : float
|
|
618
|
+
Transparency.
|
|
619
|
+
"""
|
|
620
|
+
all_angles = []
|
|
621
|
+
all_intensities = []
|
|
622
|
+
|
|
623
|
+
for i, time_data in enumerate(all_time_distributions):
|
|
624
|
+
if detection_counts[i] > 0 and len(time_data) == 3:
|
|
625
|
+
_, intensities, angles = time_data
|
|
626
|
+
all_angles.extend(angles)
|
|
627
|
+
all_intensities.extend(intensities)
|
|
628
|
+
|
|
629
|
+
if len(all_angles) == 0:
|
|
630
|
+
ax.text(
|
|
631
|
+
0.5,
|
|
632
|
+
0.5,
|
|
633
|
+
"No angular data",
|
|
634
|
+
ha="center",
|
|
635
|
+
va="center",
|
|
636
|
+
transform=ax.transAxes,
|
|
637
|
+
)
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
all_angles = np.array(all_angles)
|
|
641
|
+
all_intensities = np.array(all_intensities)
|
|
642
|
+
|
|
643
|
+
ax.hist(
|
|
644
|
+
all_angles,
|
|
645
|
+
bins=bins,
|
|
646
|
+
weights=all_intensities,
|
|
647
|
+
color=color,
|
|
648
|
+
alpha=alpha,
|
|
649
|
+
edgecolor="black",
|
|
650
|
+
)
|
|
651
|
+
setup_axis_grid(
|
|
652
|
+
ax, "Angle to Normal (degrees)", "Intensity", "Angular Distribution"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# =============================================================================
|
|
657
|
+
# Composite Figure Builders
|
|
658
|
+
# =============================================================================
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def create_wavelength_figure(
|
|
662
|
+
rays: "RayBatch",
|
|
663
|
+
bins: int = 50,
|
|
664
|
+
figsize: tuple[float, float] = (10, 5),
|
|
665
|
+
title: str = "Wavelength Distribution",
|
|
666
|
+
save_path: str | None = None,
|
|
667
|
+
) -> Figure:
|
|
668
|
+
"""
|
|
669
|
+
Create figure with wavelength histograms.
|
|
670
|
+
|
|
671
|
+
Parameters
|
|
672
|
+
----------
|
|
673
|
+
rays : RayBatch
|
|
674
|
+
Ray batch.
|
|
675
|
+
bins : int
|
|
676
|
+
Histogram bins.
|
|
677
|
+
figsize : tuple
|
|
678
|
+
Figure size.
|
|
679
|
+
title : str
|
|
680
|
+
Figure title.
|
|
681
|
+
save_path : str, optional
|
|
682
|
+
Save path.
|
|
683
|
+
|
|
684
|
+
Returns
|
|
685
|
+
-------
|
|
686
|
+
Figure
|
|
687
|
+
Matplotlib figure.
|
|
688
|
+
"""
|
|
689
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize, constrained_layout=True)
|
|
690
|
+
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
691
|
+
|
|
692
|
+
plot_wavelength_histogram(ax1, rays, bins=bins)
|
|
693
|
+
plot_wavelength_intensity_histogram(ax2, rays, bins=bins)
|
|
694
|
+
|
|
695
|
+
if save_path:
|
|
696
|
+
save_figure(fig, save_path)
|
|
697
|
+
|
|
698
|
+
return fig
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def create_beam_profile_figure(
|
|
702
|
+
rays: "RayBatch",
|
|
703
|
+
axis: str = "z",
|
|
704
|
+
num_slices: int = 5,
|
|
705
|
+
figsize: tuple[float, float] = (15, 4),
|
|
706
|
+
title: str | None = None,
|
|
707
|
+
save_path: str | None = None,
|
|
708
|
+
) -> Figure:
|
|
709
|
+
"""
|
|
710
|
+
Create figure showing beam profile at multiple slices.
|
|
711
|
+
|
|
712
|
+
Parameters
|
|
713
|
+
----------
|
|
714
|
+
rays : RayBatch
|
|
715
|
+
Ray batch.
|
|
716
|
+
axis : str
|
|
717
|
+
Propagation axis.
|
|
718
|
+
num_slices : int
|
|
719
|
+
Number of slices.
|
|
720
|
+
figsize : tuple
|
|
721
|
+
Figure size.
|
|
722
|
+
title : str, optional
|
|
723
|
+
Figure title.
|
|
724
|
+
save_path : str, optional
|
|
725
|
+
Save path.
|
|
726
|
+
|
|
727
|
+
Returns
|
|
728
|
+
-------
|
|
729
|
+
Figure
|
|
730
|
+
Matplotlib figure.
|
|
731
|
+
"""
|
|
732
|
+
fig, axes = plt.subplots(1, num_slices, figsize=figsize, constrained_layout=True)
|
|
733
|
+
|
|
734
|
+
if title is None:
|
|
735
|
+
title = f"Beam Profile Along {axis.upper()} Axis"
|
|
736
|
+
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
737
|
+
|
|
738
|
+
if num_slices == 1:
|
|
739
|
+
axes = [axes]
|
|
740
|
+
|
|
741
|
+
active_mask = rays.active
|
|
742
|
+
positions = rays.positions[active_mask]
|
|
743
|
+
|
|
744
|
+
axis_map = {"x": 0, "y": 1, "z": 2}
|
|
745
|
+
axis_idx = axis_map.get(axis.lower(), 2)
|
|
746
|
+
|
|
747
|
+
axis_pos = positions[:, axis_idx]
|
|
748
|
+
axis_min, axis_max = np.min(axis_pos), np.max(axis_pos)
|
|
749
|
+
slice_centers = np.linspace(axis_min, axis_max, num_slices)
|
|
750
|
+
slice_width = (
|
|
751
|
+
(axis_max - axis_min) / (num_slices - 1)
|
|
752
|
+
if num_slices > 1
|
|
753
|
+
else (axis_max - axis_min)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
for i, (ax, center) in enumerate(zip(axes, slice_centers, strict=False)):
|
|
757
|
+
plot_beam_slice(
|
|
758
|
+
ax,
|
|
759
|
+
rays,
|
|
760
|
+
axis=axis,
|
|
761
|
+
slice_value=center,
|
|
762
|
+
slice_width=slice_width,
|
|
763
|
+
show_colorbar=(i == num_slices - 1),
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if save_path:
|
|
767
|
+
save_figure(fig, save_path)
|
|
768
|
+
|
|
769
|
+
return fig
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def create_detector_scan_figure(
|
|
773
|
+
detector_angles_deg: np.ndarray,
|
|
774
|
+
detection_counts: np.ndarray,
|
|
775
|
+
detected_intensities: np.ndarray,
|
|
776
|
+
total_source_intensity: float,
|
|
777
|
+
mean_arrival_times: np.ndarray | None = None,
|
|
778
|
+
std_arrival_times: np.ndarray | None = None,
|
|
779
|
+
mean_angles_to_normal: np.ndarray | None = None,
|
|
780
|
+
std_angles_to_normal: np.ndarray | None = None,
|
|
781
|
+
all_time_distributions: list | None = None,
|
|
782
|
+
figsize: tuple[float, float] = (16, 12),
|
|
783
|
+
title: str = "Detector Scan Results",
|
|
784
|
+
save_path: str | None = None,
|
|
785
|
+
) -> Figure:
|
|
786
|
+
"""
|
|
787
|
+
Create comprehensive detector scan figure.
|
|
788
|
+
|
|
789
|
+
Parameters
|
|
790
|
+
----------
|
|
791
|
+
detector_angles_deg : ndarray
|
|
792
|
+
Detector angles in degrees.
|
|
793
|
+
detection_counts : ndarray
|
|
794
|
+
Number of rays detected.
|
|
795
|
+
detected_intensities : ndarray
|
|
796
|
+
Total intensity detected.
|
|
797
|
+
total_source_intensity : float
|
|
798
|
+
Source intensity.
|
|
799
|
+
mean_arrival_times : ndarray, optional
|
|
800
|
+
Mean arrival times.
|
|
801
|
+
std_arrival_times : ndarray, optional
|
|
802
|
+
Std of arrival times.
|
|
803
|
+
mean_angles_to_normal : ndarray, optional
|
|
804
|
+
Mean arrival angles.
|
|
805
|
+
std_angles_to_normal : ndarray, optional
|
|
806
|
+
Std of arrival angles.
|
|
807
|
+
all_time_distributions : list, optional
|
|
808
|
+
Timing data.
|
|
809
|
+
figsize : tuple
|
|
810
|
+
Figure size.
|
|
811
|
+
title : str
|
|
812
|
+
Figure title.
|
|
813
|
+
save_path : str, optional
|
|
814
|
+
Save path.
|
|
815
|
+
|
|
816
|
+
Returns
|
|
817
|
+
-------
|
|
818
|
+
Figure
|
|
819
|
+
Matplotlib figure.
|
|
820
|
+
"""
|
|
821
|
+
fig, axes = plt.subplots(3, 2, figsize=figsize, constrained_layout=True)
|
|
822
|
+
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
823
|
+
|
|
824
|
+
# Row 1: Detection counts and efficiency
|
|
825
|
+
plot_detection_counts(axes[0, 0], detector_angles_deg, detection_counts)
|
|
826
|
+
plot_detection_efficiency(
|
|
827
|
+
axes[0, 1], detector_angles_deg, detected_intensities, total_source_intensity
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Row 2: Arrival times and angles
|
|
831
|
+
if mean_arrival_times is not None:
|
|
832
|
+
plot_mean_arrival_time(
|
|
833
|
+
axes[1, 0],
|
|
834
|
+
detector_angles_deg,
|
|
835
|
+
mean_arrival_times,
|
|
836
|
+
std_arrival_times,
|
|
837
|
+
detection_counts,
|
|
838
|
+
)
|
|
839
|
+
else:
|
|
840
|
+
axes[1, 0].text(
|
|
841
|
+
0.5,
|
|
842
|
+
0.5,
|
|
843
|
+
"No timing data",
|
|
844
|
+
ha="center",
|
|
845
|
+
va="center",
|
|
846
|
+
transform=axes[1, 0].transAxes,
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
if mean_angles_to_normal is not None:
|
|
850
|
+
plot_arrival_angle_distribution(
|
|
851
|
+
axes[1, 1],
|
|
852
|
+
detector_angles_deg,
|
|
853
|
+
mean_angles_to_normal,
|
|
854
|
+
std_angles_to_normal,
|
|
855
|
+
detection_counts,
|
|
856
|
+
)
|
|
857
|
+
else:
|
|
858
|
+
axes[1, 1].text(
|
|
859
|
+
0.5,
|
|
860
|
+
0.5,
|
|
861
|
+
"No angular data",
|
|
862
|
+
ha="center",
|
|
863
|
+
va="center",
|
|
864
|
+
transform=axes[1, 1].transAxes,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Row 3: Distributions
|
|
868
|
+
if all_time_distributions is not None:
|
|
869
|
+
plot_timing_distribution(
|
|
870
|
+
axes[2, 0], all_time_distributions, detector_angles_deg
|
|
871
|
+
)
|
|
872
|
+
plot_angular_histogram(axes[2, 1], all_time_distributions, detection_counts)
|
|
873
|
+
else:
|
|
874
|
+
axes[2, 0].text(
|
|
875
|
+
0.5,
|
|
876
|
+
0.5,
|
|
877
|
+
"No distribution data",
|
|
878
|
+
ha="center",
|
|
879
|
+
va="center",
|
|
880
|
+
transform=axes[2, 0].transAxes,
|
|
881
|
+
)
|
|
882
|
+
axes[2, 1].text(
|
|
883
|
+
0.5,
|
|
884
|
+
0.5,
|
|
885
|
+
"No distribution data",
|
|
886
|
+
ha="center",
|
|
887
|
+
va="center",
|
|
888
|
+
transform=axes[2, 1].transAxes,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
if save_path:
|
|
892
|
+
save_figure(fig, save_path)
|
|
893
|
+
|
|
894
|
+
return fig
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def plot_detector_scan_results(
|
|
898
|
+
detector_angles_deg: np.ndarray,
|
|
899
|
+
detection_counts: np.ndarray,
|
|
900
|
+
detected_intensities: np.ndarray,
|
|
901
|
+
reflected_rays: "RayBatch",
|
|
902
|
+
surface: "Surface",
|
|
903
|
+
total_source_intensity: float,
|
|
904
|
+
mean_arrival_times: np.ndarray | None = None,
|
|
905
|
+
std_arrival_times: np.ndarray | None = None,
|
|
906
|
+
mean_angles_to_normal: np.ndarray | None = None,
|
|
907
|
+
std_angles_to_normal: np.ndarray | None = None,
|
|
908
|
+
all_time_distributions: list | None = None,
|
|
909
|
+
detector_distance: float = 1000.0,
|
|
910
|
+
detector_radius: float = 5.0,
|
|
911
|
+
water_normal: np.ndarray = None,
|
|
912
|
+
figsize: tuple[float, float] = (24, 14),
|
|
913
|
+
save_path: str | None = None,
|
|
914
|
+
) -> Figure:
|
|
915
|
+
"""
|
|
916
|
+
Create comprehensive detector scan visualization with multiple subplots.
|
|
917
|
+
|
|
918
|
+
This is the full production visualization with all panels including
|
|
919
|
+
the wave surface and ray visualizations.
|
|
920
|
+
|
|
921
|
+
Parameters
|
|
922
|
+
----------
|
|
923
|
+
detector_angles_deg : ndarray
|
|
924
|
+
Detector angles in degrees (0-90).
|
|
925
|
+
detection_counts : ndarray
|
|
926
|
+
Number of rays detected at each position.
|
|
927
|
+
detected_intensities : ndarray
|
|
928
|
+
Total intensity detected at each position.
|
|
929
|
+
reflected_rays : RayBatch
|
|
930
|
+
Batch of reflected rays.
|
|
931
|
+
surface : Surface
|
|
932
|
+
The surface object (must have _surface_z method).
|
|
933
|
+
total_source_intensity : float
|
|
934
|
+
Total intensity of source rays.
|
|
935
|
+
mean_arrival_times : ndarray, optional
|
|
936
|
+
Mean arrival time at each detector position.
|
|
937
|
+
std_arrival_times : ndarray, optional
|
|
938
|
+
Standard deviation of arrival times.
|
|
939
|
+
mean_angles_to_normal : ndarray, optional
|
|
940
|
+
Mean angle to normal at each detector.
|
|
941
|
+
std_angles_to_normal : ndarray, optional
|
|
942
|
+
Standard deviation of angles.
|
|
943
|
+
all_time_distributions : list, optional
|
|
944
|
+
List of (times, intensities, angles) tuples for each detector.
|
|
945
|
+
detector_distance : float
|
|
946
|
+
Distance to detector in meters.
|
|
947
|
+
detector_radius : float
|
|
948
|
+
Detector radius in meters.
|
|
949
|
+
water_normal : ndarray
|
|
950
|
+
Normal vector for water surface.
|
|
951
|
+
figsize : tuple
|
|
952
|
+
Figure size (width, height).
|
|
953
|
+
save_path : str, optional
|
|
954
|
+
Path to save figure.
|
|
955
|
+
|
|
956
|
+
Returns
|
|
957
|
+
-------
|
|
958
|
+
Figure
|
|
959
|
+
Matplotlib figure with comprehensive visualization.
|
|
960
|
+
"""
|
|
961
|
+
import matplotlib.gridspec as gridspec
|
|
962
|
+
|
|
963
|
+
if water_normal is None:
|
|
964
|
+
water_normal = np.array([0, 0, 1])
|
|
965
|
+
|
|
966
|
+
# Create figure with 4x4 grid layout
|
|
967
|
+
fig = plt.figure(figsize=figsize, constrained_layout=True)
|
|
968
|
+
gs = gridspec.GridSpec(4, 4, figure=fig, hspace=0.3, wspace=0.4)
|
|
969
|
+
|
|
970
|
+
# Compute detection efficiency
|
|
971
|
+
detection_efficiency = detected_intensities / total_source_intensity * 100
|
|
972
|
+
|
|
973
|
+
# 1. Ray count per detector position
|
|
974
|
+
ax1 = fig.add_subplot(gs[1, :2])
|
|
975
|
+
ax1.plot(detector_angles_deg, detection_counts, "bo-", linewidth=2, markersize=8)
|
|
976
|
+
ax1.axhline(0, color="k", linestyle="-", linewidth=0.5)
|
|
977
|
+
ax1.set_xlabel(
|
|
978
|
+
"Detector Position Angle from Surface (degrees)", fontsize=11, fontweight="bold"
|
|
979
|
+
)
|
|
980
|
+
ax1.set_ylabel("Number of Detected Rays", fontsize=11, fontweight="bold")
|
|
981
|
+
ax1.set_title("Ray Detection Count vs Detector Position", fontweight="bold")
|
|
982
|
+
ax1.grid(True, alpha=0.3)
|
|
983
|
+
ax1.set_xlim(0, 90)
|
|
984
|
+
|
|
985
|
+
# 2. Mean arrival angle vs detector position
|
|
986
|
+
if mean_angles_to_normal is not None:
|
|
987
|
+
ax2 = fig.add_subplot(gs[1, 2:])
|
|
988
|
+
valid_mask = detection_counts > 0
|
|
989
|
+
ax2.errorbar(
|
|
990
|
+
detector_angles_deg[valid_mask],
|
|
991
|
+
mean_angles_to_normal[valid_mask],
|
|
992
|
+
yerr=(
|
|
993
|
+
std_angles_to_normal[valid_mask]
|
|
994
|
+
if std_angles_to_normal is not None
|
|
995
|
+
else None
|
|
996
|
+
),
|
|
997
|
+
fmt="mo-",
|
|
998
|
+
linewidth=2,
|
|
999
|
+
markersize=8,
|
|
1000
|
+
capsize=5,
|
|
1001
|
+
alpha=0.7,
|
|
1002
|
+
)
|
|
1003
|
+
ax2.set_xlabel(
|
|
1004
|
+
"Detector Position Angle from Surface (degrees)",
|
|
1005
|
+
fontsize=11,
|
|
1006
|
+
fontweight="bold",
|
|
1007
|
+
)
|
|
1008
|
+
ax2.set_ylabel("Mean Angle to Normal (degrees)", fontsize=11, fontweight="bold")
|
|
1009
|
+
ax2.set_title("Mean Arrival Angle at Detector", fontweight="bold")
|
|
1010
|
+
ax2.grid(True, alpha=0.3)
|
|
1011
|
+
ax2.set_xlim(0, 90)
|
|
1012
|
+
|
|
1013
|
+
# 3a. Mean arrival time vs detector position
|
|
1014
|
+
if mean_arrival_times is not None:
|
|
1015
|
+
ax3a = fig.add_subplot(gs[2, :2])
|
|
1016
|
+
valid_mask = detection_counts > 0
|
|
1017
|
+
ax3a.errorbar(
|
|
1018
|
+
detector_angles_deg[valid_mask],
|
|
1019
|
+
mean_arrival_times[valid_mask] * 1e6, # Convert to microseconds
|
|
1020
|
+
yerr=(
|
|
1021
|
+
std_arrival_times[valid_mask] * 1e6
|
|
1022
|
+
if std_arrival_times is not None
|
|
1023
|
+
else None
|
|
1024
|
+
),
|
|
1025
|
+
fmt="co-",
|
|
1026
|
+
linewidth=2,
|
|
1027
|
+
markersize=8,
|
|
1028
|
+
capsize=5,
|
|
1029
|
+
alpha=0.7,
|
|
1030
|
+
)
|
|
1031
|
+
ax3a.set_xlabel(
|
|
1032
|
+
"Detector Position Angle from Surface (degrees)",
|
|
1033
|
+
fontsize=11,
|
|
1034
|
+
fontweight="bold",
|
|
1035
|
+
)
|
|
1036
|
+
ax3a.set_ylabel("Mean Arrival Time (μs)", fontsize=11, fontweight="bold")
|
|
1037
|
+
ax3a.set_title("Mean Arrival Time at Detector", fontweight="bold")
|
|
1038
|
+
ax3a.grid(True, alpha=0.3)
|
|
1039
|
+
ax3a.set_xlim(0, 90)
|
|
1040
|
+
|
|
1041
|
+
# 3b. Detection efficiency vs detector position
|
|
1042
|
+
ax3b = fig.add_subplot(gs[2, 2:])
|
|
1043
|
+
ax3b.plot(
|
|
1044
|
+
detector_angles_deg, detection_efficiency, "mo-", linewidth=2, markersize=8
|
|
1045
|
+
)
|
|
1046
|
+
ax3b.axhline(0, color="k", linestyle="-", linewidth=0.5)
|
|
1047
|
+
ax3b.set_xlabel(
|
|
1048
|
+
"Detector Position Angle from Surface (degrees)", fontsize=11, fontweight="bold"
|
|
1049
|
+
)
|
|
1050
|
+
ax3b.set_ylabel("Detection Efficiency (%)", fontsize=11, fontweight="bold")
|
|
1051
|
+
ax3b.set_title("Detection Efficiency (Detected/Source)", fontweight="bold")
|
|
1052
|
+
ax3b.grid(True, alpha=0.3)
|
|
1053
|
+
ax3b.set_xlim(0, 90)
|
|
1054
|
+
ax3b.set_ylim(
|
|
1055
|
+
0, max(detection_efficiency) * 1.1 if max(detection_efficiency) > 0 else 1
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# 4. Time distribution
|
|
1059
|
+
if all_time_distributions is not None:
|
|
1060
|
+
ax4 = fig.add_subplot(gs[3, 0:2])
|
|
1061
|
+
|
|
1062
|
+
# Find first arrival time across all detectors
|
|
1063
|
+
all_times_global = []
|
|
1064
|
+
for i, angle_deg in enumerate(detector_angles_deg):
|
|
1065
|
+
time_data = all_time_distributions[i]
|
|
1066
|
+
if len(time_data) == 3:
|
|
1067
|
+
times_raw, _, _ = time_data
|
|
1068
|
+
if len(times_raw) > 0:
|
|
1069
|
+
all_times_global.extend(times_raw)
|
|
1070
|
+
|
|
1071
|
+
if len(all_times_global) > 0:
|
|
1072
|
+
first_arrival = np.min(all_times_global)
|
|
1073
|
+
log_bin_edges = np.logspace(-9, -3, 50) # in seconds
|
|
1074
|
+
|
|
1075
|
+
# Identify positions with data
|
|
1076
|
+
positions_with_data = []
|
|
1077
|
+
for i, angle_deg in enumerate(detector_angles_deg):
|
|
1078
|
+
time_data = all_time_distributions[i]
|
|
1079
|
+
if len(time_data) == 3:
|
|
1080
|
+
times_raw, intensities_raw, _ = time_data
|
|
1081
|
+
if len(times_raw) > 0:
|
|
1082
|
+
times_relative = times_raw - first_arrival
|
|
1083
|
+
counts, _ = np.histogram(
|
|
1084
|
+
times_relative, bins=log_bin_edges, weights=intensities_raw
|
|
1085
|
+
)
|
|
1086
|
+
if counts.sum() > 0:
|
|
1087
|
+
positions_with_data.append(
|
|
1088
|
+
(i, angle_deg, times_relative, counts, intensities_raw)
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
# Plot histograms
|
|
1092
|
+
if len(positions_with_data) > 0:
|
|
1093
|
+
colors = plt.cm.turbo(np.linspace(0, 1, len(positions_with_data)))
|
|
1094
|
+
|
|
1095
|
+
for color_idx, (
|
|
1096
|
+
i,
|
|
1097
|
+
angle_deg,
|
|
1098
|
+
times_relative,
|
|
1099
|
+
counts,
|
|
1100
|
+
intensities_raw,
|
|
1101
|
+
) in enumerate(positions_with_data):
|
|
1102
|
+
bin_centers = (log_bin_edges[:-1] + log_bin_edges[1:]) / 2
|
|
1103
|
+
ax4.plot(
|
|
1104
|
+
bin_centers * 1e9, # Convert to nanoseconds
|
|
1105
|
+
counts,
|
|
1106
|
+
color=colors[color_idx],
|
|
1107
|
+
linewidth=1.5,
|
|
1108
|
+
label=f"{angle_deg:.0f}°" if color_idx % 10 == 0 else "",
|
|
1109
|
+
alpha=0.7,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
ax4.set_xlabel("Relative Arrival Time (ns)", fontsize=11, fontweight="bold")
|
|
1113
|
+
ax4.set_ylabel(
|
|
1114
|
+
"Intensity (weighted counts)", fontsize=11, fontweight="bold"
|
|
1115
|
+
)
|
|
1116
|
+
ax4.set_title("Timing Distribution (intensity-weighted)", fontweight="bold")
|
|
1117
|
+
ax4.set_xscale("log")
|
|
1118
|
+
ax4.set_yscale("log")
|
|
1119
|
+
ax4.grid(True, alpha=0.3, which="both")
|
|
1120
|
+
if len(positions_with_data) > 0:
|
|
1121
|
+
ax4.legend(loc="upper right", fontsize=8, ncol=2)
|
|
1122
|
+
|
|
1123
|
+
# 5. Angular distribution
|
|
1124
|
+
if all_time_distributions is not None:
|
|
1125
|
+
ax5 = fig.add_subplot(gs[3, 2:])
|
|
1126
|
+
|
|
1127
|
+
all_angles = []
|
|
1128
|
+
intensities_for_angles = []
|
|
1129
|
+
for i, angle_deg in enumerate(detector_angles_deg):
|
|
1130
|
+
if detection_counts[i] > 0:
|
|
1131
|
+
time_data = all_time_distributions[i]
|
|
1132
|
+
if len(time_data) == 3:
|
|
1133
|
+
_, intensities_raw, angles_raw = time_data
|
|
1134
|
+
all_angles.extend(angles_raw)
|
|
1135
|
+
intensities_for_angles.extend(intensities_raw)
|
|
1136
|
+
|
|
1137
|
+
if len(all_angles) > 0:
|
|
1138
|
+
all_angles = np.array(all_angles)
|
|
1139
|
+
intensities_for_angles = np.array(intensities_for_angles)
|
|
1140
|
+
|
|
1141
|
+
ax5.hist(
|
|
1142
|
+
all_angles,
|
|
1143
|
+
bins=50,
|
|
1144
|
+
weights=intensities_for_angles,
|
|
1145
|
+
color="purple",
|
|
1146
|
+
alpha=0.7,
|
|
1147
|
+
edgecolor="black",
|
|
1148
|
+
)
|
|
1149
|
+
ax5.set_xlabel(
|
|
1150
|
+
"Angle to Water Normal (degrees)", fontsize=11, fontweight="bold"
|
|
1151
|
+
)
|
|
1152
|
+
ax5.set_ylabel(
|
|
1153
|
+
"Intensity (weighted counts)", fontsize=11, fontweight="bold"
|
|
1154
|
+
)
|
|
1155
|
+
ax5.set_title(
|
|
1156
|
+
"Angular Distribution of Detected Rays (intensity-weighted)",
|
|
1157
|
+
fontweight="bold",
|
|
1158
|
+
)
|
|
1159
|
+
ax5.grid(True, alpha=0.3)
|
|
1160
|
+
|
|
1161
|
+
if save_path:
|
|
1162
|
+
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
1163
|
+
|
|
1164
|
+
return fig
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
# =============================================================================
|
|
1168
|
+
# Legacy Convenience Functions (Backward Compatibility)
|
|
1169
|
+
# =============================================================================
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def plot_statistics_evolution(
|
|
1173
|
+
stats_history: list["RayStatistics"],
|
|
1174
|
+
figsize: tuple[float, float] = (15, 10),
|
|
1175
|
+
save_path: str | None = None,
|
|
1176
|
+
) -> Figure:
|
|
1177
|
+
"""
|
|
1178
|
+
Create figure showing evolution of ray statistics over propagation.
|
|
1179
|
+
|
|
1180
|
+
This is a convenience function for visualizing how beam properties
|
|
1181
|
+
change during propagation.
|
|
1182
|
+
|
|
1183
|
+
Parameters
|
|
1184
|
+
----------
|
|
1185
|
+
stats_history : List[RayStatistics]
|
|
1186
|
+
List of RayStatistics objects at different propagation steps.
|
|
1187
|
+
figsize : tuple
|
|
1188
|
+
Figure size.
|
|
1189
|
+
save_path : str, optional
|
|
1190
|
+
Path to save figure.
|
|
1191
|
+
|
|
1192
|
+
Returns
|
|
1193
|
+
-------
|
|
1194
|
+
Figure
|
|
1195
|
+
Matplotlib figure with statistics evolution.
|
|
1196
|
+
"""
|
|
1197
|
+
if len(stats_history) == 0:
|
|
1198
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize)
|
|
1199
|
+
ax.text(
|
|
1200
|
+
0.5,
|
|
1201
|
+
0.5,
|
|
1202
|
+
"No statistics available",
|
|
1203
|
+
ha="center",
|
|
1204
|
+
va="center",
|
|
1205
|
+
transform=ax.transAxes,
|
|
1206
|
+
)
|
|
1207
|
+
return fig
|
|
1208
|
+
|
|
1209
|
+
# Extract statistics
|
|
1210
|
+
steps = np.arange(len(stats_history))
|
|
1211
|
+
active_rays = [s.active_rays for s in stats_history]
|
|
1212
|
+
total_power = [s.total_power for s in stats_history]
|
|
1213
|
+
mean_path = [s.mean_optical_path for s in stats_history]
|
|
1214
|
+
|
|
1215
|
+
# Optional: mean positions if available
|
|
1216
|
+
mean_x = [
|
|
1217
|
+
s.mean_position[0] if hasattr(s, "mean_position") else 0 for s in stats_history
|
|
1218
|
+
]
|
|
1219
|
+
mean_y = [
|
|
1220
|
+
s.mean_position[1] if hasattr(s, "mean_position") else 0 for s in stats_history
|
|
1221
|
+
]
|
|
1222
|
+
mean_z = [
|
|
1223
|
+
s.mean_position[2] if hasattr(s, "mean_position") else 0 for s in stats_history
|
|
1224
|
+
]
|
|
1225
|
+
|
|
1226
|
+
fig, axes = plt.subplots(2, 2, figsize=figsize, constrained_layout=True)
|
|
1227
|
+
fig.suptitle("Ray Statistics Evolution", fontsize=14, fontweight="bold")
|
|
1228
|
+
|
|
1229
|
+
# Active rays
|
|
1230
|
+
ax = axes[0, 0]
|
|
1231
|
+
ax.plot(steps, active_rays, "b-o", markersize=4)
|
|
1232
|
+
ax.set_xlabel("Step")
|
|
1233
|
+
ax.set_ylabel("Active Rays")
|
|
1234
|
+
ax.set_title("Active Ray Count")
|
|
1235
|
+
ax.grid(True, alpha=0.3)
|
|
1236
|
+
|
|
1237
|
+
# Total power
|
|
1238
|
+
ax = axes[0, 1]
|
|
1239
|
+
ax.plot(steps, total_power, "g-o", markersize=4)
|
|
1240
|
+
ax.set_xlabel("Step")
|
|
1241
|
+
ax.set_ylabel("Power (W)")
|
|
1242
|
+
ax.set_title("Total Power")
|
|
1243
|
+
ax.grid(True, alpha=0.3)
|
|
1244
|
+
|
|
1245
|
+
# Mean optical path
|
|
1246
|
+
ax = axes[1, 0]
|
|
1247
|
+
ax.plot(steps, mean_path, "r-o", markersize=4)
|
|
1248
|
+
ax.set_xlabel("Step")
|
|
1249
|
+
ax.set_ylabel("Path Length (m)")
|
|
1250
|
+
ax.set_title("Mean Optical Path")
|
|
1251
|
+
ax.grid(True, alpha=0.3)
|
|
1252
|
+
|
|
1253
|
+
# Mean position
|
|
1254
|
+
ax = axes[1, 1]
|
|
1255
|
+
ax.plot(steps, mean_z, "purple", label="Z", marker="o", markersize=4)
|
|
1256
|
+
ax.plot(steps, mean_x, "orange", label="X", marker="s", markersize=4, alpha=0.7)
|
|
1257
|
+
ax.plot(steps, mean_y, "cyan", label="Y", marker="^", markersize=4, alpha=0.7)
|
|
1258
|
+
ax.set_xlabel("Step")
|
|
1259
|
+
ax.set_ylabel("Position (m)")
|
|
1260
|
+
ax.set_title("Mean Position")
|
|
1261
|
+
ax.legend()
|
|
1262
|
+
ax.grid(True, alpha=0.3)
|
|
1263
|
+
|
|
1264
|
+
if save_path:
|
|
1265
|
+
from .common import save_figure
|
|
1266
|
+
|
|
1267
|
+
save_figure(fig, save_path)
|
|
1268
|
+
|
|
1269
|
+
return fig
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def plot_beam_profile(
|
|
1273
|
+
rays: "RayBatch",
|
|
1274
|
+
axis: str = "z",
|
|
1275
|
+
num_slices: int = 5,
|
|
1276
|
+
figsize: tuple[float, float] = (15, 4),
|
|
1277
|
+
save_path: str | None = None,
|
|
1278
|
+
) -> Figure:
|
|
1279
|
+
"""
|
|
1280
|
+
Create figure showing beam profile at multiple slices.
|
|
1281
|
+
|
|
1282
|
+
This is an alias for create_beam_profile_figure for backward compatibility.
|
|
1283
|
+
|
|
1284
|
+
Parameters
|
|
1285
|
+
----------
|
|
1286
|
+
rays : RayBatch
|
|
1287
|
+
Ray batch.
|
|
1288
|
+
axis : str
|
|
1289
|
+
Propagation axis: 'x', 'y', 'z'.
|
|
1290
|
+
num_slices : int
|
|
1291
|
+
Number of slices to show.
|
|
1292
|
+
figsize : tuple
|
|
1293
|
+
Figure size.
|
|
1294
|
+
save_path : str, optional
|
|
1295
|
+
Path to save figure.
|
|
1296
|
+
|
|
1297
|
+
Returns
|
|
1298
|
+
-------
|
|
1299
|
+
Figure
|
|
1300
|
+
Matplotlib figure.
|
|
1301
|
+
"""
|
|
1302
|
+
return create_beam_profile_figure(
|
|
1303
|
+
rays=rays,
|
|
1304
|
+
axis=axis,
|
|
1305
|
+
num_slices=num_slices,
|
|
1306
|
+
figsize=figsize,
|
|
1307
|
+
save_path=save_path,
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def plot_wavelength_distribution(
|
|
1312
|
+
rays: "RayBatch",
|
|
1313
|
+
bins: int = 50,
|
|
1314
|
+
figsize: tuple[float, float] = (10, 6),
|
|
1315
|
+
save_path: str | None = None,
|
|
1316
|
+
) -> Figure:
|
|
1317
|
+
"""
|
|
1318
|
+
Create figure showing wavelength distribution of rays.
|
|
1319
|
+
|
|
1320
|
+
This is a convenience function for quick visualization. For custom layouts,
|
|
1321
|
+
use plot_wavelength_histogram() on a single axis.
|
|
1322
|
+
|
|
1323
|
+
Parameters
|
|
1324
|
+
----------
|
|
1325
|
+
rays : RayBatch
|
|
1326
|
+
Ray batch.
|
|
1327
|
+
bins : int
|
|
1328
|
+
Number of histogram bins.
|
|
1329
|
+
figsize : tuple
|
|
1330
|
+
Figure size.
|
|
1331
|
+
save_path : str, optional
|
|
1332
|
+
Path to save figure.
|
|
1333
|
+
|
|
1334
|
+
Returns
|
|
1335
|
+
-------
|
|
1336
|
+
Figure
|
|
1337
|
+
Matplotlib figure with wavelength histogram.
|
|
1338
|
+
"""
|
|
1339
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, constrained_layout=True)
|
|
1340
|
+
|
|
1341
|
+
plot_wavelength_histogram(ax, rays, bins=bins)
|
|
1342
|
+
|
|
1343
|
+
fig.suptitle("Wavelength Distribution", fontsize=14, fontweight="bold")
|
|
1344
|
+
|
|
1345
|
+
if save_path:
|
|
1346
|
+
from .common import save_figure
|
|
1347
|
+
|
|
1348
|
+
save_figure(fig, save_path)
|
|
1349
|
+
|
|
1350
|
+
return fig
|