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,418 @@
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
+ Simple Inhomogeneous Model (Radially Symmetric)
36
+
37
+ Base class for radially-symmetric inhomogeneous materials where the
38
+ refractive index depends only on distance from a center point (altitude).
39
+
40
+ User implements only `n_at_altitude()` - a simple 1D function.
41
+ GPU support is automatic via precomputed lookup tables.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ from abc import ABC, abstractmethod
47
+ from typing import TYPE_CHECKING
48
+
49
+ import numpy as np
50
+ from numpy.typing import NDArray
51
+
52
+ from .material_field import MaterialField
53
+
54
+ if TYPE_CHECKING:
55
+ from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
56
+
57
+ # Physical constant
58
+ EARTH_RADIUS = 6_371_000.0 # meters
59
+
60
+
61
+ class SimpleInhomogeneousModel(MaterialField, ABC):
62
+ """
63
+ Base class for radially-symmetric inhomogeneous materials.
64
+
65
+ User implements only `n_at_altitude()` - a simple 1D function that returns
66
+ the refractive index at a given altitude. The system automatically provides:
67
+ - `get_refractive_index(x, y, z, wavelength)` via altitude computation
68
+ - `get_refractive_index_gradient(x, y, z, wavelength)` via radial direction
69
+ - GPU support via precomputed lookup tables
70
+
71
+ Parameters
72
+ ----------
73
+ name : str
74
+ Name of the material model
75
+ center : tuple
76
+ Center of spherical symmetry (x, y, z). Default: (0, 0, 0)
77
+ reference_radius : float
78
+ Radius at which altitude = 0 (e.g., Earth radius). Default: 6,371,000 m
79
+ altitude_range : tuple
80
+ (min_altitude, max_altitude) for lookup table. Default: (0, 200,000)
81
+ lut_resolution : int
82
+ Number of samples in lookup table. Default: 10,000
83
+ kernel : PropagationKernelID, optional
84
+ Override the default kernel. Supported: SIMPLE_EULER, SIMPLE_RK4
85
+ propagator : PropagatorID, optional
86
+ Override the default propagator. Supported: GPU_GRADIENT, CPU_GRADIENT
87
+
88
+ Example
89
+ -------
90
+ >>> class MyAtmosphere(SimpleInhomogeneousModel):
91
+ ... def n_at_altitude(self, altitude, wavelength=None):
92
+ ... return 1.0 + 0.000293 * math.exp(-altitude / 8500)
93
+ ...
94
+ >>> atm = MyAtmosphere(name="My Atmosphere")
95
+ >>> propagator = GPUGradientPropagator(atm) # Auto GPU support
96
+ """
97
+
98
+ # =========================================================================
99
+ # COMPATIBILITY DECLARATIONS
100
+ # =========================================================================
101
+ # Import lazily to avoid circular imports at class definition time
102
+ @classmethod
103
+ def _init_compatibility(cls):
104
+ """Initialize compatibility declarations (called once on first use)."""
105
+ from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
106
+
107
+ if not cls._supported_kernels:
108
+ cls._supported_kernels = [
109
+ PropagationKernelID.SIMPLE_EULER,
110
+ PropagationKernelID.SIMPLE_RK4,
111
+ ]
112
+ cls._default_kernel = PropagationKernelID.SIMPLE_RK4
113
+ cls._supported_propagators = [
114
+ PropagatorID.GPU_GRADIENT,
115
+ PropagatorID.CPU_GRADIENT,
116
+ ]
117
+ cls._default_propagator = PropagatorID.GPU_GRADIENT
118
+
119
+ def __init__(
120
+ self,
121
+ name: str = "Simple Inhomogeneous",
122
+ center: tuple[float, float, float] = (0.0, 0.0, 0.0),
123
+ reference_radius: float = EARTH_RADIUS,
124
+ altitude_range: tuple[float, float] = (0.0, 200_000.0),
125
+ lut_resolution: int = 10000,
126
+ kernel: PropagationKernelID | None = None,
127
+ propagator: PropagatorID | None = None,
128
+ ):
129
+ # Initialize compatibility declarations before calling super().__init__
130
+ self._init_compatibility()
131
+
132
+ super().__init__(name, kernel=kernel, propagator=propagator)
133
+ self._is_homogeneous = False
134
+ self.center = center
135
+ self.reference_radius = reference_radius
136
+ self.altitude_range = altitude_range
137
+ self.lut_resolution = lut_resolution
138
+
139
+ # LUT arrays initialized lazily on first GPU use
140
+ self._lut_initialized = False
141
+ self._lut_altitudes: NDArray[np.float32] | None = None
142
+ self._lut_n: NDArray[np.float32] | None = None
143
+ self._lut_dn_dh: NDArray[np.float32] | None = None
144
+ self._lut_alpha: NDArray[np.float32] | None = None # Absorption coefficient LUT
145
+ self._lut_delta_h: float = 0.0
146
+
147
+ # =========================================================================
148
+ # USER MUST IMPLEMENT
149
+ # =========================================================================
150
+
151
+ @abstractmethod
152
+ def n_at_altitude(self, altitude: float, wavelength: float | None = None) -> float:
153
+ """
154
+ Return refractive index at given altitude.
155
+
156
+ This is the only method users must implement.
157
+
158
+ Parameters
159
+ ----------
160
+ altitude : float
161
+ Altitude above reference surface in meters (altitude >= 0)
162
+ wavelength : float, optional
163
+ Wavelength in meters (for dispersion models)
164
+
165
+ Returns
166
+ -------
167
+ float
168
+ Refractive index n >= 1.0
169
+ """
170
+ pass
171
+
172
+ # =========================================================================
173
+ # USER MAY OVERRIDE
174
+ # =========================================================================
175
+
176
+ def dn_dh_at_altitude(
177
+ self, altitude: float, wavelength: float | None = None
178
+ ) -> float:
179
+ """
180
+ Return dn/d(altitude) at given altitude.
181
+
182
+ Default implementation uses numerical differentiation.
183
+ Override for analytical derivatives (more accurate).
184
+
185
+ Parameters
186
+ ----------
187
+ altitude : float
188
+ Altitude above reference surface in meters
189
+ wavelength : float, optional
190
+ Wavelength in meters
191
+
192
+ Returns
193
+ -------
194
+ float
195
+ Derivative dn/dh in m^-1
196
+ """
197
+ eps = 1.0 # 1 meter step for numerical derivative
198
+ h_plus = max(altitude + eps, 0.0)
199
+ h_minus = max(altitude - eps, 0.0)
200
+
201
+ if h_plus == h_minus:
202
+ return 0.0
203
+
204
+ return (
205
+ self.n_at_altitude(h_plus, wavelength)
206
+ - self.n_at_altitude(h_minus, wavelength)
207
+ ) / (h_plus - h_minus)
208
+
209
+ def alpha_at_altitude(
210
+ self, altitude: float, wavelength: float | None = None
211
+ ) -> float:
212
+ """
213
+ Return absorption coefficient at given altitude.
214
+
215
+ Default implementation returns 0 (no absorption).
216
+ Override in subclasses that model absorbing media.
217
+
218
+ Parameters
219
+ ----------
220
+ altitude : float
221
+ Altitude above reference surface in meters
222
+ wavelength : float, optional
223
+ Wavelength in meters
224
+
225
+ Returns
226
+ -------
227
+ float
228
+ Absorption coefficient α in m^-1 (for Beer-Lambert: I = I₀·exp(-α·s))
229
+ """
230
+ return 0.0
231
+
232
+ # =========================================================================
233
+ # AUTO-GENERATED FROM n_at_altitude()
234
+ # =========================================================================
235
+
236
+ def _compute_altitude(
237
+ self,
238
+ x: float | NDArray[np.float64],
239
+ y: float | NDArray[np.float64],
240
+ z: float | NDArray[np.float64],
241
+ ) -> float | NDArray[np.float64]:
242
+ """Compute altitude from Cartesian coordinates."""
243
+ cx, cy, cz = self.center
244
+ dx = np.asarray(x) - cx
245
+ dy = np.asarray(y) - cy
246
+ dz = np.asarray(z) - cz
247
+
248
+ r = np.sqrt(dx**2 + dy**2 + dz**2)
249
+ altitude = r - self.reference_radius
250
+
251
+ # Clamp to non-negative
252
+ if isinstance(altitude, np.ndarray):
253
+ return np.maximum(altitude, 0.0)
254
+ return max(altitude, 0.0)
255
+
256
+ def _compute_radial_direction(
257
+ self,
258
+ x: float | NDArray[np.float64],
259
+ y: float | NDArray[np.float64],
260
+ z: float | NDArray[np.float64],
261
+ ) -> tuple[
262
+ float | NDArray[np.float64],
263
+ float | NDArray[np.float64],
264
+ float | NDArray[np.float64],
265
+ ]:
266
+ """Compute unit radial vector (outward from center)."""
267
+ cx, cy, cz = self.center
268
+ dx = np.asarray(x) - cx
269
+ dy = np.asarray(y) - cy
270
+ dz = np.asarray(z) - cz
271
+
272
+ r = np.sqrt(dx**2 + dy**2 + dz**2)
273
+
274
+ if isinstance(r, np.ndarray):
275
+ r = np.where(r < 1e-10, 1.0, r)
276
+ elif r < 1e-10:
277
+ return 0.0, 0.0, 1.0 # Default to +z at center
278
+
279
+ return dx / r, dy / r, dz / r
280
+
281
+ def get_refractive_index(
282
+ self,
283
+ x: float | NDArray[np.float64],
284
+ y: float | NDArray[np.float64],
285
+ z: float | NDArray[np.float64],
286
+ wavelength: float | NDArray[np.float64],
287
+ ) -> float | NDArray[np.float64]:
288
+ """Get refractive index at position (auto-generated from n_at_altitude)."""
289
+ altitude = self._compute_altitude(x, y, z)
290
+
291
+ if isinstance(altitude, np.ndarray):
292
+ # Vectorized evaluation
293
+ return np.array(
294
+ [self.n_at_altitude(float(h), wavelength) for h in altitude.flat]
295
+ ).reshape(altitude.shape)
296
+ return self.n_at_altitude(float(altitude), wavelength)
297
+
298
+ def get_refractive_index_gradient(
299
+ self,
300
+ x: float | NDArray[np.float64],
301
+ y: float | NDArray[np.float64],
302
+ z: float | NDArray[np.float64],
303
+ wavelength: float | NDArray[np.float64],
304
+ ) -> tuple[
305
+ float | NDArray[np.float64],
306
+ float | NDArray[np.float64],
307
+ float | NDArray[np.float64],
308
+ ]:
309
+ """Get gradient of n at position (auto-generated)."""
310
+ altitude = self._compute_altitude(x, y, z)
311
+ rx, ry, rz = self._compute_radial_direction(x, y, z)
312
+
313
+ if isinstance(altitude, np.ndarray):
314
+ dn_dh = np.array(
315
+ [self.dn_dh_at_altitude(float(h), wavelength) for h in altitude.flat]
316
+ ).reshape(altitude.shape)
317
+ else:
318
+ dn_dh = self.dn_dh_at_altitude(float(altitude), wavelength)
319
+
320
+ # grad(n) = (dn/dh) * r_hat
321
+ return dn_dh * rx, dn_dh * ry, dn_dh * rz
322
+
323
+ def get_absorption_coefficient(
324
+ self,
325
+ x: float | NDArray[np.float64],
326
+ y: float | NDArray[np.float64],
327
+ z: float | NDArray[np.float64],
328
+ wavelength: float | NDArray[np.float64],
329
+ ) -> float | NDArray[np.float64]:
330
+ """Return absorption coefficient (default: 0)."""
331
+ if isinstance(x, np.ndarray):
332
+ return np.zeros_like(x)
333
+ return 0.0
334
+
335
+ def get_scattering_coefficient(
336
+ self,
337
+ x: float | NDArray[np.float64],
338
+ y: float | NDArray[np.float64],
339
+ z: float | NDArray[np.float64],
340
+ wavelength: float | NDArray[np.float64],
341
+ ) -> float | NDArray[np.float64]:
342
+ """Return scattering coefficient (default: 0)."""
343
+ if isinstance(x, np.ndarray):
344
+ return np.zeros_like(x)
345
+ return 0.0
346
+
347
+ # =========================================================================
348
+ # GPU INTERFACE (Automatic via LUT)
349
+ # =========================================================================
350
+
351
+ def _initialize_lut(self, wavelength: float | None = None) -> None:
352
+ """Build lookup table arrays for GPU."""
353
+ if self._lut_initialized:
354
+ return
355
+
356
+ min_alt, max_alt = self.altitude_range
357
+ altitudes = np.linspace(min_alt, max_alt, self.lut_resolution)
358
+
359
+ n_values = np.array(
360
+ [self.n_at_altitude(float(h), wavelength) for h in altitudes],
361
+ dtype=np.float32,
362
+ )
363
+ dn_dh_values = np.array(
364
+ [self.dn_dh_at_altitude(float(h), wavelength) for h in altitudes],
365
+ dtype=np.float32,
366
+ )
367
+ alpha_values = np.array(
368
+ [self.alpha_at_altitude(float(h), wavelength) for h in altitudes],
369
+ dtype=np.float32,
370
+ )
371
+
372
+ self._lut_altitudes = altitudes.astype(np.float32)
373
+ self._lut_n = n_values
374
+ self._lut_dn_dh = dn_dh_values
375
+ self._lut_alpha = alpha_values
376
+ self._lut_delta_h = float(altitudes[1] - altitudes[0])
377
+ self._lut_initialized = True
378
+
379
+ @property
380
+ def gpu_material_id(self) -> int:
381
+ """Return GPU material ID for kernel dispatch."""
382
+ from ...propagation.propagator_protocol import GPUMaterialID
383
+
384
+ return GPUMaterialID.SIMPLE_INHOMOGENEOUS
385
+
386
+ def get_gpu_kernels(self) -> dict:
387
+ """Return GPU kernels for propagation."""
388
+ from ...propagation.kernels.propagation import (
389
+ _kernel_simple_inhomogeneous_euler,
390
+ _kernel_simple_inhomogeneous_rk4,
391
+ )
392
+
393
+ return {
394
+ "euler": _kernel_simple_inhomogeneous_euler,
395
+ "rk4": _kernel_simple_inhomogeneous_rk4,
396
+ }
397
+
398
+ def get_gpu_parameters(self) -> tuple:
399
+ """Return scalar parameters for GPU kernel."""
400
+ self._initialize_lut()
401
+ return (
402
+ float(self.center[0]),
403
+ float(self.center[1]),
404
+ float(self.center[2]),
405
+ float(self.reference_radius),
406
+ float(self.altitude_range[0]),
407
+ float(self._lut_delta_h),
408
+ int(self.lut_resolution),
409
+ )
410
+
411
+ def get_gpu_arrays(self) -> dict:
412
+ """Return device arrays for GPU kernel."""
413
+ self._initialize_lut()
414
+ return {
415
+ "lut_n": self._lut_n,
416
+ "lut_dn_dh": self._lut_dn_dh,
417
+ "lut_alpha": self._lut_alpha,
418
+ }