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,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
+ )