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,527 @@
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
+ Material Field Base Class
36
+
37
+ Defines the abstract base class for all material types. Materials provide
38
+ spatially-varying optical properties including refractive index, absorption,
39
+ and scattering coefficients.
40
+
41
+ Design Notes
42
+ ------------
43
+ - Materials are position-dependent by default (field concept)
44
+ - Homogeneous materials can optimize for constant properties
45
+ - GPU-compatible interface for raytracing kernels
46
+ - Materials declare supported kernels and propagators for compatibility checking
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import math
52
+ from abc import ABC, abstractmethod
53
+ from typing import TYPE_CHECKING
54
+
55
+ import numpy as np
56
+ from numpy.typing import NDArray
57
+
58
+ if TYPE_CHECKING:
59
+ from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
60
+
61
+
62
+ class MaterialField(ABC):
63
+ """
64
+ Abstract base class for spatially-varying material properties.
65
+
66
+ All material properties are functions of position (x, y, z) and wavelength.
67
+ Subclasses must implement methods that can be called from both CPU and GPU.
68
+
69
+ Parameters
70
+ ----------
71
+ name : str, optional
72
+ Descriptive name for this material. Default is "Material".
73
+
74
+ Attributes
75
+ ----------
76
+ name : str
77
+ Material identifier.
78
+
79
+ Notes
80
+ -----
81
+ Material properties modeled:
82
+ - Refractive index n: Real part of complex refractive index
83
+ - Absorption coefficient α: Controls intensity decay
84
+ - Scattering coefficient μ_s: Controls scattering mean free path
85
+ - Anisotropy factor g: Henyey-Greenstein scattering parameter
86
+
87
+ References
88
+ ----------
89
+ .. [1] Born, M., & Wolf, E. (1999). Principles of Optics (7th ed.).
90
+ Cambridge University Press.
91
+ """
92
+
93
+ # =========================================================================
94
+ # CLASS-LEVEL COMPATIBILITY DECLARATIONS
95
+ # =========================================================================
96
+ # Subclasses should override these to declare supported kernels/propagators
97
+ _supported_kernels: list[PropagationKernelID] = []
98
+ _default_kernel: PropagationKernelID | None = None
99
+ _supported_propagators: list[PropagatorID] = []
100
+ _default_propagator: PropagatorID | None = None
101
+
102
+ def __init__(
103
+ self,
104
+ name: str = "Material",
105
+ kernel: PropagationKernelID | None = None,
106
+ propagator: PropagatorID | None = None,
107
+ ):
108
+ """
109
+ Initialize material field.
110
+
111
+ Parameters
112
+ ----------
113
+ name : str, optional
114
+ Descriptive name for this material. Default is "Material".
115
+ kernel : PropagationKernelID, optional
116
+ Override the default propagation kernel. Must be in supported_kernels().
117
+ If None, uses the class default.
118
+ propagator : PropagatorID, optional
119
+ Override the default propagator. Must be in supported_propagators().
120
+ If None, uses the class default.
121
+
122
+ Raises
123
+ ------
124
+ ValueError
125
+ If kernel or propagator is not supported by this material type.
126
+ """
127
+ self.name = name
128
+ self._is_homogeneous = False
129
+
130
+ # Resolve kernel preference
131
+ if kernel is None:
132
+ self._kernel_id = self._default_kernel
133
+ else:
134
+ if self._supported_kernels and kernel not in self._supported_kernels:
135
+ raise ValueError(
136
+ f"{self.__class__.__name__} does not support kernel {kernel}. "
137
+ f"Supported: {self._supported_kernels}"
138
+ )
139
+ self._kernel_id = kernel
140
+
141
+ # Resolve propagator preference
142
+ if propagator is None:
143
+ self._propagator_id = self._default_propagator
144
+ else:
145
+ if (
146
+ self._supported_propagators
147
+ and propagator not in self._supported_propagators
148
+ ):
149
+ raise ValueError(
150
+ f"{self.__class__.__name__} does not support propagator {propagator}. "
151
+ f"Supported: {self._supported_propagators}"
152
+ )
153
+ self._propagator_id = propagator
154
+
155
+ @abstractmethod
156
+ def get_refractive_index(
157
+ self,
158
+ x: float | NDArray[np.float64],
159
+ y: float | NDArray[np.float64],
160
+ z: float | NDArray[np.float64],
161
+ wavelength: float | NDArray[np.float64],
162
+ ) -> float | NDArray[np.float64]:
163
+ """
164
+ Get refractive index at position (x, y, z) for given wavelength.
165
+
166
+ Parameters
167
+ ----------
168
+ x, y, z : float or ndarray
169
+ Position coordinates in meters.
170
+ wavelength : float or ndarray
171
+ Wavelength in meters.
172
+
173
+ Returns
174
+ -------
175
+ n : float or ndarray
176
+ Real part of refractive index (dimensionless).
177
+
178
+ Notes
179
+ -----
180
+ For absorbing materials, this returns only Re(n). Use
181
+ get_absorption_coefficient() for the imaginary part.
182
+ """
183
+ pass
184
+
185
+ @abstractmethod
186
+ def get_refractive_index_gradient(
187
+ self,
188
+ x: float | NDArray[np.float64],
189
+ y: float | NDArray[np.float64],
190
+ z: float | NDArray[np.float64],
191
+ wavelength: float | NDArray[np.float64],
192
+ ) -> tuple[
193
+ float | NDArray[np.float64],
194
+ float | NDArray[np.float64],
195
+ float | NDArray[np.float64],
196
+ ]:
197
+ """
198
+ Get gradient of refractive index ∇n at position (x, y, z).
199
+
200
+ Parameters
201
+ ----------
202
+ x, y, z : float or ndarray
203
+ Position coordinates in meters.
204
+ wavelength : float or ndarray
205
+ Wavelength in meters.
206
+
207
+ Returns
208
+ -------
209
+ grad_n : tuple of (float or ndarray)
210
+ (∂n/∂x, ∂n/∂y, ∂n/∂z) in units of m⁻¹.
211
+
212
+ Notes
213
+ -----
214
+ For homogeneous materials, this returns (0, 0, 0).
215
+ Gradient drives ray curvature via the eikonal equation:
216
+ d²r/ds² = ∇n(r)/n(r)
217
+
218
+ References
219
+ ----------
220
+ .. [1] Born & Wolf (1999), Section 3.1.
221
+ """
222
+ pass
223
+
224
+ @abstractmethod
225
+ def get_absorption_coefficient(
226
+ self,
227
+ x: float | NDArray[np.float64],
228
+ y: float | NDArray[np.float64],
229
+ z: float | NDArray[np.float64],
230
+ wavelength: float | NDArray[np.float64],
231
+ ) -> float | NDArray[np.float64]:
232
+ """
233
+ Get absorption coefficient α at position (x, y, z).
234
+
235
+ Parameters
236
+ ----------
237
+ x, y, z : float or ndarray
238
+ Position coordinates in meters.
239
+ wavelength : float or ndarray
240
+ Wavelength in meters.
241
+
242
+ Returns
243
+ -------
244
+ alpha : float or ndarray
245
+ Absorption coefficient in m⁻¹.
246
+
247
+ Notes
248
+ -----
249
+ Intensity decays as I(d) = I₀ exp(-αd) (Beer-Lambert law).
250
+
251
+ References
252
+ ----------
253
+ .. [1] https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law
254
+ """
255
+ pass
256
+
257
+ @abstractmethod
258
+ def get_scattering_coefficient(
259
+ self,
260
+ x: float | NDArray[np.float64],
261
+ y: float | NDArray[np.float64],
262
+ z: float | NDArray[np.float64],
263
+ wavelength: float | NDArray[np.float64],
264
+ ) -> float | NDArray[np.float64]:
265
+ """
266
+ Get scattering coefficient μ_s at position (x, y, z).
267
+
268
+ Parameters
269
+ ----------
270
+ x, y, z : float or ndarray
271
+ Position coordinates in meters.
272
+ wavelength : float or ndarray
273
+ Wavelength in meters.
274
+
275
+ Returns
276
+ -------
277
+ mu_s : float or ndarray
278
+ Scattering coefficient in m⁻¹.
279
+
280
+ Notes
281
+ -----
282
+ Mean free path between scattering events: ℓ_s = 1/μ_s.
283
+ """
284
+ pass
285
+
286
+ def get_extinction_coefficient(
287
+ self,
288
+ x: float | NDArray[np.float64],
289
+ y: float | NDArray[np.float64],
290
+ z: float | NDArray[np.float64],
291
+ wavelength: float | NDArray[np.float64],
292
+ ) -> float | NDArray[np.float64]:
293
+ """
294
+ Get total extinction coefficient (absorption + scattering).
295
+
296
+ Parameters
297
+ ----------
298
+ x, y, z : float or ndarray
299
+ Position coordinates in meters.
300
+ wavelength : float or ndarray
301
+ Wavelength in meters.
302
+
303
+ Returns
304
+ -------
305
+ mu_t : float or ndarray
306
+ Total extinction coefficient in m⁻¹.
307
+ """
308
+ alpha = self.get_absorption_coefficient(x, y, z, wavelength)
309
+ mu_s = self.get_scattering_coefficient(x, y, z, wavelength)
310
+ return alpha + mu_s
311
+
312
+ def get_anisotropy_factor(
313
+ self,
314
+ x: float | NDArray[np.float64],
315
+ y: float | NDArray[np.float64],
316
+ z: float | NDArray[np.float64],
317
+ wavelength: float | NDArray[np.float64],
318
+ ) -> float | NDArray[np.float64]:
319
+ """
320
+ Get scattering anisotropy factor g (Henyey-Greenstein parameter).
321
+
322
+ Parameters
323
+ ----------
324
+ x, y, z : float or ndarray
325
+ Position coordinates in meters.
326
+ wavelength : float or ndarray
327
+ Wavelength in meters.
328
+
329
+ Returns
330
+ -------
331
+ g : float or ndarray
332
+ Anisotropy factor, range [-1, 1].
333
+ - g = 0: isotropic scattering
334
+ - g > 0: forward scattering
335
+ - g < 0: backward scattering
336
+
337
+ Notes
338
+ -----
339
+ Default implementation returns 0 (isotropic).
340
+ Used in Henyey-Greenstein phase function:
341
+ p(cos θ) = (1-g²) / (1 + g² - 2g cos θ)^(3/2)
342
+
343
+ References
344
+ ----------
345
+ .. [1] Henyey & Greenstein (1941). Astrophysical Journal, 93, 70-83.
346
+ """
347
+ if isinstance(x, np.ndarray):
348
+ return np.zeros_like(x)
349
+ return 0.0
350
+
351
+ def is_homogeneous(self) -> bool:
352
+ """
353
+ Check if material has uniform properties (no spatial variation).
354
+
355
+ Returns
356
+ -------
357
+ bool
358
+ True if material is homogeneous, False if inhomogeneous.
359
+
360
+ Notes
361
+ -----
362
+ Homogeneous materials enable optimizations (straight-line propagation).
363
+ """
364
+ return self._is_homogeneous
365
+
366
+ # =========================================================================
367
+ # COMPATIBILITY QUERY METHODS
368
+ # =========================================================================
369
+
370
+ @classmethod
371
+ def supported_kernels(cls) -> list[PropagationKernelID]:
372
+ """
373
+ Return list of propagation kernels supported by this material type.
374
+
375
+ Returns
376
+ -------
377
+ list[PropagationKernelID]
378
+ List of kernel IDs this material supports.
379
+
380
+ Examples
381
+ --------
382
+ >>> from lsurf.materials import ExponentialAtmosphere
383
+ >>> ExponentialAtmosphere.supported_kernels()
384
+ [<PropagationKernelID.SIMPLE_EULER: ...>, <PropagationKernelID.SIMPLE_RK4: ...>]
385
+ """
386
+
387
+ # Return a copy to prevent modification
388
+ return list(cls._supported_kernels)
389
+
390
+ @classmethod
391
+ def default_kernel(cls) -> PropagationKernelID | None:
392
+ """
393
+ Return the default propagation kernel for this material type.
394
+
395
+ Returns
396
+ -------
397
+ PropagationKernelID or None
398
+ The default kernel ID, or None if no GPU kernels are supported.
399
+
400
+ Examples
401
+ --------
402
+ >>> from lsurf.materials import ExponentialAtmosphere
403
+ >>> ExponentialAtmosphere.default_kernel()
404
+ <PropagationKernelID.SIMPLE_RK4: ...>
405
+ """
406
+ return cls._default_kernel
407
+
408
+ @classmethod
409
+ def supported_propagators(cls) -> list[PropagatorID]:
410
+ """
411
+ Return list of propagators supported by this material type.
412
+
413
+ Returns
414
+ -------
415
+ list[PropagatorID]
416
+ List of propagator IDs this material supports.
417
+
418
+ Examples
419
+ --------
420
+ >>> from lsurf.materials import ExponentialAtmosphere
421
+ >>> ExponentialAtmosphere.supported_propagators()
422
+ [<PropagatorID.GPU_GRADIENT: ...>, <PropagatorID.CPU_GRADIENT: ...>]
423
+ """
424
+
425
+ return list(cls._supported_propagators)
426
+
427
+ @classmethod
428
+ def default_propagator(cls) -> PropagatorID | None:
429
+ """
430
+ Return the default propagator for this material type.
431
+
432
+ Returns
433
+ -------
434
+ PropagatorID or None
435
+ The default propagator ID, or None if not specified.
436
+
437
+ Examples
438
+ --------
439
+ >>> from lsurf.materials import ExponentialAtmosphere
440
+ >>> ExponentialAtmosphere.default_propagator()
441
+ <PropagatorID.GPU_GRADIENT: ...>
442
+ """
443
+ return cls._default_propagator
444
+
445
+ @property
446
+ def kernel_id(self) -> PropagationKernelID | None:
447
+ """
448
+ Return the kernel ID configured for this material instance.
449
+
450
+ Returns
451
+ -------
452
+ PropagationKernelID or None
453
+ The kernel ID selected for this instance, or None if not applicable.
454
+ """
455
+ return self._kernel_id
456
+
457
+ @property
458
+ def propagator_id(self) -> PropagatorID | None:
459
+ """
460
+ Return the propagator ID configured for this material instance.
461
+
462
+ Returns
463
+ -------
464
+ PropagatorID or None
465
+ The propagator ID selected for this instance, or None if not applicable.
466
+ """
467
+ return self._propagator_id
468
+
469
+ def get_refractive_index_gradient_magnitude(
470
+ self,
471
+ x: float | NDArray[np.float64],
472
+ y: float | NDArray[np.float64],
473
+ z: float | NDArray[np.float64],
474
+ wavelength: float | NDArray[np.float64],
475
+ ) -> float | NDArray[np.float64]:
476
+ """
477
+ Get magnitude of refractive index gradient |∇n|.
478
+
479
+ Parameters
480
+ ----------
481
+ x, y, z : float or ndarray
482
+ Position coordinates in meters.
483
+ wavelength : float or ndarray
484
+ Wavelength in meters.
485
+
486
+ Returns
487
+ -------
488
+ grad_mag : float or ndarray
489
+ Magnitude of gradient |∇n| in m⁻¹.
490
+ """
491
+ grad_x, grad_y, grad_z = self.get_refractive_index_gradient(x, y, z, wavelength)
492
+
493
+ if isinstance(grad_x, np.ndarray):
494
+ return np.sqrt(grad_x**2 + grad_y**2 + grad_z**2)
495
+ else:
496
+ return math.sqrt(grad_x**2 + grad_y**2 + grad_z**2)
497
+
498
+ def compute_phase_velocity(
499
+ self,
500
+ x: float | NDArray[np.float64],
501
+ y: float | NDArray[np.float64],
502
+ z: float | NDArray[np.float64],
503
+ wavelength: float | NDArray[np.float64],
504
+ ) -> float | NDArray[np.float64]:
505
+ """
506
+ Compute phase velocity v_p = c/n at position.
507
+
508
+ Parameters
509
+ ----------
510
+ x, y, z : float or ndarray
511
+ Position coordinates in meters.
512
+ wavelength : float or ndarray
513
+ Wavelength in meters.
514
+
515
+ Returns
516
+ -------
517
+ v_p : float or ndarray
518
+ Phase velocity in m/s.
519
+ """
520
+ c = 299792458.0 # Speed of light in vacuum (m/s)
521
+ n = self.get_refractive_index(x, y, z, wavelength)
522
+ return c / n
523
+
524
+ def __repr__(self) -> str:
525
+ """Return string representation."""
526
+ homo_str = "homogeneous" if self._is_homogeneous else "inhomogeneous"
527
+ return f"<{self.__class__.__name__}(name='{self.name}', {homo_str})>"