nrl-tracker 1.7.0__py3-none-any.whl → 1.7.1__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.
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/METADATA +3 -2
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/RECORD +75 -75
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/__init__.py +15 -15
- pytcl/assignment_algorithms/gating.py +10 -10
- pytcl/assignment_algorithms/jpda.py +40 -40
- pytcl/assignment_algorithms/nd_assignment.py +5 -4
- pytcl/assignment_algorithms/network_flow.py +18 -8
- pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
- pytcl/astronomical/__init__.py +6 -6
- pytcl/astronomical/ephemerides.py +14 -11
- pytcl/astronomical/reference_frames.py +8 -4
- pytcl/astronomical/relativity.py +6 -5
- pytcl/astronomical/special_orbits.py +9 -13
- pytcl/atmosphere/__init__.py +6 -6
- pytcl/atmosphere/nrlmsise00.py +153 -152
- pytcl/clustering/dbscan.py +2 -2
- pytcl/clustering/gaussian_mixture.py +3 -3
- pytcl/clustering/hierarchical.py +15 -15
- pytcl/clustering/kmeans.py +4 -4
- pytcl/containers/base.py +3 -3
- pytcl/containers/cluster_set.py +12 -2
- pytcl/containers/covertree.py +5 -3
- pytcl/containers/rtree.py +1 -1
- pytcl/containers/vptree.py +4 -2
- pytcl/coordinate_systems/conversions/geodetic.py +31 -7
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
- pytcl/coordinate_systems/projections/projections.py +2 -2
- pytcl/coordinate_systems/rotations/rotations.py +10 -6
- pytcl/core/validation.py +3 -3
- pytcl/dynamic_estimation/__init__.py +16 -16
- pytcl/dynamic_estimation/gaussian_sum_filter.py +20 -38
- pytcl/dynamic_estimation/imm.py +14 -14
- pytcl/dynamic_estimation/kalman/__init__.py +1 -1
- pytcl/dynamic_estimation/kalman/constrained.py +35 -23
- pytcl/dynamic_estimation/kalman/extended.py +8 -8
- pytcl/dynamic_estimation/kalman/h_infinity.py +2 -2
- pytcl/dynamic_estimation/kalman/square_root.py +8 -2
- pytcl/dynamic_estimation/kalman/sr_ukf.py +3 -3
- pytcl/dynamic_estimation/kalman/ud_filter.py +11 -5
- pytcl/dynamic_estimation/kalman/unscented.py +8 -6
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +36 -40
- pytcl/gravity/spherical_harmonics.py +3 -3
- pytcl/gravity/tides.py +6 -6
- pytcl/logging_config.py +3 -3
- pytcl/magnetism/emm.py +10 -3
- pytcl/magnetism/wmm.py +4 -4
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
- pytcl/mathematical_functions/geometry/geometry.py +5 -5
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
- pytcl/mathematical_functions/signal_processing/detection.py +24 -24
- pytcl/mathematical_functions/signal_processing/filters.py +14 -14
- pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
- pytcl/mathematical_functions/special_functions/bessel.py +15 -3
- pytcl/mathematical_functions/special_functions/debye.py +5 -1
- pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +6 -4
- pytcl/mathematical_functions/transforms/fourier.py +8 -8
- pytcl/mathematical_functions/transforms/stft.py +12 -12
- pytcl/mathematical_functions/transforms/wavelets.py +9 -9
- pytcl/navigation/geodesy.py +3 -3
- pytcl/navigation/great_circle.py +5 -5
- pytcl/plotting/coordinates.py +7 -7
- pytcl/plotting/tracks.py +2 -2
- pytcl/static_estimation/maximum_likelihood.py +16 -14
- pytcl/static_estimation/robust.py +5 -5
- pytcl/terrain/loaders.py +5 -5
- pytcl/trackers/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +1 -1
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/top_level.txt +0 -0
pytcl/atmosphere/nrlmsise00.py
CHANGED
|
@@ -23,18 +23,19 @@ References
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
from typing import NamedTuple
|
|
26
|
+
|
|
26
27
|
import numpy as np
|
|
27
28
|
from numpy.typing import ArrayLike, NDArray
|
|
28
29
|
|
|
29
30
|
# Molecular weights (g/mol)
|
|
30
31
|
_MW = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
"N2": 28.014,
|
|
33
|
+
"O2": 31.999,
|
|
34
|
+
"O": 15.999,
|
|
35
|
+
"He": 4.003,
|
|
36
|
+
"H": 1.008,
|
|
37
|
+
"Ar": 39.948,
|
|
38
|
+
"N": 14.007,
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
# Gas constant (J/(mol·K))
|
|
@@ -44,7 +45,7 @@ _R_GAS = 8.31447
|
|
|
44
45
|
class NRLMSISE00Output(NamedTuple):
|
|
45
46
|
"""
|
|
46
47
|
Output from NRLMSISE-00 atmospheric model.
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
Attributes
|
|
49
50
|
----------
|
|
50
51
|
density : float or ndarray
|
|
@@ -68,7 +69,7 @@ class NRLMSISE00Output(NamedTuple):
|
|
|
68
69
|
n_density : float or ndarray
|
|
69
70
|
Atomic nitrogen density in m⁻³.
|
|
70
71
|
"""
|
|
71
|
-
|
|
72
|
+
|
|
72
73
|
density: float | NDArray[np.float64]
|
|
73
74
|
temperature: float | NDArray[np.float64]
|
|
74
75
|
exosphere_temperature: float | NDArray[np.float64]
|
|
@@ -84,7 +85,7 @@ class NRLMSISE00Output(NamedTuple):
|
|
|
84
85
|
class F107Index(NamedTuple):
|
|
85
86
|
"""
|
|
86
87
|
Solar activity indices for NRLMSISE-00.
|
|
87
|
-
|
|
88
|
+
|
|
88
89
|
Attributes
|
|
89
90
|
----------
|
|
90
91
|
f107 : float
|
|
@@ -97,7 +98,7 @@ class F107Index(NamedTuple):
|
|
|
97
98
|
Ap values for each 3-hour interval of the day (8 values).
|
|
98
99
|
If not provided, derived from ap value.
|
|
99
100
|
"""
|
|
100
|
-
|
|
101
|
+
|
|
101
102
|
f107: float
|
|
102
103
|
f107a: float
|
|
103
104
|
ap: float | NDArray[np.float64]
|
|
@@ -108,26 +109,27 @@ class F107Index(NamedTuple):
|
|
|
108
109
|
# Note: Full model requires extensive coefficient tables from NOAA
|
|
109
110
|
# These are placeholder structures that would be populated from data files
|
|
110
111
|
|
|
112
|
+
|
|
111
113
|
class NRLMSISE00:
|
|
112
114
|
"""
|
|
113
115
|
NRLMSISE-00 High-Fidelity Atmosphere Model.
|
|
114
|
-
|
|
116
|
+
|
|
115
117
|
This is a comprehensive thermosphere model covering altitudes from
|
|
116
118
|
approximately -5 km to 1000 km, with detailed chemical composition
|
|
117
119
|
and temperature profiles.
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
The model implements:
|
|
120
122
|
- Temperature profile with solar activity and magnetic coupling
|
|
121
123
|
- Molecular composition for troposphere/stratosphere/mesosphere
|
|
122
124
|
- Atomic species for thermosphere
|
|
123
125
|
- Solar flux (F10.7) and magnetic activity (Ap) variations
|
|
124
|
-
|
|
126
|
+
|
|
125
127
|
Parameters
|
|
126
128
|
----------
|
|
127
129
|
use_meter_altitude : bool, optional
|
|
128
130
|
If True, expect altitude input in meters. If False, expect km.
|
|
129
131
|
Default is True (meters).
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
Examples
|
|
132
134
|
--------
|
|
133
135
|
>>> model = NRLMSISE00()
|
|
@@ -143,7 +145,7 @@ class NRLMSISE00:
|
|
|
143
145
|
... ap=5
|
|
144
146
|
... )
|
|
145
147
|
>>> print(f"Density: {output.density:.2e} kg/m³")
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
Notes
|
|
148
150
|
-----
|
|
149
151
|
This implementation uses empirical correlations for atmospheric
|
|
@@ -151,11 +153,11 @@ class NRLMSISE00:
|
|
|
151
153
|
For highest accuracy, use the original NRLMSISE-00 Fortran code
|
|
152
154
|
from NASA/NOAA, which includes extensive coefficient tables.
|
|
153
155
|
"""
|
|
154
|
-
|
|
156
|
+
|
|
155
157
|
def __init__(self, use_meter_altitude: bool = True):
|
|
156
158
|
"""Initialize NRLMSISE-00 model."""
|
|
157
159
|
self.use_meter_altitude = use_meter_altitude
|
|
158
|
-
|
|
160
|
+
|
|
159
161
|
def __call__(
|
|
160
162
|
self,
|
|
161
163
|
latitude: ArrayLike,
|
|
@@ -170,7 +172,7 @@ class NRLMSISE00:
|
|
|
170
172
|
) -> NRLMSISE00Output:
|
|
171
173
|
"""
|
|
172
174
|
Compute atmospheric density and composition.
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
Parameters
|
|
175
177
|
----------
|
|
176
178
|
latitude : array_like
|
|
@@ -192,12 +194,12 @@ class NRLMSISE00:
|
|
|
192
194
|
ap : float or array_like, optional
|
|
193
195
|
Planetary magnetic index. Can be single value or 8-element
|
|
194
196
|
array of 3-hour Ap values. Default 4.0.
|
|
195
|
-
|
|
197
|
+
|
|
196
198
|
Returns
|
|
197
199
|
-------
|
|
198
200
|
output : NRLMSISE00Output
|
|
199
201
|
Atmospheric properties (density, temperature, composition).
|
|
200
|
-
|
|
202
|
+
|
|
201
203
|
Notes
|
|
202
204
|
-----
|
|
203
205
|
The model assumes hydrostatic equilibrium and uses empirical
|
|
@@ -207,21 +209,19 @@ class NRLMSISE00:
|
|
|
207
209
|
lat = np.atleast_1d(np.asarray(latitude, dtype=np.float64))
|
|
208
210
|
lon = np.atleast_1d(np.asarray(longitude, dtype=np.float64))
|
|
209
211
|
alt = np.atleast_1d(np.asarray(altitude, dtype=np.float64))
|
|
210
|
-
|
|
212
|
+
|
|
211
213
|
# Convert altitude to km if needed
|
|
212
214
|
if self.use_meter_altitude:
|
|
213
215
|
alt_km = alt / 1000.0
|
|
214
216
|
else:
|
|
215
217
|
alt_km = alt
|
|
216
|
-
|
|
218
|
+
|
|
217
219
|
# Broadcast arrays to common shape
|
|
218
220
|
try:
|
|
219
221
|
lat, lon, alt_km = np.broadcast_arrays(lat, lon, alt_km)
|
|
220
222
|
except ValueError:
|
|
221
|
-
raise ValueError(
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
|
|
223
|
+
raise ValueError("latitude, longitude, and altitude must be broadcastable")
|
|
224
|
+
|
|
225
225
|
# Compute Ap index as float scalar if array provided
|
|
226
226
|
if isinstance(ap, (list, tuple, np.ndarray)):
|
|
227
227
|
ap_array = np.asarray(ap, dtype=np.float64)
|
|
@@ -232,20 +232,20 @@ class NRLMSISE00:
|
|
|
232
232
|
else:
|
|
233
233
|
ap_val = float(ap)
|
|
234
234
|
ap_array = None
|
|
235
|
-
|
|
235
|
+
|
|
236
236
|
# Constrain solar/magnetic indices
|
|
237
237
|
f107 = np.clip(f107, 70.0, 300.0)
|
|
238
238
|
f107a = np.clip(f107a, 70.0, 300.0)
|
|
239
239
|
ap_val = np.clip(ap_val, 0.0, 400.0)
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
# Calculate exosphere temperature (Texo)
|
|
242
242
|
texo = self._exosphere_temperature(f107, f107a, ap_val)
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
# Calculate temperature profile
|
|
245
|
-
temperature = self._temperature_profile(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
temperature = self._temperature_profile(
|
|
246
|
+
alt_km, lat, lon, day_of_year, seconds_in_day, texo
|
|
247
|
+
)
|
|
248
|
+
|
|
249
249
|
# Calculate species densities
|
|
250
250
|
n2_dens = self._n2_density(alt_km, lat, temperature, f107a, ap_val)
|
|
251
251
|
o2_dens = self._o2_density(alt_km, lat, temperature, f107a, ap_val)
|
|
@@ -254,23 +254,27 @@ class NRLMSISE00:
|
|
|
254
254
|
h_dens = self._h_density(alt_km, lat, temperature, f107a, ap_val)
|
|
255
255
|
ar_dens = self._ar_density(alt_km, lat, temperature, f107a, ap_val)
|
|
256
256
|
n_dens = self._n_density(alt_km, lat, temperature, f107a, ap_val)
|
|
257
|
-
|
|
257
|
+
|
|
258
258
|
# Convert number densities (m^-3) to mass density (kg/m^3)
|
|
259
259
|
# ρ = Σ(ni × Mi / Nₐ) where Nₐ = 6.022e23
|
|
260
260
|
na = 6.02214076e23 # Avogadro's number
|
|
261
261
|
total_density = (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
262
|
+
(
|
|
263
|
+
n2_dens * _MW["N2"]
|
|
264
|
+
+ o2_dens * _MW["O2"]
|
|
265
|
+
+ o_dens * _MW["O"]
|
|
266
|
+
+ he_dens * _MW["He"]
|
|
267
|
+
+ h_dens * _MW["H"]
|
|
268
|
+
+ ar_dens * _MW["Ar"]
|
|
269
|
+
+ n_dens * _MW["N"]
|
|
270
|
+
)
|
|
271
|
+
/ na
|
|
272
|
+
/ 1000.0
|
|
273
|
+
) # Convert g to kg
|
|
274
|
+
|
|
271
275
|
# Return as scalar if inputs were scalar
|
|
272
|
-
scalar_input =
|
|
273
|
-
|
|
276
|
+
scalar_input = np.asarray(altitude).ndim == 0
|
|
277
|
+
|
|
274
278
|
if scalar_input:
|
|
275
279
|
total_density = float(total_density.flat[0])
|
|
276
280
|
temperature = float(temperature.flat[0])
|
|
@@ -282,7 +286,7 @@ class NRLMSISE00:
|
|
|
282
286
|
h_dens = float(h_dens.flat[0])
|
|
283
287
|
ar_dens = float(ar_dens.flat[0])
|
|
284
288
|
n_dens = float(n_dens.flat[0])
|
|
285
|
-
|
|
289
|
+
|
|
286
290
|
return NRLMSISE00Output(
|
|
287
291
|
density=total_density,
|
|
288
292
|
temperature=temperature,
|
|
@@ -295,12 +299,12 @@ class NRLMSISE00:
|
|
|
295
299
|
h_density=h_dens,
|
|
296
300
|
n_density=n_dens,
|
|
297
301
|
)
|
|
298
|
-
|
|
302
|
+
|
|
299
303
|
@staticmethod
|
|
300
304
|
def _exosphere_temperature(f107: float, f107a: float, ap: float) -> float:
|
|
301
305
|
"""
|
|
302
306
|
Calculate exosphere temperature based on solar/magnetic activity.
|
|
303
|
-
|
|
307
|
+
|
|
304
308
|
Parameters
|
|
305
309
|
----------
|
|
306
310
|
f107 : float
|
|
@@ -309,7 +313,7 @@ class NRLMSISE00:
|
|
|
309
313
|
81-day average 10.7 cm solar flux (SFU).
|
|
310
314
|
ap : float
|
|
311
315
|
Planetary magnetic index.
|
|
312
|
-
|
|
316
|
+
|
|
313
317
|
Returns
|
|
314
318
|
-------
|
|
315
319
|
texo : float
|
|
@@ -317,20 +321,20 @@ class NRLMSISE00:
|
|
|
317
321
|
"""
|
|
318
322
|
# Base temperature (quiet conditions)
|
|
319
323
|
texo_base = 900.0
|
|
320
|
-
|
|
324
|
+
|
|
321
325
|
# Solar activity influence (empirical fit)
|
|
322
326
|
# ~0.7 K per SFU for moderate activity
|
|
323
327
|
f107_corr = 0.7 * (f107a - 90.0)
|
|
324
|
-
|
|
328
|
+
|
|
325
329
|
# Magnetic activity influence (empirical fit)
|
|
326
330
|
# Ap index affects thermospheric heating
|
|
327
331
|
ap_corr = 20.0 * np.tanh(ap / 40.0)
|
|
328
|
-
|
|
332
|
+
|
|
329
333
|
texo = texo_base + f107_corr + ap_corr
|
|
330
|
-
|
|
334
|
+
|
|
331
335
|
# Constrain to physical range
|
|
332
336
|
return np.clip(texo, 500.0, 2500.0)
|
|
333
|
-
|
|
337
|
+
|
|
334
338
|
@staticmethod
|
|
335
339
|
def _temperature_profile(
|
|
336
340
|
alt_km: NDArray[np.float64],
|
|
@@ -342,13 +346,13 @@ class NRLMSISE00:
|
|
|
342
346
|
) -> NDArray[np.float64]:
|
|
343
347
|
"""
|
|
344
348
|
Calculate temperature at given altitudes.
|
|
345
|
-
|
|
349
|
+
|
|
346
350
|
Temperature varies with:
|
|
347
351
|
- Altitude (primary)
|
|
348
352
|
- Latitude (small effect)
|
|
349
353
|
- Local solar time (diurnal variation)
|
|
350
354
|
- Season (small effect)
|
|
351
|
-
|
|
355
|
+
|
|
352
356
|
Parameters
|
|
353
357
|
----------
|
|
354
358
|
alt_km : ndarray
|
|
@@ -363,7 +367,7 @@ class NRLMSISE00:
|
|
|
363
367
|
Seconds since midnight UTC.
|
|
364
368
|
texo : float
|
|
365
369
|
Exosphere temperature (K).
|
|
366
|
-
|
|
370
|
+
|
|
367
371
|
Returns
|
|
368
372
|
-------
|
|
369
373
|
temperature : ndarray
|
|
@@ -375,70 +379,70 @@ class NRLMSISE00:
|
|
|
375
379
|
# Upper Stratosphere (20-32 km): +2.8 K/km
|
|
376
380
|
# Mesosphere (32-47 km): -2.8 K/km
|
|
377
381
|
# Upper Mesosphere (47-85 km): -2 K/km (approx)
|
|
378
|
-
|
|
382
|
+
|
|
379
383
|
t_surface = 288.15 # K at sea level (15°C)
|
|
380
|
-
|
|
384
|
+
|
|
381
385
|
# Initialize temperature array
|
|
382
386
|
t = np.zeros_like(alt_km)
|
|
383
|
-
|
|
387
|
+
|
|
384
388
|
# Lower troposphere (0-11 km)
|
|
385
389
|
mask_trop = alt_km <= 11.0
|
|
386
390
|
t[mask_trop] = t_surface - 6.5 * alt_km[mask_trop]
|
|
387
|
-
|
|
391
|
+
|
|
388
392
|
# Lower stratosphere (11-20 km)
|
|
389
393
|
mask_lstrat = (alt_km > 11.0) & (alt_km <= 20.0)
|
|
390
394
|
t[mask_lstrat] = 216.65 + 1.0 * (alt_km[mask_lstrat] - 11.0)
|
|
391
|
-
|
|
395
|
+
|
|
392
396
|
# Upper stratosphere (20-32 km)
|
|
393
397
|
mask_ustrat = (alt_km > 20.0) & (alt_km <= 32.0)
|
|
394
398
|
t[mask_ustrat] = 226.65 + 2.8 * (alt_km[mask_ustrat] - 20.0)
|
|
395
|
-
|
|
399
|
+
|
|
396
400
|
# Mesosphere lower (32-47 km)
|
|
397
401
|
mask_meso1 = (alt_km > 32.0) & (alt_km <= 47.0)
|
|
398
402
|
t[mask_meso1] = 270.65 - 2.8 * (alt_km[mask_meso1] - 32.0)
|
|
399
|
-
|
|
403
|
+
|
|
400
404
|
# Mesosphere upper (47-85 km)
|
|
401
405
|
mask_meso2 = (alt_km > 47.0) & (alt_km <= 85.0)
|
|
402
406
|
t_meso_top = 214.65 # Temperature at 47 km
|
|
403
407
|
t_meso_rate = -2.0 # K/km (approximate)
|
|
404
408
|
t[mask_meso2] = t_meso_top + t_meso_rate * (alt_km[mask_meso2] - 47.0)
|
|
405
|
-
|
|
409
|
+
|
|
406
410
|
# Thermosphere (>85 km)
|
|
407
411
|
# Temperature rises from mesopause (~170 K) to Texo
|
|
408
412
|
mask_thermo = alt_km > 85.0
|
|
409
|
-
|
|
413
|
+
|
|
410
414
|
# Empirical transition function (Chapman function)
|
|
411
415
|
# Creates smooth rise from ~170 K at 85 km to Texo
|
|
412
416
|
z_ref = 85.0
|
|
413
417
|
h_scale = 40.0 # Scale height for transition
|
|
414
|
-
|
|
418
|
+
|
|
415
419
|
t_min = 170.0 # Mesopause temperature
|
|
416
|
-
|
|
420
|
+
|
|
417
421
|
# Exponential rise from mesopause to exosphere
|
|
418
422
|
z_diff = (alt_km[mask_thermo] - z_ref) / h_scale
|
|
419
423
|
t_factor = (texo - t_min) / (1.0 + np.exp(-5.0)) # Normalize
|
|
420
424
|
t[mask_thermo] = t_min + t_factor * (1.0 - np.exp(-np.maximum(z_diff, 0.0)))
|
|
421
|
-
|
|
425
|
+
|
|
422
426
|
# Ensure upper thermosphere approaches Texo
|
|
423
427
|
mask_high = alt_km > 200.0
|
|
424
428
|
t[mask_high] = np.minimum(t[mask_high], texo * 0.95)
|
|
425
429
|
mask_very_high = alt_km > 500.0
|
|
426
430
|
t[mask_very_high] = texo * 0.99
|
|
427
|
-
|
|
431
|
+
|
|
428
432
|
# Latitude variation (small - ~±10% at poles)
|
|
429
433
|
lat_variation = 1.0 + 0.05 * np.cos(2.0 * lat)
|
|
430
|
-
|
|
434
|
+
|
|
431
435
|
# Local time variation (small - ~±5% diurnal)
|
|
432
436
|
hours_utc = seconds_in_day / 3600.0
|
|
433
437
|
lst = (hours_utc + lon / np.pi * 12.0) % 24.0 # Local solar time
|
|
434
438
|
lt_variation = 1.0 + 0.03 * np.cos(2.0 * np.pi * (lst - 14.0) / 24.0)
|
|
435
|
-
|
|
439
|
+
|
|
436
440
|
# Apply variations to mesosphere and above
|
|
437
441
|
mask_var = alt_km > 15.0
|
|
438
442
|
t[mask_var] = t[mask_var] * lat_variation[mask_var] * lt_variation[mask_var]
|
|
439
|
-
|
|
443
|
+
|
|
440
444
|
return t
|
|
441
|
-
|
|
445
|
+
|
|
442
446
|
@staticmethod
|
|
443
447
|
def _n2_density(
|
|
444
448
|
alt_km: NDArray[np.float64],
|
|
@@ -449,42 +453,40 @@ class NRLMSISE00:
|
|
|
449
453
|
) -> NDArray[np.float64]:
|
|
450
454
|
"""
|
|
451
455
|
Calculate N2 number density in m^-3.
|
|
452
|
-
|
|
456
|
+
|
|
453
457
|
N2 is the primary constituent up to ~85 km, decreasing exponentially
|
|
454
458
|
above that.
|
|
455
459
|
"""
|
|
456
|
-
# Reference density at sea level (dry air ~78% N2)
|
|
457
|
-
n2_sea_level = 0.78 * 2.547e25 # m^-3 at 288.15 K, 1 atm
|
|
458
|
-
|
|
459
460
|
# Reference altitude (11 km, tropopause)
|
|
460
461
|
n2_ref_11km = 3.6e24 # m^-3
|
|
461
|
-
|
|
462
|
+
|
|
462
463
|
# Calculate scale height (function of temperature)
|
|
463
464
|
# H = R_gas * T / g / M, for N2 ~10 km at 250 K
|
|
464
465
|
scale_height = 8.5 * (temperature / 250.0)
|
|
465
|
-
|
|
466
|
+
|
|
466
467
|
# Exponential model for altitude dependence
|
|
467
|
-
t_ref = 288.15
|
|
468
468
|
alt_ref = 11.0 # Reference at tropopause
|
|
469
|
-
|
|
469
|
+
|
|
470
470
|
# Density increases below tropopause, decreases above
|
|
471
471
|
exponent = -(alt_km - alt_ref) / scale_height
|
|
472
472
|
n2_dens = n2_ref_11km * np.exp(exponent)
|
|
473
|
-
|
|
473
|
+
|
|
474
474
|
# Reduce N2 at high altitude (thermosphere transition)
|
|
475
475
|
# At 150 km, N2 is ~1e18 m^-3
|
|
476
476
|
# At 500 km, essentially zero
|
|
477
477
|
transition_alt = 85.0
|
|
478
478
|
mask_thermo = alt_km > transition_alt
|
|
479
|
-
|
|
479
|
+
|
|
480
480
|
if np.any(mask_thermo):
|
|
481
481
|
# Smooth transition above 85 km with faster decay
|
|
482
482
|
h_trans = 20.0
|
|
483
|
-
transition_factor = np.exp(
|
|
483
|
+
transition_factor = np.exp(
|
|
484
|
+
-(alt_km[mask_thermo] - transition_alt) / h_trans
|
|
485
|
+
)
|
|
484
486
|
n2_dens[mask_thermo] *= transition_factor
|
|
485
|
-
|
|
487
|
+
|
|
486
488
|
return np.maximum(n2_dens, 1e10)
|
|
487
|
-
|
|
489
|
+
|
|
488
490
|
@staticmethod
|
|
489
491
|
def _o2_density(
|
|
490
492
|
alt_km: NDArray[np.float64],
|
|
@@ -495,32 +497,33 @@ class NRLMSISE00:
|
|
|
495
497
|
) -> NDArray[np.float64]:
|
|
496
498
|
"""
|
|
497
499
|
Calculate O2 number density in m^-3.
|
|
498
|
-
|
|
500
|
+
|
|
499
501
|
O2 is the second major constituent, ~21% at sea level,
|
|
500
502
|
decreases above ~100 km.
|
|
501
503
|
"""
|
|
502
|
-
# Reference density (21% of air)
|
|
503
|
-
o2_sea_level = 0.21 * 2.547e25 # m^-3
|
|
504
|
+
# Reference density (21% of air at sea level)
|
|
504
505
|
o2_ref_11km = 9.8e23 # m^-3
|
|
505
|
-
|
|
506
|
+
|
|
506
507
|
# Similar scale height as N2
|
|
507
508
|
scale_height = 8.5 * (temperature / 250.0)
|
|
508
|
-
|
|
509
|
+
|
|
509
510
|
alt_ref = 11.0
|
|
510
511
|
exponent = -(alt_km - alt_ref) / scale_height
|
|
511
512
|
o2_dens = o2_ref_11km * np.exp(exponent)
|
|
512
|
-
|
|
513
|
+
|
|
513
514
|
# Transition above 85 km - O2 decays faster
|
|
514
515
|
transition_alt = 85.0
|
|
515
516
|
mask_thermo = alt_km > transition_alt
|
|
516
|
-
|
|
517
|
+
|
|
517
518
|
if np.any(mask_thermo):
|
|
518
519
|
h_trans = 15.0 # Faster decay than N2
|
|
519
|
-
transition_factor = np.exp(
|
|
520
|
+
transition_factor = np.exp(
|
|
521
|
+
-(alt_km[mask_thermo] - transition_alt) / h_trans
|
|
522
|
+
)
|
|
520
523
|
o2_dens[mask_thermo] *= transition_factor
|
|
521
|
-
|
|
524
|
+
|
|
522
525
|
return np.maximum(o2_dens, 1e10)
|
|
523
|
-
|
|
526
|
+
|
|
524
527
|
@staticmethod
|
|
525
528
|
def _o_density(
|
|
526
529
|
alt_km: NDArray[np.float64],
|
|
@@ -531,46 +534,44 @@ class NRLMSISE00:
|
|
|
531
534
|
) -> NDArray[np.float64]:
|
|
532
535
|
"""
|
|
533
536
|
Calculate atomic oxygen number density in m^-3.
|
|
534
|
-
|
|
537
|
+
|
|
535
538
|
O becomes the dominant species above ~130 km.
|
|
536
539
|
Strongly coupled to solar UV and thermospheric temperature.
|
|
537
540
|
"""
|
|
538
541
|
# Atomic oxygen is negligible below ~100 km
|
|
539
542
|
o_dens = np.zeros_like(alt_km)
|
|
540
|
-
|
|
543
|
+
|
|
541
544
|
# Above 100 km, increases rapidly
|
|
542
545
|
mask_high = alt_km > 100.0
|
|
543
|
-
|
|
546
|
+
|
|
544
547
|
if np.any(mask_high):
|
|
545
548
|
# Reference: ~8e15 m^-3 at 150 km
|
|
546
549
|
# Decreases with scale height ~30 km above 150 km
|
|
547
550
|
alt_ref = 150.0
|
|
548
551
|
dens_ref = 8.0e15
|
|
549
|
-
|
|
552
|
+
|
|
550
553
|
# Temperature-dependent scale height
|
|
551
554
|
h_scale = 30.0 * np.sqrt(temperature[mask_high] / 1000.0)
|
|
552
|
-
|
|
555
|
+
|
|
553
556
|
# Exponential above 100 km with altitude-dependent onset
|
|
554
557
|
onset_alt = 100.0
|
|
555
558
|
alt_onset = np.maximum(alt_km[mask_high] - onset_alt, 0.0)
|
|
556
|
-
|
|
559
|
+
|
|
557
560
|
# Smooth onset between 100-120 km
|
|
558
561
|
onset_smooth = np.minimum(alt_onset / 20.0, 1.0)
|
|
559
|
-
|
|
562
|
+
|
|
560
563
|
# Main altitude dependence (scale height increases with T)
|
|
561
564
|
z_diff = alt_km[mask_high] - alt_ref
|
|
562
565
|
exponent = -z_diff / h_scale
|
|
563
|
-
|
|
564
|
-
o_dens[mask_high] = (
|
|
565
|
-
|
|
566
|
-
)
|
|
567
|
-
|
|
566
|
+
|
|
567
|
+
o_dens[mask_high] = dens_ref * onset_smooth * np.exp(exponent)
|
|
568
|
+
|
|
568
569
|
# Solar activity effect (higher F107 → more atomic O)
|
|
569
570
|
f107_factor = 1.0 + 0.005 * (f107a - 100.0)
|
|
570
571
|
o_dens[mask_high] *= f107_factor
|
|
571
|
-
|
|
572
|
+
|
|
572
573
|
return np.maximum(o_dens, 1e12)
|
|
573
|
-
|
|
574
|
+
|
|
574
575
|
@staticmethod
|
|
575
576
|
def _he_density(
|
|
576
577
|
alt_km: NDArray[np.float64],
|
|
@@ -581,37 +582,37 @@ class NRLMSISE00:
|
|
|
581
582
|
) -> NDArray[np.float64]:
|
|
582
583
|
"""
|
|
583
584
|
Calculate helium number density in m^-3.
|
|
584
|
-
|
|
585
|
+
|
|
585
586
|
He becomes important above ~150 km, increases with altitude
|
|
586
587
|
due to low mass.
|
|
587
588
|
"""
|
|
588
589
|
he_dens = np.zeros_like(alt_km)
|
|
589
|
-
|
|
590
|
+
|
|
590
591
|
# He becomes significant above ~120 km
|
|
591
592
|
mask_high = alt_km > 120.0
|
|
592
|
-
|
|
593
|
+
|
|
593
594
|
if np.any(mask_high):
|
|
594
595
|
# Reference: ~1e15 m^-3 at 200 km
|
|
595
596
|
alt_ref = 200.0
|
|
596
597
|
dens_ref = 1.0e15
|
|
597
|
-
|
|
598
|
+
|
|
598
599
|
# He has smaller scale height due to low mass
|
|
599
600
|
# H_He ≈ (M_N2 / M_He) * H_N2
|
|
600
|
-
mass_ratio = _MW[
|
|
601
|
+
mass_ratio = _MW["N2"] / _MW["He"]
|
|
601
602
|
h_scale = 20.0 * mass_ratio * np.sqrt(temperature[mask_high] / 1000.0)
|
|
602
|
-
|
|
603
|
+
|
|
603
604
|
# Onset around 120 km
|
|
604
605
|
onset_alt = 120.0
|
|
605
606
|
alt_onset = np.maximum(alt_km[mask_high] - onset_alt, 0.0)
|
|
606
607
|
onset_smooth = np.minimum(alt_onset / 30.0, 1.0)
|
|
607
|
-
|
|
608
|
+
|
|
608
609
|
z_diff = alt_km[mask_high] - alt_ref
|
|
609
610
|
exponent = -z_diff / h_scale
|
|
610
|
-
|
|
611
|
+
|
|
611
612
|
he_dens[mask_high] = dens_ref * onset_smooth * np.exp(exponent)
|
|
612
|
-
|
|
613
|
+
|
|
613
614
|
return np.maximum(he_dens, 1e10)
|
|
614
|
-
|
|
615
|
+
|
|
615
616
|
@staticmethod
|
|
616
617
|
def _h_density(
|
|
617
618
|
alt_km: NDArray[np.float64],
|
|
@@ -622,33 +623,33 @@ class NRLMSISE00:
|
|
|
622
623
|
) -> NDArray[np.float64]:
|
|
623
624
|
"""
|
|
624
625
|
Calculate atomic hydrogen number density in m^-3.
|
|
625
|
-
|
|
626
|
+
|
|
626
627
|
H only becomes significant above ~500 km in exosphere.
|
|
627
628
|
"""
|
|
628
629
|
h_dens = np.zeros_like(alt_km)
|
|
629
|
-
|
|
630
|
+
|
|
630
631
|
# H only important above 400 km
|
|
631
632
|
mask_very_high = alt_km > 400.0
|
|
632
|
-
|
|
633
|
+
|
|
633
634
|
if np.any(mask_very_high):
|
|
634
635
|
# Reference: ~1e14 m^-3 at 600 km
|
|
635
636
|
alt_ref = 600.0
|
|
636
637
|
dens_ref = 1.0e14
|
|
637
|
-
|
|
638
|
+
|
|
638
639
|
# H has very large scale height (100+ km)
|
|
639
640
|
h_scale = 150.0 * np.sqrt(temperature[mask_very_high] / 1000.0)
|
|
640
|
-
|
|
641
|
+
|
|
641
642
|
# Smooth onset at 400 km
|
|
642
643
|
alt_onset = np.maximum(alt_km[mask_very_high] - 400.0, 0.0)
|
|
643
644
|
onset_smooth = np.minimum(alt_onset / 100.0, 1.0)
|
|
644
|
-
|
|
645
|
+
|
|
645
646
|
z_diff = alt_km[mask_very_high] - alt_ref
|
|
646
647
|
exponent = -z_diff / h_scale
|
|
647
|
-
|
|
648
|
+
|
|
648
649
|
h_dens[mask_very_high] = dens_ref * onset_smooth * np.exp(exponent)
|
|
649
|
-
|
|
650
|
+
|
|
650
651
|
return np.maximum(h_dens, 1e8)
|
|
651
|
-
|
|
652
|
+
|
|
652
653
|
@staticmethod
|
|
653
654
|
def _ar_density(
|
|
654
655
|
alt_km: NDArray[np.float64],
|
|
@@ -659,27 +660,27 @@ class NRLMSISE00:
|
|
|
659
660
|
) -> NDArray[np.float64]:
|
|
660
661
|
"""
|
|
661
662
|
Calculate argon number density in m^-3.
|
|
662
|
-
|
|
663
|
+
|
|
663
664
|
Ar is a trace gas, constant ratio to N2 in lower atmosphere (~0.93%).
|
|
664
665
|
"""
|
|
665
666
|
# Constant mixing ratio with N2
|
|
666
667
|
ar_ratio = 0.0093
|
|
667
|
-
|
|
668
|
+
|
|
668
669
|
# Calculate N2 density
|
|
669
670
|
n2_dens = NRLMSISE00._n2_density(alt_km, lat, temperature, f107a, ap)
|
|
670
|
-
|
|
671
|
+
|
|
671
672
|
# Ar proportional to N2 up to ~90 km
|
|
672
673
|
ar_dens = ar_ratio * n2_dens
|
|
673
|
-
|
|
674
|
+
|
|
674
675
|
# Ar decreases above mesosphere
|
|
675
676
|
mask_thermo = alt_km > 85.0
|
|
676
677
|
if np.any(mask_thermo):
|
|
677
678
|
h_trans = 40.0
|
|
678
679
|
transition_factor = np.exp(-(alt_km[mask_thermo] - 85.0) / h_trans)
|
|
679
680
|
ar_dens[mask_thermo] *= transition_factor
|
|
680
|
-
|
|
681
|
+
|
|
681
682
|
return np.maximum(ar_dens, 1e10)
|
|
682
|
-
|
|
683
|
+
|
|
683
684
|
@staticmethod
|
|
684
685
|
def _n_density(
|
|
685
686
|
alt_km: NDArray[np.float64],
|
|
@@ -690,37 +691,37 @@ class NRLMSISE00:
|
|
|
690
691
|
) -> NDArray[np.float64]:
|
|
691
692
|
"""
|
|
692
693
|
Calculate atomic nitrogen number density in m^-3.
|
|
693
|
-
|
|
694
|
+
|
|
694
695
|
N is a trace species, photochemically produced above ~100 km.
|
|
695
696
|
"""
|
|
696
697
|
n_dens = np.zeros_like(alt_km)
|
|
697
|
-
|
|
698
|
+
|
|
698
699
|
# N only significant above ~120 km
|
|
699
700
|
mask_high = alt_km > 120.0
|
|
700
|
-
|
|
701
|
+
|
|
701
702
|
if np.any(mask_high):
|
|
702
703
|
# Reference: ~1e15 m^-3 at 300 km
|
|
703
704
|
alt_ref = 300.0
|
|
704
705
|
dens_ref = 1.0e15
|
|
705
|
-
|
|
706
|
+
|
|
706
707
|
# Similar scale height to He
|
|
707
|
-
mass_ratio = _MW[
|
|
708
|
+
mass_ratio = _MW["N2"] / _MW["N"]
|
|
708
709
|
h_scale = 18.0 * mass_ratio * np.sqrt(temperature[mask_high] / 1000.0)
|
|
709
|
-
|
|
710
|
+
|
|
710
711
|
# Onset around 120 km
|
|
711
712
|
onset_alt = 120.0
|
|
712
713
|
alt_onset = np.maximum(alt_km[mask_high] - onset_alt, 0.0)
|
|
713
714
|
onset_smooth = np.minimum(alt_onset / 40.0, 1.0)
|
|
714
|
-
|
|
715
|
+
|
|
715
716
|
z_diff = alt_km[mask_high] - alt_ref
|
|
716
717
|
exponent = -z_diff / h_scale
|
|
717
|
-
|
|
718
|
+
|
|
718
719
|
n_dens[mask_high] = dens_ref * onset_smooth * np.exp(exponent)
|
|
719
|
-
|
|
720
|
+
|
|
720
721
|
# Solar activity effect
|
|
721
722
|
f107_factor = 1.0 + 0.001 * (f107a - 100.0)
|
|
722
723
|
n_dens[mask_high] *= f107_factor
|
|
723
|
-
|
|
724
|
+
|
|
724
725
|
return np.maximum(n_dens, 1e10)
|
|
725
726
|
|
|
726
727
|
|
|
@@ -737,9 +738,9 @@ def nrlmsise00(
|
|
|
737
738
|
) -> NRLMSISE00Output:
|
|
738
739
|
"""
|
|
739
740
|
Compute NRLMSISE-00 atmospheric properties.
|
|
740
|
-
|
|
741
|
+
|
|
741
742
|
This is a module-level convenience function wrapping the NRLMSISE00 class.
|
|
742
|
-
|
|
743
|
+
|
|
743
744
|
Parameters
|
|
744
745
|
----------
|
|
745
746
|
latitude : array_like
|
|
@@ -760,16 +761,16 @@ def nrlmsise00(
|
|
|
760
761
|
10.7 cm solar flux (81-day average, SFU). Default 150.
|
|
761
762
|
ap : float or array_like, optional
|
|
762
763
|
Planetary magnetic index. Default 4.0.
|
|
763
|
-
|
|
764
|
+
|
|
764
765
|
Returns
|
|
765
766
|
-------
|
|
766
767
|
output : NRLMSISE00Output
|
|
767
768
|
Atmospheric properties.
|
|
768
|
-
|
|
769
|
+
|
|
769
770
|
Notes
|
|
770
771
|
-----
|
|
771
772
|
See NRLMSISE00 class for more details.
|
|
772
|
-
|
|
773
|
+
|
|
773
774
|
Examples
|
|
774
775
|
--------
|
|
775
776
|
>>> # ISS altitude (~400 km), magnetic latitude = 40°, quiet geomagnetic activity
|