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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. 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")