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,257 @@
|
|
|
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
|
+
"""Properties panel - displays and edits properties of selected objects.
|
|
35
|
+
|
|
36
|
+
Shows detailed information about the currently selected surface, source,
|
|
37
|
+
or result object with editable fields where applicable.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from typing import TYPE_CHECKING, Any
|
|
41
|
+
|
|
42
|
+
import dearpygui.dearpygui as dpg
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from ..core.scene import Scene, SceneObject
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PropertiesPanel:
|
|
49
|
+
"""Properties inspector panel."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, scene: "Scene") -> None:
|
|
52
|
+
self.scene = scene
|
|
53
|
+
self._window_tag: int | None = None
|
|
54
|
+
self._content_tag: int | None = None
|
|
55
|
+
|
|
56
|
+
def create(self, parent: int | str) -> int:
|
|
57
|
+
"""Create the properties panel.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
parent: Parent container tag
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The window tag
|
|
64
|
+
"""
|
|
65
|
+
self._window_tag = parent
|
|
66
|
+
|
|
67
|
+
dpg.add_text("Properties", color=(200, 200, 200), parent=parent)
|
|
68
|
+
dpg.add_separator(parent=parent)
|
|
69
|
+
|
|
70
|
+
with dpg.group(tag="properties_content", parent=parent) as self._content_tag:
|
|
71
|
+
dpg.add_text(
|
|
72
|
+
"Select an object",
|
|
73
|
+
color=(128, 128, 128),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Register scene change callback
|
|
77
|
+
self.scene.on_change(self._update_properties)
|
|
78
|
+
|
|
79
|
+
return self._window_tag
|
|
80
|
+
|
|
81
|
+
def _update_properties(self) -> None:
|
|
82
|
+
"""Update the properties display for the selected object."""
|
|
83
|
+
if self._content_tag is None or not dpg.does_item_exist(self._content_tag):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Clear existing content
|
|
87
|
+
dpg.delete_item(self._content_tag, children_only=True)
|
|
88
|
+
|
|
89
|
+
# Get selected object
|
|
90
|
+
if self.scene.selected_object is None:
|
|
91
|
+
dpg.add_text(
|
|
92
|
+
"Select an object",
|
|
93
|
+
color=(128, 128, 128),
|
|
94
|
+
parent=self._content_tag,
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
obj = self.scene.get_object(self.scene.selected_object)
|
|
99
|
+
if obj is None:
|
|
100
|
+
dpg.add_text(
|
|
101
|
+
"Object not found",
|
|
102
|
+
color=(128, 128, 128),
|
|
103
|
+
parent=self._content_tag,
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# Display object properties
|
|
108
|
+
self._display_object_properties(obj)
|
|
109
|
+
|
|
110
|
+
def _display_object_properties(self, obj: "SceneObject") -> None:
|
|
111
|
+
"""Display properties for a scene object."""
|
|
112
|
+
from ..core.scene import ObjectType
|
|
113
|
+
|
|
114
|
+
parent = self._content_tag
|
|
115
|
+
|
|
116
|
+
# Header
|
|
117
|
+
dpg.add_text(obj.name, color=(255, 255, 100), parent=parent)
|
|
118
|
+
dpg.add_text(
|
|
119
|
+
f"Type: {obj.obj_type.name}",
|
|
120
|
+
color=(150, 150, 150),
|
|
121
|
+
parent=parent,
|
|
122
|
+
)
|
|
123
|
+
dpg.add_separator(parent=parent)
|
|
124
|
+
|
|
125
|
+
# Color picker
|
|
126
|
+
dpg.add_text("Color:", parent=parent)
|
|
127
|
+
dpg.add_color_edit(
|
|
128
|
+
default_value=obj.color,
|
|
129
|
+
callback=lambda s, a, u: self._on_color_change(u, a),
|
|
130
|
+
user_data=obj.name,
|
|
131
|
+
no_alpha=False,
|
|
132
|
+
parent=parent,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
dpg.add_separator(parent=parent)
|
|
136
|
+
|
|
137
|
+
# Type-specific properties
|
|
138
|
+
if obj.data is not None:
|
|
139
|
+
if obj.obj_type == ObjectType.SURFACE:
|
|
140
|
+
self._display_surface_properties(obj.data, parent)
|
|
141
|
+
elif obj.obj_type == ObjectType.DETECTOR:
|
|
142
|
+
self._display_surface_properties(obj.data, parent)
|
|
143
|
+
elif obj.obj_type == ObjectType.SOURCE:
|
|
144
|
+
self._display_source_properties(obj.data, parent)
|
|
145
|
+
elif obj.obj_type == ObjectType.DETECTIONS:
|
|
146
|
+
self._display_detection_properties(obj.data, parent)
|
|
147
|
+
|
|
148
|
+
def _display_surface_properties(self, surface: Any, parent: int | str) -> None:
|
|
149
|
+
"""Display properties for a surface object."""
|
|
150
|
+
dpg.add_text("Surface Properties", color=(180, 180, 180), parent=parent)
|
|
151
|
+
|
|
152
|
+
# Common properties
|
|
153
|
+
self._add_property_row("Name", surface.name, parent)
|
|
154
|
+
self._add_property_row("Role", str(surface.role.name), parent)
|
|
155
|
+
self._add_property_row("GPU Capable", str(surface.gpu_capable), parent)
|
|
156
|
+
|
|
157
|
+
# Type-specific properties
|
|
158
|
+
if hasattr(surface, "point"):
|
|
159
|
+
self._add_property_row("Point", self._format_vector(surface.point), parent)
|
|
160
|
+
if hasattr(surface, "normal"):
|
|
161
|
+
self._add_property_row(
|
|
162
|
+
"Normal", self._format_vector(surface.normal), parent
|
|
163
|
+
)
|
|
164
|
+
if hasattr(surface, "center"):
|
|
165
|
+
self._add_property_row(
|
|
166
|
+
"Center", self._format_vector(surface.center), parent
|
|
167
|
+
)
|
|
168
|
+
if hasattr(surface, "radius"):
|
|
169
|
+
self._add_property_row("Radius", f"{surface.radius:.4g}", parent)
|
|
170
|
+
if hasattr(surface, "width"):
|
|
171
|
+
self._add_property_row("Width", f"{surface.width:.4g}", parent)
|
|
172
|
+
if hasattr(surface, "height"):
|
|
173
|
+
self._add_property_row("Height", f"{surface.height:.4g}", parent)
|
|
174
|
+
if hasattr(surface, "amplitude"):
|
|
175
|
+
self._add_property_row("Amplitude", f"{surface.amplitude:.4g}", parent)
|
|
176
|
+
if hasattr(surface, "wavelength"):
|
|
177
|
+
self._add_property_row("Wavelength", f"{surface.wavelength:.4g}", parent)
|
|
178
|
+
if hasattr(surface, "direction") and not callable(surface.direction):
|
|
179
|
+
self._add_property_row(
|
|
180
|
+
"Direction", self._format_vector(surface.direction), parent
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _display_source_properties(self, source: Any, parent: int | str) -> None:
|
|
184
|
+
"""Display properties for a source object."""
|
|
185
|
+
dpg.add_text("Source Properties", color=(180, 180, 180), parent=parent)
|
|
186
|
+
|
|
187
|
+
# Common properties
|
|
188
|
+
self._add_property_row("Num Rays", str(source.num_rays), parent)
|
|
189
|
+
self._add_property_row("Power", f"{source.power:.4g} W", parent)
|
|
190
|
+
|
|
191
|
+
wavelength = source.wavelength
|
|
192
|
+
if isinstance(wavelength, tuple):
|
|
193
|
+
wl_str = f"{wavelength[0]*1e9:.1f}-{wavelength[1]*1e9:.1f} nm"
|
|
194
|
+
else:
|
|
195
|
+
wl_str = f"{wavelength*1e9:.1f} nm"
|
|
196
|
+
self._add_property_row("Wavelength", wl_str, parent)
|
|
197
|
+
|
|
198
|
+
# Type-specific
|
|
199
|
+
if hasattr(source, "position"):
|
|
200
|
+
self._add_property_row(
|
|
201
|
+
"Position", self._format_vector(source.position), parent
|
|
202
|
+
)
|
|
203
|
+
if hasattr(source, "center"):
|
|
204
|
+
self._add_property_row("Center", self._format_vector(source.center), parent)
|
|
205
|
+
if hasattr(source, "origin"):
|
|
206
|
+
self._add_property_row("Origin", self._format_vector(source.origin), parent)
|
|
207
|
+
if hasattr(source, "direction"):
|
|
208
|
+
self._add_property_row(
|
|
209
|
+
"Direction", self._format_vector(source.direction), parent
|
|
210
|
+
)
|
|
211
|
+
if hasattr(source, "mean_direction"):
|
|
212
|
+
self._add_property_row(
|
|
213
|
+
"Mean Dir", self._format_vector(source.mean_direction), parent
|
|
214
|
+
)
|
|
215
|
+
if hasattr(source, "radius"):
|
|
216
|
+
self._add_property_row("Radius", f"{source.radius:.4g} m", parent)
|
|
217
|
+
if hasattr(source, "divergence_angle"):
|
|
218
|
+
angle_deg = source.divergence_angle * 180 / 3.14159
|
|
219
|
+
self._add_property_row("Divergence", f"{angle_deg:.2f} deg", parent)
|
|
220
|
+
|
|
221
|
+
def _display_detection_properties(self, detected: Any, parent: int | str) -> None:
|
|
222
|
+
"""Display properties for detection results."""
|
|
223
|
+
dpg.add_text("Detection Results", color=(180, 180, 180), parent=parent)
|
|
224
|
+
|
|
225
|
+
if hasattr(detected, "positions") and detected.positions is not None:
|
|
226
|
+
n_detected = len(detected.positions)
|
|
227
|
+
self._add_property_row("Count", str(n_detected), parent)
|
|
228
|
+
|
|
229
|
+
if hasattr(detected, "intensities") and detected.intensities is not None:
|
|
230
|
+
total_power = detected.intensities.sum()
|
|
231
|
+
self._add_property_row("Total Power", f"{total_power:.4g} W", parent)
|
|
232
|
+
|
|
233
|
+
if len(detected.intensities) > 0:
|
|
234
|
+
mean_power = detected.intensities.mean()
|
|
235
|
+
self._add_property_row("Mean Power", f"{mean_power:.4g} W", parent)
|
|
236
|
+
|
|
237
|
+
def _add_property_row(self, label: str, value: str, parent: int | str) -> None:
|
|
238
|
+
"""Add a label-value property row."""
|
|
239
|
+
with dpg.group(horizontal=True, parent=parent):
|
|
240
|
+
dpg.add_text(f"{label}:", color=(150, 150, 150))
|
|
241
|
+
dpg.add_text(value, color=(200, 200, 200))
|
|
242
|
+
|
|
243
|
+
def _format_vector(self, vec: Any) -> str:
|
|
244
|
+
"""Format a vector for display."""
|
|
245
|
+
if hasattr(vec, "__len__"):
|
|
246
|
+
if len(vec) == 2:
|
|
247
|
+
return f"({vec[0]:.3g}, {vec[1]:.3g})"
|
|
248
|
+
elif len(vec) == 3:
|
|
249
|
+
return f"({vec[0]:.3g}, {vec[1]:.3g}, {vec[2]:.3g})"
|
|
250
|
+
return str(vec)
|
|
251
|
+
|
|
252
|
+
def _on_color_change(self, name: str, color: tuple) -> None:
|
|
253
|
+
"""Handle color change for an object."""
|
|
254
|
+
obj = self.scene.get_object(name)
|
|
255
|
+
if obj is not None:
|
|
256
|
+
obj.color = tuple(color)
|
|
257
|
+
self.scene._notify_change()
|
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
"""Results panel - displays simulation statistics and plots.
|
|
35
|
+
|
|
36
|
+
Shows simulation results including detection counts, timing statistics,
|
|
37
|
+
and optional matplotlib-based plots.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from typing import TYPE_CHECKING
|
|
41
|
+
|
|
42
|
+
import dearpygui.dearpygui as dpg
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from lsurf.simulation import SimulationResult
|
|
46
|
+
|
|
47
|
+
from ..core.scene import Scene
|
|
48
|
+
from ..core.simulation import SimulationProgress
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ResultsPanel:
|
|
52
|
+
"""Results display panel showing simulation statistics."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, scene: "Scene") -> None:
|
|
55
|
+
self.scene = scene
|
|
56
|
+
self._window_tag: int | None = None
|
|
57
|
+
self._stats_tag: int | None = None
|
|
58
|
+
self._progress_tag: int | None = None
|
|
59
|
+
|
|
60
|
+
def create(self, parent: int | str) -> int:
|
|
61
|
+
"""Create the results panel.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
parent: Parent container tag
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The window tag
|
|
68
|
+
"""
|
|
69
|
+
# Create a child window for the results section (fills remaining space)
|
|
70
|
+
with dpg.child_window(
|
|
71
|
+
parent=parent,
|
|
72
|
+
tag="results_window",
|
|
73
|
+
height=-1, # Fill remaining space after viewport
|
|
74
|
+
horizontal_scrollbar=True,
|
|
75
|
+
) as self._window_tag:
|
|
76
|
+
dpg.add_text("Results", color=(200, 200, 200))
|
|
77
|
+
dpg.add_separator()
|
|
78
|
+
|
|
79
|
+
# Progress indicator
|
|
80
|
+
with dpg.group(tag="results_progress") as self._progress_tag:
|
|
81
|
+
dpg.add_text(
|
|
82
|
+
"No simulation running",
|
|
83
|
+
color=(128, 128, 128),
|
|
84
|
+
tag="progress_text",
|
|
85
|
+
)
|
|
86
|
+
dpg.add_progress_bar(
|
|
87
|
+
default_value=0,
|
|
88
|
+
overlay="",
|
|
89
|
+
tag="progress_bar",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
dpg.add_separator()
|
|
93
|
+
|
|
94
|
+
# Statistics table
|
|
95
|
+
with dpg.group(tag="results_stats", horizontal=True) as self._stats_tag:
|
|
96
|
+
dpg.add_text("Run a simulation to see results", color=(128, 128, 128))
|
|
97
|
+
|
|
98
|
+
return self._window_tag
|
|
99
|
+
|
|
100
|
+
def update_progress(self, progress: "SimulationProgress") -> None:
|
|
101
|
+
"""Update the progress display.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
progress: Current simulation progress
|
|
105
|
+
"""
|
|
106
|
+
from ..core.simulation import SimulationState
|
|
107
|
+
|
|
108
|
+
if not dpg.does_item_exist("progress_text"):
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Update text
|
|
112
|
+
dpg.set_value("progress_text", progress.message)
|
|
113
|
+
|
|
114
|
+
# Update progress bar
|
|
115
|
+
if progress.max_bounces > 0:
|
|
116
|
+
pct = progress.bounce / progress.max_bounces
|
|
117
|
+
else:
|
|
118
|
+
pct = 0
|
|
119
|
+
|
|
120
|
+
if progress.state == SimulationState.COMPLETED:
|
|
121
|
+
pct = 1.0
|
|
122
|
+
dpg.configure_item("progress_text", color=(100, 255, 100))
|
|
123
|
+
elif progress.state == SimulationState.ERROR:
|
|
124
|
+
dpg.configure_item("progress_text", color=(255, 100, 100))
|
|
125
|
+
elif progress.state == SimulationState.RUNNING:
|
|
126
|
+
dpg.configure_item("progress_text", color=(100, 200, 255))
|
|
127
|
+
else:
|
|
128
|
+
dpg.configure_item("progress_text", color=(128, 128, 128))
|
|
129
|
+
|
|
130
|
+
dpg.set_value("progress_bar", pct)
|
|
131
|
+
dpg.configure_item(
|
|
132
|
+
"progress_bar",
|
|
133
|
+
overlay=(
|
|
134
|
+
f"{progress.rays_detected} detected"
|
|
135
|
+
if progress.rays_detected > 0
|
|
136
|
+
else ""
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def update_results(self, result: "SimulationResult | None") -> None:
|
|
141
|
+
"""Update the results display.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
result: Simulation result to display
|
|
145
|
+
"""
|
|
146
|
+
if self._stats_tag is None or not dpg.does_item_exist(self._stats_tag):
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# Clear existing stats
|
|
150
|
+
dpg.delete_item(self._stats_tag, children_only=True)
|
|
151
|
+
|
|
152
|
+
if result is None:
|
|
153
|
+
dpg.add_text(
|
|
154
|
+
"No results available",
|
|
155
|
+
color=(128, 128, 128),
|
|
156
|
+
parent=self._stats_tag,
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Create statistics columns
|
|
161
|
+
self._create_stats_column(result, self._stats_tag)
|
|
162
|
+
|
|
163
|
+
# Create per-detector breakdown
|
|
164
|
+
if result.detections_per_surface:
|
|
165
|
+
self._create_detector_column(result, self._stats_tag)
|
|
166
|
+
|
|
167
|
+
def _create_stats_column(
|
|
168
|
+
self, result: "SimulationResult", parent: int | str
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Create the main statistics column."""
|
|
171
|
+
with dpg.group(parent=parent):
|
|
172
|
+
dpg.add_text("Statistics", color=(180, 180, 180))
|
|
173
|
+
|
|
174
|
+
stats = result.statistics
|
|
175
|
+
|
|
176
|
+
self._add_stat_row("Initial Rays", stats.total_rays_initial, parent=None)
|
|
177
|
+
self._add_stat_row("Rays Created", stats.total_rays_created, parent=None)
|
|
178
|
+
self._add_stat_row(
|
|
179
|
+
"Detected", stats.rays_detected, color=(100, 255, 100), parent=None
|
|
180
|
+
)
|
|
181
|
+
self._add_stat_row("Absorbed", stats.rays_absorbed, parent=None)
|
|
182
|
+
self._add_stat_row(
|
|
183
|
+
"Low Intensity", stats.rays_terminated_intensity, parent=None
|
|
184
|
+
)
|
|
185
|
+
self._add_stat_row(
|
|
186
|
+
"Out of Bounds", stats.rays_terminated_bounds, parent=None
|
|
187
|
+
)
|
|
188
|
+
self._add_stat_row(
|
|
189
|
+
"Max Bounces", stats.rays_terminated_max_bounces, parent=None
|
|
190
|
+
)
|
|
191
|
+
self._add_stat_row("Bounces", stats.bounces_completed, parent=None)
|
|
192
|
+
|
|
193
|
+
def _create_detector_column(
|
|
194
|
+
self, result: "SimulationResult", parent: int | str
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Create per-detector breakdown column."""
|
|
197
|
+
dpg.add_spacer(width=30, parent=parent)
|
|
198
|
+
|
|
199
|
+
with dpg.group(parent=parent):
|
|
200
|
+
dpg.add_text("Per Detector", color=(180, 180, 180))
|
|
201
|
+
|
|
202
|
+
for name, count in result.detections_per_surface.items():
|
|
203
|
+
self._add_stat_row(name, count, parent=None)
|
|
204
|
+
|
|
205
|
+
def _add_stat_row(
|
|
206
|
+
self,
|
|
207
|
+
label: str,
|
|
208
|
+
value: int,
|
|
209
|
+
color: tuple | None = None,
|
|
210
|
+
parent: int | str | None = None,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Add a statistics row."""
|
|
213
|
+
if color is None:
|
|
214
|
+
color = (200, 200, 200)
|
|
215
|
+
|
|
216
|
+
with dpg.group(horizontal=True, parent=parent):
|
|
217
|
+
dpg.add_text(f"{label}:", color=(150, 150, 150))
|
|
218
|
+
dpg.add_text(f"{value:,}", color=color)
|
|
219
|
+
|
|
220
|
+
def clear(self) -> None:
|
|
221
|
+
"""Clear the results display."""
|
|
222
|
+
if self._stats_tag and dpg.does_item_exist(self._stats_tag):
|
|
223
|
+
dpg.delete_item(self._stats_tag, children_only=True)
|
|
224
|
+
dpg.add_text(
|
|
225
|
+
"Run a simulation to see results",
|
|
226
|
+
color=(128, 128, 128),
|
|
227
|
+
parent=self._stats_tag,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if dpg.does_item_exist("progress_text"):
|
|
231
|
+
dpg.set_value("progress_text", "No simulation running")
|
|
232
|
+
dpg.configure_item("progress_text", color=(128, 128, 128))
|
|
233
|
+
|
|
234
|
+
if dpg.does_item_exist("progress_bar"):
|
|
235
|
+
dpg.set_value("progress_bar", 0)
|
|
236
|
+
dpg.configure_item("progress_bar", overlay="")
|
|
237
|
+
|
|
238
|
+
def display_cli_output(self, stdout: str, stderr: str, return_code: int) -> None:
|
|
239
|
+
"""Display CLI simulation output in the results panel.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
stdout: Standard output from CLI
|
|
243
|
+
stderr: Standard error from CLI
|
|
244
|
+
return_code: CLI return code
|
|
245
|
+
"""
|
|
246
|
+
if self._stats_tag is None or not dpg.does_item_exist(self._stats_tag):
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Clear existing content
|
|
250
|
+
dpg.delete_item(self._stats_tag, children_only=True)
|
|
251
|
+
|
|
252
|
+
# Update progress text
|
|
253
|
+
if dpg.does_item_exist("progress_text"):
|
|
254
|
+
if return_code == 0:
|
|
255
|
+
dpg.set_value("progress_text", "Simulation complete")
|
|
256
|
+
dpg.configure_item("progress_text", color=(100, 255, 100))
|
|
257
|
+
dpg.set_value("progress_bar", 1.0)
|
|
258
|
+
else:
|
|
259
|
+
dpg.set_value(
|
|
260
|
+
"progress_text", f"Simulation failed (exit code {return_code})"
|
|
261
|
+
)
|
|
262
|
+
dpg.configure_item("progress_text", color=(255, 100, 100))
|
|
263
|
+
|
|
264
|
+
# Combine all output for copyable text area
|
|
265
|
+
full_output = ""
|
|
266
|
+
if stdout:
|
|
267
|
+
full_output += stdout
|
|
268
|
+
if stderr:
|
|
269
|
+
full_output += "\n\n=== Errors/Warnings ===\n" + stderr
|
|
270
|
+
|
|
271
|
+
# Add debug info if available (from print statements)
|
|
272
|
+
import sys
|
|
273
|
+
|
|
274
|
+
if hasattr(sys, "_gui_debug_info"):
|
|
275
|
+
full_output += "\n\n=== Debug Info ===\n" + sys._gui_debug_info
|
|
276
|
+
|
|
277
|
+
with dpg.group(parent=self._stats_tag):
|
|
278
|
+
dpg.add_text(
|
|
279
|
+
"CLI Output (select and Ctrl+C to copy):", color=(180, 180, 180)
|
|
280
|
+
)
|
|
281
|
+
dpg.add_separator()
|
|
282
|
+
|
|
283
|
+
# Use multiline input for copyable text
|
|
284
|
+
dpg.add_input_text(
|
|
285
|
+
default_value=full_output.strip(),
|
|
286
|
+
multiline=True,
|
|
287
|
+
readonly=True,
|
|
288
|
+
width=-1,
|
|
289
|
+
height=200,
|
|
290
|
+
tab_input=False,
|
|
291
|
+
)
|