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,200 @@
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
+ Propagator Factory
36
+
37
+ Factory function for creating propagators with proper kernel/propagator
38
+ selection based on material compatibility declarations.
39
+
40
+ This module provides a unified interface for creating propagators,
41
+ handling the selection of the appropriate propagator and kernel based on:
42
+ 1. Material's declared compatibility
43
+ 2. User overrides at propagator creation time
44
+ 3. Material instance preferences
45
+ 4. Class defaults
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from typing import TYPE_CHECKING, Any
51
+
52
+ from ..kernels.registry import PropagationKernelID, PropagatorID
53
+
54
+ if TYPE_CHECKING:
55
+ from ...materials.base.material_field import MaterialField
56
+
57
+ __all__ = ["create_propagator"]
58
+
59
+
60
+ def create_propagator(
61
+ material: MaterialField,
62
+ propagator_id: PropagatorID | None = None,
63
+ kernel_id: PropagationKernelID | None = None,
64
+ method: str | None = None,
65
+ prefer_gpu: bool = True,
66
+ **kwargs: Any,
67
+ ) -> Any:
68
+ """
69
+ Create the appropriate propagator for a material.
70
+
71
+ Selection priority (highest to lowest):
72
+ 1. If propagator_id/kernel_id passed here → use those (override)
73
+ 2. Else use material's stored preference (material._propagator_id, material._kernel_id)
74
+ 3. Else use material's class defaults
75
+
76
+ Parameters
77
+ ----------
78
+ material : MaterialField
79
+ The material to propagate through.
80
+ propagator_id : PropagatorID, optional
81
+ Override propagator selection. If None, uses material's preference.
82
+ kernel_id : PropagationKernelID, optional
83
+ Override kernel selection. If None, uses material's preference.
84
+ Note: Only used for GPU propagators.
85
+ method : str, optional
86
+ Integration method ("euler" or "rk4"). If None, determined from kernel_id.
87
+ prefer_gpu : bool, default True
88
+ If True and material supports GPU, use GPU propagator.
89
+ If False, force CPU propagator.
90
+ **kwargs
91
+ Additional propagator configuration (e.g., threads_per_block).
92
+
93
+ Returns
94
+ -------
95
+ Propagator
96
+ Configured propagator instance appropriate for the material.
97
+
98
+ Raises
99
+ ------
100
+ ValueError
101
+ If propagator_id or kernel_id is not supported by the material.
102
+
103
+ Examples
104
+ --------
105
+ >>> from lsurf.materials import ExponentialAtmosphere
106
+ >>> from lsurf.propagation.propagators import create_propagator
107
+ >>> from lsurf.propagation.kernels import PropagatorID, PropagationKernelID
108
+
109
+ >>> # Use defaults (RK4 kernel, GPU propagator)
110
+ >>> atmo = ExponentialAtmosphere(n_sea_level=1.000293)
111
+ >>> propagator = create_propagator(atmo)
112
+
113
+ >>> # Override kernel at propagator creation time
114
+ >>> propagator_euler = create_propagator(
115
+ ... atmo,
116
+ ... kernel_id=PropagationKernelID.SIMPLE_EULER
117
+ ... )
118
+
119
+ >>> # Force CPU propagator
120
+ >>> cpu_propagator = create_propagator(atmo, propagator_id=PropagatorID.CPU_GRADIENT)
121
+
122
+ >>> # Or use prefer_gpu=False
123
+ >>> cpu_propagator = create_propagator(atmo, prefer_gpu=False)
124
+ """
125
+ # Resolve propagator: override > prefer_gpu > material instance > class default
126
+ resolved_propagator_id = propagator_id
127
+ if resolved_propagator_id is None:
128
+ # If prefer_gpu is False, force CPU propagator
129
+ if not prefer_gpu:
130
+ resolved_propagator_id = PropagatorID.CPU_GRADIENT
131
+ else:
132
+ resolved_propagator_id = getattr(material, "_propagator_id", None)
133
+ if resolved_propagator_id is None:
134
+ resolved_propagator_id = material.default_propagator()
135
+
136
+ # Validate propagator is supported
137
+ supported_propagators = material.supported_propagators()
138
+ if supported_propagators and resolved_propagator_id not in supported_propagators:
139
+ raise ValueError(
140
+ f"{material.__class__.__name__} does not support {resolved_propagator_id}. "
141
+ f"Supported: {supported_propagators}"
142
+ )
143
+
144
+ # Resolve kernel: override > material instance > class default
145
+ resolved_kernel_id = kernel_id
146
+ if resolved_kernel_id is None:
147
+ resolved_kernel_id = getattr(material, "_kernel_id", None)
148
+ if resolved_kernel_id is None:
149
+ resolved_kernel_id = material.default_kernel()
150
+
151
+ # Validate kernel is supported (if applicable)
152
+ supported_kernels = material.supported_kernels()
153
+ if supported_kernels and resolved_kernel_id is not None:
154
+ if resolved_kernel_id not in supported_kernels:
155
+ raise ValueError(
156
+ f"{material.__class__.__name__} does not support {resolved_kernel_id}. "
157
+ f"Supported: {supported_kernels}"
158
+ )
159
+
160
+ # Resolve method: parameter > kernel_id > default
161
+ resolved_method = method
162
+ if resolved_method is None:
163
+ resolved_method = "rk4" # Default
164
+ if resolved_kernel_id is not None:
165
+ # Map kernel ID to method string
166
+ if "EULER" in resolved_kernel_id.name:
167
+ resolved_method = "euler"
168
+ elif "RK4" in resolved_kernel_id.name:
169
+ resolved_method = "rk4"
170
+
171
+ # Create the appropriate propagator
172
+ if resolved_propagator_id == PropagatorID.CPU_GRADIENT:
173
+ from .gradient import GradientPropagator
174
+
175
+ return GradientPropagator(method=resolved_method, **kwargs)
176
+
177
+ elif resolved_propagator_id == PropagatorID.GPU_GRADIENT:
178
+ from .gpu_gradient import GPUGradientPropagator
179
+
180
+ return GPUGradientPropagator(
181
+ material=material, method=resolved_method, **kwargs
182
+ )
183
+
184
+ elif resolved_propagator_id == PropagatorID.GPU_SPECTRAL:
185
+ from .spectral_gpu_gradient import SpectralGPUGradientPropagator
186
+
187
+ return SpectralGPUGradientPropagator(
188
+ material=material, method=resolved_method, **kwargs
189
+ )
190
+
191
+ elif resolved_propagator_id == PropagatorID.GPU_SURFACE:
192
+ from .surface_propagator import SurfacePropagator
193
+
194
+ return SurfacePropagator(material=material, method=resolved_method, **kwargs)
195
+
196
+ else:
197
+ raise ValueError(
198
+ f"Unknown propagator ID: {resolved_propagator_id}. "
199
+ f"Available: {list(PropagatorID)}"
200
+ )
@@ -0,0 +1,305 @@
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
+ Fresnel Handler
36
+
37
+ GPU memory management for Fresnel coefficient kernels.
38
+ This module provides high-level interfaces that handle cuda.to_device(),
39
+ copy_to_host(), and kernel launching for the Fresnel kernels.
40
+ """
41
+
42
+ import numpy as np
43
+ from numpy.typing import NDArray
44
+
45
+ from ..kernels.fresnel import (
46
+ kernel_fresnel_standard,
47
+ kernel_fresnel_polarized,
48
+ )
49
+
50
+ # GPU support is optional
51
+ try:
52
+ from numba import cuda
53
+
54
+ _HAS_CUDA = cuda.is_available()
55
+ except ImportError:
56
+ _HAS_CUDA = False
57
+
58
+ class _FakeCuda:
59
+ @staticmethod
60
+ def is_available():
61
+ return False
62
+
63
+ cuda = _FakeCuda() # type: ignore[assignment]
64
+
65
+
66
+ def fresnel_coefficients_gpu(
67
+ cos_theta_i: NDArray[np.float32],
68
+ n1: NDArray[np.float32] | float,
69
+ n2: NDArray[np.float32] | float,
70
+ polarization: str | NDArray[np.int32] = "unpolarized",
71
+ threads_per_block: int = 256,
72
+ ) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
73
+ """
74
+ GPU-accelerated Fresnel coefficient calculation.
75
+
76
+ Parameters
77
+ ----------
78
+ cos_theta_i : ndarray, shape (N,)
79
+ Cosine of incident angles
80
+ n1 : float or ndarray
81
+ Refractive index of incident medium
82
+ n2 : float or ndarray
83
+ Refractive index of transmitted medium
84
+ polarization : str or ndarray
85
+ Polarization state: 's', 'p', 'unpolarized', or per-ray int array
86
+ where 0=unpolarized, 1=s, 2=p
87
+ threads_per_block : int, optional
88
+ CUDA threads per block (default 256)
89
+
90
+ Returns
91
+ -------
92
+ reflectance : ndarray, shape (N,)
93
+ Reflection coefficient (0-1)
94
+ transmittance : ndarray, shape (N,)
95
+ Transmission coefficient (0-1)
96
+ """
97
+ num_rays = len(cos_theta_i)
98
+
99
+ # Handle scalar n1/n2
100
+ n1_is_scalar = not isinstance(n1, np.ndarray) or n1.ndim == 0
101
+ n2_is_scalar = not isinstance(n2, np.ndarray) or n2.ndim == 0
102
+
103
+ n1_arr = np.atleast_1d(np.float32(n1) if n1_is_scalar else n1.astype(np.float32))
104
+ n2_arr = np.atleast_1d(np.float32(n2) if n2_is_scalar else n2.astype(np.float32))
105
+
106
+ # Handle polarization
107
+ if isinstance(polarization, str):
108
+ if polarization == "s":
109
+ pol_arr = np.ones(num_rays, dtype=np.int32)
110
+ elif polarization == "p":
111
+ pol_arr = np.full(num_rays, 2, dtype=np.int32)
112
+ else: # unpolarized
113
+ pol_arr = np.zeros(num_rays, dtype=np.int32)
114
+ use_polarized_kernel = polarization in ("s", "p")
115
+ else:
116
+ pol_arr = polarization.astype(np.int32)
117
+ use_polarized_kernel = True
118
+
119
+ if _HAS_CUDA:
120
+ # Allocate output arrays
121
+ reflectance = np.zeros(num_rays, dtype=np.float32)
122
+ transmittance = np.zeros(num_rays, dtype=np.float32)
123
+
124
+ # Transfer to GPU
125
+ d_cos_theta = cuda.to_device(cos_theta_i.astype(np.float32))
126
+ d_n1 = cuda.to_device(n1_arr)
127
+ d_n2 = cuda.to_device(n2_arr)
128
+ d_reflectance = cuda.to_device(reflectance)
129
+ d_transmittance = cuda.to_device(transmittance)
130
+
131
+ blocks = (num_rays + threads_per_block - 1) // threads_per_block
132
+
133
+ if use_polarized_kernel:
134
+ d_pol = cuda.to_device(pol_arr)
135
+ kernel_fresnel_polarized[blocks, threads_per_block](
136
+ d_cos_theta,
137
+ d_n1,
138
+ d_n2,
139
+ d_pol,
140
+ d_reflectance,
141
+ d_transmittance,
142
+ n1_is_scalar,
143
+ n2_is_scalar,
144
+ )
145
+ else:
146
+ kernel_fresnel_standard[blocks, threads_per_block](
147
+ d_cos_theta,
148
+ d_n1,
149
+ d_n2,
150
+ d_reflectance,
151
+ d_transmittance,
152
+ n1_is_scalar,
153
+ n2_is_scalar,
154
+ )
155
+
156
+ cuda.synchronize()
157
+ reflectance = d_reflectance.copy_to_host()
158
+ transmittance = d_transmittance.copy_to_host()
159
+ else:
160
+ reflectance, transmittance = _fresnel_coefficients_cpu(
161
+ cos_theta_i,
162
+ n1,
163
+ n2,
164
+ polarization if isinstance(polarization, str) else "unpolarized",
165
+ )
166
+
167
+ return reflectance, transmittance
168
+
169
+
170
+ def compute_reflection_direction(
171
+ incident: NDArray[np.float32],
172
+ normal: NDArray[np.float32],
173
+ ) -> NDArray[np.float32]:
174
+ """
175
+ Compute reflected ray direction using law of reflection.
176
+
177
+ Parameters
178
+ ----------
179
+ incident : ndarray, shape (N, 3)
180
+ Incident ray directions (should be normalized)
181
+ normal : ndarray, shape (N, 3)
182
+ Surface normals (should be normalized, pointing toward incident side)
183
+
184
+ Returns
185
+ -------
186
+ reflected : ndarray, shape (N, 3)
187
+ Reflected ray directions (normalized)
188
+ """
189
+ dot_in = np.sum(incident * normal, axis=1, keepdims=True)
190
+ reflected = incident - 2.0 * dot_in * normal
191
+ norms = np.linalg.norm(reflected, axis=1, keepdims=True)
192
+ return (reflected / (norms + 1e-10)).astype(np.float32)
193
+
194
+
195
+ def compute_refraction_direction(
196
+ incident: NDArray[np.float32],
197
+ normal: NDArray[np.float32],
198
+ n1: NDArray[np.float32] | float,
199
+ n2: NDArray[np.float32] | float,
200
+ ) -> tuple[NDArray[np.float32], NDArray[np.bool_]]:
201
+ """
202
+ Compute refracted ray direction using Snell's law.
203
+
204
+ Parameters
205
+ ----------
206
+ incident : ndarray, shape (N, 3)
207
+ Incident ray directions (should be normalized)
208
+ normal : ndarray, shape (N, 3)
209
+ Surface normals (should be normalized)
210
+ n1 : float or ndarray
211
+ Refractive index of incident medium
212
+ n2 : float or ndarray
213
+ Refractive index of transmitted medium
214
+
215
+ Returns
216
+ -------
217
+ refracted : ndarray, shape (N, 3)
218
+ Refracted ray directions (normalized, zero for TIR rays)
219
+ tir_mask : ndarray, shape (N,)
220
+ Boolean mask indicating total internal reflection
221
+ """
222
+ n1 = np.atleast_1d(n1).astype(np.float32)
223
+ n2 = np.atleast_1d(n2).astype(np.float32)
224
+
225
+ cos_theta_i = -np.sum(incident * normal, axis=1)
226
+ n_ratio = n1 / n2
227
+
228
+ sin_theta_t_sq = (n_ratio**2) * (1.0 - cos_theta_i**2)
229
+ tir_mask = sin_theta_t_sq > 1.0
230
+
231
+ cos_theta_t = np.sqrt(np.clip(1.0 - sin_theta_t_sq, 0, 1))
232
+
233
+ refracted = (
234
+ n_ratio[:, np.newaxis] * incident
235
+ + (n_ratio * cos_theta_i - cos_theta_t)[:, np.newaxis] * normal
236
+ )
237
+
238
+ refracted[tir_mask] = 0.0
239
+
240
+ norms = np.linalg.norm(refracted, axis=1, keepdims=True)
241
+ refracted = np.where(norms > 1e-10, refracted / norms, 0.0)
242
+
243
+ return refracted.astype(np.float32), tir_mask
244
+
245
+
246
+ # =============================================================================
247
+ # CPU Fallback Implementation
248
+ # =============================================================================
249
+
250
+
251
+ def _fresnel_coefficients_cpu(
252
+ cos_theta_i: NDArray[np.float32],
253
+ n1: NDArray[np.float32] | float,
254
+ n2: NDArray[np.float32] | float,
255
+ polarization: str = "unpolarized",
256
+ ) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
257
+ """CPU implementation of Fresnel coefficient calculation."""
258
+ # Ensure arrays
259
+ cos_theta_i = np.atleast_1d(cos_theta_i).astype(np.float32)
260
+ n1 = np.atleast_1d(n1).astype(np.float32)
261
+ n2 = np.atleast_1d(n2).astype(np.float32)
262
+
263
+ n_ratio = n1 / n2
264
+ sin_theta_i_sq = 1.0 - cos_theta_i**2
265
+ sin_theta_t_sq = (n_ratio**2) * sin_theta_i_sq
266
+
267
+ # Check for TIR
268
+ tir_mask = sin_theta_t_sq > 1.0
269
+ cos_theta_t = np.sqrt(np.clip(1.0 - sin_theta_t_sq, 0, 1))
270
+
271
+ # Fresnel amplitude coefficients
272
+ r_s_num = n1 * cos_theta_i - n2 * cos_theta_t
273
+ r_s_den = n1 * cos_theta_i + n2 * cos_theta_t
274
+ r_s = r_s_num / (r_s_den + 1e-10)
275
+
276
+ r_p_num = n2 * cos_theta_i - n1 * cos_theta_t
277
+ r_p_den = n2 * cos_theta_i + n1 * cos_theta_t
278
+ r_p = r_p_num / (r_p_den + 1e-10)
279
+
280
+ # Intensity coefficients
281
+ R_s = r_s**2
282
+ R_p = r_p**2
283
+
284
+ # Handle TIR
285
+ R_s = np.where(tir_mask, 1.0, R_s)
286
+ R_p = np.where(tir_mask, 1.0, R_p)
287
+
288
+ # Select based on polarization
289
+ if polarization == "s":
290
+ R = R_s
291
+ elif polarization == "p":
292
+ R = R_p
293
+ else:
294
+ R = 0.5 * (R_s + R_p)
295
+
296
+ T = 1.0 - R
297
+
298
+ return R.astype(np.float32), T.astype(np.float32)
299
+
300
+
301
+ __all__ = [
302
+ "fresnel_coefficients_gpu",
303
+ "compute_reflection_direction",
304
+ "compute_refraction_direction",
305
+ ]