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,39 @@
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
+ """Core GUI components for scene and simulation management."""
35
+
36
+ from .scene import Scene, SceneObject
37
+ from .simulation import SimulationRunner
38
+
39
+ __all__ = ["Scene", "SceneObject", "SimulationRunner"]
@@ -0,0 +1,343 @@
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
+ """Scene state management for the GUI.
35
+
36
+ Manages the collection of surfaces, sources, and simulation results
37
+ that are displayed in the 3D viewport.
38
+ """
39
+
40
+ from dataclasses import dataclass, field
41
+ from enum import Enum, auto
42
+ from typing import TYPE_CHECKING, Any, Callable
43
+
44
+ import numpy as np
45
+
46
+ if TYPE_CHECKING:
47
+ from lsurf.geometry import Geometry
48
+ from lsurf.simulation import SimulationResult
49
+ from lsurf.sources import RaySource
50
+ from lsurf.surfaces import Surface
51
+
52
+
53
+ class ObjectType(Enum):
54
+ """Type of object in the scene."""
55
+
56
+ SURFACE = auto()
57
+ DETECTOR = auto()
58
+ SOURCE = auto()
59
+ RAY_PATHS = auto()
60
+ DETECTIONS = auto()
61
+
62
+
63
+ @dataclass
64
+ class SceneObject:
65
+ """A single object in the scene hierarchy."""
66
+
67
+ name: str
68
+ obj_type: ObjectType
69
+ data: Any
70
+ visible: bool = True
71
+ color: tuple[float, float, float, float] = (0.7, 0.7, 0.7, 1.0)
72
+ selected: bool = False
73
+
74
+ # Mesh data for rendering (computed by renderers)
75
+ vertices: np.ndarray | None = None
76
+ indices: np.ndarray | None = None
77
+ wireframe: bool = False
78
+
79
+
80
+ @dataclass
81
+ class Scene:
82
+ """Central state management for the GUI scene.
83
+
84
+ Holds all objects to be rendered and provides methods for
85
+ adding, removing, and querying objects.
86
+ """
87
+
88
+ objects: dict[str, SceneObject] = field(default_factory=dict)
89
+ geometry: "Geometry | None" = None
90
+ source: "RaySource | None" = None
91
+ result: "SimulationResult | None" = None
92
+
93
+ # Camera state
94
+ camera_position: np.ndarray = field(
95
+ default_factory=lambda: np.array([0.0, -50.0, 25.0])
96
+ )
97
+ camera_target: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 0.0]))
98
+ camera_up: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 1.0]))
99
+
100
+ # Selection state
101
+ selected_object: str | None = None
102
+
103
+ # Callbacks for UI updates
104
+ _on_change_callbacks: list[Callable[[], None]] = field(default_factory=list)
105
+
106
+ def add_object(self, obj: SceneObject) -> None:
107
+ """Add an object to the scene."""
108
+ self.objects[obj.name] = obj
109
+ self._notify_change()
110
+
111
+ def remove_object(self, name: str) -> None:
112
+ """Remove an object from the scene."""
113
+ if name in self.objects:
114
+ del self.objects[name]
115
+ if self.selected_object == name:
116
+ self.selected_object = None
117
+ self._notify_change()
118
+
119
+ def get_object(self, name: str) -> SceneObject | None:
120
+ """Get an object by name."""
121
+ return self.objects.get(name)
122
+
123
+ def select_object(self, name: str | None) -> None:
124
+ """Select an object (or deselect if None)."""
125
+ # Deselect previous
126
+ if self.selected_object and self.selected_object in self.objects:
127
+ self.objects[self.selected_object].selected = False
128
+
129
+ # Select new
130
+ self.selected_object = name
131
+ if name and name in self.objects:
132
+ self.objects[name].selected = True
133
+
134
+ self._notify_change()
135
+
136
+ def toggle_visibility(self, name: str) -> None:
137
+ """Toggle visibility of an object."""
138
+ if name in self.objects:
139
+ self.objects[name].visible = not self.objects[name].visible
140
+ self._notify_change()
141
+
142
+ def clear(self) -> None:
143
+ """Clear all objects from the scene."""
144
+ self.objects.clear()
145
+ self.geometry = None
146
+ self.source = None
147
+ self.result = None
148
+ self.selected_object = None
149
+ self._notify_change()
150
+
151
+ def get_objects_by_type(self, obj_type: ObjectType) -> list[SceneObject]:
152
+ """Get all objects of a specific type."""
153
+ return [obj for obj in self.objects.values() if obj.obj_type == obj_type]
154
+
155
+ def get_visible_objects(self) -> list[SceneObject]:
156
+ """Get all visible objects."""
157
+ return [obj for obj in self.objects.values() if obj.visible]
158
+
159
+ def get_bounds(self) -> tuple[np.ndarray, np.ndarray]:
160
+ """Get the bounding box of all objects.
161
+
162
+ Returns:
163
+ (min_corner, max_corner) as numpy arrays
164
+ """
165
+ all_vertices = []
166
+ for obj in self.objects.values():
167
+ if obj.visible and obj.vertices is not None and len(obj.vertices) > 0:
168
+ all_vertices.append(obj.vertices)
169
+
170
+ if not all_vertices:
171
+ # Default bounds if no objects
172
+ return np.array([-10.0, -10.0, -10.0]), np.array([10.0, 10.0, 10.0])
173
+
174
+ all_verts = np.vstack(all_vertices)
175
+ return all_verts.min(axis=0), all_verts.max(axis=0)
176
+
177
+ def fit_camera_to_scene(self) -> None:
178
+ """Adjust camera to fit all objects in view."""
179
+ min_bounds, max_bounds = self.get_bounds()
180
+ center = (min_bounds + max_bounds) / 2
181
+ size = np.linalg.norm(max_bounds - min_bounds)
182
+
183
+ # Position camera at 45-degree angle looking at center
184
+ distance = max(size * 1.5, 10.0)
185
+ self.camera_target = center
186
+ self.camera_position = center + np.array(
187
+ [distance * 0.5, -distance * 0.7, distance * 0.5]
188
+ )
189
+ self._notify_change()
190
+
191
+ def on_change(self, callback: Callable[[], None]) -> None:
192
+ """Register a callback to be called when the scene changes."""
193
+ self._on_change_callbacks.append(callback)
194
+
195
+ def _notify_change(self) -> None:
196
+ """Notify all registered callbacks of a change."""
197
+ for callback in self._on_change_callbacks:
198
+ callback()
199
+
200
+ def load_geometry(self, geometry: "Geometry") -> None:
201
+ """Load a Geometry object into the scene."""
202
+ from ..renderers import SurfaceRenderer
203
+
204
+ self.geometry = geometry
205
+
206
+ # Remove existing surfaces and detectors
207
+ to_remove = [
208
+ name
209
+ for name, obj in self.objects.items()
210
+ if obj.obj_type in (ObjectType.SURFACE, ObjectType.DETECTOR)
211
+ ]
212
+ for name in to_remove:
213
+ del self.objects[name]
214
+
215
+ renderer = SurfaceRenderer()
216
+
217
+ # Add surfaces
218
+ for surface in geometry.surfaces:
219
+ mesh_data = renderer.render(surface)
220
+ obj = SceneObject(
221
+ name=surface.name,
222
+ obj_type=ObjectType.SURFACE,
223
+ data=surface,
224
+ color=_get_surface_color(surface),
225
+ vertices=mesh_data.get("vertices"),
226
+ indices=mesh_data.get("indices"),
227
+ wireframe=mesh_data.get("wireframe", False),
228
+ )
229
+ self.objects[surface.name] = obj
230
+ print(
231
+ f"[Scene] Added surface '{surface.name}': {type(surface).__name__}, vertices={mesh_data.get('vertices') is not None}, indices={mesh_data.get('indices') is not None}"
232
+ )
233
+
234
+ # Add detectors
235
+ for detector in geometry.detectors:
236
+ mesh_data = renderer.render(detector)
237
+ obj = SceneObject(
238
+ name=detector.name,
239
+ obj_type=ObjectType.DETECTOR,
240
+ data=detector,
241
+ color=(0.2, 0.8, 0.2, 0.8), # Green for detectors
242
+ vertices=mesh_data.get("vertices"),
243
+ indices=mesh_data.get("indices"),
244
+ wireframe=mesh_data.get("wireframe", False),
245
+ )
246
+ self.objects[detector.name] = obj
247
+ print(
248
+ f"[Scene] Added detector '{detector.name}': {type(detector).__name__}, vertices={mesh_data.get('vertices') is not None}, indices={mesh_data.get('indices') is not None}"
249
+ )
250
+
251
+ print(f"[Scene] Total objects: {len(self.objects)}")
252
+ self.fit_camera_to_scene()
253
+ self._notify_change()
254
+
255
+ def load_source(self, source: "RaySource") -> None:
256
+ """Load a RaySource into the scene."""
257
+ from ..renderers import SourceRenderer
258
+
259
+ self.source = source
260
+
261
+ # Remove existing source
262
+ to_remove = [
263
+ name
264
+ for name, obj in self.objects.items()
265
+ if obj.obj_type == ObjectType.SOURCE
266
+ ]
267
+ for name in to_remove:
268
+ del self.objects[name]
269
+
270
+ renderer = SourceRenderer()
271
+ mesh_data = renderer.render(source)
272
+
273
+ obj = SceneObject(
274
+ name="Source",
275
+ obj_type=ObjectType.SOURCE,
276
+ data=source,
277
+ color=(1.0, 0.8, 0.0, 1.0), # Yellow/gold for sources
278
+ vertices=mesh_data.get("vertices"),
279
+ indices=mesh_data.get("indices"),
280
+ wireframe=mesh_data.get("wireframe", False),
281
+ )
282
+ self.objects["Source"] = obj
283
+ self._notify_change()
284
+
285
+ def load_result(self, result: "SimulationResult", show_rays: bool = True) -> None:
286
+ """Load simulation results into the scene."""
287
+ from ..renderers import RayRenderer
288
+
289
+ self.result = result
290
+
291
+ # Remove existing results
292
+ to_remove = [
293
+ name
294
+ for name, obj in self.objects.items()
295
+ if obj.obj_type in (ObjectType.RAY_PATHS, ObjectType.DETECTIONS)
296
+ ]
297
+ for name in to_remove:
298
+ del self.objects[name]
299
+
300
+ renderer = RayRenderer()
301
+
302
+ # Add detection points
303
+ if result.detected is not None and result.num_detected > 0:
304
+ mesh_data = renderer.render_detections(result.detected)
305
+ obj = SceneObject(
306
+ name="Detections",
307
+ obj_type=ObjectType.DETECTIONS,
308
+ data=result.detected,
309
+ color=(1.0, 0.2, 0.2, 1.0), # Red for detections
310
+ vertices=mesh_data.get("vertices"),
311
+ indices=mesh_data.get("indices"),
312
+ wireframe=False,
313
+ )
314
+ self.objects["Detections"] = obj
315
+
316
+ # Add ray paths if available and requested
317
+ if show_rays and result.surface_hits:
318
+ mesh_data = renderer.render_ray_paths(result.surface_hits)
319
+ if mesh_data.get("vertices") is not None:
320
+ obj = SceneObject(
321
+ name="Ray Paths",
322
+ obj_type=ObjectType.RAY_PATHS,
323
+ data=result.surface_hits,
324
+ color=(0.5, 0.5, 1.0, 0.5), # Blue for rays
325
+ vertices=mesh_data.get("vertices"),
326
+ indices=mesh_data.get("indices"),
327
+ wireframe=True,
328
+ )
329
+ self.objects["Ray Paths"] = obj
330
+
331
+ self._notify_change()
332
+
333
+
334
+ def _get_surface_color(surface: "Surface") -> tuple[float, float, float, float]:
335
+ """Get a color for a surface based on its role."""
336
+ from lsurf.surfaces import SurfaceRole
337
+
338
+ if surface.role == SurfaceRole.DETECTOR:
339
+ return (0.2, 0.8, 0.2, 0.8) # Green
340
+ elif surface.role == SurfaceRole.ABSORBER:
341
+ return (0.3, 0.3, 0.3, 0.9) # Dark gray
342
+ else: # OPTICAL
343
+ return (0.4, 0.6, 0.9, 0.7) # Light blue
@@ -0,0 +1,264 @@
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
+ """Background simulation runner for the GUI.
35
+
36
+ Runs simulations in a separate thread to keep the GUI responsive.
37
+ """
38
+
39
+ import threading
40
+ from dataclasses import dataclass
41
+ from enum import Enum, auto
42
+ from typing import TYPE_CHECKING, Callable
43
+
44
+ if TYPE_CHECKING:
45
+ from lsurf.geometry import Geometry
46
+ from lsurf.simulation import SimulationConfig, SimulationResult
47
+ from lsurf.sources import RaySource
48
+
49
+
50
+ class SimulationState(Enum):
51
+ """State of the simulation runner."""
52
+
53
+ IDLE = auto()
54
+ RUNNING = auto()
55
+ COMPLETED = auto()
56
+ ERROR = auto()
57
+ CANCELLED = auto()
58
+
59
+
60
+ @dataclass
61
+ class SimulationProgress:
62
+ """Progress information for a running simulation."""
63
+
64
+ state: SimulationState
65
+ bounce: int = 0
66
+ max_bounces: int = 0
67
+ rays_active: int = 0
68
+ rays_detected: int = 0
69
+ message: str = ""
70
+ error: Exception | None = None
71
+
72
+
73
+ class SimulationRunner:
74
+ """Runs simulations in a background thread.
75
+
76
+ Usage:
77
+ runner = SimulationRunner()
78
+ runner.on_progress(update_progress_bar)
79
+ runner.on_complete(handle_results)
80
+ runner.start(geometry, source, config)
81
+ """
82
+
83
+ def __init__(self) -> None:
84
+ self._thread: threading.Thread | None = None
85
+ self._cancel_flag = threading.Event()
86
+ self._progress = SimulationProgress(state=SimulationState.IDLE)
87
+ self._result: "SimulationResult | None" = None
88
+
89
+ # Callbacks
90
+ self._on_progress_callbacks: list[Callable[[SimulationProgress], None]] = []
91
+ self._on_complete_callbacks: list[
92
+ Callable[["SimulationResult | None"], None]
93
+ ] = []
94
+
95
+ @property
96
+ def state(self) -> SimulationState:
97
+ """Current state of the simulation."""
98
+ return self._progress.state
99
+
100
+ @property
101
+ def progress(self) -> SimulationProgress:
102
+ """Current progress information."""
103
+ return self._progress
104
+
105
+ @property
106
+ def result(self) -> "SimulationResult | None":
107
+ """The simulation result (if completed)."""
108
+ return self._result
109
+
110
+ def on_progress(self, callback: Callable[[SimulationProgress], None]) -> None:
111
+ """Register a callback for progress updates."""
112
+ self._on_progress_callbacks.append(callback)
113
+
114
+ def on_complete(
115
+ self, callback: Callable[["SimulationResult | None"], None]
116
+ ) -> None:
117
+ """Register a callback for completion."""
118
+ self._on_complete_callbacks.append(callback)
119
+
120
+ def start(
121
+ self,
122
+ geometry: "Geometry",
123
+ source: "RaySource",
124
+ config: "SimulationConfig | None" = None,
125
+ ) -> bool:
126
+ """Start a simulation in the background.
127
+
128
+ Args:
129
+ geometry: The geometry to simulate
130
+ source: The ray source
131
+ config: Optional simulation configuration
132
+
133
+ Returns:
134
+ True if started successfully, False if already running
135
+ """
136
+ if self._thread is not None and self._thread.is_alive():
137
+ return False
138
+
139
+ self._cancel_flag.clear()
140
+ self._result = None
141
+ self._progress = SimulationProgress(
142
+ state=SimulationState.RUNNING, message="Initializing..."
143
+ )
144
+ self._notify_progress()
145
+
146
+ self._thread = threading.Thread(
147
+ target=self._run_simulation,
148
+ args=(geometry, source, config),
149
+ daemon=True,
150
+ )
151
+ self._thread.start()
152
+ return True
153
+
154
+ def cancel(self) -> None:
155
+ """Request cancellation of the running simulation."""
156
+ self._cancel_flag.set()
157
+
158
+ def is_running(self) -> bool:
159
+ """Check if a simulation is currently running."""
160
+ return self._thread is not None and self._thread.is_alive()
161
+
162
+ def _run_simulation(
163
+ self,
164
+ geometry: "Geometry",
165
+ source: "RaySource",
166
+ config: "SimulationConfig | None",
167
+ ) -> None:
168
+ """Run the simulation (called in background thread)."""
169
+ try:
170
+ from lsurf.simulation import Simulation, SimulationConfig
171
+
172
+ # Create simulation
173
+ sim_config = config or SimulationConfig()
174
+ simulation = Simulation(geometry, sim_config)
175
+
176
+ self._progress.max_bounces = sim_config.max_bounces
177
+ self._progress.message = "Generating rays..."
178
+ self._notify_progress()
179
+
180
+ # Generate rays
181
+ rays = source.generate()
182
+ self._progress.rays_active = rays.num_rays
183
+ self._progress.message = "Running simulation..."
184
+ self._notify_progress()
185
+
186
+ # Check for cancellation
187
+ if self._cancel_flag.is_set():
188
+ self._progress.state = SimulationState.CANCELLED
189
+ self._progress.message = "Cancelled"
190
+ self._notify_progress()
191
+ self._notify_complete()
192
+ return
193
+
194
+ # Run simulation
195
+ # Note: For more detailed progress, we could use run_single_bounce
196
+ # in a loop, but the full run is more efficient
197
+ result = simulation.run(rays)
198
+
199
+ # Check for cancellation
200
+ if self._cancel_flag.is_set():
201
+ self._progress.state = SimulationState.CANCELLED
202
+ self._progress.message = "Cancelled"
203
+ self._notify_progress()
204
+ self._notify_complete()
205
+ return
206
+
207
+ # Success
208
+ self._result = result
209
+ self._progress.state = SimulationState.COMPLETED
210
+ self._progress.bounce = result.bounces
211
+ self._progress.rays_detected = result.num_detected
212
+ self._progress.rays_active = result.num_remaining
213
+ self._progress.message = f"Complete: {result.num_detected} rays detected"
214
+ self._notify_progress()
215
+ self._notify_complete()
216
+
217
+ except Exception as e:
218
+ self._progress.state = SimulationState.ERROR
219
+ self._progress.error = e
220
+ self._progress.message = f"Error: {e}"
221
+ self._notify_progress()
222
+ self._notify_complete()
223
+
224
+ def _notify_progress(self) -> None:
225
+ """Notify progress callbacks."""
226
+ for callback in self._on_progress_callbacks:
227
+ try:
228
+ callback(self._progress)
229
+ except Exception:
230
+ pass # Don't let callback errors crash the simulation
231
+
232
+ def _notify_complete(self) -> None:
233
+ """Notify completion callbacks."""
234
+ for callback in self._on_complete_callbacks:
235
+ try:
236
+ callback(self._result)
237
+ except Exception:
238
+ pass
239
+
240
+
241
+ def run_simulation_sync(
242
+ geometry: "Geometry",
243
+ source: "RaySource",
244
+ config: "SimulationConfig | None" = None,
245
+ ) -> "SimulationResult":
246
+ """Run a simulation synchronously (blocking).
247
+
248
+ This is a convenience function for running simulations without
249
+ the background thread infrastructure.
250
+
251
+ Args:
252
+ geometry: The geometry to simulate
253
+ source: The ray source
254
+ config: Optional simulation configuration
255
+
256
+ Returns:
257
+ The simulation result
258
+ """
259
+ from lsurf.simulation import Simulation, SimulationConfig
260
+
261
+ sim_config = config or SimulationConfig()
262
+ simulation = Simulation(geometry, sim_config)
263
+ rays = source.generate()
264
+ return simulation.run(rays)
@@ -0,0 +1,40 @@
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
+ """Renderers for converting lsurf objects to 3D visuals."""
35
+
36
+ from .ray_renderer import RayRenderer
37
+ from .source_renderer import SourceRenderer
38
+ from .surface_renderer import SurfaceRenderer
39
+
40
+ __all__ = ["SurfaceRenderer", "SourceRenderer", "RayRenderer"]