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,477 @@
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
+ """Surface renderer - converts lsurf surfaces to 3D mesh data.
35
+
36
+ Generates vertices and indices for rendering surfaces in the 3D viewport.
37
+ """
38
+
39
+ from typing import TYPE_CHECKING, Any
40
+
41
+ import numpy as np
42
+
43
+ if TYPE_CHECKING:
44
+ from lsurf.surfaces import (
45
+ AnnularPlaneSurface,
46
+ BoundedPlaneSurface,
47
+ LocalRecordingSphereSurface,
48
+ PlaneSurface,
49
+ RecordingSphereSurface,
50
+ SphereSurface,
51
+ Surface,
52
+ )
53
+
54
+
55
+ class SurfaceRenderer:
56
+ """Renders lsurf surfaces to 3D mesh data for Dear PyGui.
57
+
58
+ Each surface type has a specialized rendering method that generates
59
+ appropriate vertices and indices for visualization.
60
+ """
61
+
62
+ def __init__(self, default_size: float = 10.0, resolution: int = 32) -> None:
63
+ """Initialize the renderer.
64
+
65
+ Args:
66
+ default_size: Default size for unbounded surfaces like planes
67
+ resolution: Number of segments for curved surfaces
68
+ """
69
+ self.default_size = default_size
70
+ self.resolution = resolution
71
+
72
+ def render(self, surface: "Surface") -> dict[str, Any]:
73
+ """Render a surface to mesh data.
74
+
75
+ Args:
76
+ surface: The surface to render
77
+
78
+ Returns:
79
+ Dict with 'vertices', 'indices', and 'wireframe' keys
80
+ """
81
+ # Import here to avoid circular imports
82
+ from lsurf.surfaces import (
83
+ AnnularPlaneSurface,
84
+ BoundedPlaneSurface,
85
+ CurvedWaveSurface,
86
+ GerstnerWaveSurface,
87
+ GPUCurvedWaveSurface,
88
+ GPUGerstnerWaveSurface,
89
+ GPUMultiCurvedWaveSurface,
90
+ LocalRecordingSphereSurface,
91
+ PlaneSurface,
92
+ RecordingSphereSurface,
93
+ SphereSurface,
94
+ )
95
+
96
+ # Dispatch to appropriate renderer based on surface type
97
+ if isinstance(surface, BoundedPlaneSurface):
98
+ return self._render_bounded_plane(surface)
99
+ elif isinstance(surface, AnnularPlaneSurface):
100
+ return self._render_annular_plane(surface)
101
+ elif isinstance(surface, PlaneSurface):
102
+ return self._render_plane(surface)
103
+ elif isinstance(surface, LocalRecordingSphereSurface):
104
+ return self._render_local_sphere(surface)
105
+ elif isinstance(surface, RecordingSphereSurface):
106
+ return self._render_recording_sphere(surface)
107
+ elif isinstance(surface, SphereSurface):
108
+ return self._render_sphere(surface)
109
+ elif isinstance(
110
+ surface, (GPUGerstnerWaveSurface, GPUCurvedWaveSurface, GerstnerWaveSurface)
111
+ ):
112
+ return self._render_wave(surface)
113
+ elif isinstance(surface, (GPUMultiCurvedWaveSurface, CurvedWaveSurface)):
114
+ return self._render_multi_wave(surface)
115
+ else:
116
+ # Fallback: render as a small marker
117
+ return self._render_marker(getattr(surface, "point", (0, 0, 0)))
118
+
119
+ def _render_plane(self, surface: "PlaneSurface") -> dict[str, Any]:
120
+ """Render an infinite plane as a finite wireframe rectangle."""
121
+ point = np.array(surface.point, dtype=np.float32)
122
+ normal = np.array(surface.normal, dtype=np.float32)
123
+ normal = normal / np.linalg.norm(normal)
124
+
125
+ # Create orthonormal basis
126
+ u, v = self._create_perpendicular_basis(normal)
127
+
128
+ # Create rectangle vertices
129
+ size = self.default_size
130
+ corners = [
131
+ point + size * u + size * v,
132
+ point - size * u + size * v,
133
+ point - size * u - size * v,
134
+ point + size * u - size * v,
135
+ ]
136
+ vertices = np.array(corners, dtype=np.float32)
137
+
138
+ # Wireframe indices (line segments)
139
+ indices = np.array([0, 1, 1, 2, 2, 3, 3, 0], dtype=np.uint32)
140
+
141
+ return {"vertices": vertices, "indices": indices, "wireframe": True}
142
+
143
+ def _render_bounded_plane(self, surface: "BoundedPlaneSurface") -> dict[str, Any]:
144
+ """Render a bounded rectangular plane as a solid quad."""
145
+ point = np.array(surface.point, dtype=np.float32)
146
+ normal = np.array(surface.normal, dtype=np.float32)
147
+ normal = normal / np.linalg.norm(normal)
148
+
149
+ width = surface.width
150
+ height = surface.height
151
+
152
+ # Create orthonormal basis
153
+ u, v = self._create_perpendicular_basis(normal)
154
+
155
+ # Create rectangle vertices
156
+ hw, hh = width / 2, height / 2
157
+ corners = [
158
+ point + hw * u + hh * v,
159
+ point - hw * u + hh * v,
160
+ point - hw * u - hh * v,
161
+ point + hw * u - hh * v,
162
+ ]
163
+ vertices = np.array(corners, dtype=np.float32)
164
+
165
+ # Triangle indices for solid rendering
166
+ indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32)
167
+
168
+ return {"vertices": vertices, "indices": indices, "wireframe": False}
169
+
170
+ def _render_annular_plane(self, surface: "AnnularPlaneSurface") -> dict[str, Any]:
171
+ """Render an annular (ring) plane."""
172
+ # AnnularPlaneSurface uses 'center' not 'point'
173
+ center = np.array(surface.center, dtype=np.float32)
174
+ normal = np.array(surface.normal, dtype=np.float32)
175
+ normal = normal / np.linalg.norm(normal)
176
+
177
+ inner_r = surface.inner_radius
178
+ outer_r = surface.outer_radius
179
+
180
+ # Create orthonormal basis
181
+ u, v = self._create_perpendicular_basis(normal)
182
+
183
+ # Generate ring vertices
184
+ n_segments = self.resolution
185
+ angles = np.linspace(0, 2 * np.pi, n_segments, endpoint=False)
186
+
187
+ vertices = []
188
+ for angle in angles:
189
+ c, s = np.cos(angle), np.sin(angle)
190
+ # Inner vertex
191
+ vertices.append(center + inner_r * (c * u + s * v))
192
+ # Outer vertex
193
+ vertices.append(center + outer_r * (c * u + s * v))
194
+
195
+ vertices = np.array(vertices, dtype=np.float32)
196
+
197
+ # Triangle indices for the ring
198
+ indices = []
199
+ for i in range(n_segments):
200
+ i0 = i * 2
201
+ i1 = i * 2 + 1
202
+ i2 = ((i + 1) % n_segments) * 2
203
+ i3 = ((i + 1) % n_segments) * 2 + 1
204
+ # Two triangles per segment
205
+ indices.extend([i0, i1, i3, i0, i3, i2])
206
+
207
+ indices = np.array(indices, dtype=np.uint32)
208
+
209
+ return {"vertices": vertices, "indices": indices, "wireframe": False}
210
+
211
+ def _render_sphere(self, surface: "SphereSurface") -> dict[str, Any]:
212
+ """Render a sphere as a wireframe."""
213
+ center = np.array(surface.center, dtype=np.float32)
214
+ radius = surface.radius
215
+
216
+ return self._generate_sphere_mesh(center, radius, wireframe=True)
217
+
218
+ def _render_local_sphere(
219
+ self, surface: "LocalRecordingSphereSurface"
220
+ ) -> dict[str, Any]:
221
+ """Render a local recording sphere."""
222
+ center = np.array(surface.center, dtype=np.float32)
223
+ radius = surface.radius
224
+
225
+ return self._generate_sphere_mesh(center, radius, wireframe=True)
226
+
227
+ def _render_recording_sphere(
228
+ self, surface: "RecordingSphereSurface"
229
+ ) -> dict[str, Any]:
230
+ """Render a recording sphere at altitude.
231
+
232
+ For large spheres (like Earth-scale), we only render a small
233
+ portion near the relevant area.
234
+ """
235
+ # For now, render as a horizontal plane at the altitude
236
+ # (full Earth sphere would be too large)
237
+ earth_center = np.array(surface.earth_center, dtype=np.float32)
238
+ radius = surface.earth_radius + surface.altitude
239
+
240
+ # Create a circular section at the top of the sphere
241
+ n_segments = self.resolution
242
+ angles = np.linspace(0, 2 * np.pi, n_segments, endpoint=False)
243
+
244
+ # Size of the section to render
245
+ section_radius = min(radius * 0.1, self.default_size * 5)
246
+
247
+ # Center at top of sphere
248
+ section_center = earth_center + np.array([0, 0, radius], dtype=np.float32)
249
+
250
+ vertices = [section_center]
251
+ for angle in angles:
252
+ x = section_radius * np.cos(angle)
253
+ y = section_radius * np.sin(angle)
254
+ vertices.append(section_center + np.array([x, y, 0], dtype=np.float32))
255
+
256
+ vertices = np.array(vertices, dtype=np.float32)
257
+
258
+ # Triangle fan indices
259
+ indices = []
260
+ for i in range(n_segments):
261
+ indices.extend([0, i + 1, ((i + 1) % n_segments) + 1])
262
+
263
+ indices = np.array(indices, dtype=np.uint32)
264
+
265
+ return {"vertices": vertices, "indices": indices, "wireframe": False}
266
+
267
+ def _render_wave(self, surface: Any) -> dict[str, Any]:
268
+ """Render a single Gerstner wave surface."""
269
+ # Get wave parameters
270
+ amplitude = getattr(surface, "amplitude", 1.0)
271
+ wavelength = getattr(surface, "wavelength", 10.0)
272
+ direction = np.array(getattr(surface, "direction", (1, 0)), dtype=np.float32)
273
+ direction = direction / np.linalg.norm(direction)
274
+ reference_z = getattr(surface, "reference_z", 0.0)
275
+ phase = getattr(surface, "phase", 0.0)
276
+ time = getattr(surface, "time", 0.0)
277
+
278
+ # Generate mesh grid
279
+ size = max(wavelength * 3, self.default_size)
280
+ n = self.resolution
281
+ x = np.linspace(-size, size, n)
282
+ y = np.linspace(-size, size, n)
283
+ X, Y = np.meshgrid(x, y)
284
+
285
+ # Compute wave height
286
+ k = 2 * np.pi / wavelength
287
+ omega = np.sqrt(9.81 * k) # Deep water dispersion
288
+ dot = direction[0] * X + direction[1] * Y
289
+ Z = reference_z + amplitude * np.cos(k * dot - omega * time + phase)
290
+
291
+ return self._grid_to_mesh(X, Y, Z)
292
+
293
+ def _render_multi_wave(self, surface: Any) -> dict[str, Any]:
294
+ """Render a multi-wave surface."""
295
+ wave_params = getattr(surface, "wave_params", [])
296
+ time = getattr(surface, "time", 0.0)
297
+
298
+ if not wave_params:
299
+ # Empty wave, render as flat plane
300
+ return self._render_flat_grid(0.0)
301
+
302
+ # Determine grid size from wavelengths
303
+ max_wavelength = max(wp.wavelength for wp in wave_params)
304
+ size = max(max_wavelength * 3, self.default_size)
305
+ n = self.resolution
306
+ x = np.linspace(-size, size, n)
307
+ y = np.linspace(-size, size, n)
308
+ X, Y = np.meshgrid(x, y)
309
+
310
+ # Sum all wave contributions
311
+ Z = np.zeros_like(X)
312
+ for wp in wave_params:
313
+ direction = np.array(wp.direction_normalized, dtype=np.float32)
314
+ k = wp.wave_number
315
+ omega = wp.angular_frequency
316
+ dot = direction[0] * X + direction[1] * Y
317
+ Z += wp.amplitude * np.cos(k * dot - omega * time + wp.phase)
318
+
319
+ return self._grid_to_mesh(X, Y, Z)
320
+
321
+ def _render_flat_grid(self, z: float) -> dict[str, Any]:
322
+ """Render a flat grid at height z."""
323
+ size = self.default_size
324
+ n = self.resolution
325
+ x = np.linspace(-size, size, n)
326
+ y = np.linspace(-size, size, n)
327
+ X, Y = np.meshgrid(x, y)
328
+ Z = np.full_like(X, z)
329
+
330
+ return self._grid_to_mesh(X, Y, Z)
331
+
332
+ def _render_marker(self, position: tuple) -> dict[str, Any]:
333
+ """Render a small marker at a position (fallback)."""
334
+ pos = np.array(position, dtype=np.float32)
335
+ size = 0.5
336
+
337
+ # Small cube
338
+ vertices = np.array(
339
+ [
340
+ pos + [-size, -size, -size],
341
+ pos + [size, -size, -size],
342
+ pos + [size, size, -size],
343
+ pos + [-size, size, -size],
344
+ pos + [-size, -size, size],
345
+ pos + [size, -size, size],
346
+ pos + [size, size, size],
347
+ pos + [-size, size, size],
348
+ ],
349
+ dtype=np.float32,
350
+ )
351
+
352
+ # Wireframe indices
353
+ indices = np.array(
354
+ [
355
+ 0,
356
+ 1,
357
+ 1,
358
+ 2,
359
+ 2,
360
+ 3,
361
+ 3,
362
+ 0, # Bottom
363
+ 4,
364
+ 5,
365
+ 5,
366
+ 6,
367
+ 6,
368
+ 7,
369
+ 7,
370
+ 4, # Top
371
+ 0,
372
+ 4,
373
+ 1,
374
+ 5,
375
+ 2,
376
+ 6,
377
+ 3,
378
+ 7, # Vertical
379
+ ],
380
+ dtype=np.uint32,
381
+ )
382
+
383
+ return {"vertices": vertices, "indices": indices, "wireframe": True}
384
+
385
+ def _generate_sphere_mesh(
386
+ self, center: np.ndarray, radius: float, wireframe: bool = True
387
+ ) -> dict[str, Any]:
388
+ """Generate a sphere mesh."""
389
+ n_lat = self.resolution // 2
390
+ n_lon = self.resolution
391
+
392
+ vertices = []
393
+ for i in range(n_lat + 1):
394
+ lat = np.pi * i / n_lat - np.pi / 2
395
+ for j in range(n_lon):
396
+ lon = 2 * np.pi * j / n_lon
397
+ x = radius * np.cos(lat) * np.cos(lon)
398
+ y = radius * np.cos(lat) * np.sin(lon)
399
+ z = radius * np.sin(lat)
400
+ vertices.append(center + np.array([x, y, z], dtype=np.float32))
401
+
402
+ vertices = np.array(vertices, dtype=np.float32)
403
+
404
+ if wireframe:
405
+ # Wireframe: latitude and longitude lines
406
+ indices = []
407
+ # Latitude lines
408
+ for i in range(n_lat + 1):
409
+ for j in range(n_lon):
410
+ idx1 = i * n_lon + j
411
+ idx2 = i * n_lon + (j + 1) % n_lon
412
+ indices.extend([idx1, idx2])
413
+ # Longitude lines
414
+ for j in range(n_lon):
415
+ for i in range(n_lat):
416
+ idx1 = i * n_lon + j
417
+ idx2 = (i + 1) * n_lon + j
418
+ indices.extend([idx1, idx2])
419
+
420
+ indices = np.array(indices, dtype=np.uint32)
421
+ else:
422
+ # Solid: triangles
423
+ indices = []
424
+ for i in range(n_lat):
425
+ for j in range(n_lon):
426
+ i00 = i * n_lon + j
427
+ i10 = (i + 1) * n_lon + j
428
+ i01 = i * n_lon + (j + 1) % n_lon
429
+ i11 = (i + 1) * n_lon + (j + 1) % n_lon
430
+ indices.extend([i00, i10, i11, i00, i11, i01])
431
+
432
+ indices = np.array(indices, dtype=np.uint32)
433
+
434
+ return {"vertices": vertices, "indices": indices, "wireframe": wireframe}
435
+
436
+ def _grid_to_mesh(
437
+ self, X: np.ndarray, Y: np.ndarray, Z: np.ndarray
438
+ ) -> dict[str, Any]:
439
+ """Convert a grid to a triangle mesh."""
440
+ n_rows, n_cols = X.shape
441
+
442
+ # Create vertices
443
+ vertices = np.stack([X.ravel(), Y.ravel(), Z.ravel()], axis=1).astype(
444
+ np.float32
445
+ )
446
+
447
+ # Create triangle indices
448
+ indices = []
449
+ for i in range(n_rows - 1):
450
+ for j in range(n_cols - 1):
451
+ i00 = i * n_cols + j
452
+ i10 = (i + 1) * n_cols + j
453
+ i01 = i * n_cols + (j + 1)
454
+ i11 = (i + 1) * n_cols + (j + 1)
455
+ # Two triangles per cell
456
+ indices.extend([i00, i10, i11, i00, i11, i01])
457
+
458
+ indices = np.array(indices, dtype=np.uint32)
459
+
460
+ return {"vertices": vertices, "indices": indices, "wireframe": False}
461
+
462
+ def _create_perpendicular_basis(
463
+ self, normal: np.ndarray
464
+ ) -> tuple[np.ndarray, np.ndarray]:
465
+ """Create two perpendicular unit vectors to the given normal."""
466
+ # Find a vector not parallel to normal
467
+ if abs(normal[0]) < 0.9:
468
+ ref = np.array([1, 0, 0], dtype=np.float32)
469
+ else:
470
+ ref = np.array([0, 1, 0], dtype=np.float32)
471
+
472
+ u = np.cross(normal, ref)
473
+ u = u / np.linalg.norm(u)
474
+ v = np.cross(normal, u)
475
+ v = v / np.linalg.norm(v)
476
+
477
+ return u, v
@@ -0,0 +1,48 @@
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
+ """GUI view components."""
35
+
36
+ from .config_editor import ConfigEditorPanel
37
+ from .results import ResultsPanel
38
+ from .scene_tree import SceneTreePanel
39
+ from .viewport_3d import Viewport3D
40
+ from .visualizations import VisualizationPanel
41
+
42
+ __all__ = [
43
+ "Viewport3D",
44
+ "SceneTreePanel",
45
+ "ResultsPanel",
46
+ "ConfigEditorPanel",
47
+ "VisualizationPanel",
48
+ ]