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,435 @@
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
+ Exponential Atmosphere Material
36
+
37
+ Implements a spherically symmetric exponential atmosphere where the air density
38
+ (and thus refractive index) depends on the radial distance from Earth's center.
39
+
40
+ The refractive index follows:
41
+ n(r) = 1 + (n_0 - 1) * exp(-(r - R_E) / H)
42
+
43
+ where:
44
+ r = radial distance from Earth's center
45
+ R_E = Earth's radius
46
+ n_0 = refractive index at sea level (~1.000293)
47
+ H = scale height (~8.5 km for Earth's atmosphere)
48
+
49
+ This model is appropriate for studying atmospheric refraction effects
50
+ such as mirages, stellar aberration, and ray bending in grazing incidence
51
+ scenarios.
52
+
53
+ References
54
+ ----------
55
+ .. [1] Garfinkel, B. (1967). "Astronomical Refraction in a Polytropic
56
+ Atmosphere". The Astronomical Journal, 72, 235-254.
57
+ .. [2] Smart, W.M. (1977). "Textbook on Spherical Astronomy", 6th ed.
58
+ Cambridge University Press, Chapter XI.
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ import math
64
+ from typing import TYPE_CHECKING, Union
65
+
66
+ import numpy as np
67
+ from numpy.typing import NDArray
68
+
69
+ from ..base import SimpleInhomogeneousModel
70
+ from ..utils.constants import EARTH_RADIUS, SCALE_HEIGHT_DEFAULT, N_SEA_LEVEL
71
+
72
+ if TYPE_CHECKING:
73
+ from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
74
+
75
+
76
+ class ExponentialAtmosphere(SimpleInhomogeneousModel):
77
+ """
78
+ Exponential atmosphere with radially-dependent refractive index.
79
+
80
+ Models an atmosphere where air density decreases exponentially with
81
+ altitude, causing the refractive index to approach 1 at high altitudes.
82
+
83
+ The model is spherically symmetric about Earth's center, which is
84
+ assumed to be at the origin (0, 0, 0) by default, or can be specified.
85
+
86
+ This class inherits from SimpleInhomogeneousModel, implementing the
87
+ `n_at_altitude()` method for the exponential profile. GPU support is
88
+ provided automatically via lookup table interpolation.
89
+
90
+ Parameters
91
+ ----------
92
+ name : str, optional
93
+ Descriptive name for this material. Default is "Exponential Atmosphere".
94
+ n_sea_level : float, optional
95
+ Refractive index at sea level (Earth's surface). Default is 1.000293.
96
+ scale_height : float, optional
97
+ Atmospheric scale height H in meters. Default is 8500.0 m.
98
+ earth_radius : float, optional
99
+ Radius of Earth in meters. Default is 6,371,000 m.
100
+ earth_center : tuple of float, optional
101
+ Position of Earth's center in meters. Default is (0, 0, 0).
102
+ absorption_coef : float, optional
103
+ Absorption coefficient at sea level in m⁻¹. Default is 0.0.
104
+ absorption_scale_height : float, optional
105
+ Scale height for absorption (can differ from density). Default is same as scale_height.
106
+
107
+ Examples
108
+ --------
109
+ >>> atmosphere = ExponentialAtmosphere()
110
+ >>> # Get refractive index at 10 km altitude
111
+ >>> n = atmosphere.get_refractive_index(0, 0, EARTH_RADIUS + 10000, 532e-9)
112
+ >>> print(f"n at 10 km: {n:.6f}") # ~1.000089
113
+
114
+ >>> # Get gradient at sea level directly above Earth's center
115
+ >>> grad = atmosphere.get_refractive_index_gradient(0, 0, EARTH_RADIUS, 532e-9)
116
+ >>> print(f"dn/dz at sea level: {grad[2]:.2e}") # ~ -3.4e-8 m^-1
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ name: str = "Exponential Atmosphere",
122
+ n_sea_level: float = N_SEA_LEVEL,
123
+ scale_height: float = SCALE_HEIGHT_DEFAULT,
124
+ earth_radius: float = EARTH_RADIUS,
125
+ earth_center: tuple[float, float, float] = (0.0, 0.0, 0.0),
126
+ absorption_coef: float = 0.0,
127
+ absorption_scale_height: float | None = None,
128
+ kernel: PropagationKernelID | None = None,
129
+ propagator: PropagatorID | None = None,
130
+ ):
131
+ # Validate inputs
132
+ if scale_height <= 0:
133
+ raise ValueError(f"Scale height must be positive, got {scale_height}")
134
+ if n_sea_level < 1.0:
135
+ raise ValueError(f"Refractive index must be >= 1.0, got {n_sea_level}")
136
+
137
+ # Initialize base class
138
+ super().__init__(
139
+ name=name,
140
+ center=earth_center,
141
+ reference_radius=earth_radius,
142
+ altitude_range=(0.0, 15 * scale_height), # ~127 km for default
143
+ lut_resolution=10000,
144
+ kernel=kernel,
145
+ propagator=propagator,
146
+ )
147
+
148
+ # Store exponential atmosphere specific parameters
149
+ self.n_sea_level = n_sea_level
150
+ self.scale_height = scale_height
151
+ self.earth_radius = earth_radius
152
+ self.earth_center = earth_center
153
+ self.absorption_coef_sea_level = absorption_coef
154
+ self.absorption_scale_height = (
155
+ absorption_scale_height
156
+ if absorption_scale_height is not None
157
+ else scale_height
158
+ )
159
+
160
+ # Precompute refractivity
161
+ self.delta_n = n_sea_level - 1.0
162
+
163
+ # =========================================================================
164
+ # SimpleInhomogeneousModel Interface (Required)
165
+ # =========================================================================
166
+
167
+ def n_at_altitude(self, altitude: float, wavelength: float | None = None) -> float:
168
+ """
169
+ Return refractive index at given altitude.
170
+
171
+ Implements the exponential profile:
172
+ n(h) = 1 + delta_n * exp(-h / H)
173
+
174
+ Parameters
175
+ ----------
176
+ altitude : float
177
+ Altitude above Earth's surface in meters (clamped to >= 0)
178
+ wavelength : float, optional
179
+ Wavelength in meters (not used - no dispersion)
180
+
181
+ Returns
182
+ -------
183
+ float
184
+ Refractive index at altitude
185
+ """
186
+ altitude_clamped = max(altitude, 0.0)
187
+ return 1.0 + self.delta_n * math.exp(-altitude_clamped / self.scale_height)
188
+
189
+ def dn_dh_at_altitude(
190
+ self, altitude: float, wavelength: float | None = None
191
+ ) -> float:
192
+ """
193
+ Return analytical dn/dh at given altitude.
194
+
195
+ Derivative of exponential profile:
196
+ dn/dh = -(delta_n / H) * exp(-h / H)
197
+
198
+ Parameters
199
+ ----------
200
+ altitude : float
201
+ Altitude above Earth's surface in meters
202
+ wavelength : float, optional
203
+ Wavelength in meters (not used)
204
+
205
+ Returns
206
+ -------
207
+ float
208
+ Derivative dn/dh in m^-1
209
+ """
210
+ if altitude < 0:
211
+ return 0.0
212
+ return -(self.delta_n / self.scale_height) * math.exp(
213
+ -altitude / self.scale_height
214
+ )
215
+
216
+ def alpha_at_altitude(
217
+ self, altitude: float, wavelength: float | None = None
218
+ ) -> float:
219
+ """
220
+ Return absorption coefficient at given altitude.
221
+
222
+ Implements exponential absorption profile:
223
+ α(h) = α₀ * exp(-h / H_α)
224
+
225
+ Parameters
226
+ ----------
227
+ altitude : float
228
+ Altitude above Earth's surface in meters
229
+ wavelength : float, optional
230
+ Wavelength in meters (not used)
231
+
232
+ Returns
233
+ -------
234
+ float
235
+ Absorption coefficient α in m⁻¹
236
+ """
237
+ if self.absorption_coef_sea_level == 0.0:
238
+ return 0.0
239
+ altitude_clamped = max(altitude, 0.0)
240
+ return self.absorption_coef_sea_level * math.exp(
241
+ -altitude_clamped / self.absorption_scale_height
242
+ )
243
+
244
+ # =========================================================================
245
+ # Absorption (Override base class default)
246
+ # =========================================================================
247
+
248
+ def get_absorption_coefficient(
249
+ self,
250
+ x: Union[float, NDArray[np.float64]],
251
+ y: Union[float, NDArray[np.float64]],
252
+ z: Union[float, NDArray[np.float64]],
253
+ wavelength: Union[float, NDArray[np.float64]],
254
+ ) -> Union[float, NDArray[np.float64]]:
255
+ """
256
+ Get absorption coefficient at position (x, y, z).
257
+
258
+ Absorption follows an exponential profile with altitude.
259
+
260
+ Parameters
261
+ ----------
262
+ x, y, z : float or ndarray
263
+ Position coordinates in meters.
264
+ wavelength : float or ndarray
265
+ Wavelength in meters.
266
+
267
+ Returns
268
+ -------
269
+ alpha : float or ndarray
270
+ Absorption coefficient in m⁻¹.
271
+ """
272
+ if self.absorption_coef_sea_level == 0.0:
273
+ if isinstance(x, np.ndarray):
274
+ return np.zeros_like(x)
275
+ return 0.0
276
+
277
+ altitude = self._compute_altitude(x, y, z)
278
+
279
+ if isinstance(altitude, np.ndarray):
280
+ alpha = self.absorption_coef_sea_level * np.exp(
281
+ -altitude / self.absorption_scale_height
282
+ )
283
+ else:
284
+ alpha = self.absorption_coef_sea_level * math.exp(
285
+ -altitude / self.absorption_scale_height
286
+ )
287
+
288
+ return alpha
289
+
290
+ # =========================================================================
291
+ # Atmosphere-Specific Utilities
292
+ # =========================================================================
293
+
294
+ def get_density_ratio(
295
+ self,
296
+ x: Union[float, NDArray[np.float64]],
297
+ y: Union[float, NDArray[np.float64]],
298
+ z: Union[float, NDArray[np.float64]],
299
+ ) -> Union[float, NDArray[np.float64]]:
300
+ """
301
+ Get atmospheric density ratio relative to sea level.
302
+
303
+ Parameters
304
+ ----------
305
+ x, y, z : float or ndarray
306
+ Position coordinates in meters.
307
+
308
+ Returns
309
+ -------
310
+ rho_ratio : float or ndarray
311
+ Density relative to sea level (dimensionless).
312
+ rho_ratio = rho(r) / rho_0 = exp(-altitude / H)
313
+ """
314
+ altitude = self._compute_altitude(x, y, z)
315
+
316
+ if isinstance(altitude, np.ndarray):
317
+ return np.exp(-altitude / self.scale_height)
318
+ else:
319
+ return math.exp(-altitude / self.scale_height)
320
+
321
+ def compute_curvature_radius(
322
+ self,
323
+ x: Union[float, NDArray[np.float64]],
324
+ y: Union[float, NDArray[np.float64]],
325
+ z: Union[float, NDArray[np.float64]],
326
+ wavelength: float = 532e-9,
327
+ ) -> Union[float, NDArray[np.float64]]:
328
+ """
329
+ Compute the radius of curvature for a ray at given position.
330
+
331
+ The radius of curvature is R_c = n / |∇n|.
332
+
333
+ Parameters
334
+ ----------
335
+ x, y, z : float or ndarray
336
+ Position coordinates in meters.
337
+ wavelength : float, optional
338
+ Wavelength in meters. Default is 532 nm.
339
+
340
+ Returns
341
+ -------
342
+ R_c : float or ndarray
343
+ Radius of curvature in meters.
344
+ Very large values indicate nearly straight propagation.
345
+ """
346
+ n = self.get_refractive_index(x, y, z, wavelength)
347
+ grad_mag = self.get_refractive_index_gradient_magnitude(x, y, z, wavelength)
348
+
349
+ if isinstance(grad_mag, np.ndarray):
350
+ grad_mag_safe = np.where(grad_mag < 1e-15, 1e-15, grad_mag)
351
+ return n / grad_mag_safe
352
+ else:
353
+ if grad_mag < 1e-15:
354
+ return float("inf")
355
+ return n / grad_mag
356
+
357
+ def __repr__(self) -> str:
358
+ """Return string representation."""
359
+ return (
360
+ f"<ExponentialAtmosphere("
361
+ f"n_sea_level={self.n_sea_level:.6f}, "
362
+ f"H={self.scale_height/1000:.1f} km, "
363
+ f"R_E={self.earth_radius/1000:.0f} km)>"
364
+ )
365
+
366
+
367
+ # Pre-configured atmosphere instances
368
+
369
+ STANDARD_ATMOSPHERE = ExponentialAtmosphere(
370
+ name="Standard Atmosphere",
371
+ n_sea_level=N_SEA_LEVEL,
372
+ scale_height=SCALE_HEIGHT_DEFAULT,
373
+ )
374
+ """
375
+ Standard exponential atmosphere model.
376
+
377
+ Uses typical values:
378
+ - Sea level refractive index: 1.000293
379
+ - Scale height: 8.5 km
380
+ - Earth radius: 6371 km
381
+ - Earth center at origin
382
+ """
383
+
384
+
385
+ def create_exponential_atmosphere(
386
+ n_sea_level: float = N_SEA_LEVEL,
387
+ scale_height: float = SCALE_HEIGHT_DEFAULT,
388
+ earth_radius: float = EARTH_RADIUS,
389
+ earth_center: tuple[float, float, float] = (0.0, 0.0, 0.0),
390
+ name: str = "Custom Exponential Atmosphere",
391
+ ) -> ExponentialAtmosphere:
392
+ """
393
+ Factory function to create an exponential atmosphere.
394
+
395
+ Parameters
396
+ ----------
397
+ n_sea_level : float, optional
398
+ Refractive index at sea level. Default is 1.000293.
399
+ scale_height : float, optional
400
+ Atmospheric scale height in meters. Default is 8500.0 m.
401
+ earth_radius : float, optional
402
+ Radius of Earth in meters. Default is 6,371,000 m.
403
+ earth_center : tuple of float, optional
404
+ Position of Earth's center. Default is (0, 0, 0).
405
+ name : str, optional
406
+ Descriptive name for the material.
407
+
408
+ Returns
409
+ -------
410
+ ExponentialAtmosphere
411
+ Configured atmosphere material.
412
+
413
+ Examples
414
+ --------
415
+ >>> # Mars-like atmosphere (thinner, smaller planet)
416
+ >>> mars_atmo = create_exponential_atmosphere(
417
+ ... n_sea_level=1.0001,
418
+ ... scale_height=11_100.0, # Mars scale height
419
+ ... earth_radius=3_389_500.0, # Mars radius
420
+ ... )
421
+ """
422
+ return ExponentialAtmosphere(
423
+ name=name,
424
+ n_sea_level=n_sea_level,
425
+ scale_height=scale_height,
426
+ earth_radius=earth_radius,
427
+ earth_center=earth_center,
428
+ )
429
+
430
+
431
+ __all__ = [
432
+ "ExponentialAtmosphere",
433
+ "STANDARD_ATMOSPHERE",
434
+ "create_exponential_atmosphere",
435
+ ]
@@ -0,0 +1,120 @@
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
+ Gaussian Lens Atmosphere Implementation
36
+
37
+ Atmosphere with a Gaussian refractive index perturbation (thermal lens).
38
+ """
39
+
40
+ import numpy as np
41
+
42
+ from ..base import GridInhomogeneousModel
43
+
44
+
45
+ class GaussianLensAtmosphere(GridInhomogeneousModel):
46
+ """
47
+ Atmosphere with a Gaussian refractive index perturbation (thermal lens).
48
+
49
+ Models a localized region of different refractive index, such as might
50
+ be caused by a heat source or thermal plume.
51
+
52
+ Parameters
53
+ ----------
54
+ bounds : tuple
55
+ ((x_min, x_max), (y_min, y_max), (z_min, z_max)) in meters
56
+ grid_resolution : tuple
57
+ (nx, ny, nz) grid points
58
+ lens_center : tuple
59
+ (x, y, z) center of the Gaussian perturbation
60
+ lens_sigma : tuple
61
+ (sigma_x, sigma_y, sigma_z) width parameters
62
+ delta_n : float
63
+ Peak refractive index change at center. Positive = higher n.
64
+ background_n : float
65
+ Background refractive index. Default 1.0003.
66
+
67
+ Example
68
+ -------
69
+ >>> # Create a thermal lens centered at (0, 0, 5000)
70
+ >>> lens = GaussianLensAtmosphere(
71
+ ... bounds=((-2000, 2000), (-2000, 2000), (0, 10000)),
72
+ ... lens_center=(0, 0, 5000),
73
+ ... lens_sigma=(500, 500, 1000),
74
+ ... delta_n=-0.00005, # Hot air = lower n
75
+ ... )
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ bounds: tuple[tuple[float, float], ...],
81
+ grid_resolution: tuple[int, int, int] = (64, 64, 64),
82
+ lens_center: tuple[float, float, float] = (0.0, 0.0, 5000.0),
83
+ lens_sigma: tuple[float, float, float] = (500.0, 500.0, 1000.0),
84
+ delta_n: float = -0.00005,
85
+ background_n: float = 1.0003,
86
+ ):
87
+ nx, ny, nz = grid_resolution
88
+ (x_min, x_max), (y_min, y_max), (z_min, z_max) = bounds
89
+
90
+ # Create coordinate grids
91
+ x = np.linspace(x_min, x_max, nx)
92
+ y = np.linspace(y_min, y_max, ny)
93
+ z = np.linspace(z_min, z_max, nz)
94
+ X, Y, Z = np.meshgrid(x, y, z, indexing="ij")
95
+
96
+ # Gaussian perturbation
97
+ cx, cy, cz = lens_center
98
+ sx, sy, sz = lens_sigma
99
+
100
+ gaussian = np.exp(
101
+ -((X - cx) ** 2 / (2 * sx**2))
102
+ - ((Y - cy) ** 2 / (2 * sy**2))
103
+ - ((Z - cz) ** 2 / (2 * sz**2))
104
+ )
105
+
106
+ n_grid = background_n + delta_n * gaussian
107
+
108
+ super().__init__(
109
+ name="Gaussian Lens Atmosphere",
110
+ n_grid=n_grid.astype(np.float32),
111
+ bounds=bounds,
112
+ )
113
+
114
+ self.lens_center = lens_center
115
+ self.lens_sigma = lens_sigma
116
+ self.delta_n = delta_n
117
+ self.background_n = background_n
118
+
119
+
120
+ __all__ = ["GaussianLensAtmosphere"]
@@ -0,0 +1,123 @@
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
+ Interpolated Data Atmosphere Implementation (CPU-only)
36
+
37
+ Atmosphere from interpolated 3D data using scipy's RegularGridInterpolator.
38
+ """
39
+
40
+ import numpy as np
41
+ from numpy.typing import NDArray
42
+
43
+ from ..base import FullInhomogeneousModel
44
+
45
+
46
+ class InterpolatedDataAtmosphere(FullInhomogeneousModel):
47
+ """
48
+ Atmosphere from interpolated 3D data (CPU-only).
49
+
50
+ Uses scipy's RegularGridInterpolator for smooth interpolation of
51
+ arbitrary 3D data. This is useful for weather model output or
52
+ measured atmospheric data.
53
+
54
+ Parameters
55
+ ----------
56
+ x_coords : ndarray
57
+ 1D array of x coordinates
58
+ y_coords : ndarray
59
+ 1D array of y coordinates
60
+ z_coords : ndarray
61
+ 1D array of z coordinates (altitude)
62
+ n_data : ndarray
63
+ 3D array of refractive index values, shape (nx, ny, nz)
64
+ fill_value : float
65
+ Value to return outside data bounds. Default 1.0.
66
+
67
+ Example
68
+ -------
69
+ >>> # Load or generate data
70
+ >>> x = np.linspace(-10000, 10000, 50)
71
+ >>> y = np.linspace(-10000, 10000, 50)
72
+ >>> z = np.linspace(0, 50000, 100)
73
+ >>> n_data = generate_weather_model_data(x, y, z)
74
+ >>> atm = InterpolatedDataAtmosphere(x, y, z, n_data)
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ x_coords: NDArray[np.float64],
80
+ y_coords: NDArray[np.float64],
81
+ z_coords: NDArray[np.float64],
82
+ n_data: NDArray[np.float64],
83
+ fill_value: float = 1.0,
84
+ ):
85
+ super().__init__(name="Interpolated Data Atmosphere")
86
+
87
+ from scipy.interpolate import RegularGridInterpolator
88
+
89
+ self._interpolator = RegularGridInterpolator(
90
+ (x_coords, y_coords, z_coords),
91
+ n_data,
92
+ method="linear",
93
+ bounds_error=False,
94
+ fill_value=fill_value,
95
+ )
96
+
97
+ self._bounds = (
98
+ (x_coords.min(), x_coords.max()),
99
+ (y_coords.min(), y_coords.max()),
100
+ (z_coords.min(), z_coords.max()),
101
+ )
102
+
103
+ def get_refractive_index(
104
+ self,
105
+ x: float | NDArray[np.float64],
106
+ y: float | NDArray[np.float64],
107
+ z: float | NDArray[np.float64],
108
+ wavelength: float | NDArray[np.float64],
109
+ ) -> float | NDArray[np.float64]:
110
+ """Interpolate n at arbitrary positions."""
111
+ x = np.atleast_1d(x)
112
+ y = np.atleast_1d(y)
113
+ z = np.atleast_1d(z)
114
+
115
+ points = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
116
+ result = self._interpolator(points)
117
+
118
+ if result.size == 1:
119
+ return float(result[0])
120
+ return result.reshape(x.shape)
121
+
122
+
123
+ __all__ = ["InterpolatedDataAtmosphere"]