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,311 @@
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
+ Sphere Surface (GPU-Capable)
36
+
37
+ Implements a spherical surface with GPU acceleration support.
38
+ Can be used as DETECTOR, OPTICAL, or ABSORBER.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from dataclasses import dataclass, field
44
+ from typing import TYPE_CHECKING, Any
45
+
46
+ import numpy as np
47
+ import numpy.typing as npt
48
+
49
+ from ..protocol import Surface, SurfaceRole
50
+ from ..registry import register_surface_type
51
+
52
+ if TYPE_CHECKING:
53
+ from ...propagation.kernels.registry import IntersectionKernelID
54
+
55
+
56
+ @dataclass
57
+ class SphereSurface(Surface):
58
+ """
59
+ Sphere surface with GPU acceleration.
60
+
61
+ The sphere is defined by center and radius.
62
+ Signed distance from point p: |p - center| - radius
63
+ (positive outside, negative inside)
64
+
65
+ Parameters
66
+ ----------
67
+ center : tuple of float
68
+ Center point (cx, cy, cz).
69
+ radius : float
70
+ Sphere radius (positive for convex, negative for concave).
71
+ role : SurfaceRole
72
+ What happens when a ray hits (DETECTOR, OPTICAL, or ABSORBER).
73
+ name : str
74
+ Human-readable name.
75
+ material_front : MaterialField, optional
76
+ Material outside the sphere. Required for OPTICAL.
77
+ material_back : MaterialField, optional
78
+ Material inside the sphere. Required for OPTICAL.
79
+
80
+ Examples
81
+ --------
82
+ >>> # Earth's surface as ocean
83
+ >>> EARTH_RADIUS = 6.371e6
84
+ >>> ocean = SphereSurface(
85
+ ... center=(0, 0, -EARTH_RADIUS),
86
+ ... radius=EARTH_RADIUS,
87
+ ... role=SurfaceRole.OPTICAL,
88
+ ... material_front=atmosphere,
89
+ ... material_back=seawater,
90
+ ... name="ocean_surface",
91
+ ... )
92
+ >>>
93
+ >>> # Spherical detector
94
+ >>> detector = SphereSurface(
95
+ ... center=(0, 0, 0),
96
+ ... radius=1000.0,
97
+ ... role=SurfaceRole.DETECTOR,
98
+ ... name="spherical_detector",
99
+ ... )
100
+ """
101
+
102
+ center: tuple[float, float, float]
103
+ radius: float
104
+ role: SurfaceRole
105
+ name: str = "sphere"
106
+ material_front: Any = None
107
+ material_back: Any = None
108
+
109
+ # GPU capability
110
+ _gpu_capable: bool = field(default=True, init=False, repr=False)
111
+ _geometry_id: int = field(default=2, init=False, repr=False) # sphere = 2
112
+
113
+ # Kernel ID for this instance (set in __post_init__)
114
+ _kernel_id: IntersectionKernelID | None = field(
115
+ default=None, init=False, repr=False
116
+ )
117
+
118
+ @classmethod
119
+ def _get_supported_kernels(cls) -> list[IntersectionKernelID]:
120
+ """Get supported intersection kernels (lazy initialization)."""
121
+ from ...propagation.kernels.registry import IntersectionKernelID
122
+
123
+ return [IntersectionKernelID.SPHERE_ANALYTICAL]
124
+
125
+ @classmethod
126
+ def _get_default_kernel(cls) -> IntersectionKernelID:
127
+ """Get default intersection kernel."""
128
+ from ...propagation.kernels.registry import IntersectionKernelID
129
+
130
+ return IntersectionKernelID.SPHERE_ANALYTICAL
131
+
132
+ @classmethod
133
+ def supported_kernels(cls) -> list[IntersectionKernelID]:
134
+ """Return list of intersection kernels supported by this surface type."""
135
+ return cls._get_supported_kernels()
136
+
137
+ @classmethod
138
+ def default_kernel(cls) -> IntersectionKernelID:
139
+ """Return the default intersection kernel for this surface type."""
140
+ return cls._get_default_kernel()
141
+
142
+ def __post_init__(self) -> None:
143
+ if self.radius == 0:
144
+ raise ValueError("Radius cannot be zero")
145
+
146
+ # Set default kernel
147
+ self._kernel_id = self._get_default_kernel()
148
+
149
+ @property
150
+ def gpu_capable(self) -> bool:
151
+ """This surface supports GPU acceleration."""
152
+ return True
153
+
154
+ @property
155
+ def geometry_id(self) -> int:
156
+ """GPU geometry type ID (sphere = 2)."""
157
+ return 2
158
+
159
+ def get_gpu_parameters(self) -> tuple:
160
+ """
161
+ Return parameters for GPU kernel.
162
+
163
+ Returns
164
+ -------
165
+ tuple
166
+ (center_x, center_y, center_z, radius)
167
+ """
168
+ return (
169
+ self.center[0],
170
+ self.center[1],
171
+ self.center[2],
172
+ self.radius,
173
+ )
174
+
175
+ def get_materials(self) -> tuple | None:
176
+ """
177
+ Return (material_front, material_back) for Fresnel calculation.
178
+
179
+ Returns
180
+ -------
181
+ tuple or None
182
+ (material_front, material_back) or None if not OPTICAL
183
+ """
184
+ if self.role == SurfaceRole.OPTICAL:
185
+ return (self.material_front, self.material_back)
186
+ return None
187
+
188
+ def signed_distance(
189
+ self,
190
+ positions: npt.NDArray[np.float32],
191
+ ) -> npt.NDArray[np.float32]:
192
+ """
193
+ Compute signed distance from positions to sphere surface.
194
+
195
+ Parameters
196
+ ----------
197
+ positions : ndarray, shape (N, 3)
198
+ Points to compute distance for
199
+
200
+ Returns
201
+ -------
202
+ ndarray, shape (N,)
203
+ Signed distance (positive outside, negative inside)
204
+ """
205
+ center = np.array(self.center, dtype=np.float32)
206
+ diff = positions - center
207
+ dist = np.linalg.norm(diff, axis=1)
208
+ return (dist - abs(self.radius)).astype(np.float32)
209
+
210
+ def intersect(
211
+ self,
212
+ origins: npt.NDArray[np.float32],
213
+ directions: npt.NDArray[np.float32],
214
+ min_distance: float = 1e-6,
215
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.bool_]]:
216
+ """
217
+ Compute ray-sphere intersection.
218
+
219
+ Parameters
220
+ ----------
221
+ origins : ndarray, shape (N, 3)
222
+ Ray origins
223
+ directions : ndarray, shape (N, 3)
224
+ Ray directions (normalized)
225
+ min_distance : float
226
+ Minimum valid intersection distance
227
+
228
+ Returns
229
+ -------
230
+ distances : ndarray, shape (N,)
231
+ Distance to intersection (inf if no hit)
232
+ hit_mask : ndarray, shape (N,)
233
+ Boolean mask of valid intersections
234
+ """
235
+ center = np.array(self.center, dtype=np.float32)
236
+ r = abs(self.radius)
237
+
238
+ # Ray-sphere intersection:
239
+ # |origin + t*direction - center|^2 = r^2
240
+ oc = origins - center
241
+
242
+ a = np.sum(directions * directions, axis=1)
243
+ b = 2.0 * np.sum(oc * directions, axis=1)
244
+ c = np.sum(oc * oc, axis=1) - r * r
245
+
246
+ discriminant = b * b - 4 * a * c
247
+
248
+ # No intersection if discriminant < 0
249
+ no_hit = discriminant < 0
250
+
251
+ # Compute both roots
252
+ sqrt_disc = np.sqrt(np.maximum(discriminant, 0))
253
+ t1 = (-b - sqrt_disc) / (2 * a)
254
+ t2 = (-b + sqrt_disc) / (2 * a)
255
+
256
+ # Choose closest positive intersection >= min_distance
257
+ t1_valid = t1 >= min_distance
258
+ t2_valid = t2 >= min_distance
259
+
260
+ # Prefer t1 (closer) if valid, else t2
261
+ t = np.where(t1_valid, t1, np.where(t2_valid, t2, np.inf))
262
+
263
+ hit_mask = (~no_hit) & (t1_valid | t2_valid)
264
+ distances = np.where(hit_mask, t, np.inf)
265
+
266
+ return distances.astype(np.float32), hit_mask
267
+
268
+ def normal_at(
269
+ self,
270
+ positions: npt.NDArray[np.float32],
271
+ incoming_directions: npt.NDArray[np.float32] | None = None,
272
+ ) -> npt.NDArray[np.float32]:
273
+ """
274
+ Compute surface normal at positions.
275
+
276
+ For a sphere, normal points radially outward from center.
277
+
278
+ Parameters
279
+ ----------
280
+ positions : ndarray, shape (N, 3)
281
+ Points on the surface
282
+ incoming_directions : ndarray, shape (N, 3), optional
283
+ Ray directions (used to flip normal if needed)
284
+
285
+ Returns
286
+ -------
287
+ ndarray, shape (N, 3)
288
+ Normal vectors at each position
289
+ """
290
+ center = np.array(self.center, dtype=np.float32)
291
+ diff = positions - center
292
+
293
+ # Normalize
294
+ norms = np.linalg.norm(diff, axis=1, keepdims=True)
295
+ normals = diff / np.maximum(norms, 1e-12)
296
+
297
+ # For negative radius (concave), flip normals
298
+ if self.radius < 0:
299
+ normals = -normals
300
+
301
+ # Optionally flip normals to face incoming rays
302
+ if incoming_directions is not None:
303
+ dot = np.sum(normals * incoming_directions, axis=1)
304
+ flip_mask = dot > 0
305
+ normals[flip_mask] = -normals[flip_mask]
306
+
307
+ return normals.astype(np.float32)
308
+
309
+
310
+ # Register class with registry
311
+ register_surface_type("sphere", "gpu", 2, SphereSurface)
@@ -0,0 +1,336 @@
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
+ Unified Surface Protocol
36
+
37
+ Defines the base class and enums for all surfaces in the ray tracing framework.
38
+ All surfaces follow this protocol regardless of GPU capability.
39
+
40
+ GPU-capable surfaces additionally provide signed_distance() and get_gpu_parameters()
41
+ for accelerated computation. CPU fallback (intersect, normal_at) is always available.
42
+
43
+ See registry.py for the geometry type registry.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from abc import ABC, abstractmethod
49
+ from enum import IntEnum
50
+ from typing import TYPE_CHECKING, Any
51
+
52
+ import numpy as np
53
+ from numpy.typing import NDArray
54
+
55
+ if TYPE_CHECKING:
56
+ from ..materials import MaterialField
57
+ from ..propagation.kernels.registry import IntersectionKernelID
58
+
59
+
60
+ class SurfaceRole(IntEnum):
61
+ """What happens when a ray hits this surface."""
62
+
63
+ DETECTOR = 1 # Record ray data and deactivate
64
+ OPTICAL = 2 # Reflect/refract based on Fresnel equations
65
+ ABSORBER = 3 # Apply absorption coefficient, possibly deactivate
66
+
67
+
68
+ class Surface(ABC):
69
+ """
70
+ Abstract base class for all surfaces.
71
+
72
+ All surfaces provide CPU-based ray intersection and normal computation.
73
+ GPU-capable surfaces additionally provide signed_distance() and
74
+ get_gpu_parameters() for accelerated GPU computation.
75
+
76
+ Parameters
77
+ ----------
78
+ role : SurfaceRole
79
+ What happens when a ray hits (DETECTOR, OPTICAL, ABSORBER).
80
+ name : str
81
+ Human-readable identifier for the surface.
82
+ material_front : MaterialField, optional
83
+ Material on the front side (direction of normal). Required for OPTICAL.
84
+ material_back : MaterialField, optional
85
+ Material on the back side. Required for OPTICAL.
86
+
87
+ Attributes
88
+ ----------
89
+ role : SurfaceRole
90
+ Surface role.
91
+ name : str
92
+ Surface identifier.
93
+ material_front : MaterialField or None
94
+ Front-side material.
95
+ material_back : MaterialField or None
96
+ Back-side material.
97
+ gpu_capable : bool
98
+ Whether this surface supports GPU acceleration.
99
+ geometry_id : int
100
+ GPU geometry type ID (0 for CPU-only surfaces).
101
+
102
+ Examples
103
+ --------
104
+ >>> # Check if surface can use GPU
105
+ >>> if surface.gpu_capable:
106
+ ... params = surface.get_gpu_parameters()
107
+ ... sd = surface.signed_distance(positions)
108
+ >>> else:
109
+ ... distances, hits = surface.intersect(origins, directions)
110
+ """
111
+
112
+ # Subclasses should override these class attributes
113
+ _gpu_capable: bool = False
114
+ _geometry_id: int = 0 # 0 = CPU-only, no GPU geometry
115
+
116
+ # Intersection kernel compatibility declarations
117
+ _supported_kernels: list[IntersectionKernelID] = []
118
+ _default_kernel: IntersectionKernelID | None = None
119
+
120
+ def __init__(
121
+ self,
122
+ role: SurfaceRole,
123
+ name: str,
124
+ material_front: "MaterialField | None" = None,
125
+ material_back: "MaterialField | None" = None,
126
+ kernel: "IntersectionKernelID | None" = None,
127
+ ):
128
+ self.role = role
129
+ self.name = name
130
+ self.material_front = material_front
131
+ self.material_back = material_back
132
+
133
+ # Validate kernel selection
134
+ if kernel is None:
135
+ self._kernel_id = self._default_kernel
136
+ else:
137
+ if self._supported_kernels and kernel not in self._supported_kernels:
138
+ raise ValueError(
139
+ f"{self.__class__.__name__} does not support kernel {kernel}. "
140
+ f"Supported: {self._supported_kernels}"
141
+ )
142
+ self._kernel_id = kernel
143
+
144
+ @property
145
+ def gpu_capable(self) -> bool:
146
+ """Whether this surface supports GPU acceleration."""
147
+ return self._gpu_capable
148
+
149
+ @property
150
+ def geometry_id(self) -> int:
151
+ """
152
+ GPU geometry type ID.
153
+
154
+ Returns 0 for CPU-only surfaces. GPU-capable surfaces return
155
+ a positive integer registered in the geometry registry.
156
+ """
157
+ return self._geometry_id
158
+
159
+ # Backwards compatibility alias
160
+ @property
161
+ def geometry(self) -> int:
162
+ """Deprecated: use geometry_id instead."""
163
+ return self._geometry_id
164
+
165
+ # =========================================================================
166
+ # COMPATIBILITY QUERY METHODS
167
+ # =========================================================================
168
+
169
+ @classmethod
170
+ def supported_kernels(cls) -> list[IntersectionKernelID]:
171
+ """
172
+ Return list of intersection kernels supported by this surface type.
173
+
174
+ Returns
175
+ -------
176
+ list[IntersectionKernelID]
177
+ List of kernel IDs this surface supports.
178
+
179
+ Examples
180
+ --------
181
+ >>> from lsurf.surfaces import PlaneSurface
182
+ >>> PlaneSurface.supported_kernels()
183
+ [<IntersectionKernelID.PLANE_ANALYTICAL: ...>]
184
+ """
185
+
186
+ return list(cls._supported_kernels)
187
+
188
+ @classmethod
189
+ def default_kernel(cls) -> IntersectionKernelID | None:
190
+ """
191
+ Return the default intersection kernel for this surface type.
192
+
193
+ Returns
194
+ -------
195
+ IntersectionKernelID or None
196
+ The default kernel ID, or None if no GPU kernels are supported.
197
+ """
198
+ return cls._default_kernel
199
+
200
+ @property
201
+ def kernel_id(self) -> IntersectionKernelID | None:
202
+ """
203
+ Return the kernel ID configured for this surface instance.
204
+
205
+ Returns
206
+ -------
207
+ IntersectionKernelID or None
208
+ The kernel ID selected for this instance.
209
+ """
210
+ return self._kernel_id
211
+
212
+ @abstractmethod
213
+ def intersect(
214
+ self,
215
+ origins: NDArray[np.float32],
216
+ directions: NDArray[np.float32],
217
+ min_distance: float = 1e-6,
218
+ ) -> tuple[NDArray[np.float32], NDArray[np.bool_]]:
219
+ """
220
+ Compute ray-surface intersections (CPU).
221
+
222
+ Parameters
223
+ ----------
224
+ origins : ndarray, shape (N, 3)
225
+ Ray origin positions.
226
+ directions : ndarray, shape (N, 3)
227
+ Ray direction unit vectors.
228
+ min_distance : float, optional
229
+ Minimum valid intersection distance. Default is 1e-6.
230
+
231
+ Returns
232
+ -------
233
+ distances : ndarray, shape (N,)
234
+ Distance to intersection (inf if no hit).
235
+ hit_mask : ndarray, shape (N,), dtype=bool
236
+ True for rays that hit the surface.
237
+ """
238
+ ...
239
+
240
+ @abstractmethod
241
+ def normal_at(
242
+ self,
243
+ positions: NDArray[np.float32],
244
+ incoming_directions: NDArray[np.float32] | None = None,
245
+ ) -> NDArray[np.float32]:
246
+ """
247
+ Compute surface normal at given positions.
248
+
249
+ Parameters
250
+ ----------
251
+ positions : ndarray, shape (N, 3)
252
+ Points on the surface.
253
+ incoming_directions : ndarray, shape (N, 3), optional
254
+ Incoming ray directions (for orienting normals).
255
+
256
+ Returns
257
+ -------
258
+ normals : ndarray, shape (N, 3)
259
+ Unit normal vectors.
260
+ """
261
+ ...
262
+
263
+ def get_materials(self) -> tuple[Any, Any] | None:
264
+ """
265
+ Return (material_front, material_back) for Fresnel calculation.
266
+
267
+ Returns
268
+ -------
269
+ tuple or None
270
+ (material_front, material_back) for OPTICAL surfaces, None otherwise.
271
+ """
272
+ if self.role == SurfaceRole.OPTICAL:
273
+ return (self.material_front, self.material_back)
274
+ return None
275
+
276
+ # --- GPU-specific methods (override in GPU-capable surfaces) ---
277
+
278
+ def signed_distance(
279
+ self,
280
+ positions: NDArray[np.float32],
281
+ ) -> NDArray[np.float32]:
282
+ """
283
+ Compute signed distance from positions to surface.
284
+
285
+ Only available for GPU-capable surfaces. Raises NotImplementedError
286
+ for CPU-only surfaces.
287
+
288
+ Parameters
289
+ ----------
290
+ positions : ndarray, shape (N, 3)
291
+ Points to compute distance for.
292
+
293
+ Returns
294
+ -------
295
+ ndarray, shape (N,)
296
+ Signed distance (positive on front side, negative on back side).
297
+
298
+ Raises
299
+ ------
300
+ NotImplementedError
301
+ If surface is not GPU-capable.
302
+ """
303
+ raise NotImplementedError(
304
+ f"{self.__class__.__name__} does not support GPU acceleration. "
305
+ "Use intersect() for CPU-based ray tracing."
306
+ )
307
+
308
+ def get_gpu_parameters(self) -> tuple:
309
+ """
310
+ Return parameters for GPU kernel.
311
+
312
+ Only available for GPU-capable surfaces. Raises NotImplementedError
313
+ for CPU-only surfaces.
314
+
315
+ Returns
316
+ -------
317
+ tuple
318
+ Geometry-dependent parameters for GPU signed distance calculation.
319
+
320
+ Raises
321
+ ------
322
+ NotImplementedError
323
+ If surface is not GPU-capable.
324
+ """
325
+ raise NotImplementedError(
326
+ f"{self.__class__.__name__} does not support GPU acceleration."
327
+ )
328
+
329
+ def __repr__(self) -> str:
330
+ """Return string representation."""
331
+ gpu_str = "GPU" if self.gpu_capable else "CPU"
332
+ return f"{self.__class__.__name__}(name='{self.name}', role={self.role.name}, {gpu_str})"
333
+
334
+
335
+ # For backwards compatibility, also export as GPUSurface
336
+ GPUSurface = Surface