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,390 @@
|
|
|
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
|
+
Atmospheric Duct Material
|
|
36
|
+
|
|
37
|
+
Implements an exponential atmosphere with a refractive index duct (inversion layer).
|
|
38
|
+
The duct creates a localized modification to the refractive index profile that can
|
|
39
|
+
trap or guide electromagnetic waves.
|
|
40
|
+
|
|
41
|
+
The refractive index follows:
|
|
42
|
+
n(h) = 1 + delta_n * exp(-h / H) * D(h)
|
|
43
|
+
|
|
44
|
+
where D(h) is a duct factor based on hyperbolic tangent functions that creates
|
|
45
|
+
a localized perturbation at a specified altitude.
|
|
46
|
+
|
|
47
|
+
This model is appropriate for studying:
|
|
48
|
+
- Atmospheric ducting phenomena
|
|
49
|
+
- Anomalous radio propagation
|
|
50
|
+
- Mirage formation in inversion layers
|
|
51
|
+
- Over-the-horizon radar propagation
|
|
52
|
+
|
|
53
|
+
References
|
|
54
|
+
----------
|
|
55
|
+
.. [1] Hitney, H.V. et al. (1985). "Tropospheric radio propagation assessment."
|
|
56
|
+
Proceedings of the IEEE, 73(2), 265-283.
|
|
57
|
+
.. [2] Turton, J.D. et al. (1988). "The structure of evaporation ducts."
|
|
58
|
+
Radio Science, 23(4), 519-528.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
import math
|
|
62
|
+
|
|
63
|
+
import numpy as np
|
|
64
|
+
|
|
65
|
+
from ..base import SimpleInhomogeneousModel
|
|
66
|
+
from ..utils.constants import EARTH_RADIUS, SCALE_HEIGHT_DEFAULT, N_SEA_LEVEL
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DuctAtmosphere(SimpleInhomogeneousModel):
|
|
70
|
+
"""
|
|
71
|
+
Exponential atmosphere with a refractive index duct layer.
|
|
72
|
+
|
|
73
|
+
Models an atmosphere where air density decreases exponentially with
|
|
74
|
+
altitude, modified by a duct (inversion layer) at a specified altitude.
|
|
75
|
+
The duct creates a localized region where the refractive index gradient
|
|
76
|
+
is modified, which can trap or guide electromagnetic waves.
|
|
77
|
+
|
|
78
|
+
The duct is modeled using hyperbolic tangent functions to create smooth
|
|
79
|
+
transitions at the duct boundaries.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
name : str, optional
|
|
84
|
+
Descriptive name for this material. Default is "Duct Atmosphere".
|
|
85
|
+
n_sea_level : float, optional
|
|
86
|
+
Refractive index at sea level. Default is 1.000293.
|
|
87
|
+
scale_height : float, optional
|
|
88
|
+
Atmospheric scale height H in meters. Default is 8500.0 m.
|
|
89
|
+
earth_radius : float, optional
|
|
90
|
+
Radius of Earth in meters. Default is 6,371,000 m.
|
|
91
|
+
earth_center : tuple of float, optional
|
|
92
|
+
Position of Earth's center in meters. Default is (0, 0, 0).
|
|
93
|
+
duct_center : float, optional
|
|
94
|
+
Center altitude of the duct layer in meters. Default is 0.0.
|
|
95
|
+
duct_width : float, optional
|
|
96
|
+
Width of the duct layer in meters. Default is 100.0.
|
|
97
|
+
duct_intensity : float, optional
|
|
98
|
+
Intensity of the duct effect (0 = no duct, 1 = full strength).
|
|
99
|
+
Default is 0.0.
|
|
100
|
+
duct_sharpness : float, optional
|
|
101
|
+
Sharpness of duct boundaries (0 = gradual, 1 = sharp).
|
|
102
|
+
Default is 0.5.
|
|
103
|
+
|
|
104
|
+
Examples
|
|
105
|
+
--------
|
|
106
|
+
>>> # Surface duct at 100m altitude
|
|
107
|
+
>>> atmosphere = DuctAtmosphere(
|
|
108
|
+
... duct_center=100.0,
|
|
109
|
+
... duct_width=50.0,
|
|
110
|
+
... duct_intensity=0.5,
|
|
111
|
+
... duct_sharpness=0.8,
|
|
112
|
+
... )
|
|
113
|
+
|
|
114
|
+
>>> # Elevated duct for radio propagation study
|
|
115
|
+
>>> atmosphere = DuctAtmosphere(
|
|
116
|
+
... duct_center=1000.0,
|
|
117
|
+
... duct_width=200.0,
|
|
118
|
+
... duct_intensity=0.8,
|
|
119
|
+
... duct_sharpness=0.9,
|
|
120
|
+
... earth_center=(0.0, 0.0, -EARTH_RADIUS),
|
|
121
|
+
... )
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
name: str = "Duct Atmosphere",
|
|
127
|
+
n_sea_level: float = N_SEA_LEVEL,
|
|
128
|
+
scale_height: float = SCALE_HEIGHT_DEFAULT,
|
|
129
|
+
earth_radius: float = EARTH_RADIUS,
|
|
130
|
+
earth_center: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
131
|
+
duct_center: float = 0.0,
|
|
132
|
+
duct_width: float = 100.0,
|
|
133
|
+
duct_intensity: float = 0.0,
|
|
134
|
+
duct_sharpness: float = 0.5,
|
|
135
|
+
):
|
|
136
|
+
# Validate inputs
|
|
137
|
+
if scale_height <= 0:
|
|
138
|
+
raise ValueError(f"Scale height must be positive, got {scale_height}")
|
|
139
|
+
if n_sea_level < 1.0:
|
|
140
|
+
raise ValueError(f"Refractive index must be >= 1.0, got {n_sea_level}")
|
|
141
|
+
if duct_width <= 0:
|
|
142
|
+
raise ValueError(f"Duct width must be positive, got {duct_width}")
|
|
143
|
+
if not 0.0 <= duct_sharpness <= 1.0:
|
|
144
|
+
raise ValueError(f"Duct sharpness must be in [0, 1], got {duct_sharpness}")
|
|
145
|
+
|
|
146
|
+
# Initialize base class
|
|
147
|
+
super().__init__(
|
|
148
|
+
name=name,
|
|
149
|
+
center=earth_center,
|
|
150
|
+
reference_radius=earth_radius,
|
|
151
|
+
altitude_range=(0.0, 15 * scale_height),
|
|
152
|
+
lut_resolution=10000,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Store atmosphere parameters
|
|
156
|
+
self.n_sea_level = n_sea_level
|
|
157
|
+
self.scale_height = scale_height
|
|
158
|
+
self.earth_radius = earth_radius
|
|
159
|
+
self.earth_center = earth_center
|
|
160
|
+
|
|
161
|
+
# Store duct parameters
|
|
162
|
+
self.duct_center = duct_center
|
|
163
|
+
self.duct_width = duct_width
|
|
164
|
+
self.duct_intensity = duct_intensity
|
|
165
|
+
self.duct_sharpness = duct_sharpness
|
|
166
|
+
|
|
167
|
+
# Precompute refractivity
|
|
168
|
+
self.delta_n = n_sea_level - 1.0
|
|
169
|
+
|
|
170
|
+
def _compute_duct_factor(self, altitude: float) -> float:
|
|
171
|
+
"""
|
|
172
|
+
Compute the duct modification factor at given altitude.
|
|
173
|
+
|
|
174
|
+
The duct factor modifies the exponential profile to create a
|
|
175
|
+
localized perturbation. Uses hyperbolic tangent functions for
|
|
176
|
+
smooth transitions.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
altitude : float
|
|
181
|
+
Altitude above Earth's surface in meters.
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
float
|
|
186
|
+
Duct factor (1.0 = no modification, <1.0 = reduced refractivity).
|
|
187
|
+
"""
|
|
188
|
+
if self.duct_intensity == 0.0:
|
|
189
|
+
return 1.0
|
|
190
|
+
|
|
191
|
+
# Compute transition width based on sharpness
|
|
192
|
+
alpha_min = self.duct_width / 50
|
|
193
|
+
alpha_max = self.duct_width / 2
|
|
194
|
+
alpha = alpha_max * (1 - self.duct_sharpness) + alpha_min * self.duct_sharpness
|
|
195
|
+
|
|
196
|
+
# Normalization factor
|
|
197
|
+
norm = 2 * np.tanh(self.duct_width / (2 * alpha))
|
|
198
|
+
|
|
199
|
+
# Duct profile using difference of tanh functions
|
|
200
|
+
lower_edge = (altitude - self.duct_center + self.duct_width / 2) / alpha
|
|
201
|
+
upper_edge = (altitude - self.duct_center - self.duct_width / 2) / alpha
|
|
202
|
+
duct_factor = 1 - self.duct_intensity / norm * (
|
|
203
|
+
np.tanh(lower_edge) - np.tanh(upper_edge)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return float(duct_factor)
|
|
207
|
+
|
|
208
|
+
def _compute_duct_factor_derivative(self, altitude: float) -> float:
|
|
209
|
+
"""
|
|
210
|
+
Compute the derivative of the duct factor with respect to altitude.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
altitude : float
|
|
215
|
+
Altitude above Earth's surface in meters.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
float
|
|
220
|
+
Derivative dD/dh of the duct factor.
|
|
221
|
+
"""
|
|
222
|
+
if self.duct_intensity == 0.0:
|
|
223
|
+
return 0.0
|
|
224
|
+
|
|
225
|
+
# Compute transition width based on sharpness
|
|
226
|
+
alpha_min = self.duct_width / 50
|
|
227
|
+
alpha_max = self.duct_width / 2
|
|
228
|
+
alpha = alpha_max * (1 - self.duct_sharpness) + alpha_min * self.duct_sharpness
|
|
229
|
+
|
|
230
|
+
# Normalization factor
|
|
231
|
+
norm = 2 * np.tanh(self.duct_width / (2 * alpha))
|
|
232
|
+
|
|
233
|
+
# Derivative of tanh is sech^2
|
|
234
|
+
lower_edge = (altitude - self.duct_center + self.duct_width / 2) / alpha
|
|
235
|
+
upper_edge = (altitude - self.duct_center - self.duct_width / 2) / alpha
|
|
236
|
+
|
|
237
|
+
d_duct_dh = (
|
|
238
|
+
self.duct_intensity
|
|
239
|
+
/ (norm * alpha)
|
|
240
|
+
* (1 / np.cosh(lower_edge) ** 2 - 1 / np.cosh(upper_edge) ** 2)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return float(d_duct_dh)
|
|
244
|
+
|
|
245
|
+
# =========================================================================
|
|
246
|
+
# SimpleInhomogeneousModel Interface (Required)
|
|
247
|
+
# =========================================================================
|
|
248
|
+
|
|
249
|
+
def n_at_altitude(self, altitude: float, wavelength: float | None = None) -> float:
|
|
250
|
+
"""
|
|
251
|
+
Return refractive index at given altitude.
|
|
252
|
+
|
|
253
|
+
Implements the exponential profile with duct modification:
|
|
254
|
+
n(h) = 1 + delta_n * exp(-h / H) * D(h)
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
altitude : float
|
|
259
|
+
Altitude above Earth's surface in meters (clamped to >= 0).
|
|
260
|
+
wavelength : float, optional
|
|
261
|
+
Wavelength in meters (not used - no dispersion).
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
float
|
|
266
|
+
Refractive index at altitude.
|
|
267
|
+
"""
|
|
268
|
+
altitude_clamped = max(altitude, 0.0)
|
|
269
|
+
duct_factor = self._compute_duct_factor(altitude_clamped)
|
|
270
|
+
return float(
|
|
271
|
+
1.0
|
|
272
|
+
+ self.delta_n
|
|
273
|
+
* math.exp(-altitude_clamped / self.scale_height)
|
|
274
|
+
* duct_factor
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def dn_dh_at_altitude(
|
|
278
|
+
self, altitude: float, wavelength: float | None = None
|
|
279
|
+
) -> float:
|
|
280
|
+
"""
|
|
281
|
+
Return analytical dn/dh at given altitude.
|
|
282
|
+
|
|
283
|
+
Uses the product rule for the derivative:
|
|
284
|
+
dn/dh = delta_n * [exp' * D + exp * D']
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
altitude : float
|
|
289
|
+
Altitude above Earth's surface in meters.
|
|
290
|
+
wavelength : float, optional
|
|
291
|
+
Wavelength in meters (not used).
|
|
292
|
+
|
|
293
|
+
Returns
|
|
294
|
+
-------
|
|
295
|
+
float
|
|
296
|
+
Derivative dn/dh in m^-1.
|
|
297
|
+
"""
|
|
298
|
+
if altitude < 0:
|
|
299
|
+
return 0.0
|
|
300
|
+
|
|
301
|
+
# Exponential factor and its derivative
|
|
302
|
+
exp_factor = math.exp(-altitude / self.scale_height)
|
|
303
|
+
d_exp_dh = -exp_factor / self.scale_height
|
|
304
|
+
|
|
305
|
+
# Duct factor and its derivative
|
|
306
|
+
duct_factor = self._compute_duct_factor(altitude)
|
|
307
|
+
d_duct_dh = self._compute_duct_factor_derivative(altitude)
|
|
308
|
+
|
|
309
|
+
# Product rule: d(exp * D)/dh = exp' * D + exp * D'
|
|
310
|
+
return float(self.delta_n * (d_exp_dh * duct_factor + exp_factor * d_duct_dh))
|
|
311
|
+
|
|
312
|
+
def __repr__(self) -> str:
|
|
313
|
+
"""Return string representation."""
|
|
314
|
+
return (
|
|
315
|
+
f"<DuctAtmosphere("
|
|
316
|
+
f"n_sea_level={self.n_sea_level:.6f}, "
|
|
317
|
+
f"H={self.scale_height / 1000:.1f} km, "
|
|
318
|
+
f"duct_center={self.duct_center:.0f} m, "
|
|
319
|
+
f"duct_width={self.duct_width:.0f} m, "
|
|
320
|
+
f"duct_intensity={self.duct_intensity:.2f})>"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def create_duct_atmosphere(
|
|
325
|
+
n_sea_level: float = N_SEA_LEVEL,
|
|
326
|
+
scale_height: float = SCALE_HEIGHT_DEFAULT,
|
|
327
|
+
earth_radius: float = EARTH_RADIUS,
|
|
328
|
+
earth_center: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
329
|
+
duct_center: float = 0.0,
|
|
330
|
+
duct_width: float = 100.0,
|
|
331
|
+
duct_intensity: float = 0.0,
|
|
332
|
+
duct_sharpness: float = 0.5,
|
|
333
|
+
name: str = "Custom Duct Atmosphere",
|
|
334
|
+
) -> DuctAtmosphere:
|
|
335
|
+
"""
|
|
336
|
+
Factory function to create a duct atmosphere.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
n_sea_level : float, optional
|
|
341
|
+
Refractive index at sea level. Default is 1.000293.
|
|
342
|
+
scale_height : float, optional
|
|
343
|
+
Atmospheric scale height in meters. Default is 8500.0 m.
|
|
344
|
+
earth_radius : float, optional
|
|
345
|
+
Radius of Earth in meters. Default is 6,371,000 m.
|
|
346
|
+
earth_center : tuple of float, optional
|
|
347
|
+
Position of Earth's center. Default is (0, 0, 0).
|
|
348
|
+
duct_center : float, optional
|
|
349
|
+
Center altitude of the duct in meters. Default is 0.0.
|
|
350
|
+
duct_width : float, optional
|
|
351
|
+
Width of the duct in meters. Default is 100.0.
|
|
352
|
+
duct_intensity : float, optional
|
|
353
|
+
Intensity of the duct (0-1). Default is 0.0.
|
|
354
|
+
duct_sharpness : float, optional
|
|
355
|
+
Sharpness of duct edges (0-1). Default is 0.5.
|
|
356
|
+
name : str, optional
|
|
357
|
+
Descriptive name for the material.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
DuctAtmosphere
|
|
362
|
+
Configured atmosphere material with duct.
|
|
363
|
+
|
|
364
|
+
Examples
|
|
365
|
+
--------
|
|
366
|
+
>>> # Create a surface evaporation duct
|
|
367
|
+
>>> atmo = create_duct_atmosphere(
|
|
368
|
+
... duct_center=50.0,
|
|
369
|
+
... duct_width=30.0,
|
|
370
|
+
... duct_intensity=0.7,
|
|
371
|
+
... duct_sharpness=0.9,
|
|
372
|
+
... )
|
|
373
|
+
"""
|
|
374
|
+
return DuctAtmosphere(
|
|
375
|
+
name=name,
|
|
376
|
+
n_sea_level=n_sea_level,
|
|
377
|
+
scale_height=scale_height,
|
|
378
|
+
earth_radius=earth_radius,
|
|
379
|
+
earth_center=earth_center,
|
|
380
|
+
duct_center=duct_center,
|
|
381
|
+
duct_width=duct_width,
|
|
382
|
+
duct_intensity=duct_intensity,
|
|
383
|
+
duct_sharpness=duct_sharpness,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
__all__ = [
|
|
388
|
+
"DuctAtmosphere",
|
|
389
|
+
"create_duct_atmosphere",
|
|
390
|
+
]
|