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,712 @@
|
|
|
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
|
+
"""Visualization panel - displays matplotlib plots of simulation results.
|
|
35
|
+
|
|
36
|
+
Provides various visualization options for simulation results including
|
|
37
|
+
detector heatmaps, timing histograms, and spatial distributions.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from typing import TYPE_CHECKING, Callable
|
|
41
|
+
import numpy as np
|
|
42
|
+
|
|
43
|
+
import dearpygui.dearpygui as dpg
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from lsurf.simulation import SimulationResult
|
|
47
|
+
|
|
48
|
+
# Check for matplotlib
|
|
49
|
+
try:
|
|
50
|
+
import matplotlib
|
|
51
|
+
|
|
52
|
+
matplotlib.use("Agg") # Non-interactive backend for rendering to buffer
|
|
53
|
+
import matplotlib.pyplot as plt
|
|
54
|
+
from matplotlib.figure import Figure
|
|
55
|
+
|
|
56
|
+
HAS_MATPLOTLIB = True
|
|
57
|
+
except ImportError:
|
|
58
|
+
HAS_MATPLOTLIB = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class VisualizationPanel:
|
|
62
|
+
"""Panel for displaying matplotlib visualizations of simulation results."""
|
|
63
|
+
|
|
64
|
+
# Available visualization types
|
|
65
|
+
VISUALIZATIONS = [
|
|
66
|
+
("detector_heatmap", "Detector Heatmap (X-Y)"),
|
|
67
|
+
("detector_heatmap_xz", "Detector Heatmap (X-Z)"),
|
|
68
|
+
("arrival_time", "Arrival Time Distribution"),
|
|
69
|
+
("intensity_dist", "Intensity Distribution"),
|
|
70
|
+
("wavelength_dist", "Wavelength Distribution"),
|
|
71
|
+
("position_scatter", "Detection Positions (3D Projections)"),
|
|
72
|
+
("time_vs_position", "Arrival Time vs Position"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
on_close: Callable[[], None] | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Initialize visualization panel.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
on_close: Callback when panel is closed (to return to 3D view)
|
|
83
|
+
"""
|
|
84
|
+
self._on_close = on_close
|
|
85
|
+
self._result: "SimulationResult | None" = None
|
|
86
|
+
self._detected = None # Can also hold DetectorResult directly
|
|
87
|
+
self._window_tag: int | None = None
|
|
88
|
+
self._image_tag: int | None = None
|
|
89
|
+
self._texture_tag: int | None = None
|
|
90
|
+
self._current_viz: str = "detector_heatmap"
|
|
91
|
+
self._fig_width = 800
|
|
92
|
+
self._fig_height = 600
|
|
93
|
+
|
|
94
|
+
def create(self, parent: int | str) -> int:
|
|
95
|
+
"""Create the visualization panel.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
parent: Parent container tag
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The window tag
|
|
102
|
+
"""
|
|
103
|
+
with dpg.child_window(
|
|
104
|
+
parent=parent,
|
|
105
|
+
tag="viz_panel",
|
|
106
|
+
height=-1,
|
|
107
|
+
horizontal_scrollbar=True,
|
|
108
|
+
) as self._window_tag:
|
|
109
|
+
# Header with close button and visualization selector
|
|
110
|
+
with dpg.group(horizontal=True):
|
|
111
|
+
dpg.add_text("Visualizations", color=(100, 200, 255))
|
|
112
|
+
dpg.add_spacer(width=20)
|
|
113
|
+
|
|
114
|
+
# Visualization type selector
|
|
115
|
+
dpg.add_combo(
|
|
116
|
+
items=[v[1] for v in self.VISUALIZATIONS],
|
|
117
|
+
default_value=self.VISUALIZATIONS[0][1],
|
|
118
|
+
callback=self._on_viz_changed,
|
|
119
|
+
tag="viz_selector",
|
|
120
|
+
width=250,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
dpg.add_spacer(width=20)
|
|
124
|
+
|
|
125
|
+
# Load results button
|
|
126
|
+
dpg.add_button(
|
|
127
|
+
label="Load Results...",
|
|
128
|
+
callback=self._on_load_results,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
dpg.add_spacer(width=10)
|
|
132
|
+
|
|
133
|
+
# Save results button
|
|
134
|
+
dpg.add_button(
|
|
135
|
+
label="Save Results...",
|
|
136
|
+
callback=self._on_save_results,
|
|
137
|
+
tag="viz_save_btn",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
dpg.add_spacer(width=10)
|
|
141
|
+
|
|
142
|
+
# Save plot button
|
|
143
|
+
dpg.add_button(
|
|
144
|
+
label="Save Plot...",
|
|
145
|
+
callback=self._on_save_plot,
|
|
146
|
+
tag="viz_save_plot_btn",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
dpg.add_spacer(width=10)
|
|
150
|
+
|
|
151
|
+
# Close button to return to 3D view
|
|
152
|
+
dpg.add_button(
|
|
153
|
+
label="Back to 3D View",
|
|
154
|
+
callback=self._on_close_clicked,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
dpg.add_separator()
|
|
158
|
+
|
|
159
|
+
# Placeholder for when no results
|
|
160
|
+
dpg.add_text(
|
|
161
|
+
"Run a simulation or load results (.npz) to see visualizations",
|
|
162
|
+
color=(128, 128, 128),
|
|
163
|
+
tag="viz_placeholder",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Container for the plot image
|
|
167
|
+
with dpg.group(tag="viz_image_container", show=False):
|
|
168
|
+
pass # Image will be added dynamically
|
|
169
|
+
|
|
170
|
+
return self._window_tag
|
|
171
|
+
|
|
172
|
+
def show(self) -> None:
|
|
173
|
+
"""Show the visualization panel."""
|
|
174
|
+
if self._window_tag and dpg.does_item_exist(self._window_tag):
|
|
175
|
+
dpg.show_item(self._window_tag)
|
|
176
|
+
|
|
177
|
+
def hide(self) -> None:
|
|
178
|
+
"""Hide the visualization panel."""
|
|
179
|
+
if self._window_tag and dpg.does_item_exist(self._window_tag):
|
|
180
|
+
dpg.hide_item(self._window_tag)
|
|
181
|
+
|
|
182
|
+
def set_result(self, result: "SimulationResult | None") -> None:
|
|
183
|
+
"""Set the simulation result to visualize.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
result: Simulation result or None to clear
|
|
187
|
+
"""
|
|
188
|
+
self._result = result
|
|
189
|
+
if result is not None:
|
|
190
|
+
self._detected = result.detected
|
|
191
|
+
else:
|
|
192
|
+
self._detected = None
|
|
193
|
+
self._update_visualization()
|
|
194
|
+
|
|
195
|
+
def set_detected(self, detected) -> None:
|
|
196
|
+
"""Set DetectorResult directly for visualization.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
detected: DetectorResult object or None to clear
|
|
200
|
+
"""
|
|
201
|
+
self._result = None
|
|
202
|
+
self._detected = detected
|
|
203
|
+
self._update_visualization()
|
|
204
|
+
|
|
205
|
+
def _on_load_results(self) -> None:
|
|
206
|
+
"""Handle load results button click."""
|
|
207
|
+
|
|
208
|
+
def callback(sender, app_data):
|
|
209
|
+
if app_data.get("file_path_name"):
|
|
210
|
+
file_path = app_data["file_path_name"]
|
|
211
|
+
self._load_results_file(file_path)
|
|
212
|
+
|
|
213
|
+
with dpg.file_dialog(
|
|
214
|
+
callback=callback,
|
|
215
|
+
width=800,
|
|
216
|
+
height=500,
|
|
217
|
+
modal=True,
|
|
218
|
+
show=True,
|
|
219
|
+
):
|
|
220
|
+
dpg.add_file_extension(".npz", color=(0, 255, 0))
|
|
221
|
+
dpg.add_file_extension(".h5", color=(0, 200, 255))
|
|
222
|
+
dpg.add_file_extension(".hdf5", color=(0, 200, 255))
|
|
223
|
+
|
|
224
|
+
def _load_results_file(self, file_path: str) -> None:
|
|
225
|
+
"""Load results from a file."""
|
|
226
|
+
try:
|
|
227
|
+
from lsurf.detectors.results import DetectorResult
|
|
228
|
+
|
|
229
|
+
if file_path.endswith(".npz"):
|
|
230
|
+
detected = DetectorResult.load_npz(file_path)
|
|
231
|
+
elif file_path.endswith((".h5", ".hdf5")):
|
|
232
|
+
detected = DetectorResult.load_hdf5(file_path)
|
|
233
|
+
else:
|
|
234
|
+
self._show_error(f"Unsupported file format: {file_path}")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
self.set_detected(detected)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self._show_error(f"Failed to load results: {e}")
|
|
241
|
+
|
|
242
|
+
def _on_save_results(self) -> None:
|
|
243
|
+
"""Handle save results button click."""
|
|
244
|
+
if self._detected is None or self._detected.is_empty:
|
|
245
|
+
self._show_error("No results to save")
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
def callback(sender, app_data):
|
|
249
|
+
if app_data.get("file_path_name"):
|
|
250
|
+
file_path = app_data["file_path_name"]
|
|
251
|
+
self._save_results_file(file_path)
|
|
252
|
+
|
|
253
|
+
with dpg.file_dialog(
|
|
254
|
+
callback=callback,
|
|
255
|
+
width=800,
|
|
256
|
+
height=500,
|
|
257
|
+
modal=True,
|
|
258
|
+
show=True,
|
|
259
|
+
default_filename="results.npz",
|
|
260
|
+
):
|
|
261
|
+
dpg.add_file_extension(".npz", color=(0, 255, 0))
|
|
262
|
+
dpg.add_file_extension(".h5", color=(0, 200, 255))
|
|
263
|
+
|
|
264
|
+
def _save_results_file(self, file_path: str) -> None:
|
|
265
|
+
"""Save results to a file."""
|
|
266
|
+
if self._detected is None:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
if file_path.endswith(".npz"):
|
|
271
|
+
self._detected.save_npz(file_path)
|
|
272
|
+
elif file_path.endswith((".h5", ".hdf5")):
|
|
273
|
+
self._detected.save_hdf5(file_path)
|
|
274
|
+
else:
|
|
275
|
+
# Default to npz
|
|
276
|
+
if not file_path.endswith(".npz"):
|
|
277
|
+
file_path += ".npz"
|
|
278
|
+
self._detected.save_npz(file_path)
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self._show_error(f"Failed to save results: {e}")
|
|
282
|
+
|
|
283
|
+
def _on_save_plot(self) -> None:
|
|
284
|
+
"""Handle save plot button click."""
|
|
285
|
+
if self._detected is None or self._detected.is_empty:
|
|
286
|
+
self._show_error("No plot to save")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
def callback(sender, app_data):
|
|
290
|
+
if app_data.get("file_path_name"):
|
|
291
|
+
file_path = app_data["file_path_name"]
|
|
292
|
+
self._save_plot_file(file_path)
|
|
293
|
+
|
|
294
|
+
with dpg.file_dialog(
|
|
295
|
+
callback=callback,
|
|
296
|
+
width=800,
|
|
297
|
+
height=500,
|
|
298
|
+
modal=True,
|
|
299
|
+
show=True,
|
|
300
|
+
default_filename="plot.png",
|
|
301
|
+
):
|
|
302
|
+
dpg.add_file_extension(".png", color=(0, 255, 0))
|
|
303
|
+
dpg.add_file_extension(".pdf", color=(255, 100, 100))
|
|
304
|
+
dpg.add_file_extension(".svg", color=(100, 100, 255))
|
|
305
|
+
|
|
306
|
+
def _save_plot_file(self, file_path: str) -> None:
|
|
307
|
+
"""Save current plot to a file."""
|
|
308
|
+
if not HAS_MATPLOTLIB:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
fig = self._create_figure()
|
|
313
|
+
if fig is None:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
fig.savefig(file_path, dpi=150, bbox_inches="tight")
|
|
317
|
+
plt.close(fig)
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
self._show_error(f"Failed to save plot: {e}")
|
|
321
|
+
|
|
322
|
+
def _on_viz_changed(self, sender, app_data) -> None:
|
|
323
|
+
"""Handle visualization type change."""
|
|
324
|
+
# Find the visualization key from the display name
|
|
325
|
+
for key, display_name in self.VISUALIZATIONS:
|
|
326
|
+
if display_name == app_data:
|
|
327
|
+
self._current_viz = key
|
|
328
|
+
break
|
|
329
|
+
self._update_visualization()
|
|
330
|
+
|
|
331
|
+
def _on_close_clicked(self) -> None:
|
|
332
|
+
"""Handle close button click."""
|
|
333
|
+
if self._on_close:
|
|
334
|
+
self._on_close()
|
|
335
|
+
|
|
336
|
+
def _update_visualization(self) -> None:
|
|
337
|
+
"""Update the displayed visualization."""
|
|
338
|
+
if not HAS_MATPLOTLIB:
|
|
339
|
+
self._show_error("matplotlib is required for visualizations")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
if self._detected is None or self._detected.is_empty:
|
|
343
|
+
# Show placeholder
|
|
344
|
+
if dpg.does_item_exist("viz_placeholder"):
|
|
345
|
+
dpg.show_item("viz_placeholder")
|
|
346
|
+
if dpg.does_item_exist("viz_image_container"):
|
|
347
|
+
dpg.hide_item("viz_image_container")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
# Hide placeholder, show image container
|
|
351
|
+
if dpg.does_item_exist("viz_placeholder"):
|
|
352
|
+
dpg.hide_item("viz_placeholder")
|
|
353
|
+
if dpg.does_item_exist("viz_image_container"):
|
|
354
|
+
dpg.show_item("viz_image_container")
|
|
355
|
+
|
|
356
|
+
# Generate the plot
|
|
357
|
+
fig = self._create_figure()
|
|
358
|
+
if fig is None:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Render to texture and display
|
|
362
|
+
self._render_figure_to_texture(fig)
|
|
363
|
+
plt.close(fig)
|
|
364
|
+
|
|
365
|
+
def _create_figure(self) -> "Figure | None":
|
|
366
|
+
"""Create the matplotlib figure for current visualization."""
|
|
367
|
+
if self._detected is None:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
detected = self._detected
|
|
371
|
+
|
|
372
|
+
# Dispatch to appropriate visualization function
|
|
373
|
+
viz_funcs = {
|
|
374
|
+
"detector_heatmap": self._plot_detector_heatmap,
|
|
375
|
+
"detector_heatmap_xz": self._plot_detector_heatmap_xz,
|
|
376
|
+
"arrival_time": self._plot_arrival_time,
|
|
377
|
+
"intensity_dist": self._plot_intensity_distribution,
|
|
378
|
+
"wavelength_dist": self._plot_wavelength_distribution,
|
|
379
|
+
"position_scatter": self._plot_position_scatter,
|
|
380
|
+
"time_vs_position": self._plot_time_vs_position,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
func = viz_funcs.get(self._current_viz)
|
|
384
|
+
if func:
|
|
385
|
+
return func(detected)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def _plot_detector_heatmap(self, detected) -> "Figure":
|
|
389
|
+
"""Create detector heatmap (X-Y view)."""
|
|
390
|
+
fig, ax = plt.subplots(figsize=(8, 6), dpi=100)
|
|
391
|
+
|
|
392
|
+
positions = detected.positions
|
|
393
|
+
intensities = detected.intensities
|
|
394
|
+
|
|
395
|
+
# 2D histogram weighted by intensity
|
|
396
|
+
h = ax.hist2d(
|
|
397
|
+
positions[:, 0],
|
|
398
|
+
positions[:, 1],
|
|
399
|
+
bins=50,
|
|
400
|
+
weights=intensities,
|
|
401
|
+
cmap="hot",
|
|
402
|
+
)
|
|
403
|
+
plt.colorbar(h[3], ax=ax, label="Intensity")
|
|
404
|
+
|
|
405
|
+
ax.set_xlabel("X Position (m)")
|
|
406
|
+
ax.set_ylabel("Y Position (m)")
|
|
407
|
+
ax.set_title(f"Detector Heatmap (X-Y) - {detected.num_rays:,} rays")
|
|
408
|
+
ax.set_aspect("equal")
|
|
409
|
+
|
|
410
|
+
fig.tight_layout()
|
|
411
|
+
return fig
|
|
412
|
+
|
|
413
|
+
def _plot_detector_heatmap_xz(self, detected) -> "Figure":
|
|
414
|
+
"""Create detector heatmap (X-Z view)."""
|
|
415
|
+
fig, ax = plt.subplots(figsize=(8, 6), dpi=100)
|
|
416
|
+
|
|
417
|
+
positions = detected.positions
|
|
418
|
+
intensities = detected.intensities
|
|
419
|
+
|
|
420
|
+
h = ax.hist2d(
|
|
421
|
+
positions[:, 0],
|
|
422
|
+
positions[:, 2],
|
|
423
|
+
bins=50,
|
|
424
|
+
weights=intensities,
|
|
425
|
+
cmap="hot",
|
|
426
|
+
)
|
|
427
|
+
plt.colorbar(h[3], ax=ax, label="Intensity")
|
|
428
|
+
|
|
429
|
+
ax.set_xlabel("X Position (m)")
|
|
430
|
+
ax.set_ylabel("Z Position (m)")
|
|
431
|
+
ax.set_title(f"Detector Heatmap (X-Z) - {detected.num_rays:,} rays")
|
|
432
|
+
ax.set_aspect("equal")
|
|
433
|
+
|
|
434
|
+
fig.tight_layout()
|
|
435
|
+
return fig
|
|
436
|
+
|
|
437
|
+
def _plot_arrival_time(self, detected) -> "Figure":
|
|
438
|
+
"""Create arrival time histogram."""
|
|
439
|
+
fig, ax = plt.subplots(figsize=(8, 6), dpi=100)
|
|
440
|
+
|
|
441
|
+
times = detected.times
|
|
442
|
+
intensities = detected.intensities
|
|
443
|
+
|
|
444
|
+
# Convert to nanoseconds for readability
|
|
445
|
+
times_ns = times * 1e9
|
|
446
|
+
|
|
447
|
+
# Weighted histogram
|
|
448
|
+
ax.hist(times_ns, bins=50, weights=intensities, color="steelblue", alpha=0.7)
|
|
449
|
+
|
|
450
|
+
ax.set_xlabel("Arrival Time (ns)")
|
|
451
|
+
ax.set_ylabel("Intensity")
|
|
452
|
+
ax.set_title(f"Arrival Time Distribution - {detected.num_rays:,} rays")
|
|
453
|
+
|
|
454
|
+
# Add statistics
|
|
455
|
+
stats = detected.compute_statistics()
|
|
456
|
+
stats_text = (
|
|
457
|
+
f"Mean: {stats['mean_time']*1e9:.2f} ns\n"
|
|
458
|
+
f"Std: {stats['std_time']*1e9:.2f} ns\n"
|
|
459
|
+
f"Spread: {stats['time_spread']*1e9:.2f} ns"
|
|
460
|
+
)
|
|
461
|
+
ax.text(
|
|
462
|
+
0.95,
|
|
463
|
+
0.95,
|
|
464
|
+
stats_text,
|
|
465
|
+
transform=ax.transAxes,
|
|
466
|
+
verticalalignment="top",
|
|
467
|
+
horizontalalignment="right",
|
|
468
|
+
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
fig.tight_layout()
|
|
472
|
+
return fig
|
|
473
|
+
|
|
474
|
+
def _plot_intensity_distribution(self, detected) -> "Figure":
|
|
475
|
+
"""Create intensity distribution histogram."""
|
|
476
|
+
fig, ax = plt.subplots(figsize=(8, 6), dpi=100)
|
|
477
|
+
|
|
478
|
+
intensities = detected.intensities
|
|
479
|
+
|
|
480
|
+
# Use log scale if range is large
|
|
481
|
+
int_range = intensities.max() / (intensities.min() + 1e-10)
|
|
482
|
+
if int_range > 100:
|
|
483
|
+
ax.hist(
|
|
484
|
+
intensities[intensities > 0],
|
|
485
|
+
bins=50,
|
|
486
|
+
color="green",
|
|
487
|
+
alpha=0.7,
|
|
488
|
+
log=True,
|
|
489
|
+
)
|
|
490
|
+
ax.set_ylabel("Count (log scale)")
|
|
491
|
+
else:
|
|
492
|
+
ax.hist(intensities, bins=50, color="green", alpha=0.7)
|
|
493
|
+
ax.set_ylabel("Count")
|
|
494
|
+
|
|
495
|
+
ax.set_xlabel("Intensity")
|
|
496
|
+
ax.set_title(f"Intensity Distribution - {detected.num_rays:,} rays")
|
|
497
|
+
|
|
498
|
+
# Add statistics
|
|
499
|
+
stats_text = (
|
|
500
|
+
f"Total: {detected.total_intensity:.3e}\n"
|
|
501
|
+
f"Mean: {np.mean(intensities):.3e}\n"
|
|
502
|
+
f"Max: {np.max(intensities):.3e}"
|
|
503
|
+
)
|
|
504
|
+
ax.text(
|
|
505
|
+
0.95,
|
|
506
|
+
0.95,
|
|
507
|
+
stats_text,
|
|
508
|
+
transform=ax.transAxes,
|
|
509
|
+
verticalalignment="top",
|
|
510
|
+
horizontalalignment="right",
|
|
511
|
+
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
fig.tight_layout()
|
|
515
|
+
return fig
|
|
516
|
+
|
|
517
|
+
def _plot_wavelength_distribution(self, detected) -> "Figure":
|
|
518
|
+
"""Create wavelength distribution histogram."""
|
|
519
|
+
fig, ax = plt.subplots(figsize=(8, 6), dpi=100)
|
|
520
|
+
|
|
521
|
+
wavelengths = detected.wavelengths * 1e9 # Convert to nm
|
|
522
|
+
intensities = detected.intensities
|
|
523
|
+
|
|
524
|
+
ax.hist(wavelengths, bins=50, weights=intensities, color="purple", alpha=0.7)
|
|
525
|
+
|
|
526
|
+
ax.set_xlabel("Wavelength (nm)")
|
|
527
|
+
ax.set_ylabel("Intensity")
|
|
528
|
+
ax.set_title(f"Wavelength Distribution - {detected.num_rays:,} rays")
|
|
529
|
+
|
|
530
|
+
# Add statistics
|
|
531
|
+
mean_wl = np.average(wavelengths, weights=intensities)
|
|
532
|
+
stats_text = f"Mean: {mean_wl:.1f} nm\nRange: {wavelengths.min():.1f} - {wavelengths.max():.1f} nm"
|
|
533
|
+
ax.text(
|
|
534
|
+
0.95,
|
|
535
|
+
0.95,
|
|
536
|
+
stats_text,
|
|
537
|
+
transform=ax.transAxes,
|
|
538
|
+
verticalalignment="top",
|
|
539
|
+
horizontalalignment="right",
|
|
540
|
+
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
fig.tight_layout()
|
|
544
|
+
return fig
|
|
545
|
+
|
|
546
|
+
def _plot_position_scatter(self, detected) -> "Figure":
|
|
547
|
+
"""Create 3-panel position scatter plots."""
|
|
548
|
+
fig, axes = plt.subplots(1, 3, figsize=(12, 4), dpi=100)
|
|
549
|
+
|
|
550
|
+
positions = detected.positions
|
|
551
|
+
intensities = detected.intensities
|
|
552
|
+
|
|
553
|
+
# Normalize intensities for coloring
|
|
554
|
+
int_norm = intensities / (intensities.max() + 1e-10)
|
|
555
|
+
|
|
556
|
+
# Sample if too many points
|
|
557
|
+
max_points = 5000
|
|
558
|
+
if len(positions) > max_points:
|
|
559
|
+
idx = np.random.choice(len(positions), max_points, replace=False)
|
|
560
|
+
positions = positions[idx]
|
|
561
|
+
int_norm = int_norm[idx]
|
|
562
|
+
|
|
563
|
+
# X-Y view
|
|
564
|
+
sc = axes[0].scatter(
|
|
565
|
+
positions[:, 0],
|
|
566
|
+
positions[:, 1],
|
|
567
|
+
c=int_norm,
|
|
568
|
+
cmap="viridis",
|
|
569
|
+
s=2,
|
|
570
|
+
alpha=0.5,
|
|
571
|
+
)
|
|
572
|
+
axes[0].set_xlabel("X (m)")
|
|
573
|
+
axes[0].set_ylabel("Y (m)")
|
|
574
|
+
axes[0].set_title("X-Y View")
|
|
575
|
+
axes[0].set_aspect("equal")
|
|
576
|
+
|
|
577
|
+
# X-Z view
|
|
578
|
+
axes[1].scatter(
|
|
579
|
+
positions[:, 0],
|
|
580
|
+
positions[:, 2],
|
|
581
|
+
c=int_norm,
|
|
582
|
+
cmap="viridis",
|
|
583
|
+
s=2,
|
|
584
|
+
alpha=0.5,
|
|
585
|
+
)
|
|
586
|
+
axes[1].set_xlabel("X (m)")
|
|
587
|
+
axes[1].set_ylabel("Z (m)")
|
|
588
|
+
axes[1].set_title("X-Z View")
|
|
589
|
+
axes[1].set_aspect("equal")
|
|
590
|
+
|
|
591
|
+
# Y-Z view
|
|
592
|
+
axes[2].scatter(
|
|
593
|
+
positions[:, 1],
|
|
594
|
+
positions[:, 2],
|
|
595
|
+
c=int_norm,
|
|
596
|
+
cmap="viridis",
|
|
597
|
+
s=2,
|
|
598
|
+
alpha=0.5,
|
|
599
|
+
)
|
|
600
|
+
axes[2].set_xlabel("Y (m)")
|
|
601
|
+
axes[2].set_ylabel("Z (m)")
|
|
602
|
+
axes[2].set_title("Y-Z View")
|
|
603
|
+
axes[2].set_aspect("equal")
|
|
604
|
+
|
|
605
|
+
fig.colorbar(sc, ax=axes, label="Normalized Intensity", shrink=0.8)
|
|
606
|
+
fig.suptitle(f"Detection Positions - {detected.num_rays:,} rays")
|
|
607
|
+
fig.tight_layout()
|
|
608
|
+
return fig
|
|
609
|
+
|
|
610
|
+
def _plot_time_vs_position(self, detected) -> "Figure":
|
|
611
|
+
"""Create time vs position plot."""
|
|
612
|
+
fig, axes = plt.subplots(1, 3, figsize=(12, 4), dpi=100)
|
|
613
|
+
|
|
614
|
+
positions = detected.positions
|
|
615
|
+
times = detected.times * 1e9 # ns
|
|
616
|
+
intensities = detected.intensities
|
|
617
|
+
|
|
618
|
+
# Sample if too many points
|
|
619
|
+
max_points = 5000
|
|
620
|
+
if len(positions) > max_points:
|
|
621
|
+
idx = np.random.choice(len(positions), max_points, replace=False)
|
|
622
|
+
positions = positions[idx]
|
|
623
|
+
times = times[idx]
|
|
624
|
+
intensities = intensities[idx]
|
|
625
|
+
|
|
626
|
+
# Normalize intensities for alpha
|
|
627
|
+
int_norm = intensities / (intensities.max() + 1e-10)
|
|
628
|
+
|
|
629
|
+
# Time vs X
|
|
630
|
+
sc = axes[0].scatter(
|
|
631
|
+
positions[:, 0], times, c=int_norm, cmap="plasma", s=2, alpha=0.5
|
|
632
|
+
)
|
|
633
|
+
axes[0].set_xlabel("X Position (m)")
|
|
634
|
+
axes[0].set_ylabel("Arrival Time (ns)")
|
|
635
|
+
axes[0].set_title("Time vs X")
|
|
636
|
+
|
|
637
|
+
# Time vs Y
|
|
638
|
+
axes[1].scatter(
|
|
639
|
+
positions[:, 1], times, c=int_norm, cmap="plasma", s=2, alpha=0.5
|
|
640
|
+
)
|
|
641
|
+
axes[1].set_xlabel("Y Position (m)")
|
|
642
|
+
axes[1].set_ylabel("Arrival Time (ns)")
|
|
643
|
+
axes[1].set_title("Time vs Y")
|
|
644
|
+
|
|
645
|
+
# Time vs Z
|
|
646
|
+
axes[2].scatter(
|
|
647
|
+
positions[:, 2], times, c=int_norm, cmap="plasma", s=2, alpha=0.5
|
|
648
|
+
)
|
|
649
|
+
axes[2].set_xlabel("Z Position (m)")
|
|
650
|
+
axes[2].set_ylabel("Arrival Time (ns)")
|
|
651
|
+
axes[2].set_title("Time vs Z")
|
|
652
|
+
|
|
653
|
+
fig.colorbar(sc, ax=axes, label="Normalized Intensity", shrink=0.8)
|
|
654
|
+
fig.suptitle(f"Arrival Time vs Position - {detected.num_rays:,} rays")
|
|
655
|
+
fig.tight_layout()
|
|
656
|
+
return fig
|
|
657
|
+
|
|
658
|
+
def _render_figure_to_texture(self, fig: "Figure") -> None:
|
|
659
|
+
"""Render matplotlib figure to DearPyGui texture."""
|
|
660
|
+
# Render figure to RGB buffer
|
|
661
|
+
fig.canvas.draw()
|
|
662
|
+
width, height = fig.canvas.get_width_height()
|
|
663
|
+
|
|
664
|
+
# Get RGB buffer
|
|
665
|
+
buf = fig.canvas.buffer_rgba()
|
|
666
|
+
img_array = np.frombuffer(buf, dtype=np.uint8).reshape(height, width, 4)
|
|
667
|
+
|
|
668
|
+
# Convert to float for DearPyGui (RGBA, 0-1 range)
|
|
669
|
+
img_float = img_array.astype(np.float32) / 255.0
|
|
670
|
+
|
|
671
|
+
# Flatten for DearPyGui texture
|
|
672
|
+
texture_data = img_float.flatten().tolist()
|
|
673
|
+
|
|
674
|
+
# Clean up old texture if exists
|
|
675
|
+
if self._texture_tag and dpg.does_item_exist(self._texture_tag):
|
|
676
|
+
dpg.delete_item(self._texture_tag)
|
|
677
|
+
if self._image_tag and dpg.does_item_exist(self._image_tag):
|
|
678
|
+
dpg.delete_item(self._image_tag)
|
|
679
|
+
|
|
680
|
+
# Create texture registry if needed
|
|
681
|
+
if not dpg.does_item_exist("viz_texture_registry"):
|
|
682
|
+
dpg.add_texture_registry(tag="viz_texture_registry")
|
|
683
|
+
|
|
684
|
+
# Create new texture
|
|
685
|
+
self._texture_tag = dpg.add_dynamic_texture(
|
|
686
|
+
width=width,
|
|
687
|
+
height=height,
|
|
688
|
+
default_value=texture_data,
|
|
689
|
+
parent="viz_texture_registry",
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Add image to container
|
|
693
|
+
if dpg.does_item_exist("viz_image_container"):
|
|
694
|
+
dpg.delete_item("viz_image_container", children_only=True)
|
|
695
|
+
self._image_tag = dpg.add_image(
|
|
696
|
+
self._texture_tag,
|
|
697
|
+
parent="viz_image_container",
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def _show_error(self, message: str) -> None:
|
|
701
|
+
"""Show error message in panel."""
|
|
702
|
+
if dpg.does_item_exist("viz_placeholder"):
|
|
703
|
+
dpg.set_value("viz_placeholder", message)
|
|
704
|
+
dpg.configure_item("viz_placeholder", color=(255, 100, 100))
|
|
705
|
+
dpg.show_item("viz_placeholder")
|
|
706
|
+
|
|
707
|
+
def cleanup(self) -> None:
|
|
708
|
+
"""Clean up resources."""
|
|
709
|
+
if self._texture_tag and dpg.does_item_exist(self._texture_tag):
|
|
710
|
+
dpg.delete_item(self._texture_tag)
|
|
711
|
+
if dpg.does_item_exist("viz_texture_registry"):
|
|
712
|
+
dpg.delete_item("viz_texture_registry")
|