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.
- lsurf/__init__.py +471 -0
- lsurf/analysis/__init__.py +107 -0
- lsurf/analysis/healpix_utils.py +418 -0
- lsurf/analysis/sphere_viz.py +1280 -0
- lsurf/cli/__init__.py +48 -0
- lsurf/cli/build.py +398 -0
- lsurf/cli/config_schema.py +318 -0
- lsurf/cli/gui_cmd.py +76 -0
- lsurf/cli/interactive.py +850 -0
- lsurf/cli/main.py +81 -0
- lsurf/cli/run.py +806 -0
- lsurf/detectors/__init__.py +266 -0
- lsurf/detectors/analysis.py +289 -0
- lsurf/detectors/base.py +284 -0
- lsurf/detectors/constant_size_rings.py +485 -0
- lsurf/detectors/directional.py +45 -0
- lsurf/detectors/extended/__init__.py +73 -0
- lsurf/detectors/extended/local_sphere.py +353 -0
- lsurf/detectors/extended/recording_sphere.py +368 -0
- lsurf/detectors/planar.py +45 -0
- lsurf/detectors/protocol.py +187 -0
- lsurf/detectors/recording_spheres.py +63 -0
- lsurf/detectors/results.py +1140 -0
- lsurf/detectors/small/__init__.py +79 -0
- lsurf/detectors/small/directional.py +330 -0
- lsurf/detectors/small/planar.py +401 -0
- lsurf/detectors/small/spherical.py +450 -0
- lsurf/detectors/spherical.py +45 -0
- lsurf/geometry/__init__.py +199 -0
- lsurf/geometry/builder.py +478 -0
- lsurf/geometry/cell.py +228 -0
- lsurf/geometry/cell_geometry.py +247 -0
- lsurf/geometry/detector_arrays.py +1785 -0
- lsurf/geometry/geometry.py +222 -0
- lsurf/geometry/surface_analysis.py +375 -0
- lsurf/geometry/validation.py +91 -0
- lsurf/gui/__init__.py +51 -0
- lsurf/gui/app.py +903 -0
- lsurf/gui/core/__init__.py +39 -0
- lsurf/gui/core/scene.py +343 -0
- lsurf/gui/core/simulation.py +264 -0
- lsurf/gui/renderers/__init__.py +40 -0
- lsurf/gui/renderers/ray_renderer.py +353 -0
- lsurf/gui/renderers/source_renderer.py +505 -0
- lsurf/gui/renderers/surface_renderer.py +477 -0
- lsurf/gui/views/__init__.py +48 -0
- lsurf/gui/views/config_editor.py +3199 -0
- lsurf/gui/views/properties.py +257 -0
- lsurf/gui/views/results.py +291 -0
- lsurf/gui/views/scene_tree.py +180 -0
- lsurf/gui/views/viewport_3d.py +555 -0
- lsurf/gui/views/visualizations.py +712 -0
- lsurf/materials/__init__.py +169 -0
- lsurf/materials/base/__init__.py +64 -0
- lsurf/materials/base/full_inhomogeneous.py +208 -0
- lsurf/materials/base/grid_inhomogeneous.py +319 -0
- lsurf/materials/base/homogeneous.py +342 -0
- lsurf/materials/base/material_field.py +527 -0
- lsurf/materials/base/simple_inhomogeneous.py +418 -0
- lsurf/materials/base/spectral_inhomogeneous.py +497 -0
- lsurf/materials/implementations/__init__.py +120 -0
- lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
- lsurf/materials/implementations/duct_atmosphere.py +390 -0
- lsurf/materials/implementations/exponential_atmosphere.py +435 -0
- lsurf/materials/implementations/gaussian_lens.py +120 -0
- lsurf/materials/implementations/interpolated_data.py +123 -0
- lsurf/materials/implementations/layered_atmosphere.py +134 -0
- lsurf/materials/implementations/linear_gradient.py +109 -0
- lsurf/materials/implementations/linsley_atmosphere.py +764 -0
- lsurf/materials/implementations/standard_materials.py +126 -0
- lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
- lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
- lsurf/materials/utils/__init__.py +77 -0
- lsurf/materials/utils/constants.py +45 -0
- lsurf/materials/utils/device_functions.py +117 -0
- lsurf/materials/utils/dispersion.py +160 -0
- lsurf/materials/utils/factories.py +142 -0
- lsurf/propagation/__init__.py +91 -0
- lsurf/propagation/detector_gpu.py +67 -0
- lsurf/propagation/gpu_device_rays.py +294 -0
- lsurf/propagation/kernels/__init__.py +175 -0
- lsurf/propagation/kernels/absorption/__init__.py +61 -0
- lsurf/propagation/kernels/absorption/grid.py +240 -0
- lsurf/propagation/kernels/absorption/simple.py +232 -0
- lsurf/propagation/kernels/absorption/spectral.py +410 -0
- lsurf/propagation/kernels/detection/__init__.py +64 -0
- lsurf/propagation/kernels/detection/protocol.py +102 -0
- lsurf/propagation/kernels/detection/spherical.py +255 -0
- lsurf/propagation/kernels/device_functions.py +790 -0
- lsurf/propagation/kernels/fresnel/__init__.py +64 -0
- lsurf/propagation/kernels/fresnel/protocol.py +97 -0
- lsurf/propagation/kernels/fresnel/standard.py +258 -0
- lsurf/propagation/kernels/intersection/__init__.py +79 -0
- lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
- lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
- lsurf/propagation/kernels/intersection/plane.py +166 -0
- lsurf/propagation/kernels/intersection/protocol.py +95 -0
- lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
- lsurf/propagation/kernels/intersection/sphere.py +190 -0
- lsurf/propagation/kernels/propagation/__init__.py +85 -0
- lsurf/propagation/kernels/propagation/grid.py +527 -0
- lsurf/propagation/kernels/propagation/protocol.py +105 -0
- lsurf/propagation/kernels/propagation/simple.py +460 -0
- lsurf/propagation/kernels/propagation/spectral.py +875 -0
- lsurf/propagation/kernels/registry.py +331 -0
- lsurf/propagation/kernels/surface/__init__.py +72 -0
- lsurf/propagation/kernels/surface/bisection.py +232 -0
- lsurf/propagation/kernels/surface/detection.py +402 -0
- lsurf/propagation/kernels/surface/reduction.py +166 -0
- lsurf/propagation/propagator_protocol.py +222 -0
- lsurf/propagation/propagators/__init__.py +101 -0
- lsurf/propagation/propagators/detector_handler.py +354 -0
- lsurf/propagation/propagators/factory.py +200 -0
- lsurf/propagation/propagators/fresnel_handler.py +305 -0
- lsurf/propagation/propagators/gpu_gradient.py +566 -0
- lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
- lsurf/propagation/propagators/gradient.py +429 -0
- lsurf/propagation/propagators/intersection_handler.py +327 -0
- lsurf/propagation/propagators/material_propagator.py +398 -0
- lsurf/propagation/propagators/signed_distance_handler.py +522 -0
- lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
- lsurf/propagation/propagators/surface_interaction.py +616 -0
- lsurf/propagation/propagators/surface_propagator.py +719 -0
- lsurf/py.typed +1 -0
- lsurf/simulation/__init__.py +70 -0
- lsurf/simulation/config.py +164 -0
- lsurf/simulation/orchestrator.py +462 -0
- lsurf/simulation/result.py +299 -0
- lsurf/simulation/simulation.py +262 -0
- lsurf/sources/__init__.py +128 -0
- lsurf/sources/base.py +264 -0
- lsurf/sources/collimated.py +252 -0
- lsurf/sources/custom.py +409 -0
- lsurf/sources/diverging.py +228 -0
- lsurf/sources/gaussian.py +272 -0
- lsurf/sources/parallel_from_positions.py +197 -0
- lsurf/sources/point.py +172 -0
- lsurf/sources/uniform_diverging.py +258 -0
- lsurf/surfaces/__init__.py +184 -0
- lsurf/surfaces/cpu/__init__.py +50 -0
- lsurf/surfaces/cpu/curved_wave.py +463 -0
- lsurf/surfaces/cpu/gerstner_wave.py +381 -0
- lsurf/surfaces/cpu/wave_params.py +118 -0
- lsurf/surfaces/gpu/__init__.py +72 -0
- lsurf/surfaces/gpu/annular_plane.py +453 -0
- lsurf/surfaces/gpu/bounded_plane.py +390 -0
- lsurf/surfaces/gpu/curved_wave.py +483 -0
- lsurf/surfaces/gpu/gerstner_wave.py +377 -0
- lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
- lsurf/surfaces/gpu/plane.py +299 -0
- lsurf/surfaces/gpu/recording_sphere.py +587 -0
- lsurf/surfaces/gpu/sphere.py +311 -0
- lsurf/surfaces/protocol.py +336 -0
- lsurf/surfaces/registry.py +373 -0
- lsurf/utilities/__init__.py +175 -0
- lsurf/utilities/detector_analysis.py +814 -0
- lsurf/utilities/fresnel.py +628 -0
- lsurf/utilities/interactions.py +1215 -0
- lsurf/utilities/propagation.py +602 -0
- lsurf/utilities/ray_data.py +532 -0
- lsurf/utilities/recording_sphere.py +745 -0
- lsurf/utilities/time_spread.py +463 -0
- lsurf/visualization/__init__.py +329 -0
- lsurf/visualization/absorption_plots.py +334 -0
- lsurf/visualization/atmospheric_plots.py +754 -0
- lsurf/visualization/common.py +348 -0
- lsurf/visualization/detector_plots.py +1350 -0
- lsurf/visualization/detector_sphere_plots.py +1173 -0
- lsurf/visualization/fresnel_plots.py +1061 -0
- lsurf/visualization/ocean_simulation_plots.py +999 -0
- lsurf/visualization/polarization_plots.py +916 -0
- lsurf/visualization/raytracing_plots.py +1521 -0
- lsurf/visualization/ring_detector_plots.py +1867 -0
- lsurf/visualization/time_spread_plots.py +531 -0
- lsurf-1.0.0.dist-info/METADATA +381 -0
- lsurf-1.0.0.dist-info/RECORD +180 -0
- lsurf-1.0.0.dist-info/WHEEL +5 -0
- lsurf-1.0.0.dist-info/entry_points.txt +2 -0
- lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
- 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"]
|