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.
Files changed (75) hide show
  1. {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/METADATA +3 -2
  2. {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/RECORD +75 -75
  3. pytcl/__init__.py +2 -2
  4. pytcl/assignment_algorithms/__init__.py +15 -15
  5. pytcl/assignment_algorithms/gating.py +10 -10
  6. pytcl/assignment_algorithms/jpda.py +40 -40
  7. pytcl/assignment_algorithms/nd_assignment.py +5 -4
  8. pytcl/assignment_algorithms/network_flow.py +18 -8
  9. pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
  10. pytcl/astronomical/__init__.py +6 -6
  11. pytcl/astronomical/ephemerides.py +14 -11
  12. pytcl/astronomical/reference_frames.py +8 -4
  13. pytcl/astronomical/relativity.py +6 -5
  14. pytcl/astronomical/special_orbits.py +9 -13
  15. pytcl/atmosphere/__init__.py +6 -6
  16. pytcl/atmosphere/nrlmsise00.py +153 -152
  17. pytcl/clustering/dbscan.py +2 -2
  18. pytcl/clustering/gaussian_mixture.py +3 -3
  19. pytcl/clustering/hierarchical.py +15 -15
  20. pytcl/clustering/kmeans.py +4 -4
  21. pytcl/containers/base.py +3 -3
  22. pytcl/containers/cluster_set.py +12 -2
  23. pytcl/containers/covertree.py +5 -3
  24. pytcl/containers/rtree.py +1 -1
  25. pytcl/containers/vptree.py +4 -2
  26. pytcl/coordinate_systems/conversions/geodetic.py +31 -7
  27. pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
  28. pytcl/coordinate_systems/projections/projections.py +2 -2
  29. pytcl/coordinate_systems/rotations/rotations.py +10 -6
  30. pytcl/core/validation.py +3 -3
  31. pytcl/dynamic_estimation/__init__.py +16 -16
  32. pytcl/dynamic_estimation/gaussian_sum_filter.py +20 -38
  33. pytcl/dynamic_estimation/imm.py +14 -14
  34. pytcl/dynamic_estimation/kalman/__init__.py +1 -1
  35. pytcl/dynamic_estimation/kalman/constrained.py +35 -23
  36. pytcl/dynamic_estimation/kalman/extended.py +8 -8
  37. pytcl/dynamic_estimation/kalman/h_infinity.py +2 -2
  38. pytcl/dynamic_estimation/kalman/square_root.py +8 -2
  39. pytcl/dynamic_estimation/kalman/sr_ukf.py +3 -3
  40. pytcl/dynamic_estimation/kalman/ud_filter.py +11 -5
  41. pytcl/dynamic_estimation/kalman/unscented.py +8 -6
  42. pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
  43. pytcl/dynamic_estimation/rbpf.py +36 -40
  44. pytcl/gravity/spherical_harmonics.py +3 -3
  45. pytcl/gravity/tides.py +6 -6
  46. pytcl/logging_config.py +3 -3
  47. pytcl/magnetism/emm.py +10 -3
  48. pytcl/magnetism/wmm.py +4 -4
  49. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
  50. pytcl/mathematical_functions/geometry/geometry.py +5 -5
  51. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
  52. pytcl/mathematical_functions/signal_processing/detection.py +24 -24
  53. pytcl/mathematical_functions/signal_processing/filters.py +14 -14
  54. pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
  55. pytcl/mathematical_functions/special_functions/bessel.py +15 -3
  56. pytcl/mathematical_functions/special_functions/debye.py +5 -1
  57. pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
  58. pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
  59. pytcl/mathematical_functions/special_functions/hypergeometric.py +6 -4
  60. pytcl/mathematical_functions/transforms/fourier.py +8 -8
  61. pytcl/mathematical_functions/transforms/stft.py +12 -12
  62. pytcl/mathematical_functions/transforms/wavelets.py +9 -9
  63. pytcl/navigation/geodesy.py +3 -3
  64. pytcl/navigation/great_circle.py +5 -5
  65. pytcl/plotting/coordinates.py +7 -7
  66. pytcl/plotting/tracks.py +2 -2
  67. pytcl/static_estimation/maximum_likelihood.py +16 -14
  68. pytcl/static_estimation/robust.py +5 -5
  69. pytcl/terrain/loaders.py +5 -5
  70. pytcl/trackers/hypothesis.py +1 -1
  71. pytcl/trackers/mht.py +9 -9
  72. pytcl/trackers/multi_target.py +1 -1
  73. {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/LICENSE +0 -0
  74. {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/WHEEL +0 -0
  75. {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.1.dist-info}/top_level.txt +0 -0
@@ -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
- 'N2': 28.014,
32
- 'O2': 31.999,
33
- 'O': 15.999,
34
- 'He': 4.003,
35
- 'H': 1.008,
36
- 'Ar': 39.948,
37
- 'N': 14.007,
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
- "latitude, longitude, and altitude must be broadcastable"
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(alt_km, lat, lon,
246
- day_of_year, seconds_in_day,
247
- texo)
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
- n2_dens * _MW['N2'] +
263
- o2_dens * _MW['O2'] +
264
- o_dens * _MW['O'] +
265
- he_dens * _MW['He'] +
266
- h_dens * _MW['H'] +
267
- ar_dens * _MW['Ar'] +
268
- n_dens * _MW['N']
269
- ) / na / 1000.0 # Convert g to kg
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 = (np.asarray(altitude).ndim == 0)
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(-(alt_km[mask_thermo] - transition_alt) / h_trans)
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(-(alt_km[mask_thermo] - transition_alt) / h_trans)
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
- dens_ref * onset_smooth * np.exp(exponent)
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['N2'] / _MW['He']
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['N2'] / _MW['N']
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