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,381 @@
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
+ """
35
+ Gerstner Wave Surface (CPU-Only)
36
+
37
+ Flat-earth Gerstner wave ocean surface. Uses ray marching for intersection
38
+ since the surface shape is too complex for closed-form GPU signed distance.
39
+
40
+ For curved Earth surfaces with waves, see CurvedWaveSurface.
41
+ """
42
+
43
+ from dataclasses import dataclass, field
44
+ from typing import Any
45
+
46
+ import numpy as np
47
+ from numpy.typing import NDArray
48
+
49
+ from ..protocol import Surface, SurfaceRole
50
+ from ..registry import register_surface_type
51
+ from .wave_params import GerstnerWaveParams
52
+
53
+
54
+ @dataclass
55
+ class GerstnerWaveSurface(Surface):
56
+ """
57
+ Flat ocean surface with Gerstner wave physics (CPU-only).
58
+
59
+ Implements the Gerstner (trochoidal) wave model where water particles
60
+ move in circular orbits, producing realistic ocean wave shapes with
61
+ sharp crests and flat troughs.
62
+
63
+ Parameters
64
+ ----------
65
+ wave_params : list of GerstnerWaveParams
66
+ List of wave components to superimpose.
67
+ role : SurfaceRole
68
+ What happens when a ray hits (typically OPTICAL).
69
+ name : str
70
+ Human-readable name.
71
+ time : float, optional
72
+ Time for wave animation in seconds. Default is 0.0.
73
+ reference_z : float, optional
74
+ Mean sea level z-coordinate in meters. Default is 0.0.
75
+ material_front : MaterialField, optional
76
+ Material above surface (air).
77
+ material_back : MaterialField, optional
78
+ Material below surface (water).
79
+ max_distance : float, optional
80
+ Maximum ray marching distance in meters. Default is 10000.0.
81
+
82
+ Examples
83
+ --------
84
+ >>> from lsurf.surfaces import GerstnerWaveSurface, GerstnerWaveParams, SurfaceRole
85
+ >>> from lsurf.materials import AIR_STP, WATER
86
+ >>>
87
+ >>> wave = GerstnerWaveParams(amplitude=1.0, wavelength=50.0)
88
+ >>> surface = GerstnerWaveSurface(
89
+ ... wave_params=[wave],
90
+ ... role=SurfaceRole.OPTICAL,
91
+ ... name="ocean",
92
+ ... material_front=AIR_STP,
93
+ ... material_back=WATER,
94
+ ... )
95
+ """
96
+
97
+ wave_params: list[GerstnerWaveParams]
98
+ role: SurfaceRole
99
+ name: str = "gerstner_wave"
100
+ time: float = 0.0
101
+ reference_z: float = 0.0
102
+ material_front: Any = None
103
+ material_back: Any = None
104
+ max_distance: float = 10000.0
105
+
106
+ # CPU-only surface
107
+ _gpu_capable: bool = field(default=False, init=False, repr=False)
108
+ _geometry_id: int = field(
109
+ default=0, init=False, repr=False
110
+ ) # CPU-only, no GPU geometry
111
+
112
+ # Precomputed wave arrays (set in __post_init__)
113
+ _amplitudes: NDArray = field(default=None, init=False, repr=False)
114
+ _wave_numbers: NDArray = field(default=None, init=False, repr=False)
115
+ _frequencies: NDArray = field(default=None, init=False, repr=False)
116
+ _directions: NDArray = field(default=None, init=False, repr=False)
117
+ _phases: NDArray = field(default=None, init=False, repr=False)
118
+ _steepness: NDArray = field(default=None, init=False, repr=False)
119
+
120
+ def __post_init__(self) -> None:
121
+ self._precompute_wave_params()
122
+
123
+ def _precompute_wave_params(self) -> None:
124
+ """Precompute wave parameters as arrays for fast evaluation."""
125
+ n_waves = len(self.wave_params)
126
+ if n_waves == 0:
127
+ self._amplitudes = np.array([], dtype=np.float64)
128
+ self._wave_numbers = np.array([], dtype=np.float64)
129
+ self._frequencies = np.array([], dtype=np.float64)
130
+ self._directions = np.zeros((0, 2), dtype=np.float64)
131
+ self._phases = np.array([], dtype=np.float64)
132
+ self._steepness = np.array([], dtype=np.float64)
133
+ return
134
+
135
+ self._amplitudes = np.array(
136
+ [w.amplitude for w in self.wave_params], dtype=np.float64
137
+ )
138
+ self._wave_numbers = np.array(
139
+ [w.wave_number for w in self.wave_params], dtype=np.float64
140
+ )
141
+ self._frequencies = np.array(
142
+ [w.angular_frequency for w in self.wave_params], dtype=np.float64
143
+ )
144
+ self._directions = np.array(
145
+ [w.direction_normalized for w in self.wave_params], dtype=np.float64
146
+ )
147
+ self._phases = np.array([w.phase for w in self.wave_params], dtype=np.float64)
148
+ self._steepness = np.array(
149
+ [w.steepness for w in self.wave_params], dtype=np.float64
150
+ )
151
+
152
+ @property
153
+ def gpu_capable(self) -> bool:
154
+ """This surface does NOT support GPU acceleration."""
155
+ return False
156
+
157
+ @property
158
+ def geometry_id(self) -> int:
159
+ """GPU geometry type ID (0 = CPU-only)."""
160
+ return 0
161
+
162
+ def get_materials(self) -> tuple | None:
163
+ """Return materials for Fresnel calculation."""
164
+ if self.role == SurfaceRole.OPTICAL:
165
+ return (self.material_front, self.material_back)
166
+ return None
167
+
168
+ def get_max_wave_height(self) -> float:
169
+ """Get maximum possible wave height above reference_z."""
170
+ if len(self._amplitudes) == 0:
171
+ return 0.0
172
+ return float(np.sum(self._amplitudes))
173
+
174
+ def _compute_displacement(
175
+ self,
176
+ x: NDArray[np.float64],
177
+ y: NDArray[np.float64],
178
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
179
+ """Compute Gerstner wave displacement at positions (x, y)."""
180
+ x = np.atleast_1d(x)
181
+ y = np.atleast_1d(y)
182
+
183
+ n_points = len(x)
184
+ dx = np.zeros(n_points, dtype=np.float64)
185
+ dy = np.zeros(n_points, dtype=np.float64)
186
+ dz = np.zeros(n_points, dtype=np.float64)
187
+
188
+ for i in range(len(self.wave_params)):
189
+ A = self._amplitudes[i]
190
+ k = self._wave_numbers[i]
191
+ omega = self._frequencies[i]
192
+ dir_x, dir_y = self._directions[i]
193
+ phi = self._phases[i]
194
+ Q = self._steepness[i]
195
+
196
+ phase = k * (dir_x * x + dir_y * y) - omega * self.time + phi
197
+
198
+ cos_phase = np.cos(phase)
199
+ sin_phase = np.sin(phase)
200
+
201
+ dx -= Q * A * dir_x * sin_phase
202
+ dy -= Q * A * dir_y * sin_phase
203
+ dz += A * cos_phase
204
+
205
+ return dx, dy, dz
206
+
207
+ def _compute_normal(
208
+ self,
209
+ x: NDArray[np.float64],
210
+ y: NDArray[np.float64],
211
+ ) -> NDArray[np.float64]:
212
+ """Compute surface normal at positions (x, y)."""
213
+ n_points = len(x)
214
+
215
+ nx = np.zeros(n_points, dtype=np.float64)
216
+ ny = np.zeros(n_points, dtype=np.float64)
217
+ nz = np.ones(n_points, dtype=np.float64)
218
+
219
+ for i in range(len(self.wave_params)):
220
+ A = self._amplitudes[i]
221
+ k = self._wave_numbers[i]
222
+ omega = self._frequencies[i]
223
+ dir_x, dir_y = self._directions[i]
224
+ phi = self._phases[i]
225
+ Q = self._steepness[i]
226
+
227
+ phase = k * (dir_x * x + dir_y * y) - omega * self.time + phi
228
+
229
+ cos_phase = np.cos(phase)
230
+ sin_phase = np.sin(phase)
231
+
232
+ WA = k * A
233
+
234
+ nx += dir_x * WA * sin_phase
235
+ ny += dir_y * WA * sin_phase
236
+ nz -= Q * WA * cos_phase
237
+
238
+ norms = np.sqrt(nx**2 + ny**2 + nz**2)
239
+ norms = np.maximum(norms, 1e-10)
240
+
241
+ normals = np.stack([nx / norms, ny / norms, nz / norms], axis=-1)
242
+ return normals.astype(np.float32)
243
+
244
+ def _surface_z(
245
+ self,
246
+ x: NDArray[np.float64],
247
+ y: NDArray[np.float64],
248
+ ) -> NDArray[np.float64]:
249
+ """Compute surface height z at positions (x, y)."""
250
+ is_scalar = np.isscalar(x) and np.isscalar(y)
251
+ _, _, dz = self._compute_displacement(x, y)
252
+ result = self.reference_z + dz
253
+ if is_scalar:
254
+ return float(result[0])
255
+ return result
256
+
257
+ def intersect(
258
+ self,
259
+ origins: NDArray[np.float32],
260
+ directions: NDArray[np.float32],
261
+ min_distance: float = 1e-6,
262
+ ) -> tuple[NDArray[np.float32], NDArray[np.bool_]]:
263
+ """
264
+ Find ray-surface intersections using ray marching.
265
+
266
+ Parameters
267
+ ----------
268
+ origins : ndarray, shape (N, 3)
269
+ Ray origin positions.
270
+ directions : ndarray, shape (N, 3)
271
+ Ray direction unit vectors.
272
+ min_distance : float
273
+ Minimum valid intersection distance.
274
+
275
+ Returns
276
+ -------
277
+ distances : ndarray, shape (N,)
278
+ Distance to intersection (inf if no hit).
279
+ hit_mask : ndarray, shape (N,), dtype=bool
280
+ True for rays that hit the surface.
281
+ """
282
+ origins = origins.astype(np.float64)
283
+ directions = directions.astype(np.float64)
284
+ n_rays = len(origins)
285
+
286
+ distances = np.full(n_rays, np.inf, dtype=np.float64)
287
+ hit_mask = np.zeros(n_rays, dtype=bool)
288
+
289
+ max_wave_height = self.get_max_wave_height()
290
+ z_max = self.reference_z + max_wave_height
291
+ z_min = self.reference_z - max_wave_height
292
+
293
+ tolerance = 1e-4
294
+ max_iterations = 100
295
+
296
+ for i in range(n_rays):
297
+ ox, oy, oz = origins[i]
298
+ dx, dy, dz = directions[i]
299
+
300
+ # Skip rays parallel to surface
301
+ if abs(dz) < 1e-10:
302
+ if oz < z_min or oz > z_max:
303
+ continue
304
+
305
+ # Initial guess: intersection with mean plane
306
+ if abs(dz) > 1e-10:
307
+ t = (self.reference_z - oz) / dz
308
+ else:
309
+ t = 0.0
310
+
311
+ t = max(t - max_wave_height / max(abs(dz), 0.1), min_distance)
312
+
313
+ for _ in range(max_iterations):
314
+ px = ox + t * dx
315
+ py = oy + t * dy
316
+ pz = oz + t * dz
317
+
318
+ z_surf = self._surface_z(
319
+ np.array([px], dtype=np.float64), np.array([py], dtype=np.float64)
320
+ )[0]
321
+
322
+ signed_dist = pz - z_surf
323
+
324
+ if abs(signed_dist) < tolerance:
325
+ if t > min_distance:
326
+ distances[i] = t
327
+ hit_mask[i] = True
328
+ break
329
+
330
+ if abs(dz) > 0.01:
331
+ step = signed_dist / abs(dz) * 0.8
332
+ else:
333
+ step = signed_dist * 0.5
334
+
335
+ step = np.clip(step, -self.max_distance * 0.1, self.max_distance * 0.1)
336
+ t += step
337
+
338
+ if t < 0 or t > self.max_distance:
339
+ break
340
+
341
+ return distances.astype(np.float32), hit_mask
342
+
343
+ def normal_at(
344
+ self,
345
+ positions: NDArray[np.float32],
346
+ incoming_directions: NDArray[np.float32] | None = None,
347
+ ) -> NDArray[np.float32]:
348
+ """
349
+ Compute surface normal at given positions.
350
+
351
+ Parameters
352
+ ----------
353
+ positions : ndarray, shape (N, 3)
354
+ Points on the surface.
355
+ incoming_directions : ndarray, shape (N, 3), optional
356
+ Incoming ray directions.
357
+
358
+ Returns
359
+ -------
360
+ normals : ndarray, shape (N, 3)
361
+ Unit normal vectors.
362
+ """
363
+ x = positions[:, 0].astype(np.float64)
364
+ y = positions[:, 1].astype(np.float64)
365
+
366
+ normals = self._compute_normal(x, y)
367
+
368
+ if incoming_directions is not None:
369
+ dot_products = np.sum(incoming_directions * normals, axis=1)
370
+ flip_mask = dot_products > 0
371
+ normals[flip_mask] *= -1
372
+
373
+ return normals
374
+
375
+ def set_time(self, time: float) -> None:
376
+ """Update the wave animation time."""
377
+ self.time = time
378
+
379
+
380
+ # Register class with registry
381
+ register_surface_type("gerstner_wave", "cpu", cls=GerstnerWaveSurface)
@@ -0,0 +1,118 @@
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
+ """
35
+ Gerstner Wave Parameters
36
+
37
+ Dataclass for configuring individual Gerstner wave components.
38
+ Used by both GerstnerWaveSurface and CurvedWaveSurface.
39
+ """
40
+
41
+ from dataclasses import dataclass
42
+
43
+ import numpy as np
44
+
45
+
46
+ @dataclass
47
+ class GerstnerWaveParams:
48
+ """
49
+ Parameters for a single Gerstner wave component.
50
+
51
+ Gerstner waves describe the motion of water particles in circular
52
+ orbits, producing realistic wave shapes with sharp crests and
53
+ flat troughs.
54
+
55
+ Parameters
56
+ ----------
57
+ amplitude : float
58
+ Wave amplitude (vertical displacement) in meters.
59
+ wavelength : float
60
+ Wave wavelength (crest-to-crest distance) in meters.
61
+ direction : tuple of float, optional
62
+ Wave propagation direction as (dx, dy), will be normalized.
63
+ Default is (1.0, 0.0) for propagation in +x direction.
64
+ phase : float, optional
65
+ Initial phase offset in radians. Default is 0.0.
66
+ steepness : float, optional
67
+ Wave steepness Q (0 to 1). Controls horizontal displacement.
68
+ Q=0 gives pure vertical sinusoidal motion.
69
+ Q=1 gives maximum sharpening (breaking wave limit).
70
+ Default is 0.5.
71
+
72
+ Attributes
73
+ ----------
74
+ wave_number : float
75
+ Wave number k = 2π/λ in radians per meter.
76
+ angular_frequency : float
77
+ Angular frequency from deep water dispersion: ω = √(gk).
78
+ direction_normalized : tuple of float
79
+ Normalized direction vector.
80
+
81
+ Examples
82
+ --------
83
+ >>> # Simple wave propagating in +x direction
84
+ >>> wave = GerstnerWaveParams(amplitude=1.0, wavelength=50.0)
85
+
86
+ >>> # Steep wave at 45 degrees
87
+ >>> wave = GerstnerWaveParams(
88
+ ... amplitude=2.0,
89
+ ... wavelength=30.0,
90
+ ... direction=(1.0, 1.0),
91
+ ... steepness=0.8
92
+ ... )
93
+ """
94
+
95
+ amplitude: float
96
+ wavelength: float
97
+ direction: tuple[float, float] = (1.0, 0.0)
98
+ phase: float = 0.0
99
+ steepness: float = 0.5
100
+
101
+ @property
102
+ def wave_number(self) -> float:
103
+ """Wave number k = 2π/λ."""
104
+ return 2.0 * np.pi / self.wavelength
105
+
106
+ @property
107
+ def angular_frequency(self) -> float:
108
+ """Angular frequency from deep water dispersion: ω = √(gk)."""
109
+ g = 9.81 # gravitational acceleration m/s²
110
+ return np.sqrt(g * self.wave_number)
111
+
112
+ @property
113
+ def direction_normalized(self) -> tuple[float, float]:
114
+ """Normalized direction vector."""
115
+ norm = np.sqrt(self.direction[0] ** 2 + self.direction[1] ** 2)
116
+ if norm < 1e-10:
117
+ return (1.0, 0.0)
118
+ return (self.direction[0] / norm, self.direction[1] / norm)
@@ -0,0 +1,72 @@
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
+ """
35
+ GPU-Capable Surfaces
36
+
37
+ Surfaces that support GPU-accelerated signed distance computation.
38
+ These surfaces provide both CPU fallback (intersect, normal_at) and
39
+ GPU methods (signed_distance, get_gpu_parameters).
40
+
41
+ Surface types:
42
+ - PlaneSurface (geometry_id=1): Infinite flat plane
43
+ - SphereSurface (geometry_id=2): Spherical surface
44
+ - RecordingSphereSurface (geometry_id=2): Earth-altitude recording sphere
45
+ - LocalRecordingSphereSurface (geometry_id=2): Local recording sphere
46
+ - GPUGerstnerWaveSurface (geometry_id=3): Flat-earth Gerstner wave (single wave)
47
+ - GPUCurvedWaveSurface (geometry_id=4): Curved-earth wave (single wave)
48
+ - GPUMultiCurvedWaveSurface (geometry_id=5): Curved-earth wave (multi-wave, up to 8)
49
+ - BoundedPlaneSurface (geometry_id=6): Bounded rectangular plane
50
+ - AnnularPlaneSurface (geometry_id=7): Annular (ring-shaped) plane
51
+ """
52
+
53
+ from .plane import PlaneSurface
54
+ from .sphere import SphereSurface
55
+ from .recording_sphere import RecordingSphereSurface, LocalRecordingSphereSurface
56
+ from .gerstner_wave import GPUGerstnerWaveSurface
57
+ from .curved_wave import GPUCurvedWaveSurface
58
+ from .multi_curved_wave import GPUMultiCurvedWaveSurface
59
+ from .bounded_plane import BoundedPlaneSurface
60
+ from .annular_plane import AnnularPlaneSurface
61
+
62
+ __all__ = [
63
+ "PlaneSurface",
64
+ "SphereSurface",
65
+ "RecordingSphereSurface",
66
+ "LocalRecordingSphereSurface",
67
+ "GPUGerstnerWaveSurface",
68
+ "GPUCurvedWaveSurface",
69
+ "GPUMultiCurvedWaveSurface",
70
+ "BoundedPlaneSurface",
71
+ "AnnularPlaneSurface",
72
+ ]