lsurf 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. lsurf-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ ]