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,764 @@
|
|
|
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
|
+
Linsley Atmosphere Model
|
|
36
|
+
|
|
37
|
+
GPU-compatible implementation of Linsley's 5-layer atmospheric model with
|
|
38
|
+
wavelength-dependent refraction (dispersion) and optional extinction data.
|
|
39
|
+
|
|
40
|
+
Physics:
|
|
41
|
+
- Density: ρ(z) = (b[layer] / c[layer]) * exp(-z / c[layer])
|
|
42
|
+
- Refraction: n(z, λ) = 1 + 0.283e-3 * (ρ(z)/ρ₀) * (0.967 + 0.033 * (400/λ)^2.5)
|
|
43
|
+
- Extinction: From Elterman (1968) data tables + Rayleigh above 50km
|
|
44
|
+
|
|
45
|
+
References:
|
|
46
|
+
- J. Linsley, Proc. 15th ICRC, 12:89, 1977
|
|
47
|
+
- L. Elterman, Number Tech. Rep. AFCRL-68-0153, 1968
|
|
48
|
+
- K. Bernlohr, Astropart. Phys., 30:149-158, 2008
|
|
49
|
+
- Handbook of Chemistry and Physics, 67th Edition
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
from numpy.typing import NDArray
|
|
56
|
+
from scipy.interpolate import RegularGridInterpolator
|
|
57
|
+
|
|
58
|
+
from ..base.spectral_inhomogeneous import SpectralInhomogeneousModel
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# Linsley (1981) US Standard Atmosphere Parameterization
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
# Layer boundaries in km: 0-4, 4-10, 10-40, 40-100, 100+
|
|
65
|
+
LAYER_BOUNDARIES_KM = np.array([4.0, 10.0, 40.0, 100.0])
|
|
66
|
+
|
|
67
|
+
# Linsley parameters for each layer (5 layers)
|
|
68
|
+
# b values in g/cm²
|
|
69
|
+
ATMOS_B = np.array([1222.6562, 1144.9069, 1305.5948, 540.1778, 1.0])
|
|
70
|
+
# c values in cm (scale heights)
|
|
71
|
+
ATMOS_C = np.array([994186.38, 878153.55, 636143.04, 772170.16, 1e9])
|
|
72
|
+
|
|
73
|
+
# Physical constants
|
|
74
|
+
N_AVOGADRO = 6.02214076e23 # mol⁻¹
|
|
75
|
+
MM_AIR = 28.966 # g/mol (mean molecular mass of air)
|
|
76
|
+
Z_MAX_KM = 112.8 # Maximum altitude in model (km)
|
|
77
|
+
ALT_MAX_KM = 100.0 # Maximum altitude for extinction data (km)
|
|
78
|
+
|
|
79
|
+
# Standard conditions
|
|
80
|
+
N_STP_MINUS_1 = 0.283e-3 # (n - 1) at STP
|
|
81
|
+
|
|
82
|
+
# Earth radius in meters
|
|
83
|
+
EARTH_RADIUS_M = 6_371_000.0
|
|
84
|
+
|
|
85
|
+
# King factor for Rayleigh scattering (depolarization correction)
|
|
86
|
+
# From: Thalman et al., J. Quant. Spectrosc. Radiat. Transf., 147:171-177, 2014
|
|
87
|
+
FK_KING = 1.0608
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LinsleyAtmosphere(SpectralInhomogeneousModel):
|
|
91
|
+
"""
|
|
92
|
+
Linsley's 5-layer atmospheric model with wavelength dispersion.
|
|
93
|
+
|
|
94
|
+
This model provides:
|
|
95
|
+
- Atmospheric density profile from Linsley's parameterization
|
|
96
|
+
- Wavelength-dependent refractive index with dispersion
|
|
97
|
+
- Optional Elterman extinction coefficients for optical depth
|
|
98
|
+
- Rayleigh scattering coefficients
|
|
99
|
+
|
|
100
|
+
The refractive index formula combines:
|
|
101
|
+
- Baseline n(z) from density ratio: n = 1 + 0.283e-3 * (ρ/ρ₀)
|
|
102
|
+
- Wavelength correction: factor = 0.967 + 0.033 * (400/λ)^2.5
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
name : str
|
|
107
|
+
Name of the atmosphere model
|
|
108
|
+
earth_radius : float
|
|
109
|
+
Earth radius in meters. Default: 6,371,000
|
|
110
|
+
earth_center : tuple
|
|
111
|
+
Center of Earth (x, y, z) in meters. Default: (0, 0, 0)
|
|
112
|
+
altitude_range : tuple
|
|
113
|
+
(min, max) altitude in meters for LUT. Default: (0, 120,000)
|
|
114
|
+
altitude_resolution : int
|
|
115
|
+
Number of altitude samples in LUT. Default: 1200
|
|
116
|
+
wavelength_range : tuple
|
|
117
|
+
(min, max) wavelength in meters. Default: (270e-9, 4000e-9)
|
|
118
|
+
wavelength_resolution : int
|
|
119
|
+
Number of wavelength samples in LUT. Default: 100
|
|
120
|
+
extinction_data_file : str or None
|
|
121
|
+
Path to extinction coefficient data file. If None, uses default.
|
|
122
|
+
|
|
123
|
+
Example
|
|
124
|
+
-------
|
|
125
|
+
>>> atm = LinsleyAtmosphere()
|
|
126
|
+
>>> n = atm.n_at_altitude(10000, 550e-9) # n at 10km, 550nm
|
|
127
|
+
>>> rho = atm.density_at_altitude(5000) # density at 5km
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
name: str = "Linsley Atmosphere",
|
|
133
|
+
earth_radius: float = EARTH_RADIUS_M,
|
|
134
|
+
earth_center: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
135
|
+
altitude_range: tuple[float, float] = (0.0, 120_000.0),
|
|
136
|
+
altitude_resolution: int = 1200,
|
|
137
|
+
wavelength_range: tuple[float, float] = (270e-9, 4000e-9),
|
|
138
|
+
wavelength_resolution: int = 100,
|
|
139
|
+
extinction_data_file: str | Path | None = None,
|
|
140
|
+
):
|
|
141
|
+
super().__init__(
|
|
142
|
+
name=name,
|
|
143
|
+
center=earth_center,
|
|
144
|
+
reference_radius=earth_radius,
|
|
145
|
+
altitude_range=altitude_range,
|
|
146
|
+
altitude_resolution=altitude_resolution,
|
|
147
|
+
wavelength_range=wavelength_range,
|
|
148
|
+
wavelength_resolution=wavelength_resolution,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Precompute sea-level density for normalization
|
|
152
|
+
self._rho_sea_level = self._compute_density_km(0.0)
|
|
153
|
+
|
|
154
|
+
# Load extinction data
|
|
155
|
+
self._ext_wavelengths: NDArray[np.float64] | None = None
|
|
156
|
+
self._ext_altitudes: NDArray[np.float64] | None = None
|
|
157
|
+
self._ext_coefficients: NDArray[np.float64] | None = None
|
|
158
|
+
self._ext_interpolator: RegularGridInterpolator | None = None
|
|
159
|
+
|
|
160
|
+
if extinction_data_file is not None:
|
|
161
|
+
self._load_extinction_data(Path(extinction_data_file))
|
|
162
|
+
else:
|
|
163
|
+
# Try to load default data file
|
|
164
|
+
default_path = (
|
|
165
|
+
Path(__file__).parent
|
|
166
|
+
/ "data"
|
|
167
|
+
/ "alpha_values_typical_atmosphere_updated.txt"
|
|
168
|
+
)
|
|
169
|
+
if default_path.exists():
|
|
170
|
+
self._load_extinction_data(default_path)
|
|
171
|
+
|
|
172
|
+
# =========================================================================
|
|
173
|
+
# CORE PHYSICS: Density Profile
|
|
174
|
+
# =========================================================================
|
|
175
|
+
|
|
176
|
+
def _get_layer_index(
|
|
177
|
+
self, z_km: float | NDArray[np.float64]
|
|
178
|
+
) -> int | NDArray[np.int64]:
|
|
179
|
+
"""Get atmospheric layer index for altitude(s) in km."""
|
|
180
|
+
z_km = np.asarray(z_km)
|
|
181
|
+
scalar_input = z_km.ndim == 0
|
|
182
|
+
z_km = np.atleast_1d(z_km)
|
|
183
|
+
|
|
184
|
+
# Find layer: 0 for 0-4km, 1 for 4-10km, etc.
|
|
185
|
+
layer = np.searchsorted(LAYER_BOUNDARIES_KM, z_km, side="right")
|
|
186
|
+
layer = np.clip(layer, 0, 4)
|
|
187
|
+
|
|
188
|
+
return int(layer[0]) if scalar_input else layer
|
|
189
|
+
|
|
190
|
+
def _compute_density_km(
|
|
191
|
+
self, z_km: float | NDArray[np.float64]
|
|
192
|
+
) -> float | NDArray[np.float64]:
|
|
193
|
+
"""
|
|
194
|
+
Compute atmospheric density in g/cm³ for altitude in km.
|
|
195
|
+
|
|
196
|
+
Uses Linsley's 5-layer parameterization.
|
|
197
|
+
"""
|
|
198
|
+
z_km = np.asarray(z_km)
|
|
199
|
+
scalar_input = z_km.ndim == 0
|
|
200
|
+
z_km = np.atleast_1d(z_km)
|
|
201
|
+
|
|
202
|
+
layer = self._get_layer_index(z_km)
|
|
203
|
+
|
|
204
|
+
# Vectorized computation
|
|
205
|
+
b = ATMOS_B[layer]
|
|
206
|
+
c = ATMOS_C[layer]
|
|
207
|
+
|
|
208
|
+
# Convert altitude to cm for c values (which are in cm)
|
|
209
|
+
z_cm = z_km * 1e5
|
|
210
|
+
|
|
211
|
+
density = np.where(
|
|
212
|
+
layer < 4,
|
|
213
|
+
(b / c) * np.exp(-z_cm / c),
|
|
214
|
+
1.0 / c, # Layer 4 is constant (very thin)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Above maximum altitude, density is 0
|
|
218
|
+
density = np.where(z_km > Z_MAX_KM, 0.0, density)
|
|
219
|
+
|
|
220
|
+
return float(density[0]) if scalar_input else density
|
|
221
|
+
|
|
222
|
+
def density_at_altitude(self, altitude: float) -> float:
|
|
223
|
+
"""
|
|
224
|
+
Get atmospheric density at altitude.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
altitude : float
|
|
229
|
+
Altitude in meters
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
float
|
|
234
|
+
Density in g/cm³
|
|
235
|
+
"""
|
|
236
|
+
return float(self._compute_density_km(altitude / 1000.0))
|
|
237
|
+
|
|
238
|
+
def density_ratio(self, altitude: float) -> float:
|
|
239
|
+
"""
|
|
240
|
+
Get density ratio ρ(z)/ρ(0) at altitude.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
altitude : float
|
|
245
|
+
Altitude in meters
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
float
|
|
250
|
+
Density ratio (dimensionless)
|
|
251
|
+
"""
|
|
252
|
+
return self.density_at_altitude(altitude) / self._rho_sea_level
|
|
253
|
+
|
|
254
|
+
def number_density(self, altitude: float) -> float:
|
|
255
|
+
"""
|
|
256
|
+
Get particle number density at altitude.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
altitude : float
|
|
261
|
+
Altitude in meters
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
float
|
|
266
|
+
Number density in particles/cm³
|
|
267
|
+
"""
|
|
268
|
+
rho = self.density_at_altitude(altitude)
|
|
269
|
+
return rho * (N_AVOGADRO / MM_AIR)
|
|
270
|
+
|
|
271
|
+
# =========================================================================
|
|
272
|
+
# CORE PHYSICS: Refractive Index
|
|
273
|
+
# =========================================================================
|
|
274
|
+
|
|
275
|
+
def n_at_altitude(self, altitude: float, wavelength: float) -> float:
|
|
276
|
+
"""
|
|
277
|
+
Compute refractive index at altitude and wavelength.
|
|
278
|
+
|
|
279
|
+
Uses baseline approximation from Handbook of Chemistry and Physics
|
|
280
|
+
with wavelength correction from Bernlohr (2008).
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
altitude : float
|
|
285
|
+
Altitude in meters
|
|
286
|
+
wavelength : float
|
|
287
|
+
Wavelength in meters
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
float
|
|
292
|
+
Refractive index n
|
|
293
|
+
"""
|
|
294
|
+
# Density ratio
|
|
295
|
+
rho_ratio = self.density_ratio(altitude)
|
|
296
|
+
|
|
297
|
+
# Baseline n at this density
|
|
298
|
+
n_minus_1_base = N_STP_MINUS_1 * rho_ratio
|
|
299
|
+
|
|
300
|
+
# Wavelength correction factor
|
|
301
|
+
# Convert wavelength to nm for the formula
|
|
302
|
+
lambda_nm = wavelength * 1e9
|
|
303
|
+
correction = 0.967 + 0.033 * (400.0 / lambda_nm) ** 2.5
|
|
304
|
+
|
|
305
|
+
return 1.0 + n_minus_1_base * correction
|
|
306
|
+
|
|
307
|
+
def dn_dh_at_altitude(self, altitude: float, wavelength: float) -> float:
|
|
308
|
+
"""
|
|
309
|
+
Compute analytical derivative dn/dh at altitude and wavelength.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
altitude : float
|
|
314
|
+
Altitude in meters
|
|
315
|
+
wavelength : float
|
|
316
|
+
Wavelength in meters
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
float
|
|
321
|
+
Derivative dn/dh in m⁻¹
|
|
322
|
+
"""
|
|
323
|
+
# Get layer parameters
|
|
324
|
+
z_km = altitude / 1000.0
|
|
325
|
+
|
|
326
|
+
# Handle altitude above model
|
|
327
|
+
if z_km > Z_MAX_KM:
|
|
328
|
+
return 0.0
|
|
329
|
+
|
|
330
|
+
layer = self._get_layer_index(z_km)
|
|
331
|
+
|
|
332
|
+
# For layer 4 (constant density), derivative is 0
|
|
333
|
+
if layer >= 4:
|
|
334
|
+
return 0.0
|
|
335
|
+
|
|
336
|
+
c_cm = ATMOS_C[layer]
|
|
337
|
+
c_m = c_cm / 100.0 # Convert to meters
|
|
338
|
+
|
|
339
|
+
# d(n-1)/dh = -(n-1) / c for exponential atmosphere
|
|
340
|
+
n = self.n_at_altitude(altitude, wavelength)
|
|
341
|
+
n_minus_1 = n - 1.0
|
|
342
|
+
|
|
343
|
+
# Derivative: dn/dh = d(n-1)/dh = -(n-1) / c
|
|
344
|
+
return -n_minus_1 / c_m
|
|
345
|
+
|
|
346
|
+
def alpha_at_altitude(self, altitude: float, wavelength: float) -> float:
|
|
347
|
+
"""
|
|
348
|
+
Return absorption/extinction coefficient at altitude and wavelength.
|
|
349
|
+
|
|
350
|
+
Uses Elterman extinction data if available, otherwise Rayleigh scattering.
|
|
351
|
+
This enables Beer-Lambert absorption during ray propagation.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
altitude : float
|
|
356
|
+
Altitude in meters
|
|
357
|
+
wavelength : float
|
|
358
|
+
Wavelength in meters
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
float
|
|
363
|
+
Extinction coefficient α in m⁻¹
|
|
364
|
+
"""
|
|
365
|
+
return self.get_extinction_coefficient(altitude, wavelength)
|
|
366
|
+
|
|
367
|
+
# =========================================================================
|
|
368
|
+
# EXTINCTION AND SCATTERING
|
|
369
|
+
# =========================================================================
|
|
370
|
+
|
|
371
|
+
def _load_extinction_data(self, file_path: Path) -> None:
|
|
372
|
+
"""
|
|
373
|
+
Load Elterman (1968) extinction coefficients.
|
|
374
|
+
|
|
375
|
+
File format:
|
|
376
|
+
- Row 1: Wavelengths in nm
|
|
377
|
+
- Row 2: Altitudes in km
|
|
378
|
+
- Rows 3+: Extinction coefficients in km⁻¹ for each wavelength
|
|
379
|
+
"""
|
|
380
|
+
# Load wavelengths (nm)
|
|
381
|
+
wavelengths = np.loadtxt(file_path, delimiter=",", max_rows=1)
|
|
382
|
+
|
|
383
|
+
# Load altitudes (km)
|
|
384
|
+
altitudes = np.loadtxt(file_path, delimiter=",", skiprows=1, max_rows=1)
|
|
385
|
+
|
|
386
|
+
# Load extinction coefficients (km⁻¹)
|
|
387
|
+
coefficients = np.loadtxt(file_path, delimiter=",", skiprows=2)
|
|
388
|
+
|
|
389
|
+
# Remove any 0 values (replace with small number)
|
|
390
|
+
coefficients[coefficients == 0.0] = 1e-10
|
|
391
|
+
|
|
392
|
+
# Convert to m⁻¹ and take log for smooth interpolation
|
|
393
|
+
# Store log(alpha) to enable interpolation in log space
|
|
394
|
+
self._ext_coefficients_log = np.log(coefficients / 1000.0) # m⁻¹
|
|
395
|
+
|
|
396
|
+
# Store raw data
|
|
397
|
+
self._ext_wavelengths = wavelengths # nm
|
|
398
|
+
self._ext_altitudes = altitudes # km
|
|
399
|
+
self._ext_coefficients = coefficients / 1000.0 # m⁻¹
|
|
400
|
+
|
|
401
|
+
# Create interpolator (wavelength, altitude) -> log(extinction)
|
|
402
|
+
# Note: coefficients shape is (n_wavelengths, n_altitudes)
|
|
403
|
+
self._ext_interpolator = RegularGridInterpolator(
|
|
404
|
+
(wavelengths, altitudes),
|
|
405
|
+
self._ext_coefficients_log,
|
|
406
|
+
method="linear",
|
|
407
|
+
bounds_error=False,
|
|
408
|
+
fill_value=-20.0, # exp(-20) ≈ 0
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def get_extinction_coefficient(
|
|
412
|
+
self, altitude: float, wavelength: float, use_rayleigh_above_50km: bool = True
|
|
413
|
+
) -> float:
|
|
414
|
+
"""
|
|
415
|
+
Get extinction coefficient from Elterman data.
|
|
416
|
+
|
|
417
|
+
Parameters
|
|
418
|
+
----------
|
|
419
|
+
altitude : float
|
|
420
|
+
Altitude in meters
|
|
421
|
+
wavelength : float
|
|
422
|
+
Wavelength in meters
|
|
423
|
+
use_rayleigh_above_50km : bool
|
|
424
|
+
If True, use Rayleigh scattering above 50km instead of data
|
|
425
|
+
|
|
426
|
+
Returns
|
|
427
|
+
-------
|
|
428
|
+
float
|
|
429
|
+
Extinction coefficient in m⁻¹
|
|
430
|
+
"""
|
|
431
|
+
z_km = altitude / 1000.0
|
|
432
|
+
lambda_nm = wavelength * 1e9
|
|
433
|
+
|
|
434
|
+
# Above max altitude, no extinction
|
|
435
|
+
if z_km > ALT_MAX_KM:
|
|
436
|
+
return 0.0
|
|
437
|
+
|
|
438
|
+
# Above 50km, optionally use Rayleigh only
|
|
439
|
+
if use_rayleigh_above_50km and z_km > 50.0:
|
|
440
|
+
return self.rayleigh_extinction(altitude, wavelength)
|
|
441
|
+
|
|
442
|
+
# Interpolate from data
|
|
443
|
+
if self._ext_interpolator is not None:
|
|
444
|
+
log_alpha = float(self._ext_interpolator((lambda_nm, z_km)))
|
|
445
|
+
return np.exp(log_alpha)
|
|
446
|
+
|
|
447
|
+
# Fallback to Rayleigh if no data
|
|
448
|
+
return self.rayleigh_extinction(altitude, wavelength)
|
|
449
|
+
|
|
450
|
+
def rayleigh_extinction(self, altitude: float, wavelength: float) -> float:
|
|
451
|
+
"""
|
|
452
|
+
Compute Rayleigh scattering extinction coefficient.
|
|
453
|
+
|
|
454
|
+
From Lord Rayleigh with King factor correction.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
altitude : float
|
|
459
|
+
Altitude in meters
|
|
460
|
+
wavelength : float
|
|
461
|
+
Wavelength in meters
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
float
|
|
466
|
+
Rayleigh extinction coefficient in m⁻¹
|
|
467
|
+
"""
|
|
468
|
+
# Get refractive index and number density
|
|
469
|
+
n = self.n_at_altitude(altitude, wavelength)
|
|
470
|
+
n_squared = n * n
|
|
471
|
+
N = self.number_density(altitude) * 1e6 # Convert cm⁻³ to m⁻³
|
|
472
|
+
|
|
473
|
+
# Avoid division by zero
|
|
474
|
+
if N < 1e-10:
|
|
475
|
+
return 0.0
|
|
476
|
+
|
|
477
|
+
# Rayleigh formula with King factor
|
|
478
|
+
alpha_rayleigh = (
|
|
479
|
+
24.0
|
|
480
|
+
* np.pi**3
|
|
481
|
+
* ((n_squared - 1) / (n_squared + 2)) ** 2
|
|
482
|
+
* FK_KING
|
|
483
|
+
/ (wavelength**4 * N)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return float(alpha_rayleigh)
|
|
487
|
+
|
|
488
|
+
def get_scattering_coefficient(
|
|
489
|
+
self,
|
|
490
|
+
x: float | NDArray[np.float64],
|
|
491
|
+
y: float | NDArray[np.float64],
|
|
492
|
+
z: float | NDArray[np.float64],
|
|
493
|
+
wavelength: float | NDArray[np.float64],
|
|
494
|
+
) -> float | NDArray[np.float64]:
|
|
495
|
+
"""
|
|
496
|
+
Get scattering coefficient at position.
|
|
497
|
+
|
|
498
|
+
Uses extinction data below 50km, Rayleigh above.
|
|
499
|
+
"""
|
|
500
|
+
altitude = self._compute_altitude(x, y, z)
|
|
501
|
+
|
|
502
|
+
if isinstance(altitude, np.ndarray):
|
|
503
|
+
wavelength = np.atleast_1d(wavelength)
|
|
504
|
+
if altitude.shape != wavelength.shape:
|
|
505
|
+
altitude, wavelength = np.broadcast_arrays(altitude, wavelength)
|
|
506
|
+
return np.array(
|
|
507
|
+
[
|
|
508
|
+
self.get_extinction_coefficient(float(h), float(wl))
|
|
509
|
+
for h, wl in zip(altitude.flat, wavelength.flat)
|
|
510
|
+
]
|
|
511
|
+
).reshape(altitude.shape)
|
|
512
|
+
|
|
513
|
+
return self.get_extinction_coefficient(float(altitude), float(wavelength))
|
|
514
|
+
|
|
515
|
+
# =========================================================================
|
|
516
|
+
# OPTICAL DEPTH CALCULATION
|
|
517
|
+
# =========================================================================
|
|
518
|
+
|
|
519
|
+
def optical_depth_vertical(
|
|
520
|
+
self,
|
|
521
|
+
z1: float,
|
|
522
|
+
z2: float,
|
|
523
|
+
wavelength: float,
|
|
524
|
+
num_points: int = 100,
|
|
525
|
+
use_rayleigh_above_50km: bool = True,
|
|
526
|
+
) -> float:
|
|
527
|
+
"""
|
|
528
|
+
Compute optical depth for vertical path between two altitudes.
|
|
529
|
+
|
|
530
|
+
Parameters
|
|
531
|
+
----------
|
|
532
|
+
z1 : float
|
|
533
|
+
Starting altitude in meters (lower)
|
|
534
|
+
z2 : float
|
|
535
|
+
Ending altitude in meters (upper)
|
|
536
|
+
wavelength : float
|
|
537
|
+
Wavelength in meters
|
|
538
|
+
num_points : int
|
|
539
|
+
Number of integration points
|
|
540
|
+
use_rayleigh_above_50km : bool
|
|
541
|
+
Use Rayleigh scattering above 50km
|
|
542
|
+
|
|
543
|
+
Returns
|
|
544
|
+
-------
|
|
545
|
+
float
|
|
546
|
+
Optical depth (dimensionless)
|
|
547
|
+
"""
|
|
548
|
+
# Integration points
|
|
549
|
+
altitudes = np.linspace(z1, z2, num_points)
|
|
550
|
+
|
|
551
|
+
# Get extinction at each point
|
|
552
|
+
extinctions = np.array(
|
|
553
|
+
[
|
|
554
|
+
self.get_extinction_coefficient(h, wavelength, use_rayleigh_above_50km)
|
|
555
|
+
for h in altitudes
|
|
556
|
+
]
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Trapezoidal integration
|
|
560
|
+
return float(np.trapezoid(extinctions, altitudes))
|
|
561
|
+
|
|
562
|
+
def optical_depth_slant(
|
|
563
|
+
self,
|
|
564
|
+
positions: NDArray[np.float64],
|
|
565
|
+
wavelength: float,
|
|
566
|
+
use_rayleigh_above_50km: bool = True,
|
|
567
|
+
) -> NDArray[np.float64]:
|
|
568
|
+
"""
|
|
569
|
+
Compute cumulative optical depth along a slant path.
|
|
570
|
+
|
|
571
|
+
Parameters
|
|
572
|
+
----------
|
|
573
|
+
positions : ndarray of shape (N, 3)
|
|
574
|
+
Positions along the path in meters
|
|
575
|
+
wavelength : float
|
|
576
|
+
Wavelength in meters
|
|
577
|
+
use_rayleigh_above_50km : bool
|
|
578
|
+
Use Rayleigh scattering above 50km
|
|
579
|
+
|
|
580
|
+
Returns
|
|
581
|
+
-------
|
|
582
|
+
ndarray of shape (N,)
|
|
583
|
+
Cumulative optical depth at each position
|
|
584
|
+
"""
|
|
585
|
+
# Compute altitudes
|
|
586
|
+
altitudes = self._compute_altitude(
|
|
587
|
+
positions[:, 0], positions[:, 1], positions[:, 2]
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Get extinctions
|
|
591
|
+
extinctions = np.array(
|
|
592
|
+
[
|
|
593
|
+
self.get_extinction_coefficient(
|
|
594
|
+
float(h), wavelength, use_rayleigh_above_50km
|
|
595
|
+
)
|
|
596
|
+
for h in altitudes
|
|
597
|
+
]
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Compute path lengths between points
|
|
601
|
+
diffs = np.diff(positions, axis=0)
|
|
602
|
+
path_lengths = np.sqrt(np.sum(diffs**2, axis=1))
|
|
603
|
+
|
|
604
|
+
# Cumulative integration using midpoint rule
|
|
605
|
+
mid_extinctions = (extinctions[:-1] + extinctions[1:]) / 2
|
|
606
|
+
optical_depths = np.zeros(len(positions))
|
|
607
|
+
optical_depths[1:] = np.cumsum(mid_extinctions * path_lengths)
|
|
608
|
+
|
|
609
|
+
return optical_depths
|
|
610
|
+
|
|
611
|
+
# =========================================================================
|
|
612
|
+
# ADDITIONAL ATMOSPHERIC PROPERTIES
|
|
613
|
+
# =========================================================================
|
|
614
|
+
|
|
615
|
+
def moliere_radius(self, altitude: float) -> float:
|
|
616
|
+
"""
|
|
617
|
+
Compute Molière radius at altitude.
|
|
618
|
+
|
|
619
|
+
The Molière radius characterizes multiple Coulomb scattering.
|
|
620
|
+
|
|
621
|
+
Parameters
|
|
622
|
+
----------
|
|
623
|
+
altitude : float
|
|
624
|
+
Altitude in meters
|
|
625
|
+
|
|
626
|
+
Returns
|
|
627
|
+
-------
|
|
628
|
+
float
|
|
629
|
+
Molière radius in meters
|
|
630
|
+
"""
|
|
631
|
+
# Molière radius: r_M = X_0 / (rho * 9.6 MeV/c)
|
|
632
|
+
# X_0 for air ≈ 36.62 g/cm²
|
|
633
|
+
X_0_AIR = 36.62 # g/cm²
|
|
634
|
+
rho = self.density_at_altitude(altitude) # g/cm³
|
|
635
|
+
|
|
636
|
+
if rho < 1e-20:
|
|
637
|
+
return float("inf")
|
|
638
|
+
|
|
639
|
+
# r_M in cm, convert to m
|
|
640
|
+
r_M_cm = X_0_AIR / (rho * 9.6)
|
|
641
|
+
return r_M_cm / 100.0
|
|
642
|
+
|
|
643
|
+
def scale_height(self, altitude: float) -> float:
|
|
644
|
+
"""
|
|
645
|
+
Get atmospheric scale height at altitude.
|
|
646
|
+
|
|
647
|
+
Parameters
|
|
648
|
+
----------
|
|
649
|
+
altitude : float
|
|
650
|
+
Altitude in meters
|
|
651
|
+
|
|
652
|
+
Returns
|
|
653
|
+
-------
|
|
654
|
+
float
|
|
655
|
+
Scale height in meters
|
|
656
|
+
"""
|
|
657
|
+
z_km = altitude / 1000.0
|
|
658
|
+
|
|
659
|
+
if z_km > Z_MAX_KM:
|
|
660
|
+
return float("inf")
|
|
661
|
+
|
|
662
|
+
layer = self._get_layer_index(z_km)
|
|
663
|
+
|
|
664
|
+
if layer >= 4:
|
|
665
|
+
return float("inf")
|
|
666
|
+
|
|
667
|
+
# c is scale height in cm, convert to m
|
|
668
|
+
return ATMOS_C[layer] / 100.0
|
|
669
|
+
|
|
670
|
+
def pressure_at_altitude(self, altitude: float) -> float:
|
|
671
|
+
"""
|
|
672
|
+
Estimate atmospheric pressure at altitude.
|
|
673
|
+
|
|
674
|
+
Uses ideal gas approximation: P ∝ ρ * T, with isothermal assumption.
|
|
675
|
+
|
|
676
|
+
Parameters
|
|
677
|
+
----------
|
|
678
|
+
altitude : float
|
|
679
|
+
Altitude in meters
|
|
680
|
+
|
|
681
|
+
Returns
|
|
682
|
+
-------
|
|
683
|
+
float
|
|
684
|
+
Pressure in Pa (approximate)
|
|
685
|
+
"""
|
|
686
|
+
# Sea level pressure
|
|
687
|
+
P_0 = 101325.0 # Pa
|
|
688
|
+
|
|
689
|
+
# Pressure ratio ≈ density ratio for isothermal atmosphere
|
|
690
|
+
return P_0 * self.density_ratio(altitude)
|
|
691
|
+
|
|
692
|
+
def temperature_at_altitude(self, altitude: float) -> float:
|
|
693
|
+
"""
|
|
694
|
+
Estimate temperature at altitude using standard lapse rate.
|
|
695
|
+
|
|
696
|
+
Parameters
|
|
697
|
+
----------
|
|
698
|
+
altitude : float
|
|
699
|
+
Altitude in meters
|
|
700
|
+
|
|
701
|
+
Returns
|
|
702
|
+
-------
|
|
703
|
+
float
|
|
704
|
+
Temperature in Kelvin (approximate)
|
|
705
|
+
"""
|
|
706
|
+
z_km = altitude / 1000.0
|
|
707
|
+
|
|
708
|
+
# Standard atmosphere temperature profile (simplified)
|
|
709
|
+
if z_km < 11.0:
|
|
710
|
+
# Troposphere: -6.5 K/km lapse rate
|
|
711
|
+
return 288.15 - 6.5 * z_km
|
|
712
|
+
elif z_km < 20.0:
|
|
713
|
+
# Tropopause: isothermal
|
|
714
|
+
return 216.65
|
|
715
|
+
elif z_km < 47.0:
|
|
716
|
+
# Stratosphere: +1-3 K/km
|
|
717
|
+
return 216.65 + 1.0 * (z_km - 20.0)
|
|
718
|
+
else:
|
|
719
|
+
# Upper atmosphere: complex, use constant
|
|
720
|
+
return 270.0
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# =============================================================================
|
|
724
|
+
# Factory Functions
|
|
725
|
+
# =============================================================================
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def create_linsley_atmosphere(
|
|
729
|
+
earth_center: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
730
|
+
earth_radius: float = EARTH_RADIUS_M,
|
|
731
|
+
with_extinction: bool = True,
|
|
732
|
+
) -> LinsleyAtmosphere:
|
|
733
|
+
"""
|
|
734
|
+
Create a LinsleyAtmosphere with standard parameters.
|
|
735
|
+
|
|
736
|
+
Parameters
|
|
737
|
+
----------
|
|
738
|
+
earth_center : tuple
|
|
739
|
+
Center of Earth in meters
|
|
740
|
+
earth_radius : float
|
|
741
|
+
Earth radius in meters
|
|
742
|
+
with_extinction : bool
|
|
743
|
+
Whether to load extinction data
|
|
744
|
+
|
|
745
|
+
Returns
|
|
746
|
+
-------
|
|
747
|
+
LinsleyAtmosphere
|
|
748
|
+
Configured atmosphere model
|
|
749
|
+
"""
|
|
750
|
+
extinction_file = None
|
|
751
|
+
if with_extinction:
|
|
752
|
+
default_path = (
|
|
753
|
+
Path(__file__).parent
|
|
754
|
+
/ "data"
|
|
755
|
+
/ "alpha_values_typical_atmosphere_updated.txt"
|
|
756
|
+
)
|
|
757
|
+
if default_path.exists():
|
|
758
|
+
extinction_file = default_path
|
|
759
|
+
|
|
760
|
+
return LinsleyAtmosphere(
|
|
761
|
+
earth_center=earth_center,
|
|
762
|
+
earth_radius=earth_radius,
|
|
763
|
+
extinction_data_file=extinction_file,
|
|
764
|
+
)
|