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,319 @@
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
+ Grid Inhomogeneous Model (3D Grid Data)
36
+
37
+ Base class for 3D inhomogeneous materials defined on a regular grid.
38
+ User provides a 3D array of refractive index values.
39
+ GPU support is automatic via trilinear interpolation.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from typing import TYPE_CHECKING
45
+
46
+ import numpy as np
47
+ from numpy.typing import NDArray
48
+
49
+ from .material_field import MaterialField
50
+
51
+ if TYPE_CHECKING:
52
+ from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
53
+
54
+
55
+ class GridInhomogeneousModel(MaterialField):
56
+ """
57
+ Base class for 3D inhomogeneous materials defined on a grid.
58
+
59
+ User provides a 3D array of refractive index values. The system provides:
60
+ - Trilinear interpolation for arbitrary positions
61
+ - Automatic gradient computation via finite differences
62
+ - GPU support via 3D array interpolation
63
+
64
+ Parameters
65
+ ----------
66
+ name : str
67
+ Name of the material model
68
+ n_grid : ndarray (nx, ny, nz)
69
+ Refractive index values on 3D grid
70
+ bounds : tuple
71
+ ((x_min, x_max), (y_min, y_max), (z_min, z_max))
72
+ gradient_grid : ndarray (nx, ny, nz, 3), optional
73
+ Pre-computed gradients. If None, computed via finite differences.
74
+ alpha_grid : ndarray (nx, ny, nz), optional
75
+ Absorption coefficient values on 3D grid. If None, defaults to zeros.
76
+
77
+ Example
78
+ -------
79
+ >>> n_grid = np.ones((100, 100, 50)) * 1.000293
80
+ >>> n_grid += generate_turbulence(100, 100, 50) # Add perturbations
81
+ >>> material = GridInhomogeneousModel(
82
+ ... name="Turbulent Field",
83
+ ... n_grid=n_grid,
84
+ ... bounds=((-5000, 5000), (-5000, 5000), (0, 25000))
85
+ ... )
86
+ """
87
+
88
+ # =========================================================================
89
+ # COMPATIBILITY DECLARATIONS
90
+ # =========================================================================
91
+ @classmethod
92
+ def _init_compatibility(cls):
93
+ """Initialize compatibility declarations (called once on first use)."""
94
+ from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
95
+
96
+ if not cls._supported_kernels:
97
+ cls._supported_kernels = [
98
+ PropagationKernelID.GRID_EULER,
99
+ PropagationKernelID.GRID_RK4,
100
+ ]
101
+ cls._default_kernel = PropagationKernelID.GRID_RK4
102
+ cls._supported_propagators = [
103
+ PropagatorID.GPU_GRADIENT,
104
+ PropagatorID.CPU_GRADIENT,
105
+ ]
106
+ cls._default_propagator = PropagatorID.GPU_GRADIENT
107
+
108
+ def __init__(
109
+ self,
110
+ name: str = "Grid Inhomogeneous",
111
+ n_grid: NDArray[np.float32] | None = None,
112
+ bounds: tuple[tuple[float, float], ...] | None = None,
113
+ gradient_grid: NDArray[np.float32] | None = None,
114
+ alpha_grid: NDArray[np.float32] | None = None,
115
+ kernel: PropagationKernelID | None = None,
116
+ propagator: PropagatorID | None = None,
117
+ ):
118
+ # Initialize compatibility declarations before calling super().__init__
119
+ self._init_compatibility()
120
+
121
+ super().__init__(name, kernel=kernel, propagator=propagator)
122
+ self._is_homogeneous = False
123
+
124
+ if n_grid is None:
125
+ raise ValueError("n_grid is required")
126
+ if bounds is None:
127
+ raise ValueError("bounds is required")
128
+ if len(bounds) != 3:
129
+ raise ValueError("bounds must have 3 elements: ((x_min, x_max), ...)")
130
+
131
+ self._n_grid = n_grid.astype(np.float32)
132
+ self._bounds = bounds
133
+ self._shape = n_grid.shape
134
+
135
+ # Compute grid spacing
136
+ (x_min, x_max), (y_min, y_max), (z_min, z_max) = bounds
137
+ nx, ny, nz = self._shape
138
+ self._dx = (x_max - x_min) / (nx - 1) if nx > 1 else 1.0
139
+ self._dy = (y_max - y_min) / (ny - 1) if ny > 1 else 1.0
140
+ self._dz = (z_max - z_min) / (nz - 1) if nz > 1 else 1.0
141
+ self._origin = (x_min, y_min, z_min)
142
+
143
+ # Compute or store gradient grid
144
+ if gradient_grid is not None:
145
+ self._grad_grid = gradient_grid.astype(np.float32)
146
+ else:
147
+ self._grad_grid = self._compute_gradient_grid()
148
+
149
+ # Store or create zero absorption grid
150
+ if alpha_grid is not None:
151
+ self._alpha_grid = alpha_grid.astype(np.float32)
152
+ else:
153
+ self._alpha_grid = np.zeros(self._shape, dtype=np.float32)
154
+
155
+ def _compute_gradient_grid(self) -> NDArray[np.float32]:
156
+ """Compute gradient via finite differences."""
157
+ grad_x = np.gradient(self._n_grid, self._dx, axis=0)
158
+ grad_y = np.gradient(self._n_grid, self._dy, axis=1)
159
+ grad_z = np.gradient(self._n_grid, self._dz, axis=2)
160
+
161
+ return np.stack([grad_x, grad_y, grad_z], axis=-1).astype(np.float32)
162
+
163
+ def _trilinear_interpolate(
164
+ self,
165
+ x: float | NDArray,
166
+ y: float | NDArray,
167
+ z: float | NDArray,
168
+ grid: NDArray,
169
+ ) -> float | NDArray:
170
+ """Trilinear interpolation in 3D grid."""
171
+ x = np.asarray(x)
172
+ y = np.asarray(y)
173
+ z = np.asarray(z)
174
+
175
+ # Normalize to grid coordinates
176
+ x_norm = (x - self._origin[0]) / self._dx
177
+ y_norm = (y - self._origin[1]) / self._dy
178
+ z_norm = (z - self._origin[2]) / self._dz
179
+
180
+ # Clamp to valid range
181
+ x_norm = np.clip(x_norm, 0, self._shape[0] - 1.001)
182
+ y_norm = np.clip(y_norm, 0, self._shape[1] - 1.001)
183
+ z_norm = np.clip(z_norm, 0, self._shape[2] - 1.001)
184
+
185
+ # Get integer indices and fractions
186
+ x0 = np.floor(x_norm).astype(int)
187
+ y0 = np.floor(y_norm).astype(int)
188
+ z0 = np.floor(z_norm).astype(int)
189
+
190
+ x1 = np.minimum(x0 + 1, self._shape[0] - 1)
191
+ y1 = np.minimum(y0 + 1, self._shape[1] - 1)
192
+ z1 = np.minimum(z0 + 1, self._shape[2] - 1)
193
+
194
+ xd = x_norm - x0
195
+ yd = y_norm - y0
196
+ zd = z_norm - z0
197
+
198
+ # Trilinear interpolation
199
+ if grid.ndim == 3:
200
+ # Scalar field
201
+ c000 = grid[x0, y0, z0]
202
+ c001 = grid[x0, y0, z1]
203
+ c010 = grid[x0, y1, z0]
204
+ c011 = grid[x0, y1, z1]
205
+ c100 = grid[x1, y0, z0]
206
+ c101 = grid[x1, y0, z1]
207
+ c110 = grid[x1, y1, z0]
208
+ c111 = grid[x1, y1, z1]
209
+
210
+ c00 = c000 * (1 - xd) + c100 * xd
211
+ c01 = c001 * (1 - xd) + c101 * xd
212
+ c10 = c010 * (1 - xd) + c110 * xd
213
+ c11 = c011 * (1 - xd) + c111 * xd
214
+
215
+ c0 = c00 * (1 - yd) + c10 * yd
216
+ c1 = c01 * (1 - yd) + c11 * yd
217
+
218
+ return c0 * (1 - zd) + c1 * zd
219
+ else:
220
+ # Vector field (gradient)
221
+ result = []
222
+ for i in range(grid.shape[-1]):
223
+ result.append(self._trilinear_interpolate(x, y, z, grid[..., i]))
224
+ return tuple(result)
225
+
226
+ def get_refractive_index(
227
+ self,
228
+ x: float | NDArray[np.float64],
229
+ y: float | NDArray[np.float64],
230
+ z: float | NDArray[np.float64],
231
+ wavelength: float | NDArray[np.float64],
232
+ ) -> float | NDArray[np.float64]:
233
+ """Get refractive index via trilinear interpolation."""
234
+ return self._trilinear_interpolate(x, y, z, self._n_grid)
235
+
236
+ def get_refractive_index_gradient(
237
+ self,
238
+ x: float | NDArray[np.float64],
239
+ y: float | NDArray[np.float64],
240
+ z: float | NDArray[np.float64],
241
+ wavelength: float | NDArray[np.float64],
242
+ ) -> tuple[
243
+ float | NDArray[np.float64],
244
+ float | NDArray[np.float64],
245
+ float | NDArray[np.float64],
246
+ ]:
247
+ """Get gradient via trilinear interpolation in gradient grid."""
248
+ return self._trilinear_interpolate(x, y, z, self._grad_grid)
249
+
250
+ def get_absorption_coefficient(
251
+ self,
252
+ x: float | NDArray[np.float64],
253
+ y: float | NDArray[np.float64],
254
+ z: float | NDArray[np.float64],
255
+ wavelength: float | NDArray[np.float64],
256
+ ) -> float | NDArray[np.float64]:
257
+ """Return absorption coefficient (default: 0)."""
258
+ if isinstance(x, np.ndarray):
259
+ return np.zeros_like(x)
260
+ return 0.0
261
+
262
+ def get_scattering_coefficient(
263
+ self,
264
+ x: float | NDArray[np.float64],
265
+ y: float | NDArray[np.float64],
266
+ z: float | NDArray[np.float64],
267
+ wavelength: float | NDArray[np.float64],
268
+ ) -> float | NDArray[np.float64]:
269
+ """Return scattering coefficient (default: 0)."""
270
+ if isinstance(x, np.ndarray):
271
+ return np.zeros_like(x)
272
+ return 0.0
273
+
274
+ # =========================================================================
275
+ # GPU INTERFACE
276
+ # =========================================================================
277
+
278
+ @property
279
+ def gpu_material_id(self) -> int:
280
+ """Return GPU material ID for kernel dispatch."""
281
+ from ...propagation.propagator_protocol import GPUMaterialID
282
+
283
+ return GPUMaterialID.GRID_INHOMOGENEOUS
284
+
285
+ def get_gpu_kernels(self) -> dict:
286
+ """Return GPU kernels for propagation."""
287
+ from ...propagation.kernels.propagation import (
288
+ _kernel_grid_inhomogeneous_euler,
289
+ _kernel_grid_inhomogeneous_rk4,
290
+ )
291
+
292
+ return {
293
+ "euler": _kernel_grid_inhomogeneous_euler,
294
+ "rk4": _kernel_grid_inhomogeneous_rk4,
295
+ }
296
+
297
+ def get_gpu_parameters(self) -> tuple:
298
+ """Return scalar parameters for GPU kernel."""
299
+ (x_min, x_max), (y_min, y_max), (z_min, z_max) = self._bounds
300
+ nx, ny, nz = self._shape
301
+ return (
302
+ float(x_min),
303
+ float(y_min),
304
+ float(z_min),
305
+ float(self._dx),
306
+ float(self._dy),
307
+ float(self._dz),
308
+ int(nx),
309
+ int(ny),
310
+ int(nz),
311
+ )
312
+
313
+ def get_gpu_arrays(self) -> dict:
314
+ """Return device arrays for GPU kernel."""
315
+ return {
316
+ "n_grid": self._n_grid,
317
+ "grad_grid": self._grad_grid,
318
+ "alpha_grid": self._alpha_grid,
319
+ }
@@ -0,0 +1,342 @@
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
+ Homogeneous Material Implementation
36
+
37
+ A material with uniform optical properties throughout the volume.
38
+ Supports wavelength-dependent refractive index via Sellmeier or Cauchy models.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from collections.abc import Callable
44
+ from typing import TYPE_CHECKING
45
+
46
+ import numpy as np
47
+ from numpy.typing import NDArray
48
+
49
+ from .material_field import MaterialField
50
+
51
+ if TYPE_CHECKING:
52
+ from ...propagation.kernels.registry import PropagatorID
53
+
54
+
55
+ class HomogeneousMaterial(MaterialField):
56
+ """
57
+ Homogeneous material with constant optical properties.
58
+
59
+ Properties are uniform throughout space but can depend on wavelength.
60
+ Supports analytic dispersion models (Sellmeier, Cauchy) or custom functions.
61
+
62
+ Parameters
63
+ ----------
64
+ name : str
65
+ Descriptive name for this material.
66
+ refractive_index : float or callable
67
+ Refractive index value or function f(wavelength) -> n.
68
+ If float, uses constant n for all wavelengths.
69
+ If callable, wavelength is in meters.
70
+ absorption_coef : float, optional
71
+ Absorption coefficient α in m⁻¹. Default is 0.
72
+ scattering_coef : float, optional
73
+ Scattering coefficient μ_s in m⁻¹. Default is 0.
74
+ anisotropy : float, optional
75
+ Henyey-Greenstein anisotropy factor g ∈ [-1, 1]. Default is 0.
76
+
77
+ Attributes
78
+ ----------
79
+ refractive_index : float or callable
80
+ Refractive index specification.
81
+ absorption_coef : float
82
+ Absorption coefficient in m⁻¹.
83
+ scattering_coef : float
84
+ Scattering coefficient in m⁻¹.
85
+ anisotropy : float
86
+ Scattering anisotropy factor.
87
+
88
+ Examples
89
+ --------
90
+ >>> # Constant refractive index
91
+ >>> glass = HomogeneousMaterial("Glass", 1.5)
92
+
93
+ >>> # Wavelength-dependent (Cauchy dispersion)
94
+ >>> def cauchy_n(wavelength):
95
+ ... wl_um = wavelength * 1e6
96
+ ... return 1.5 + 0.01 / wl_um**2
97
+ >>> glass = HomogeneousMaterial("Glass", cauchy_n)
98
+
99
+ >>> # With absorption (colored glass)
100
+ >>> colored_glass = HomogeneousMaterial(
101
+ ... "Red Glass", 1.52, absorption_coef=0.1
102
+ ... )
103
+
104
+ Notes
105
+ -----
106
+ For homogeneous materials:
107
+ - Rays propagate in straight lines (no gradient-driven bending)
108
+ - Reflection/refraction occurs only at interfaces
109
+ - Beer-Lambert absorption: I(d) = I₀ exp(-αd)
110
+ - No GPU kernels are needed (straight-line propagation)
111
+ """
112
+
113
+ # =========================================================================
114
+ # COMPATIBILITY DECLARATIONS
115
+ # =========================================================================
116
+ @classmethod
117
+ def _init_compatibility(cls):
118
+ """Initialize compatibility declarations (called once on first use)."""
119
+ from ...propagation.kernels.registry import PropagatorID
120
+
121
+ if not cls._supported_propagators:
122
+ # Homogeneous materials don't need GPU kernels - straight-line propagation
123
+ cls._supported_kernels = [] # No GPU kernels needed
124
+ cls._default_kernel = None
125
+ cls._supported_propagators = [PropagatorID.CPU_GRADIENT]
126
+ cls._default_propagator = PropagatorID.CPU_GRADIENT
127
+
128
+ def __init__(
129
+ self,
130
+ name: str,
131
+ refractive_index: float | Callable[[float], float],
132
+ absorption_coef: float = 0.0,
133
+ scattering_coef: float = 0.0,
134
+ anisotropy: float = 0.0,
135
+ propagator: PropagatorID | None = None,
136
+ ):
137
+ """
138
+ Initialize homogeneous material.
139
+
140
+ Parameters
141
+ ----------
142
+ name : str
143
+ Descriptive name for this material.
144
+ refractive_index : float or callable
145
+ Refractive index value or function f(wavelength) -> n.
146
+ absorption_coef : float, optional
147
+ Absorption coefficient α in m⁻¹. Default is 0.
148
+ scattering_coef : float, optional
149
+ Scattering coefficient μ_s in m⁻¹. Default is 0.
150
+ anisotropy : float, optional
151
+ Henyey-Greenstein anisotropy factor g ∈ [-1, 1]. Default is 0.
152
+ propagator : PropagatorID, optional
153
+ Override the default propagator. Only CPU_GRADIENT is supported.
154
+
155
+ Raises
156
+ ------
157
+ ValueError
158
+ If anisotropy is outside [-1, 1].
159
+ """
160
+ # Initialize compatibility declarations before calling super().__init__
161
+ self._init_compatibility()
162
+
163
+ # Homogeneous materials don't use GPU kernels
164
+ super().__init__(name, kernel=None, propagator=propagator)
165
+ self._is_homogeneous = True
166
+
167
+ # Validate inputs
168
+ if not -1.0 <= anisotropy <= 1.0:
169
+ raise ValueError(f"Anisotropy must be in [-1, 1], got {anisotropy}")
170
+ if absorption_coef < 0:
171
+ raise ValueError(
172
+ f"Absorption coefficient must be >= 0, got {absorption_coef}"
173
+ )
174
+ if scattering_coef < 0:
175
+ raise ValueError(
176
+ f"Scattering coefficient must be >= 0, got {scattering_coef}"
177
+ )
178
+
179
+ # Store properties
180
+ self.refractive_index = refractive_index
181
+ self.absorption_coef = absorption_coef
182
+ self.scattering_coef = scattering_coef
183
+ self.anisotropy = anisotropy
184
+
185
+ # Check if n is callable or constant
186
+ self._n_is_callable = callable(refractive_index)
187
+
188
+ def get_refractive_index(
189
+ self,
190
+ x: float | NDArray[np.float64],
191
+ y: float | NDArray[np.float64],
192
+ z: float | NDArray[np.float64],
193
+ wavelength: float | NDArray[np.float64],
194
+ ) -> float | NDArray[np.float64]:
195
+ """
196
+ Get refractive index (position-independent, wavelength-dependent).
197
+
198
+ Parameters
199
+ ----------
200
+ x, y, z : float or ndarray
201
+ Position coordinates in meters (ignored for homogeneous).
202
+ wavelength : float or ndarray
203
+ Wavelength in meters.
204
+
205
+ Returns
206
+ -------
207
+ n : float or ndarray
208
+ Refractive index.
209
+ """
210
+ if self._n_is_callable:
211
+ if isinstance(wavelength, np.ndarray):
212
+ # Vectorize the callable
213
+ return np.array([self.refractive_index(wl) for wl in wavelength])
214
+ return self.refractive_index(wavelength)
215
+ else:
216
+ if isinstance(x, np.ndarray):
217
+ return np.full_like(x, self.refractive_index)
218
+ return self.refractive_index
219
+
220
+ def get_refractive_index_gradient(
221
+ self,
222
+ x: float | NDArray[np.float64],
223
+ y: float | NDArray[np.float64],
224
+ z: float | NDArray[np.float64],
225
+ wavelength: float | NDArray[np.float64],
226
+ ) -> tuple[
227
+ float | NDArray[np.float64],
228
+ float | NDArray[np.float64],
229
+ float | NDArray[np.float64],
230
+ ]:
231
+ """
232
+ Get refractive index gradient (always zero for homogeneous).
233
+
234
+ Parameters
235
+ ----------
236
+ x, y, z : float or ndarray
237
+ Position coordinates in meters.
238
+ wavelength : float or ndarray
239
+ Wavelength in meters (unused).
240
+
241
+ Returns
242
+ -------
243
+ grad_n : tuple of (float or ndarray)
244
+ (0, 0, 0) since ∇n = 0 for homogeneous materials.
245
+ """
246
+ if isinstance(x, np.ndarray):
247
+ zeros = np.zeros_like(x)
248
+ return (zeros, zeros, zeros)
249
+ return (0.0, 0.0, 0.0)
250
+
251
+ def get_absorption_coefficient(
252
+ self,
253
+ x: float | NDArray[np.float64],
254
+ y: float | NDArray[np.float64],
255
+ z: float | NDArray[np.float64],
256
+ wavelength: float | NDArray[np.float64],
257
+ ) -> float | NDArray[np.float64]:
258
+ """
259
+ Get absorption coefficient (constant for homogeneous).
260
+
261
+ Parameters
262
+ ----------
263
+ x, y, z : float or ndarray
264
+ Position coordinates in meters (ignored).
265
+ wavelength : float or ndarray
266
+ Wavelength in meters (currently ignored, could extend).
267
+
268
+ Returns
269
+ -------
270
+ alpha : float or ndarray
271
+ Absorption coefficient in m⁻¹.
272
+ """
273
+ if isinstance(x, np.ndarray):
274
+ return np.full_like(x, self.absorption_coef)
275
+ return self.absorption_coef
276
+
277
+ def get_scattering_coefficient(
278
+ self,
279
+ x: float | NDArray[np.float64],
280
+ y: float | NDArray[np.float64],
281
+ z: float | NDArray[np.float64],
282
+ wavelength: float | NDArray[np.float64],
283
+ ) -> float | NDArray[np.float64]:
284
+ """
285
+ Get scattering coefficient (constant for homogeneous).
286
+
287
+ Parameters
288
+ ----------
289
+ x, y, z : float or ndarray
290
+ Position coordinates in meters (ignored).
291
+ wavelength : float or ndarray
292
+ Wavelength in meters (currently ignored, could extend).
293
+
294
+ Returns
295
+ -------
296
+ mu_s : float or ndarray
297
+ Scattering coefficient in m⁻¹.
298
+ """
299
+ if isinstance(x, np.ndarray):
300
+ return np.full_like(x, self.scattering_coef)
301
+ return self.scattering_coef
302
+
303
+ def get_anisotropy_factor(
304
+ self,
305
+ x: float | NDArray[np.float64],
306
+ y: float | NDArray[np.float64],
307
+ z: float | NDArray[np.float64],
308
+ wavelength: float | NDArray[np.float64],
309
+ ) -> float | NDArray[np.float64]:
310
+ """
311
+ Get scattering anisotropy factor (constant for homogeneous).
312
+
313
+ Parameters
314
+ ----------
315
+ x, y, z : float or ndarray
316
+ Position coordinates in meters (ignored).
317
+ wavelength : float or ndarray
318
+ Wavelength in meters (ignored).
319
+
320
+ Returns
321
+ -------
322
+ g : float or ndarray
323
+ Anisotropy factor.
324
+ """
325
+ if isinstance(x, np.ndarray):
326
+ return np.full_like(x, self.anisotropy)
327
+ return self.anisotropy
328
+
329
+ def __repr__(self) -> str:
330
+ """Return string representation with key properties."""
331
+ if self._n_is_callable:
332
+ n_str = "n(λ)"
333
+ else:
334
+ n_str = f"n={self.refractive_index:.4f}"
335
+
336
+ props = [n_str]
337
+ if self.absorption_coef > 0:
338
+ props.append(f"α={self.absorption_coef:.2e}")
339
+ if self.scattering_coef > 0:
340
+ props.append(f"μ_s={self.scattering_coef:.2e}")
341
+
342
+ return f"<HomogeneousMaterial('{self.name}', {', '.join(props)})>"