Skip to main content

sidereon_core/astro/
atmosphere.rs

1//! NRLMSISE-00 empirical neutral-atmosphere model (Picone et al., 2002).
2//!
3//! This is the full NRLMSISE-00 model: all eight species (He, O, N2, O2, Ar,
4//! H, N, and anomalous oxygen), total mass density, and exospheric plus
5//! altitude temperature, from the surface to the lower exosphere (~1000 km),
6//! as a function of position, time, and solar/geomagnetic activity. It is the
7//! standard model used for satellite-drag prediction.
8//!
9//! `gtd7` returns the total mass density excluding anomalous oxygen; `gtd7d`
10//! returns the effective total mass density for drag, folding anomalous oxygen
11//! into `d[5]` (relevant above ~500 km). The supporting call tree (`gts7`,
12//! `globe7`/`glob7s`, `densu`/`densm`, `ccor`/`ccor2`, `scalh`, `dnet`,
13//! `spline`/`splint`/`splini`, `zeta`) mirrors the reference.
14//!
15//! Provenance and license: the model was developed by Mike Picone, Alan Hedin,
16//! and Doug Drob at the US Naval Research Laboratory and is in the public
17//! domain. The model logic and the full coefficient tables in `tables` were
18//! transcribed verbatim from Dominik Brodowski's public-domain C port
19//! (`nrlmsise-00.c` / `nrlmsise-00_data.c`, release 20041227,
20//! <https://www.brodo.de/space/nrlmsise/>). The coefficient tables are
21//! reproduced bit-for-bit; the implementation reproduces the reference's own
22//! constants (its `re`/`gsurf` datum, gas constant, and species masses) rather
23//! than substituting this crate's WGS84/GM values, so output matches the
24//! reference oracle. Reference: Picone, J.M., Hedin, A.E., Drob, D.P., and
25//! Aikin, A.C., "NRLMSISE-00 empirical model of the atmosphere", J. Geophys.
26//! Res., 107(A12), 1468, 2002.
27
28// The model is a verbatim numeric port: many loops index parallel coefficient
29// and term arrays by position (`t[i]` against `sw[i+1]`), which is clearer than
30// an iterator and preserves the reference structure.
31#![allow(clippy::needless_range_loop)]
32
33mod tables;
34
35/// Magnetic-activity history (the `ap_a` array), used when switch 9 is -1.
36///
37/// Elements: daily AP; 3 hr AP for current time; 3 hr AP at -3/-6/-9 hr;
38/// average of eight 3 hr indices from 12 to 33 hr prior; average of eight from
39/// 36 to 57 hr prior.
40pub type ApArray = [f64; 7];
41
42use crate::astro::constants::time::SECONDS_PER_HOUR;
43
44/// Documented upper bound of the NRLMSISE-00 altitude domain (km).
45///
46/// The model spans the surface to the lower exosphere; above this the profile
47/// is an extrapolation rather than a fit, so the API rejects it.
48pub const MAX_ALTITUDE_KM: f64 = 1000.0;
49
50/// Error returned when an evaluation cannot be performed for the requested
51/// inputs or switch configuration.
52///
53/// The model itself is total: it will emit a number for any input. These
54/// variants guard the API boundary so a misconfigured call surfaces a typed
55/// failure instead of a plausible-but-wrong or non-finite result.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
57pub enum AtmosphereError {
58    /// Switch 9 is set to -1 (Ap-history mode) but [`NrlmsiseInput::ap_array`]
59    /// was not supplied. There is no defensible default Ap history, so the call
60    /// is rejected rather than silently substituting zeros.
61    #[error("ap_array is required when switch 9 is -1 (Ap-history mode)")]
62    MissingApArray,
63    /// A model input was not finite (NaN or infinity). The offending field name
64    /// is carried for diagnostics.
65    #[error("non-finite input: {0}")]
66    NonFiniteInput(&'static str),
67    /// A model input was outside the documented valid domain (altitude in
68    /// `[0, MAX_ALTITUDE_KM]`; `f107`, `f107a`, `ap`, and every `ap_array`
69    /// element finite and non-negative). The offending field name is carried.
70    #[error("input out of domain: {0}")]
71    OutOfDomain(&'static str),
72}
73
74/// Validate inputs and switch configuration at the API boundary.
75///
76/// Policy: reject Ap-history mode without an `ap_array`; reject any non-finite
77/// numeric input; require altitude in `[0, MAX_ALTITUDE_KM]` and the solar and
78/// geomagnetic indices (`f107`, `f107a`, `ap`, and any `ap_array` element) to
79/// be finite and non-negative. Latitude, longitude, time, and local solar time
80/// are only checked for finiteness; longitude keeps its `<= -1000` sentinel
81/// (longitude variation off), which the finite check still admits.
82fn validate(input: &NrlmsiseInput, flags: &Flags) -> Result<(), AtmosphereError> {
83    use AtmosphereError::*;
84
85    if flags.switches[9] == -1 && input.ap_array.is_none() {
86        return Err(MissingApArray);
87    }
88
89    for (value, name) in [
90        (input.alt, "alt"),
91        (input.g_lat, "g_lat"),
92        (input.g_long, "g_long"),
93        (input.sec, "sec"),
94        (input.lst, "lst"),
95        (input.f107, "f107"),
96        (input.f107a, "f107a"),
97        (input.ap, "ap"),
98    ] {
99        if !value.is_finite() {
100            return Err(NonFiniteInput(name));
101        }
102    }
103
104    if !(0.0..=MAX_ALTITUDE_KM).contains(&input.alt) {
105        return Err(OutOfDomain("alt"));
106    }
107    if input.f107 < 0.0 {
108        return Err(OutOfDomain("f107"));
109    }
110    if input.f107a < 0.0 {
111        return Err(OutOfDomain("f107a"));
112    }
113    if input.ap < 0.0 {
114        return Err(OutOfDomain("ap"));
115    }
116    if let Some(ap_array) = input.ap_array {
117        for v in ap_array {
118            if !v.is_finite() {
119                return Err(NonFiniteInput("ap_array"));
120            }
121            if v < 0.0 {
122                return Err(OutOfDomain("ap_array"));
123            }
124        }
125    }
126
127    Ok(())
128}
129
130/// Inputs to the neutral-atmosphere evaluation (mirrors the reference
131/// `nrlmsise_input`).
132#[derive(Debug, Clone, Copy)]
133pub struct NrlmsiseInput {
134    /// Year (ignored by the model; kept for API stability).
135    pub year: i32,
136    /// Day of year (1-366).
137    pub doy: i32,
138    /// Seconds in day (UT).
139    pub sec: f64,
140    /// Geodetic altitude (km).
141    pub alt: f64,
142    /// Geodetic latitude (deg).
143    pub g_lat: f64,
144    /// Geodetic longitude (deg).
145    pub g_long: f64,
146    /// Local apparent solar time (hours); for consistency use
147    /// `sec/3600 + g_long/15`.
148    pub lst: f64,
149    /// 81-day average of F10.7 flux (centered on `doy`).
150    pub f107a: f64,
151    /// Daily F10.7 flux for the previous day.
152    pub f107: f64,
153    /// Daily magnetic Ap index.
154    pub ap: f64,
155    /// Optional Ap history; required when switch 9 is set to -1.
156    pub ap_array: Option<ApArray>,
157}
158
159/// Outputs of the neutral-atmosphere evaluation (mirrors the reference
160/// `nrlmsise_output`).
161///
162/// `d` holds number densities (He, O, N2, O2, Ar, _total_, H, N, anomalous O);
163/// index 5 is the total mass density. `t[0]` is exospheric temperature, `t[1]`
164/// is temperature at altitude. Units follow the switch-0 flag: with metric
165/// output, densities are m^-3, total mass density is kg/m^3; otherwise cm^-3
166/// and g/cm^3.
167#[derive(Debug, Clone, Copy, Default, PartialEq)]
168pub struct NrlmsiseOutput {
169    /// Densities: He, O, N2, O2, Ar, total mass density, H, N, anomalous O.
170    pub d: [f64; 9],
171    /// Temperatures: exospheric, at altitude.
172    pub t: [f64; 2],
173}
174
175impl NrlmsiseOutput {
176    /// Total mass density (`d[5]`).
177    pub fn density(&self) -> f64 {
178        self.d[5]
179    }
180    /// Exospheric temperature (`t[0]`).
181    pub fn temperature_exo(&self) -> f64 {
182        self.t[0]
183    }
184    /// Temperature at the requested altitude (`t[1]`).
185    pub fn temperature_alt(&self) -> f64 {
186        self.t[1]
187    }
188}
189
190/// Variation switches (mirrors the reference `nrlmsise_flags`).
191///
192/// `switches[0]` selects metric output (m/kg) when nonzero; entries 1..=23
193/// enable individual variations (0 off, 1 on, 2 main effects off but cross
194/// terms on). Entry 9 set to -1 selects Ap-history mode and requires
195/// [`NrlmsiseInput::ap_array`].
196#[derive(Debug, Clone, Copy)]
197pub struct Flags {
198    /// Caller-set variation switches.
199    pub switches: [i32; 24],
200    sw: [f64; 24],
201    swc: [f64; 24],
202}
203
204impl Flags {
205    /// Build flags from a raw switch vector, deriving the internal `sw`/`swc`.
206    pub fn new(switches: [i32; 24]) -> Self {
207        let mut f = Flags {
208            switches,
209            sw: [0.0; 24],
210            swc: [0.0; 24],
211        };
212        f.tselec();
213        f
214    }
215
216    /// Reference standard flags: CGS output, all variations on (the
217    /// configuration of the reference test program).
218    pub fn standard() -> Self {
219        let mut s = [1i32; 24];
220        s[0] = 0;
221        Flags::new(s)
222    }
223
224    /// All variations on with metric (m/kg) output.
225    pub fn metric() -> Self {
226        Flags::new([1i32; 24])
227    }
228
229    /// Derive `sw`/`swc` from `switches` (reference `tselec`).
230    fn tselec(&mut self) {
231        for i in 0..24 {
232            if i != 9 {
233                self.sw[i] = if self.switches[i] == 1 { 1.0 } else { 0.0 };
234                self.swc[i] = if self.switches[i] > 0 { 1.0 } else { 0.0 };
235            } else {
236                self.sw[i] = self.switches[i] as f64;
237                self.swc[i] = self.switches[i] as f64;
238            }
239        }
240    }
241}
242
243/// Local apparent solar time (hours) from UT seconds and longitude (deg).
244pub fn local_solar_time(sec: f64, g_long: f64) -> f64 {
245    let lst = sec / SECONDS_PER_HOUR + g_long / 15.0;
246    ((lst % 24.0) + 24.0) % 24.0
247}
248
249/// Default daily F10.7 solar-radio flux (previous day), solar flux units.
250///
251/// This is the NRLMSISE-00 reference test-program standard input (release
252/// 20041227) used throughout the oracle cases. Bindings offering a sane default
253/// (rather than requiring live space-weather input) read this.
254pub const DEFAULT_F107: f64 = 150.0;
255
256/// Default 81-day centred average F10.7 solar-radio flux, solar flux units.
257///
258/// This is also the model's own reference baseline: the `globe7` flux term is
259/// centred on `f107a - 150.0`, so 150 is the neutral value. Matches the
260/// NRLMSISE-00 reference test-program standard input.
261pub const DEFAULT_F107A: f64 = 150.0;
262
263/// Default daily magnetic Ap index (dimensionless).
264///
265/// NRLMSISE-00 reference test-program standard input (release 20041227),
266/// representing quiet geomagnetic conditions.
267pub const DEFAULT_AP: f64 = 4.0;
268
269const DGTR: f64 = 1.74533E-2;
270const DR: f64 = 1.72142E-2;
271const HR: f64 = 0.2618;
272const SR: f64 = 7.2722E-5;
273const RGAS: f64 = 831.4;
274
275/// Chemistry/dissociation correction (reference `ccor`).
276fn ccor(alt: f64, r: f64, h1: f64, zh: f64) -> f64 {
277    let e = (alt - zh) / h1;
278    if e > 70.0 {
279        return libm::exp(0.0_f64);
280    }
281    if e < -70.0 {
282        return libm::exp(r);
283    }
284    let ex = libm::exp(e);
285    let e = r / (1.0 + ex);
286    libm::exp(e)
287}
288
289/// Chemistry/dissociation correction with two scale lengths (reference
290/// `ccor2`).
291fn ccor2(alt: f64, r: f64, h1: f64, zh: f64, h2: f64) -> f64 {
292    let e1 = (alt - zh) / h1;
293    let e2 = (alt - zh) / h2;
294    if (e1 > 70.0) || (e2 > 70.0) {
295        return libm::exp(0.0_f64);
296    }
297    if (e1 < -70.0) && (e2 < -70.0) {
298        return libm::exp(r);
299    }
300    let ex1 = libm::exp(e1);
301    let ex2 = libm::exp(e2);
302    let ccor2v = r / (1.0 + 0.5 * (ex1 + ex2));
303    libm::exp(ccor2v)
304}
305
306/// Turbopause density blend (reference `dnet`).
307fn dnet(dd: f64, dm: f64, zhm: f64, xmm: f64, xm: f64) -> f64 {
308    let a = zhm / (xmm - xm);
309    let (mut dd, dm) = (dd, dm);
310    if !((dm > 0.0) && (dd > 0.0)) {
311        if (dd == 0.0) && (dm == 0.0) {
312            dd = 1.0;
313        }
314        if dm == 0.0 {
315            return dd;
316        }
317        if dd == 0.0 {
318            return dm;
319        }
320    }
321    let ylog = a * libm::log(dm / dd);
322    if ylog < -10.0 {
323        return dd;
324    }
325    if ylog > 10.0 {
326        return dm;
327    }
328    dd * libm::pow(1.0 + libm::exp(ylog), 1.0 / a)
329}
330
331/// Integrate a cubic spline from `xa[0]` to `x` (reference `splini`).
332fn splini(xa: &[f64], ya: &[f64], y2a: &[f64], n: usize, x: f64) -> f64 {
333    let mut yi = 0.0;
334    let mut klo = 0usize;
335    let mut khi = 1usize;
336    while (x > xa[klo]) && (khi < n) {
337        let mut xx = x;
338        if khi < (n - 1) {
339            if x < xa[khi] {
340                xx = x;
341            } else {
342                xx = xa[khi];
343            }
344        }
345        let h = xa[khi] - xa[klo];
346        let a = (xa[khi] - xx) / h;
347        let b = (xx - xa[klo]) / h;
348        let a2 = a * a;
349        let b2 = b * b;
350        yi += ((1.0 - a2) * ya[klo] / 2.0
351            + b2 * ya[khi] / 2.0
352            + ((-(1.0 + a2 * a2) / 4.0 + a2 / 2.0) * y2a[klo]
353                + (b2 * b2 / 4.0 - b2 / 2.0) * y2a[khi])
354                * h
355                * h
356                / 6.0)
357            * h;
358        klo += 1;
359        khi += 1;
360    }
361    yi
362}
363
364/// Cubic-spline interpolation (reference `splint`).
365fn splint(xa: &[f64], ya: &[f64], y2a: &[f64], n: usize, x: f64) -> f64 {
366    let mut klo = 0usize;
367    let mut khi = n - 1;
368    while (khi - klo) > 1 {
369        let k = (khi + klo) / 2;
370        if xa[k] > x {
371            khi = k;
372        } else {
373            klo = k;
374        }
375    }
376    let h = xa[khi] - xa[klo];
377    let a = (xa[khi] - x) / h;
378    let b = (x - xa[klo]) / h;
379    a * ya[klo]
380        + b * ya[khi]
381        + ((a * a * a - a) * y2a[klo] + (b * b * b - b) * y2a[khi]) * h * h / 6.0
382}
383
384/// Second derivatives of a cubic-spline interpolant (reference `spline`).
385fn spline(x: &[f64], y: &[f64], n: usize, yp1: f64, ypn: f64, y2: &mut [f64]) {
386    let mut u = [0.0_f64; 10];
387    if yp1 > 0.99E30 {
388        y2[0] = 0.0;
389        u[0] = 0.0;
390    } else {
391        y2[0] = -0.5;
392        u[0] = (3.0 / (x[1] - x[0])) * ((y[1] - y[0]) / (x[1] - x[0]) - yp1);
393    }
394    for i in 1..(n - 1) {
395        let sig = (x[i] - x[i - 1]) / (x[i + 1] - x[i - 1]);
396        let p = sig * y2[i - 1] + 2.0;
397        y2[i] = (sig - 1.0) / p;
398        u[i] = (6.0
399            * ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1]))
400            / (x[i + 1] - x[i - 1])
401            - sig * u[i - 1])
402            / p;
403    }
404    let (qn, un) = if ypn > 0.99E30 {
405        (0.0, 0.0)
406    } else {
407        (
408            0.5,
409            (3.0 / (x[n - 1] - x[n - 2])) * (ypn - (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2])),
410        )
411    };
412    y2[n - 1] = (un - qn * u[n - 2]) / (qn * y2[n - 2] + 1.0);
413    for k in (0..=(n - 2)).rev() {
414        y2[k] = y2[k] * y2[k + 1] + u[k];
415    }
416}
417
418/// 3 hr magnetic-activity term (reference `g0`).
419fn g0(a: f64, p: &[f64]) -> f64 {
420    a - 4.0
421        + (p[25] - 1.0)
422            * (a - 4.0
423                + (libm::exp(-(p[24] * p[24]).sqrt() * (a - 4.0)) - 1.0) / (p[24] * p[24]).sqrt())
424}
425
426/// 3 hr magnetic-activity sum normalizer (reference `sumex`).
427fn sumex(ex: f64) -> f64 {
428    1.0 + (1.0 - libm::pow(ex, 19.0)) / (1.0 - ex) * libm::pow(ex, 0.5)
429}
430
431/// 3 hr magnetic-activity weighted sum (reference `sg0`).
432fn sg0(ex: f64, p: &[f64], ap: &[f64]) -> f64 {
433    (g0(ap[1], p)
434        + (g0(ap[2], p) * ex
435            + g0(ap[3], p) * ex * ex
436            + g0(ap[4], p) * libm::pow(ex, 3.0)
437            + (g0(ap[5], p) * libm::pow(ex, 4.0) + g0(ap[6], p) * libm::pow(ex, 12.0))
438                * (1.0 - libm::pow(ex, 8.0))
439                / (1.0 - ex)))
440        / sumex(ex)
441}
442
443/// Working state for one evaluation: the reference's shared (static) variables,
444/// scoped to a single call tree instead of file-global mutable state.
445#[derive(Clone)]
446struct State {
447    gsurf: f64,
448    re: f64,
449    dd: f64,
450    dm04: f64,
451    dm16: f64,
452    dm28: f64,
453    dm32: f64,
454    dm40: f64,
455    dm01: f64,
456    dm14: f64,
457    meso_tn1: [f64; 5],
458    meso_tn2: [f64; 4],
459    meso_tn3: [f64; 5],
460    meso_tgn1: [f64; 2],
461    meso_tgn2: [f64; 2],
462    meso_tgn3: [f64; 2],
463    dfa: f64,
464    plg: [[f64; 9]; 4],
465    ctloc: f64,
466    stloc: f64,
467    c2tloc: f64,
468    s2tloc: f64,
469    s3tloc: f64,
470    c3tloc: f64,
471    apdf: f64,
472    apt: [f64; 4],
473}
474
475impl State {
476    fn new() -> Self {
477        State {
478            gsurf: 0.0,
479            re: 0.0,
480            dd: 0.0,
481            dm04: 0.0,
482            dm16: 0.0,
483            dm28: 0.0,
484            dm32: 0.0,
485            dm40: 0.0,
486            dm01: 0.0,
487            dm14: 0.0,
488            meso_tn1: [0.0; 5],
489            meso_tn2: [0.0; 4],
490            meso_tn3: [0.0; 5],
491            meso_tgn1: [0.0; 2],
492            meso_tgn2: [0.0; 2],
493            meso_tgn3: [0.0; 2],
494            dfa: 0.0,
495            plg: [[0.0; 9]; 4],
496            ctloc: 0.0,
497            stloc: 0.0,
498            c2tloc: 0.0,
499            s2tloc: 0.0,
500            s3tloc: 0.0,
501            c3tloc: 0.0,
502            apdf: 0.0,
503            apt: [0.0; 4],
504        }
505    }
506
507    /// Latitude variation of gravity; sets `gsurf` and `re` (reference
508    /// `glatf`).
509    fn glatf(&mut self, lat: f64) {
510        let c2 = libm::cos(2.0 * DGTR * lat);
511        self.gsurf = 980.616 * (1.0 - 0.0026373 * c2);
512        self.re = 2.0 * self.gsurf / (3.085462E-6 + 2.27E-9 * c2) * 1.0E-5;
513    }
514
515    /// Geopotential difference (reference `zeta`).
516    fn zeta(&self, zz: f64, zl: f64) -> f64 {
517        (zz - zl) * (self.re + zl) / (self.re + zz)
518    }
519
520    /// Scale height (reference `scalh`).
521    fn scalh(&self, alt: f64, xm: f64, temp: f64) -> f64 {
522        let g = self.gsurf / libm::pow(1.0 + alt / self.re, 2.0);
523        RGAS * temp / (g * xm)
524    }
525
526    /// Temperature and density profiles for the lower atmosphere (reference
527    /// `densm`). Returns density (or, when `xm == 0`, the temperature) and
528    /// writes the temperature at altitude into `tz`.
529    #[allow(clippy::too_many_arguments)]
530    fn densm(
531        &self,
532        alt: f64,
533        d0: f64,
534        xm: f64,
535        tz: &mut f64,
536        mn3: usize,
537        zn3: &[f64],
538        tn3: &[f64],
539        tgn3: &[f64],
540        mn2: usize,
541        zn2: &[f64],
542        tn2: &[f64],
543        tgn2: &[f64],
544    ) -> f64 {
545        let mut xs = [0.0_f64; 10];
546        let mut ys = [0.0_f64; 10];
547        let mut y2out = [0.0_f64; 10];
548        let mut densm_tmp = d0;
549
550        if alt > zn2[0] {
551            if xm == 0.0 {
552                return *tz;
553            } else {
554                return d0;
555            }
556        }
557
558        // Stratosphere / mesosphere temperature.
559        let z = if alt > zn2[mn2 - 1] {
560            alt
561        } else {
562            zn2[mn2 - 1]
563        };
564        let mn = mn2;
565        let z1 = zn2[0];
566        let z2 = zn2[mn - 1];
567        let t1 = tn2[0];
568        let t2 = tn2[mn - 1];
569        let zg = self.zeta(z, z1);
570        let zgdif = self.zeta(z2, z1);
571
572        for k in 0..mn {
573            xs[k] = self.zeta(zn2[k], z1) / zgdif;
574            ys[k] = 1.0 / tn2[k];
575        }
576        let yd1 = -tgn2[0] / (t1 * t1) * zgdif;
577        let yd2 = -tgn2[1] / (t2 * t2) * zgdif * libm::pow((self.re + z2) / (self.re + z1), 2.0);
578
579        spline(&xs, &ys, mn, yd1, yd2, &mut y2out);
580        let x = zg / zgdif;
581        let y = splint(&xs, &ys, &y2out, mn, x);
582
583        *tz = 1.0 / y;
584        if xm != 0.0 {
585            let glb = self.gsurf / libm::pow(1.0 + z1 / self.re, 2.0);
586            let gamm = xm * glb * zgdif / RGAS;
587            let yi = splini(&xs, &ys, &y2out, mn, x);
588            let mut expl = gamm * yi;
589            if expl > 50.0 {
590                expl = 50.0;
591            }
592            densm_tmp = densm_tmp * (t1 / *tz) * libm::exp(-expl);
593        }
594
595        if alt > zn3[0] {
596            if xm == 0.0 {
597                return *tz;
598            } else {
599                return densm_tmp;
600            }
601        }
602
603        // Troposphere / stratosphere temperature.
604        let z = alt;
605        let mn = mn3;
606        let z1 = zn3[0];
607        let z2 = zn3[mn - 1];
608        let t1 = tn3[0];
609        let t2 = tn3[mn - 1];
610        let zg = self.zeta(z, z1);
611        let zgdif = self.zeta(z2, z1);
612
613        for k in 0..mn {
614            xs[k] = self.zeta(zn3[k], z1) / zgdif;
615            ys[k] = 1.0 / tn3[k];
616        }
617        let yd1 = -tgn3[0] / (t1 * t1) * zgdif;
618        let yd2 = -tgn3[1] / (t2 * t2) * zgdif * libm::pow((self.re + z2) / (self.re + z1), 2.0);
619
620        spline(&xs, &ys, mn, yd1, yd2, &mut y2out);
621        let x = zg / zgdif;
622        let y = splint(&xs, &ys, &y2out, mn, x);
623
624        *tz = 1.0 / y;
625        if xm != 0.0 {
626            let glb = self.gsurf / libm::pow(1.0 + z1 / self.re, 2.0);
627            let gamm = xm * glb * zgdif / RGAS;
628            let yi = splini(&xs, &ys, &y2out, mn, x);
629            let mut expl = gamm * yi;
630            if expl > 50.0 {
631                expl = 50.0;
632            }
633            densm_tmp = densm_tmp * (t1 / *tz) * libm::exp(-expl);
634        }
635        if xm == 0.0 {
636            *tz
637        } else {
638            densm_tmp
639        }
640    }
641
642    /// Temperature and density profiles for the thermosphere (reference
643    /// `densu`). Returns density (or temperature when `xm == 0`) and writes the
644    /// temperature at altitude into `tz`. `tn1`/`tgn1` end nodes are updated in
645    /// place, matching the reference.
646    #[allow(clippy::too_many_arguments)]
647    fn densu(
648        &self,
649        alt: f64,
650        dlb: f64,
651        tinf: f64,
652        tlb: f64,
653        xm: f64,
654        alpha: f64,
655        tz: &mut f64,
656        zlb: f64,
657        s2: f64,
658        mn1: usize,
659        zn1: &[f64],
660        tn1: &mut [f64],
661        tgn1: &mut [f64],
662    ) -> f64 {
663        let mut xs = [0.0_f64; 5];
664        let mut ys = [0.0_f64; 5];
665        let mut y2out = [0.0_f64; 5];
666        let mut densu_temp;
667
668        let za = zn1[0];
669        let z = if alt > za { alt } else { za };
670
671        let zg2 = self.zeta(z, zlb);
672
673        let tt = tinf - (tinf - tlb) * libm::exp(-s2 * zg2);
674        let ta = tt;
675        *tz = tt;
676        densu_temp = *tz;
677
678        // Spline node bookkeeping reused for the sub-`za` density branch.
679        let mut z1 = 0.0;
680        let mut zgdif = 0.0;
681        let mut mn = mn1;
682        let mut x = 0.0;
683        let mut t1 = 0.0;
684
685        if alt < za {
686            let dta = (tinf - ta) * s2 * libm::pow((self.re + zlb) / (self.re + za), 2.0);
687            tgn1[0] = dta;
688            tn1[0] = ta;
689            let z = if alt > zn1[mn1 - 1] {
690                alt
691            } else {
692                zn1[mn1 - 1]
693            };
694            mn = mn1;
695            z1 = zn1[0];
696            let z2 = zn1[mn - 1];
697            t1 = tn1[0];
698            let t2 = tn1[mn - 1];
699            let zg = self.zeta(z, z1);
700            zgdif = self.zeta(z2, z1);
701            for k in 0..mn {
702                xs[k] = self.zeta(zn1[k], z1) / zgdif;
703                ys[k] = 1.0 / tn1[k];
704            }
705            let yd1 = -tgn1[0] / (t1 * t1) * zgdif;
706            let yd2 =
707                -tgn1[1] / (t2 * t2) * zgdif * libm::pow((self.re + z2) / (self.re + z1), 2.0);
708            spline(&xs, &ys, mn, yd1, yd2, &mut y2out);
709            x = zg / zgdif;
710            let y = splint(&xs, &ys, &y2out, mn, x);
711            *tz = 1.0 / y;
712            densu_temp = *tz;
713        }
714        if xm == 0.0 {
715            return densu_temp;
716        }
717
718        let glb = self.gsurf / libm::pow(1.0 + zlb / self.re, 2.0);
719        let gamma = xm * glb / (s2 * RGAS * tinf);
720        let mut expl = libm::exp(-s2 * gamma * zg2);
721        if expl > 50.0 {
722            expl = 50.0;
723        }
724        if tt <= 0.0 {
725            expl = 50.0;
726        }
727
728        let densa = dlb * libm::pow(tlb / tt, 1.0 + alpha + gamma) * expl;
729        densu_temp = densa;
730        if alt >= za {
731            return densu_temp;
732        }
733
734        let glb = self.gsurf / libm::pow(1.0 + z1 / self.re, 2.0);
735        let gamm = xm * glb * zgdif / RGAS;
736
737        let yi = splini(&xs, &ys, &y2out, mn, x);
738        let mut expl = gamm * yi;
739        if expl > 50.0 {
740            expl = 50.0;
741        }
742        if *tz <= 0.0 {
743            expl = 50.0;
744        }
745
746        densu_temp * libm::pow(t1 / *tz, 1.0 + alpha) * libm::exp(-expl)
747    }
748
749    /// Upper-thermosphere G(L) expansion (reference `globe7`). Sets the shared
750    /// Legendre/time/activity state used by [`State::glob7s`].
751    fn globe7(&mut self, p: &[f64], input: &NrlmsiseInput, flags: &Flags) -> f64 {
752        let mut t = [0.0_f64; 15];
753        let tloc = input.lst;
754
755        let c = libm::sin(input.g_lat * DGTR);
756        let s = libm::cos(input.g_lat * DGTR);
757        let c2 = c * c;
758        let c4 = c2 * c2;
759        let s2 = s * s;
760
761        self.plg[0][1] = c;
762        self.plg[0][2] = 0.5 * (3.0 * c2 - 1.0);
763        self.plg[0][3] = 0.5 * (5.0 * c * c2 - 3.0 * c);
764        self.plg[0][4] = (35.0 * c4 - 30.0 * c2 + 3.0) / 8.0;
765        self.plg[0][5] = (63.0 * c2 * c2 * c - 70.0 * c2 * c + 15.0 * c) / 8.0;
766        self.plg[0][6] = (11.0 * c * self.plg[0][5] - 5.0 * self.plg[0][4]) / 6.0;
767        self.plg[1][1] = s;
768        self.plg[1][2] = 3.0 * c * s;
769        self.plg[1][3] = 1.5 * (5.0 * c2 - 1.0) * s;
770        self.plg[1][4] = 2.5 * (7.0 * c2 * c - 3.0 * c) * s;
771        self.plg[1][5] = 1.875 * (21.0 * c4 - 14.0 * c2 + 1.0) * s;
772        self.plg[1][6] = (11.0 * c * self.plg[1][5] - 6.0 * self.plg[1][4]) / 5.0;
773        self.plg[2][2] = 3.0 * s2;
774        self.plg[2][3] = 15.0 * s2 * c;
775        self.plg[2][4] = 7.5 * (7.0 * c2 - 1.0) * s2;
776        self.plg[2][5] = 3.0 * c * self.plg[2][4] - 2.0 * self.plg[2][3];
777        self.plg[2][6] = (11.0 * c * self.plg[2][5] - 7.0 * self.plg[2][4]) / 4.0;
778        self.plg[2][7] = (13.0 * c * self.plg[2][6] - 8.0 * self.plg[2][5]) / 5.0;
779        self.plg[3][3] = 15.0 * s2 * s;
780        self.plg[3][4] = 105.0 * s2 * s * c;
781        self.plg[3][5] = (9.0 * c * self.plg[3][4] - 7.0 * self.plg[3][3]) / 2.0;
782        self.plg[3][6] = (11.0 * c * self.plg[3][5] - 8.0 * self.plg[3][4]) / 3.0;
783
784        if !(((flags.sw[7] == 0.0) && (flags.sw[8] == 0.0)) && (flags.sw[14] == 0.0)) {
785            self.stloc = libm::sin(HR * tloc);
786            self.ctloc = libm::cos(HR * tloc);
787            self.s2tloc = libm::sin(2.0 * HR * tloc);
788            self.c2tloc = libm::cos(2.0 * HR * tloc);
789            self.s3tloc = libm::sin(3.0 * HR * tloc);
790            self.c3tloc = libm::cos(3.0 * HR * tloc);
791        }
792
793        let doy = input.doy as f64;
794        let cd32 = libm::cos(DR * (doy - p[31]));
795        let cd18 = libm::cos(2.0 * DR * (doy - p[17]));
796        let cd14 = libm::cos(DR * (doy - p[13]));
797        let cd39 = libm::cos(2.0 * DR * (doy - p[38]));
798
799        // F10.7 effect.
800        let df = input.f107 - input.f107a;
801        self.dfa = input.f107a - 150.0;
802        let dfa = self.dfa;
803        t[0] = p[19] * df * (1.0 + p[59] * dfa)
804            + p[20] * df * df
805            + p[21] * dfa
806            + p[29] * libm::pow(dfa, 2.0);
807        let f1 = 1.0 + (p[47] * dfa + p[19] * df + p[20] * df * df) * flags.swc[1];
808        let f2 = 1.0 + (p[49] * dfa + p[19] * df + p[20] * df * df) * flags.swc[1];
809
810        // Time independent.
811        t[1] = (p[1] * self.plg[0][2] + p[2] * self.plg[0][4] + p[22] * self.plg[0][6])
812            + (p[14] * self.plg[0][2]) * dfa * flags.swc[1]
813            + p[26] * self.plg[0][1];
814
815        // Symmetrical annual.
816        t[2] = p[18] * cd32;
817
818        // Symmetrical semiannual.
819        t[3] = (p[15] + p[16] * self.plg[0][2]) * cd18;
820
821        // Asymmetrical annual.
822        t[4] = f1 * (p[9] * self.plg[0][1] + p[10] * self.plg[0][3]) * cd14;
823
824        // Asymmetrical semiannual.
825        t[5] = p[37] * self.plg[0][1] * cd39;
826
827        // Diurnal.
828        if flags.sw[7] != 0.0 {
829            let t71 = (p[11] * self.plg[1][2]) * cd14 * flags.swc[5];
830            let t72 = (p[12] * self.plg[1][2]) * cd14 * flags.swc[5];
831            t[6] = f2
832                * ((p[3] * self.plg[1][1] + p[4] * self.plg[1][3] + p[27] * self.plg[1][5] + t71)
833                    * self.ctloc
834                    + (p[6] * self.plg[1][1]
835                        + p[7] * self.plg[1][3]
836                        + p[28] * self.plg[1][5]
837                        + t72)
838                        * self.stloc);
839        }
840
841        // Semidiurnal.
842        if flags.sw[8] != 0.0 {
843            let t81 = (p[23] * self.plg[2][3] + p[35] * self.plg[2][5]) * cd14 * flags.swc[5];
844            let t82 = (p[33] * self.plg[2][3] + p[36] * self.plg[2][5]) * cd14 * flags.swc[5];
845            t[7] = f2
846                * ((p[5] * self.plg[2][2] + p[41] * self.plg[2][4] + t81) * self.c2tloc
847                    + (p[8] * self.plg[2][2] + p[42] * self.plg[2][4] + t82) * self.s2tloc);
848        }
849
850        // Terdiurnal.
851        if flags.sw[14] != 0.0 {
852            t[13] = f2
853                * ((p[39] * self.plg[3][3]
854                    + (p[93] * self.plg[3][4] + p[46] * self.plg[3][6]) * cd14 * flags.swc[5])
855                    * self.s3tloc
856                    + (p[40] * self.plg[3][3]
857                        + (p[94] * self.plg[3][4] + p[48] * self.plg[3][6]) * cd14 * flags.swc[5])
858                        * self.c3tloc);
859        }
860
861        // Magnetic activity.
862        if flags.sw[9] == -1.0 {
863            // Ap-history mode. The public entry points validate that `ap_array`
864            // is present whenever switch 9 is -1, so this never substitutes a
865            // default; the expect documents that boundary invariant.
866            let ap = input
867                .ap_array
868                .expect("ap_array must be present in Ap-history mode (validated at entry)");
869            if p[51] != 0.0 {
870                // The reference clamps p[24] in place; do so on a local copy
871                // (the clamp never fires for the standard tables).
872                let mut pc = [0.0_f64; 150];
873                pc.copy_from_slice(p);
874                let mut exp1 = libm::exp(
875                    -10800.0 * (pc[51] * pc[51]).sqrt()
876                        / (1.0 + pc[138] * (45.0 - (input.g_lat * input.g_lat).sqrt())),
877                );
878                if exp1 > 0.99999 {
879                    exp1 = 0.99999;
880                }
881                if pc[24] < 1.0E-4 {
882                    pc[24] = 1.0E-4;
883                }
884                self.apt[0] = sg0(exp1, &pc, &ap);
885                if flags.sw[9] != 0.0 {
886                    t[8] = self.apt[0]
887                        * (p[50]
888                            + p[96] * self.plg[0][2]
889                            + p[54] * self.plg[0][4]
890                            + (p[125] * self.plg[0][1]
891                                + p[126] * self.plg[0][3]
892                                + p[127] * self.plg[0][5])
893                                * cd14
894                                * flags.swc[5]
895                            + (p[128] * self.plg[1][1]
896                                + p[129] * self.plg[1][3]
897                                + p[130] * self.plg[1][5])
898                                * flags.swc[7]
899                                * libm::cos(HR * (tloc - p[131])));
900                }
901            }
902        } else {
903            let apd = input.ap - 4.0;
904            let mut p44 = p[43];
905            let p45 = p[44];
906            if p44 < 0.0 {
907                p44 = 1.0E-5;
908            }
909            self.apdf = apd + (p45 - 1.0) * (apd + (libm::exp(-p44 * apd) - 1.0) / p44);
910            if flags.sw[9] != 0.0 {
911                t[8] = self.apdf
912                    * (p[32]
913                        + p[45] * self.plg[0][2]
914                        + p[34] * self.plg[0][4]
915                        + (p[100] * self.plg[0][1]
916                            + p[101] * self.plg[0][3]
917                            + p[102] * self.plg[0][5])
918                            * cd14
919                            * flags.swc[5]
920                        + (p[121] * self.plg[1][1]
921                            + p[122] * self.plg[1][3]
922                            + p[123] * self.plg[1][5])
923                            * flags.swc[7]
924                            * libm::cos(HR * (tloc - p[124])));
925            }
926        }
927
928        if (flags.sw[10] != 0.0) && (input.g_long > -1000.0) {
929            // Longitudinal.
930            if flags.sw[11] != 0.0 {
931                t[10] = (1.0 + p[80] * dfa * flags.swc[1])
932                    * ((p[64] * self.plg[1][2]
933                        + p[65] * self.plg[1][4]
934                        + p[66] * self.plg[1][6]
935                        + p[103] * self.plg[1][1]
936                        + p[104] * self.plg[1][3]
937                        + p[105] * self.plg[1][5]
938                        + flags.swc[5]
939                            * (p[109] * self.plg[1][1]
940                                + p[110] * self.plg[1][3]
941                                + p[111] * self.plg[1][5])
942                            * cd14)
943                        * libm::cos(DGTR * input.g_long)
944                        + (p[90] * self.plg[1][2]
945                            + p[91] * self.plg[1][4]
946                            + p[92] * self.plg[1][6]
947                            + p[106] * self.plg[1][1]
948                            + p[107] * self.plg[1][3]
949                            + p[108] * self.plg[1][5]
950                            + flags.swc[5]
951                                * (p[112] * self.plg[1][1]
952                                    + p[113] * self.plg[1][3]
953                                    + p[114] * self.plg[1][5])
954                                * cd14)
955                            * libm::sin(DGTR * input.g_long));
956            }
957
958            // UT and mixed UT/longitude.
959            if flags.sw[12] != 0.0 {
960                t[11] = (1.0 + p[95] * self.plg[0][1])
961                    * (1.0 + p[81] * dfa * flags.swc[1])
962                    * (1.0 + p[119] * self.plg[0][1] * flags.swc[5] * cd14)
963                    * ((p[68] * self.plg[0][1] + p[69] * self.plg[0][3] + p[70] * self.plg[0][5])
964                        * libm::cos(SR * (input.sec - p[71])));
965                t[11] += flags.swc[11]
966                    * (p[76] * self.plg[2][3] + p[77] * self.plg[2][5] + p[78] * self.plg[2][7])
967                    * libm::cos(SR * (input.sec - p[79]) + 2.0 * DGTR * input.g_long)
968                    * (1.0 + p[137] * dfa * flags.swc[1]);
969            }
970
971            // UT/longitude magnetic activity.
972            if flags.sw[13] != 0.0 {
973                if flags.sw[9] == -1.0 {
974                    if p[51] != 0.0 {
975                        t[12] = self.apt[0]
976                            * flags.swc[11]
977                            * (1.0 + p[132] * self.plg[0][1])
978                            * ((p[52] * self.plg[1][2]
979                                + p[98] * self.plg[1][4]
980                                + p[67] * self.plg[1][6])
981                                * libm::cos(DGTR * (input.g_long - p[97])))
982                            + self.apt[0]
983                                * flags.swc[11]
984                                * flags.swc[5]
985                                * (p[133] * self.plg[1][1]
986                                    + p[134] * self.plg[1][3]
987                                    + p[135] * self.plg[1][5])
988                                * cd14
989                                * libm::cos(DGTR * (input.g_long - p[136]))
990                            + self.apt[0]
991                                * flags.swc[12]
992                                * (p[55] * self.plg[0][1]
993                                    + p[56] * self.plg[0][3]
994                                    + p[57] * self.plg[0][5])
995                                * libm::cos(SR * (input.sec - p[58]));
996                    }
997                } else {
998                    t[12] = self.apdf
999                        * flags.swc[11]
1000                        * (1.0 + p[120] * self.plg[0][1])
1001                        * ((p[60] * self.plg[1][2]
1002                            + p[61] * self.plg[1][4]
1003                            + p[62] * self.plg[1][6])
1004                            * libm::cos(DGTR * (input.g_long - p[63])))
1005                        + self.apdf
1006                            * flags.swc[11]
1007                            * flags.swc[5]
1008                            * (p[115] * self.plg[1][1]
1009                                + p[116] * self.plg[1][3]
1010                                + p[117] * self.plg[1][5])
1011                            * cd14
1012                            * libm::cos(DGTR * (input.g_long - p[118]))
1013                        + self.apdf
1014                            * flags.swc[12]
1015                            * (p[83] * self.plg[0][1]
1016                                + p[84] * self.plg[0][3]
1017                                + p[85] * self.plg[0][5])
1018                            * libm::cos(SR * (input.sec - p[75]));
1019                }
1020            }
1021        }
1022
1023        let mut tinf = p[30];
1024        for i in 0..14 {
1025            tinf += flags.sw[i + 1].abs() * t[i];
1026        }
1027        tinf
1028    }
1029
1030    /// Lower-atmosphere G(L) expansion (reference `glob7s`). Reads shared state
1031    /// produced by [`State::globe7`].
1032    fn glob7s(&self, p: &[f64], input: &NrlmsiseInput, flags: &Flags) -> f64 {
1033        let pset = 2.0;
1034        let mut t = [0.0_f64; 14];
1035        let p99 = if p[99] == 0.0 { pset } else { p[99] };
1036        if p99 != pset {
1037            return -1.0;
1038        }
1039        let doy = input.doy as f64;
1040        let cd32 = libm::cos(DR * (doy - p[31]));
1041        let cd18 = libm::cos(2.0 * DR * (doy - p[17]));
1042        let cd14 = libm::cos(DR * (doy - p[13]));
1043        let cd39 = libm::cos(2.0 * DR * (doy - p[38]));
1044
1045        // F10.7.
1046        t[0] = p[21] * self.dfa;
1047
1048        // Time independent.
1049        t[1] = p[1] * self.plg[0][2]
1050            + p[2] * self.plg[0][4]
1051            + p[22] * self.plg[0][6]
1052            + p[26] * self.plg[0][1]
1053            + p[14] * self.plg[0][3]
1054            + p[59] * self.plg[0][5];
1055
1056        // Symmetrical annual.
1057        t[2] = (p[18] + p[47] * self.plg[0][2] + p[29] * self.plg[0][4]) * cd32;
1058
1059        // Symmetrical semiannual.
1060        t[3] = (p[15] + p[16] * self.plg[0][2] + p[30] * self.plg[0][4]) * cd18;
1061
1062        // Asymmetrical annual.
1063        t[4] = (p[9] * self.plg[0][1] + p[10] * self.plg[0][3] + p[20] * self.plg[0][5]) * cd14;
1064
1065        // Asymmetrical semiannual.
1066        t[5] = (p[37] * self.plg[0][1]) * cd39;
1067
1068        // Diurnal.
1069        if flags.sw[7] != 0.0 {
1070            let t71 = p[11] * self.plg[1][2] * cd14 * flags.swc[5];
1071            let t72 = p[12] * self.plg[1][2] * cd14 * flags.swc[5];
1072            t[6] = (p[3] * self.plg[1][1] + p[4] * self.plg[1][3] + t71) * self.ctloc
1073                + (p[6] * self.plg[1][1] + p[7] * self.plg[1][3] + t72) * self.stloc;
1074        }
1075
1076        // Semidiurnal.
1077        if flags.sw[8] != 0.0 {
1078            let t81 = (p[23] * self.plg[2][3] + p[35] * self.plg[2][5]) * cd14 * flags.swc[5];
1079            let t82 = (p[33] * self.plg[2][3] + p[36] * self.plg[2][5]) * cd14 * flags.swc[5];
1080            t[7] = (p[5] * self.plg[2][2] + p[41] * self.plg[2][4] + t81) * self.c2tloc
1081                + (p[8] * self.plg[2][2] + p[42] * self.plg[2][4] + t82) * self.s2tloc;
1082        }
1083
1084        // Terdiurnal.
1085        if flags.sw[14] != 0.0 {
1086            t[13] = p[39] * self.plg[3][3] * self.s3tloc + p[40] * self.plg[3][3] * self.c3tloc;
1087        }
1088
1089        // Magnetic activity.
1090        if flags.sw[9] != 0.0 {
1091            if flags.sw[9] == 1.0 {
1092                t[8] = self.apdf * (p[32] + p[45] * self.plg[0][2] * flags.swc[2]);
1093            }
1094            if flags.sw[9] == -1.0 {
1095                t[8] = p[50] * self.apt[0] + p[96] * self.plg[0][2] * self.apt[0] * flags.swc[2];
1096            }
1097        }
1098
1099        // Longitudinal.
1100        if !((flags.sw[10] == 0.0) || (flags.sw[11] == 0.0) || (input.g_long <= -1000.0)) {
1101            t[10] = (1.0
1102                + self.plg[0][1]
1103                    * (p[80] * flags.swc[5] * libm::cos(DR * (doy - p[81]))
1104                        + p[85] * flags.swc[6] * libm::cos(2.0 * DR * (doy - p[86])))
1105                + p[83] * flags.swc[3] * libm::cos(DR * (doy - p[84]))
1106                + p[87] * flags.swc[4] * libm::cos(2.0 * DR * (doy - p[88])))
1107                * ((p[64] * self.plg[1][2]
1108                    + p[65] * self.plg[1][4]
1109                    + p[66] * self.plg[1][6]
1110                    + p[74] * self.plg[1][1]
1111                    + p[75] * self.plg[1][3]
1112                    + p[76] * self.plg[1][5])
1113                    * libm::cos(DGTR * input.g_long)
1114                    + (p[90] * self.plg[1][2]
1115                        + p[91] * self.plg[1][4]
1116                        + p[92] * self.plg[1][6]
1117                        + p[77] * self.plg[1][1]
1118                        + p[78] * self.plg[1][3]
1119                        + p[79] * self.plg[1][5])
1120                        * libm::sin(DGTR * input.g_long));
1121        }
1122        let mut tt = 0.0;
1123        for i in 0..14 {
1124            tt += flags.sw[i + 1].abs() * t[i];
1125        }
1126        tt
1127    }
1128}
1129
1130impl State {
1131    /// Thermospheric portion of NRLMSISE-00 (reference `gts7`). Valid for
1132    /// `alt > 72.5 km`.
1133    fn gts7(&mut self, input: &NrlmsiseInput, flags: &Flags, output: &mut NrlmsiseOutput) {
1134        use tables::*;
1135
1136        let mut zn1 = [120.0, 110.0, 100.0, 90.0, 72.5];
1137        let mn1 = 5usize;
1138        let alpha = [-0.38, 0.0, 0.0, 0.0, 0.17, 0.0, -0.38, 0.0, 0.0];
1139        let altl = [200.0, 300.0, 160.0, 250.0, 240.0, 450.0, 320.0, 450.0];
1140
1141        let za = PDL[1][15];
1142        zn1[0] = za;
1143        for j in 0..9 {
1144            output.d[j] = 0.0;
1145        }
1146
1147        // Tinf variations are unimportant below za / zn1[0].
1148        let tinf = if input.alt > zn1[0] {
1149            PTM[0] * PT[0] * (1.0 + flags.sw[16] * self.globe7(&PT, input, flags))
1150        } else {
1151            PTM[0] * PT[0]
1152        };
1153        output.t[0] = tinf;
1154
1155        // Gradient variations are unimportant below zn1[4].
1156        let g0 = if input.alt > zn1[4] {
1157            PTM[3] * PS[0] * (1.0 + flags.sw[19] * self.globe7(&PS, input, flags))
1158        } else {
1159            PTM[3] * PS[0]
1160        };
1161        let tlb = PTM[1] * (1.0 + flags.sw[17] * self.globe7(&PD[3], input, flags)) * PD[3][0];
1162        let s = g0 / (tinf - tlb);
1163
1164        // Lower-thermosphere temperature variations are insignificant for
1165        // density above 300 km.
1166        if input.alt < 300.0 {
1167            self.meso_tn1[1] =
1168                PTM[6] * PTL[0][0] / (1.0 - flags.sw[18] * self.glob7s(&PTL[0], input, flags));
1169            self.meso_tn1[2] =
1170                PTM[2] * PTL[1][0] / (1.0 - flags.sw[18] * self.glob7s(&PTL[1], input, flags));
1171            self.meso_tn1[3] =
1172                PTM[7] * PTL[2][0] / (1.0 - flags.sw[18] * self.glob7s(&PTL[2], input, flags));
1173            self.meso_tn1[4] = PTM[4] * PTL[3][0]
1174                / (1.0 - flags.sw[18] * flags.sw[20] * self.glob7s(&PTL[3], input, flags));
1175            self.meso_tgn1[1] = PTM[8]
1176                * PMA[8][0]
1177                * (1.0 + flags.sw[18] * flags.sw[20] * self.glob7s(&PMA[8], input, flags))
1178                * self.meso_tn1[4]
1179                * self.meso_tn1[4]
1180                / libm::pow(PTM[4] * PTL[3][0], 2.0);
1181        } else {
1182            self.meso_tn1[1] = PTM[6] * PTL[0][0];
1183            self.meso_tn1[2] = PTM[2] * PTL[1][0];
1184            self.meso_tn1[3] = PTM[7] * PTL[2][0];
1185            self.meso_tn1[4] = PTM[4] * PTL[3][0];
1186            self.meso_tgn1[1] = PTM[8] * PMA[8][0] * self.meso_tn1[4] * self.meso_tn1[4]
1187                / libm::pow(PTM[4] * PTL[3][0], 2.0);
1188        }
1189
1190        // N2 variation factor at Zlb.
1191        let g28 = flags.sw[21] * self.globe7(&PD[2], input, flags);
1192
1193        // Variation of turbopause height.
1194        let zhf = PDL[1][24]
1195            * (1.0
1196                + flags.sw[5]
1197                    * PDL[0][24]
1198                    * libm::sin(DGTR * input.g_lat)
1199                    * libm::cos(DR * (input.doy as f64 - PT[13])));
1200        output.t[0] = tinf;
1201        let xmm = PDM[2][4];
1202        let z = input.alt;
1203
1204        // Extract meso end nodes so densu can borrow them mutably while we read
1205        // the shared state; copy back afterward.
1206        let mut tn1 = self.meso_tn1;
1207        let mut tgn1 = self.meso_tgn1;
1208
1209        // N2 density.
1210        let db28 = PDM[2][0] * libm::exp(g28) * PD[2][0];
1211        output.d[2] = self.densu(
1212            z,
1213            db28,
1214            tinf,
1215            tlb,
1216            28.0,
1217            alpha[2],
1218            &mut output.t[1],
1219            PTM[5],
1220            s,
1221            mn1,
1222            &zn1,
1223            &mut tn1,
1224            &mut tgn1,
1225        );
1226        let zh28 = PDM[2][2] * zhf;
1227        let zhm28 = PDM[2][3] * PDL[1][5];
1228        let xmd = 28.0 - xmm;
1229        let mut tz = 0.0;
1230        let b28 = self.densu(
1231            zh28,
1232            db28,
1233            tinf,
1234            tlb,
1235            xmd,
1236            alpha[2] - 1.0,
1237            &mut tz,
1238            PTM[5],
1239            s,
1240            mn1,
1241            &zn1,
1242            &mut tn1,
1243            &mut tgn1,
1244        );
1245        if (flags.sw[15] != 0.0) && (z <= altl[2]) {
1246            self.dm28 = self.densu(
1247                z, b28, tinf, tlb, xmm, alpha[2], &mut tz, PTM[5], s, mn1, &zn1, &mut tn1,
1248                &mut tgn1,
1249            );
1250            output.d[2] = dnet(output.d[2], self.dm28, zhm28, xmm, 28.0);
1251        }
1252
1253        // He density.
1254        let g4 = flags.sw[21] * self.globe7(&PD[0], input, flags);
1255        let db04 = PDM[0][0] * libm::exp(g4) * PD[0][0];
1256        output.d[0] = self.densu(
1257            z,
1258            db04,
1259            tinf,
1260            tlb,
1261            4.0,
1262            alpha[0],
1263            &mut output.t[1],
1264            PTM[5],
1265            s,
1266            mn1,
1267            &zn1,
1268            &mut tn1,
1269            &mut tgn1,
1270        );
1271        if (flags.sw[15] != 0.0) && (z < altl[0]) {
1272            let zh04 = PDM[0][2];
1273            let b04 = self.densu(
1274                zh04,
1275                db04,
1276                tinf,
1277                tlb,
1278                4.0 - xmm,
1279                alpha[0] - 1.0,
1280                &mut output.t[1],
1281                PTM[5],
1282                s,
1283                mn1,
1284                &zn1,
1285                &mut tn1,
1286                &mut tgn1,
1287            );
1288            self.dm04 = self.densu(
1289                z,
1290                b04,
1291                tinf,
1292                tlb,
1293                xmm,
1294                0.0,
1295                &mut output.t[1],
1296                PTM[5],
1297                s,
1298                mn1,
1299                &zn1,
1300                &mut tn1,
1301                &mut tgn1,
1302            );
1303            let zhm04 = zhm28;
1304            output.d[0] = dnet(output.d[0], self.dm04, zhm04, xmm, 4.0);
1305            let rl = libm::log(b28 * PDM[0][1] / b04);
1306            let zc04 = PDM[0][4] * PDL[1][0];
1307            let hc04 = PDM[0][5] * PDL[1][1];
1308            output.d[0] *= ccor(z, rl, hc04, zc04);
1309        }
1310
1311        // O density.
1312        let g16 = flags.sw[21] * self.globe7(&PD[1], input, flags);
1313        let db16 = PDM[1][0] * libm::exp(g16) * PD[1][0];
1314        output.d[1] = self.densu(
1315            z,
1316            db16,
1317            tinf,
1318            tlb,
1319            16.0,
1320            alpha[1],
1321            &mut output.t[1],
1322            PTM[5],
1323            s,
1324            mn1,
1325            &zn1,
1326            &mut tn1,
1327            &mut tgn1,
1328        );
1329        if (flags.sw[15] != 0.0) && (z <= altl[1]) {
1330            let zh16 = PDM[1][2];
1331            let b16 = self.densu(
1332                zh16,
1333                db16,
1334                tinf,
1335                tlb,
1336                16.0 - xmm,
1337                alpha[1] - 1.0,
1338                &mut output.t[1],
1339                PTM[5],
1340                s,
1341                mn1,
1342                &zn1,
1343                &mut tn1,
1344                &mut tgn1,
1345            );
1346            self.dm16 = self.densu(
1347                z,
1348                b16,
1349                tinf,
1350                tlb,
1351                xmm,
1352                0.0,
1353                &mut output.t[1],
1354                PTM[5],
1355                s,
1356                mn1,
1357                &zn1,
1358                &mut tn1,
1359                &mut tgn1,
1360            );
1361            let zhm16 = zhm28;
1362            output.d[1] = dnet(output.d[1], self.dm16, zhm16, xmm, 16.0);
1363            let rl =
1364                PDM[1][1] * PDL[1][16] * (1.0 + flags.sw[1] * PDL[0][23] * (input.f107a - 150.0));
1365            let hc16 = PDM[1][5] * PDL[1][3];
1366            let zc16 = PDM[1][4] * PDL[1][2];
1367            let hc216 = PDM[1][5] * PDL[1][4];
1368            output.d[1] *= ccor2(z, rl, hc16, zc16, hc216);
1369            let hcc16 = PDM[1][7] * PDL[1][13];
1370            let zcc16 = PDM[1][6] * PDL[1][12];
1371            let rc16 = PDM[1][3] * PDL[1][14];
1372            output.d[1] *= ccor(z, rc16, hcc16, zcc16);
1373        }
1374
1375        // O2 density.
1376        let g32 = flags.sw[21] * self.globe7(&PD[4], input, flags);
1377        let db32 = PDM[3][0] * libm::exp(g32) * PD[4][0];
1378        output.d[3] = self.densu(
1379            z,
1380            db32,
1381            tinf,
1382            tlb,
1383            32.0,
1384            alpha[3],
1385            &mut output.t[1],
1386            PTM[5],
1387            s,
1388            mn1,
1389            &zn1,
1390            &mut tn1,
1391            &mut tgn1,
1392        );
1393        if flags.sw[15] != 0.0 {
1394            if z <= altl[3] {
1395                let zh32 = PDM[3][2];
1396                let b32 = self.densu(
1397                    zh32,
1398                    db32,
1399                    tinf,
1400                    tlb,
1401                    32.0 - xmm,
1402                    alpha[3] - 1.0,
1403                    &mut output.t[1],
1404                    PTM[5],
1405                    s,
1406                    mn1,
1407                    &zn1,
1408                    &mut tn1,
1409                    &mut tgn1,
1410                );
1411                self.dm32 = self.densu(
1412                    z,
1413                    b32,
1414                    tinf,
1415                    tlb,
1416                    xmm,
1417                    0.0,
1418                    &mut output.t[1],
1419                    PTM[5],
1420                    s,
1421                    mn1,
1422                    &zn1,
1423                    &mut tn1,
1424                    &mut tgn1,
1425                );
1426                let zhm32 = zhm28;
1427                output.d[3] = dnet(output.d[3], self.dm32, zhm32, xmm, 32.0);
1428                let rl = libm::log(b28 * PDM[3][1] / b32);
1429                let hc32 = PDM[3][5] * PDL[1][7];
1430                let zc32 = PDM[3][4] * PDL[1][6];
1431                output.d[3] *= ccor(z, rl, hc32, zc32);
1432            }
1433            let hcc32 = PDM[3][7] * PDL[1][22];
1434            let hcc232 = PDM[3][7] * PDL[0][22];
1435            let zcc32 = PDM[3][6] * PDL[1][21];
1436            let rc32 =
1437                PDM[3][3] * PDL[1][23] * (1.0 + flags.sw[1] * PDL[0][23] * (input.f107a - 150.0));
1438            output.d[3] *= ccor2(z, rc32, hcc32, zcc32, hcc232);
1439        }
1440
1441        // Ar density.
1442        let g40 = flags.sw[21] * self.globe7(&PD[5], input, flags);
1443        let db40 = PDM[4][0] * libm::exp(g40) * PD[5][0];
1444        output.d[4] = self.densu(
1445            z,
1446            db40,
1447            tinf,
1448            tlb,
1449            40.0,
1450            alpha[4],
1451            &mut output.t[1],
1452            PTM[5],
1453            s,
1454            mn1,
1455            &zn1,
1456            &mut tn1,
1457            &mut tgn1,
1458        );
1459        if (flags.sw[15] != 0.0) && (z <= altl[4]) {
1460            let zh40 = PDM[4][2];
1461            let b40 = self.densu(
1462                zh40,
1463                db40,
1464                tinf,
1465                tlb,
1466                40.0 - xmm,
1467                alpha[4] - 1.0,
1468                &mut output.t[1],
1469                PTM[5],
1470                s,
1471                mn1,
1472                &zn1,
1473                &mut tn1,
1474                &mut tgn1,
1475            );
1476            self.dm40 = self.densu(
1477                z,
1478                b40,
1479                tinf,
1480                tlb,
1481                xmm,
1482                0.0,
1483                &mut output.t[1],
1484                PTM[5],
1485                s,
1486                mn1,
1487                &zn1,
1488                &mut tn1,
1489                &mut tgn1,
1490            );
1491            let zhm40 = zhm28;
1492            output.d[4] = dnet(output.d[4], self.dm40, zhm40, xmm, 40.0);
1493            let rl = libm::log(b28 * PDM[4][1] / b40);
1494            let hc40 = PDM[4][5] * PDL[1][9];
1495            let zc40 = PDM[4][4] * PDL[1][8];
1496            output.d[4] *= ccor(z, rl, hc40, zc40);
1497        }
1498
1499        // Hydrogen density.
1500        let g1 = flags.sw[21] * self.globe7(&PD[6], input, flags);
1501        let db01 = PDM[5][0] * libm::exp(g1) * PD[6][0];
1502        output.d[6] = self.densu(
1503            z,
1504            db01,
1505            tinf,
1506            tlb,
1507            1.0,
1508            alpha[6],
1509            &mut output.t[1],
1510            PTM[5],
1511            s,
1512            mn1,
1513            &zn1,
1514            &mut tn1,
1515            &mut tgn1,
1516        );
1517        if (flags.sw[15] != 0.0) && (z <= altl[6]) {
1518            let zh01 = PDM[5][2];
1519            let b01 = self.densu(
1520                zh01,
1521                db01,
1522                tinf,
1523                tlb,
1524                1.0 - xmm,
1525                alpha[6] - 1.0,
1526                &mut output.t[1],
1527                PTM[5],
1528                s,
1529                mn1,
1530                &zn1,
1531                &mut tn1,
1532                &mut tgn1,
1533            );
1534            self.dm01 = self.densu(
1535                z,
1536                b01,
1537                tinf,
1538                tlb,
1539                xmm,
1540                0.0,
1541                &mut output.t[1],
1542                PTM[5],
1543                s,
1544                mn1,
1545                &zn1,
1546                &mut tn1,
1547                &mut tgn1,
1548            );
1549            let zhm01 = zhm28;
1550            output.d[6] = dnet(output.d[6], self.dm01, zhm01, xmm, 1.0);
1551            let rl = libm::log(b28 * PDM[5][1] * (PDL[1][17] * PDL[1][17]).sqrt() / b01);
1552            let hc01 = PDM[5][5] * PDL[1][11];
1553            let zc01 = PDM[5][4] * PDL[1][10];
1554            output.d[6] *= ccor(z, rl, hc01, zc01);
1555            let hcc01 = PDM[5][7] * PDL[1][19];
1556            let zcc01 = PDM[5][6] * PDL[1][18];
1557            let rc01 = PDM[5][3] * PDL[1][20];
1558            output.d[6] *= ccor(z, rc01, hcc01, zcc01);
1559        }
1560
1561        // Atomic nitrogen density.
1562        let g14 = flags.sw[21] * self.globe7(&PD[7], input, flags);
1563        let db14 = PDM[6][0] * libm::exp(g14) * PD[7][0];
1564        output.d[7] = self.densu(
1565            z,
1566            db14,
1567            tinf,
1568            tlb,
1569            14.0,
1570            alpha[7],
1571            &mut output.t[1],
1572            PTM[5],
1573            s,
1574            mn1,
1575            &zn1,
1576            &mut tn1,
1577            &mut tgn1,
1578        );
1579        if (flags.sw[15] != 0.0) && (z <= altl[7]) {
1580            let zh14 = PDM[6][2];
1581            let b14 = self.densu(
1582                zh14,
1583                db14,
1584                tinf,
1585                tlb,
1586                14.0 - xmm,
1587                alpha[7] - 1.0,
1588                &mut output.t[1],
1589                PTM[5],
1590                s,
1591                mn1,
1592                &zn1,
1593                &mut tn1,
1594                &mut tgn1,
1595            );
1596            self.dm14 = self.densu(
1597                z,
1598                b14,
1599                tinf,
1600                tlb,
1601                xmm,
1602                0.0,
1603                &mut output.t[1],
1604                PTM[5],
1605                s,
1606                mn1,
1607                &zn1,
1608                &mut tn1,
1609                &mut tgn1,
1610            );
1611            let zhm14 = zhm28;
1612            output.d[7] = dnet(output.d[7], self.dm14, zhm14, xmm, 14.0);
1613            let rl = libm::log(b28 * PDM[6][1] * (PDL[0][2] * PDL[0][2]).sqrt() / b14);
1614            let hc14 = PDM[6][5] * PDL[0][1];
1615            let zc14 = PDM[6][4] * PDL[0][0];
1616            output.d[7] *= ccor(z, rl, hc14, zc14);
1617            let hcc14 = PDM[6][7] * PDL[0][4];
1618            let zcc14 = PDM[6][6] * PDL[0][3];
1619            let rc14 = PDM[6][3] * PDL[0][5];
1620            output.d[7] *= ccor(z, rc14, hcc14, zcc14);
1621        }
1622
1623        // Anomalous oxygen density.
1624        let g16h = flags.sw[21] * self.globe7(&PD[8], input, flags);
1625        let db16h = PDM[7][0] * libm::exp(g16h) * PD[8][0];
1626        let tho = PDM[7][9] * PDL[0][6];
1627        let dd = self.densu(
1628            z,
1629            db16h,
1630            tho,
1631            tho,
1632            16.0,
1633            alpha[8],
1634            &mut output.t[1],
1635            PTM[5],
1636            s,
1637            mn1,
1638            &zn1,
1639            &mut tn1,
1640            &mut tgn1,
1641        );
1642        let zsht = PDM[7][5];
1643        let zmho = PDM[7][4];
1644        let zsho = self.scalh(zmho, 16.0, tho);
1645        output.d[8] = dd * libm::exp(-zsht / zsho * (libm::exp(-(z - zmho) / zsht) - 1.0));
1646
1647        // Total mass density.
1648        output.d[5] = 1.66E-24
1649            * (4.0 * output.d[0]
1650                + 16.0 * output.d[1]
1651                + 28.0 * output.d[2]
1652                + 32.0 * output.d[3]
1653                + 40.0 * output.d[4]
1654                + output.d[6]
1655                + 14.0 * output.d[7]);
1656
1657        // Temperature at altitude.
1658        let z = (input.alt * input.alt).sqrt();
1659        let _ = self.densu(
1660            z,
1661            1.0,
1662            tinf,
1663            tlb,
1664            0.0,
1665            0.0,
1666            &mut output.t[1],
1667            PTM[5],
1668            s,
1669            mn1,
1670            &zn1,
1671            &mut tn1,
1672            &mut tgn1,
1673        );
1674
1675        // Copy back the meso end nodes mutated by densu.
1676        self.meso_tn1 = tn1;
1677        self.meso_tgn1 = tgn1;
1678
1679        if flags.sw[0] != 0.0 {
1680            for i in 0..9 {
1681                output.d[i] *= 1.0E6;
1682            }
1683            output.d[5] /= 1000.0;
1684        }
1685    }
1686
1687    /// Full model from surface to lower exosphere (reference `gtd7`).
1688    fn gtd7(&mut self, input: &NrlmsiseInput, flags: &Flags, output: &mut NrlmsiseOutput) {
1689        use tables::*;
1690
1691        let mn3 = 5usize;
1692        let zn3 = [32.5, 20.0, 15.0, 10.0, 0.0];
1693        let mn2 = 4usize;
1694        let zn2 = [72.5, 55.0, 45.0, 32.5];
1695        let zmix = 62.5;
1696
1697        // Latitude variation of gravity (none for sw[2] == 0).
1698        let xlat = if flags.sw[2] == 0.0 {
1699            45.0
1700        } else {
1701            input.g_lat
1702        };
1703        self.glatf(xlat);
1704
1705        let xmm = PDM[2][4];
1706
1707        // Thermosphere / mesosphere (above zn2[0]).
1708        let altt = if input.alt > zn2[0] {
1709            input.alt
1710        } else {
1711            zn2[0]
1712        };
1713
1714        let mut sinput = *input;
1715        sinput.alt = altt;
1716        let mut soutput = NrlmsiseOutput::default();
1717        self.gts7(&sinput, flags, &mut soutput);
1718
1719        let dm28m = if flags.sw[0] != 0.0 {
1720            self.dm28 * 1.0E6
1721        } else {
1722            self.dm28
1723        };
1724        output.t[0] = soutput.t[0];
1725        output.t[1] = soutput.t[1];
1726        if input.alt >= zn2[0] {
1727            output.d = soutput.d;
1728            return;
1729        }
1730
1731        // Lower mesosphere / upper stratosphere (between zn3[0] and zn2[0]).
1732        self.meso_tgn2[0] = self.meso_tgn1[1];
1733        self.meso_tn2[0] = self.meso_tn1[4];
1734        self.meso_tn2[1] =
1735            PMA[0][0] * PAVGM[0] / (1.0 - flags.sw[20] * self.glob7s(&PMA[0], input, flags));
1736        self.meso_tn2[2] =
1737            PMA[1][0] * PAVGM[1] / (1.0 - flags.sw[20] * self.glob7s(&PMA[1], input, flags));
1738        self.meso_tn2[3] = PMA[2][0] * PAVGM[2]
1739            / (1.0 - flags.sw[20] * flags.sw[22] * self.glob7s(&PMA[2], input, flags));
1740        self.meso_tgn2[1] = PAVGM[8]
1741            * PMA[9][0]
1742            * (1.0 + flags.sw[20] * flags.sw[22] * self.glob7s(&PMA[9], input, flags))
1743            * self.meso_tn2[3]
1744            * self.meso_tn2[3]
1745            / libm::pow(PMA[2][0] * PAVGM[2], 2.0);
1746        self.meso_tn3[0] = self.meso_tn2[3];
1747
1748        if input.alt < zn3[0] {
1749            // Lower stratosphere and troposphere (below zn3[0]).
1750            self.meso_tgn3[0] = self.meso_tgn2[1];
1751            self.meso_tn3[1] =
1752                PMA[3][0] * PAVGM[3] / (1.0 - flags.sw[22] * self.glob7s(&PMA[3], input, flags));
1753            self.meso_tn3[2] =
1754                PMA[4][0] * PAVGM[4] / (1.0 - flags.sw[22] * self.glob7s(&PMA[4], input, flags));
1755            self.meso_tn3[3] =
1756                PMA[5][0] * PAVGM[5] / (1.0 - flags.sw[22] * self.glob7s(&PMA[5], input, flags));
1757            self.meso_tn3[4] =
1758                PMA[6][0] * PAVGM[6] / (1.0 - flags.sw[22] * self.glob7s(&PMA[6], input, flags));
1759            self.meso_tgn3[1] = PMA[7][0]
1760                * PAVGM[7]
1761                * (1.0 + flags.sw[22] * self.glob7s(&PMA[7], input, flags))
1762                * self.meso_tn3[4]
1763                * self.meso_tn3[4]
1764                / libm::pow(PMA[6][0] * PAVGM[6], 2.0);
1765        }
1766
1767        // Linear transition to full mixing below zn2[0].
1768        let mut dmc = 0.0;
1769        if input.alt > zmix {
1770            dmc = 1.0 - (zn2[0] - input.alt) / (zn2[0] - zmix);
1771        }
1772        let dz28 = soutput.d[2];
1773
1774        // Snapshot meso temperature arrays for densm reads.
1775        let tn2 = self.meso_tn2;
1776        let tgn2 = self.meso_tgn2;
1777        let tn3 = self.meso_tn3;
1778        let tgn3 = self.meso_tgn3;
1779        let mut tz = 0.0;
1780
1781        // N2 density.
1782        let dmr = soutput.d[2] / dm28m - 1.0;
1783        output.d[2] = self.densm(
1784            input.alt, dm28m, xmm, &mut tz, mn3, &zn3, &tn3, &tgn3, mn2, &zn2, &tn2, &tgn2,
1785        );
1786        output.d[2] *= 1.0 + dmr * dmc;
1787
1788        // He density.
1789        let dmr = soutput.d[0] / (dz28 * PDM[0][1]) - 1.0;
1790        output.d[0] = output.d[2] * PDM[0][1] * (1.0 + dmr * dmc);
1791
1792        // O density.
1793        output.d[1] = 0.0;
1794        output.d[8] = 0.0;
1795
1796        // O2 density.
1797        let dmr = soutput.d[3] / (dz28 * PDM[3][1]) - 1.0;
1798        output.d[3] = output.d[2] * PDM[3][1] * (1.0 + dmr * dmc);
1799
1800        // Ar density.
1801        let dmr = soutput.d[4] / (dz28 * PDM[4][1]) - 1.0;
1802        output.d[4] = output.d[2] * PDM[4][1] * (1.0 + dmr * dmc);
1803
1804        // Hydrogen density.
1805        output.d[6] = 0.0;
1806
1807        // Atomic nitrogen density.
1808        output.d[7] = 0.0;
1809
1810        // Total mass density.
1811        output.d[5] = 1.66E-24
1812            * (4.0 * output.d[0]
1813                + 16.0 * output.d[1]
1814                + 28.0 * output.d[2]
1815                + 32.0 * output.d[3]
1816                + 40.0 * output.d[4]
1817                + output.d[6]
1818                + 14.0 * output.d[7]);
1819
1820        if flags.sw[0] != 0.0 {
1821            output.d[5] /= 1000.0;
1822        }
1823
1824        // Temperature at altitude.
1825        self.dd = self.densm(
1826            input.alt, 1.0, 0.0, &mut tz, mn3, &zn3, &tn3, &tgn3, mn2, &zn2, &tn2, &tgn2,
1827        );
1828        output.t[1] = tz;
1829    }
1830
1831    /// Full model with effective total mass density for drag (reference
1832    /// `gtd7d`): anomalous oxygen folded into `d[5]`.
1833    fn gtd7d(&mut self, input: &NrlmsiseInput, flags: &Flags, output: &mut NrlmsiseOutput) {
1834        self.gtd7(input, flags, output);
1835        output.d[5] = 1.66E-24
1836            * (4.0 * output.d[0]
1837                + 16.0 * output.d[1]
1838                + 28.0 * output.d[2]
1839                + 32.0 * output.d[3]
1840                + 40.0 * output.d[4]
1841                + output.d[6]
1842                + 14.0 * output.d[7]
1843                + 16.0 * output.d[8]);
1844        if flags.sw[0] != 0.0 {
1845            output.d[5] /= 1000.0;
1846        }
1847    }
1848}
1849
1850/// Evaluate the full NRLMSISE-00 model (`gtd7`), excluding anomalous oxygen
1851/// from the total mass density (`d[5]`).
1852///
1853/// For satellite-drag total density (anomalous oxygen folded in) use [`gtd7d`].
1854/// Inputs are validated at the boundary; see [`AtmosphereError`].
1855pub fn gtd7(input: &NrlmsiseInput, flags: &Flags) -> Result<NrlmsiseOutput, AtmosphereError> {
1856    validate(input, flags)?;
1857    let mut output = NrlmsiseOutput::default();
1858    let mut state = State::new();
1859    state.gtd7(input, flags, &mut output);
1860    Ok(output)
1861}
1862
1863/// Evaluate the full NRLMSISE-00 model with effective total mass density for
1864/// drag (`gtd7d`): anomalous oxygen is folded into the total mass density
1865/// (`d[5]`), which matters above ~500 km.
1866///
1867/// Inputs are validated at the boundary; see [`AtmosphereError`].
1868pub fn gtd7d(input: &NrlmsiseInput, flags: &Flags) -> Result<NrlmsiseOutput, AtmosphereError> {
1869    validate(input, flags)?;
1870    let mut output = NrlmsiseOutput::default();
1871    let mut state = State::new();
1872    state.gtd7d(input, flags, &mut output);
1873    Ok(output)
1874}
1875
1876/// Convenience evaluation with all variations on and metric (m/kg) output.
1877///
1878/// Returns the full species set, total mass density (kg/m^3), and temperatures
1879/// (K). Uses [`gtd7d`], so `d[5]` is the drag-effective total mass density
1880/// including anomalous oxygen, which is the correct quantity for drag users.
1881/// Use [`gtd7`] explicitly if you need the total mass density without anomalous
1882/// oxygen.
1883pub fn nrlmsise00(input: &NrlmsiseInput) -> Result<NrlmsiseOutput, AtmosphereError> {
1884    gtd7d(input, &Flags::metric())
1885}
1886
1887/// [`nrlmsise00`] with the local apparent solar time supplied or derived.
1888///
1889/// When `lst` is `Some`, that value is used as [`NrlmsiseInput::lst`]; when it is
1890/// `None`, the consistent value `local_solar_time(input.sec, input.g_long)` is
1891/// derived internally so a thin binding need not compute it itself. All other
1892/// inputs and the evaluation are exactly [`nrlmsise00`]'s, so with an explicit
1893/// `lst` equal to `input.lst` the result is bit-identical; this wrapper adds no
1894/// new numeric behaviour, only the optional derivation.
1895pub fn nrlmsise00_with_lst(
1896    input: &NrlmsiseInput,
1897    lst: Option<f64>,
1898) -> Result<NrlmsiseOutput, AtmosphereError> {
1899    let mut input = *input;
1900    input.lst = lst.unwrap_or_else(|| local_solar_time(input.sec, input.g_long));
1901    nrlmsise00(&input)
1902}
1903
1904#[cfg(test)]
1905mod tests {
1906    // The oracle fixture holds full-precision f64 literals transcribed from the
1907    // reference test program output; keep them verbatim.
1908    #![allow(clippy::excessive_precision, clippy::unreadable_literal)]
1909    use super::*;
1910
1911    // gtd7 reference output, release 20041227, 17 standard cases.
1912    // Columns: d[0..8] (He,O,N2,O2,Ar,total,H,N,anom-O), t[0]=Tinf, t[1]=Talt.
1913    const GTD7_REF: [[f64; 11]; 17] = [
1914        [
1915            6.66517690495152026E+05,
1916            1.13880555975221708E+08,
1917            1.99821092557345442E+07,
1918            4.02276358571251098E+05,
1919            3.55746499451588579E+03,
1920            4.07471353275722314E-15,
1921            3.47531239971714167E+04,
1922            4.09591326829300169E+06,
1923            2.66727320933586889E+04,
1924            1.25053994356079943E+03,
1925            1.24141613001912060E+03,
1926        ],
1927        [
1928            3.40729322316091415E+06,
1929            1.58633336956916809E+08,
1930            1.39111736546111498E+07,
1931            3.26255950959554641E+05,
1932            1.55961815050122459E+03,
1933            5.00184572907224415E-15,
1934            4.85420846334025409E+04,
1935            4.38096671289862506E+06,
1936            6.95668195594226836E+03,
1937            1.16675438375720887E+03,
1938            1.16171045188704238E+03,
1939        ],
1940        [
1941            1.12376724403793560E+05,
1942            6.93413008676059981E+04,
1943            4.24710521747708185E+01,
1944            1.32275014147492764E-01,
1945            2.61884841823217900E-05,
1946            2.75677231926887105E-18,
1947            2.01674985432143185E+04,
1948            5.74125593414717332E+03,
1949            2.37439415198959796E+04,
1950            1.23989211171666511E+03,
1951            1.23989064013305870E+03,
1952        ],
1953        [
1954            5.41155437993667349E+07,
1955            1.91889344393930878E+11,
1956            6.11582559822463086E+12,
1957            1.22520105174012402E+12,
1958            6.02321197308486633E+10,
1959            3.58442630411333278E-10,
1960            1.05987969774054065E+07,
1961            2.61573669370513933E+05,
1962            2.81987935592833352E-42,
1963            1.02731846489999998E+03,
1964            2.06887776403605500E+02,
1965        ],
1966        [
1967            1.85112248619252769E+06,
1968            1.47655483792746186E+08,
1969            1.57935622826449610E+07,
1970            2.63379497731231386E+05,
1971            1.58878139838393008E+03,
1972            4.80963023940745105E-15,
1973            5.81616678078747354E+04,
1974            5.47898447906879056E+06,
1975            1.26444594176100850E+03,
1976            1.21239615212120930E+03,
1977            1.20813542521239174E+03,
1978        ],
1979        [
1980            8.67309523390615708E+05,
1981            1.27886176801412776E+08,
1982            1.82257662717170008E+07,
1983            2.92221419061824679E+05,
1984            2.40296243642370064E+03,
1985            4.35586564264464703E-15,
1986            3.68638924375054994E+04,
1987            3.89727550372696389E+06,
1988            2.66727320933586889E+04,
1989            1.22014641791503209E+03,
1990            1.21271208321180620E+03,
1991        ],
1992        [
1993            5.77625121602324420E+05,
1994            6.97913869366019815E+07,
1995            1.23681355982170273E+07,
1996            2.49286771542910225E+05,
1997            1.40573867417784300E+03,
1998            2.47065139166313234E-15,
1999            5.29198556706664021E+04,
2000            1.06981410936656618E+06,
2001            2.66727320933586780E+04,
2002            1.11638537604315161E+03,
2003            1.11299856821731100E+03,
2004        ],
2005        [
2006            3.74030410550766566E+05,
2007            4.78272012361134216E+07,
2008            5.24038003332420439E+06,
2009            1.75987464039060724E+05,
2010            5.50164877956996406E+02,
2011            1.57188873925484437E-15,
2012            8.89677572293503763E+04,
2013            1.97974083623295487E+06,
2014            9.12181487599149295E+03,
2015            1.03124744071455893E+03,
2016            1.02484849221300897E+03,
2017        ],
2018        [
2019            6.74833876662362367E+05,
2020            1.24531526044373140E+08,
2021            2.36900954105298519E+07,
2022            4.91158315474982315E+05,
2023            4.57878109905442034E+03,
2024            4.56442024536117137E-15,
2025            3.24459477516109328E+04,
2026            5.37083308708603773E+06,
2027            2.66727320933586889E+04,
2028            1.30605204202729215E+03,
2029            1.29337404038953400E+03,
2030        ],
2031        [
2032            5.52860084164518747E+05,
2033            1.19804132404135779E+08,
2034            3.49579776455820650E+07,
2035            9.33961835502814618E+05,
2036            1.09625476549342875E+04,
2037            4.97454311032222049E-15,
2038            2.68642785625980869E+04,
2039            4.88997423297139723E+06,
2040            2.80544483712566507E+04,
2041            1.36186802078492315E+03,
2042            1.34738918372970147E+03,
2043        ],
2044        [
2045            1.37548758418628516E+14,
2046            0.00000000000000000E+00,
2047            2.04968704429075456E+19,
2048            5.49869543371880755E+18,
2049            2.45173315802838592E+17,
2050            1.26106566111855011E-03,
2051            0.00000000000000000E+00,
2052            0.00000000000000000E+00,
2053            0.00000000000000000E+00,
2054            1.02731846489999998E+03,
2055            2.81464757663215607E+02,
2056        ],
2057        [
2058            4.42744258767709297E+13,
2059            0.00000000000000000E+00,
2060            6.59756715773731123E+18,
2061            1.76992934140618854E+18,
2062            7.89167995572748480E+16,
2063            4.05913937579917825E-04,
2064            0.00000000000000000E+00,
2065            0.00000000000000000E+00,
2066            0.00000000000000000E+00,
2067            1.02731846489999998E+03,
2068            2.27417980827261800E+02,
2069        ],
2070        [
2071            2.12782875620718823E+12,
2072            0.00000000000000000E+00,
2073            3.17079055035404288E+17,
2074            8.50627980943479040E+16,
2075            3.79274111680598850E+15,
2076            1.95082224517562141E-05,
2077            0.00000000000000000E+00,
2078            0.00000000000000000E+00,
2079            0.00000000000000000E+00,
2080            1.02731846489999998E+03,
2081            2.37438914587726885E+02,
2082        ],
2083        [
2084            1.41218354559285187E+11,
2085            0.00000000000000000E+00,
2086            2.10436964378315880E+16,
2087            5.64539244337708000E+15,
2088            2.51714174941122531E+14,
2089            1.29470901592856755E-06,
2090            0.00000000000000000E+00,
2091            0.00000000000000000E+00,
2092            0.00000000000000000E+00,
2093            1.02731846489999998E+03,
2094            2.79555112954127935E+02,
2095        ],
2096        [
2097            1.25488440027266960E+10,
2098            0.00000000000000000E+00,
2099            1.87453282921902300E+15,
2100            4.92305098078476250E+14,
2101            2.23968541385638359E+13,
2102            1.14766767151153664E-07,
2103            0.00000000000000000E+00,
2104            0.00000000000000000E+00,
2105            0.00000000000000000E+00,
2106            1.02731846489999998E+03,
2107            2.19073231364195721E+02,
2108        ],
2109        [
2110            5.19647740297288226E+05,
2111            1.27449407296046287E+08,
2112            4.85044986985335723E+07,
2113            1.72083798257490038E+06,
2114            2.35448659054442614E+04,
2115            5.88194044865163260E-15,
2116            2.50007839108092958E+04,
2117            6.27920982501879986E+06,
2118            2.66727320933586780E+04,
2119            1.42641166228242469E+03,
2120            1.40860779555326394E+03,
2121        ],
2122        [
2123            4.26085974879412130E+07,
2124            1.24134201554874313E+11,
2125            4.92956154248814258E+12,
2126            1.04840674909283203E+12,
2127            4.99346508305550461E+10,
2128            2.91430355030879247E-10,
2129            8.83122859257159382E+06,
2130            2.25251550862615462E+05,
2131            2.41524592964891382E-42,
2132            1.02731846489999998E+03,
2133            1.93407106257668147E+02,
2134        ],
2135    ];
2136    // gtd7d total mass density d[5] (anomalous O folded in).
2137    const GTD7D_RHO_REF: [f64; 17] = [
2138        4.07542196052162235E-15,
2139        5.00203049854499384E-15,
2140        3.38741140603730843E-18,
2141        3.58442630411333278E-10,
2142        4.80966382309166367E-15,
2143        4.35657407040904624E-15,
2144        2.47135981942753195E-15,
2145        1.57213101465795068E-15,
2146        4.56512867312557136E-15,
2147        4.97528823647096049E-15,
2148        1.26106566111855011E-03,
2149        4.05913937579917825E-04,
2150        1.95082224517562141E-05,
2151        1.29470901592856755E-06,
2152        1.14766767151153664E-07,
2153        5.88264887641603259E-15,
2154        2.91430355030879247E-10,
2155    ];
2156
2157    /// Build the 17 reference test cases (reference `test_gtd7`).
2158    fn reference_cases() -> ([NrlmsiseInput; 17], usize, usize) {
2159        let aph: ApArray = [100.0; 7];
2160        let base = NrlmsiseInput {
2161            year: 0,
2162            doy: 172,
2163            sec: 29000.0,
2164            alt: 400.0,
2165            g_lat: 60.0,
2166            g_long: -70.0,
2167            lst: 16.0,
2168            f107a: 150.0,
2169            f107: 150.0,
2170            ap: 4.0,
2171            ap_array: None,
2172        };
2173        let mut input = [base; 17];
2174        input[1].doy = 81;
2175        input[2].sec = 75000.0;
2176        input[2].alt = 1000.0;
2177        input[3].alt = 100.0;
2178        input[10].alt = 0.0;
2179        input[11].alt = 10.0;
2180        input[12].alt = 30.0;
2181        input[13].alt = 50.0;
2182        input[14].alt = 70.0;
2183        input[16].alt = 100.0;
2184        input[4].g_lat = 0.0;
2185        input[5].g_long = 0.0;
2186        input[6].lst = 4.0;
2187        input[7].f107a = 70.0;
2188        input[8].f107 = 180.0;
2189        input[9].ap = 40.0;
2190        input[15].ap_array = Some(aph);
2191        input[16].ap_array = Some(aph);
2192        // Cases 0..15 use scalar daily ap (switch 9 = 1); 15..17 use the ap
2193        // history array (switch 9 = -1).
2194        (input, 15, 17)
2195    }
2196
2197    fn rel_err(got: f64, want: f64) -> f64 {
2198        if want == 0.0 {
2199            got.abs()
2200        } else {
2201            ((got - want) / want).abs()
2202        }
2203    }
2204
2205    /// Reference-agreement gate: every output of the 17 standard `gtd7` cases
2206    /// must match Brodowski's release-20041227 oracle to within this relative
2207    /// bound. The reference oracle was produced at full f64 precision from the
2208    /// same model logic and tables. The only source of residual is
2209    /// floating-point operation ordering in the libm `powf`/`exp`/trig calls,
2210    /// not any model difference: the measured worst-case relative error is
2211    /// about 2.6e-16 (roughly one f64 ULP). This bound keeps generous margin
2212    /// for cross-platform libm variation while staying far tighter than any
2213    /// physical tolerance. Do not loosen it to mask a port defect; a real
2214    /// defect shifts results by orders of magnitude, not ULPs.
2215    const REF_TOL: f64 = 1.0e-13;
2216
2217    #[test]
2218    fn gtd7_matches_reference_oracle() {
2219        let (input, n_scalar, n_total) = reference_cases();
2220        let mut worst = 0.0_f64;
2221        for (i, inp) in input.iter().enumerate() {
2222            let mut flags = Flags::standard();
2223            if i >= n_scalar && i < n_total {
2224                flags = Flags::new({
2225                    let mut s = [1i32; 24];
2226                    s[0] = 0;
2227                    s[9] = -1;
2228                    s
2229                });
2230            }
2231            let out = gtd7(inp, &flags).unwrap();
2232            for j in 0..9 {
2233                let want = GTD7_REF[i][j];
2234                let got = out.d[j];
2235                let e = rel_err(got, want);
2236                worst = worst.max(e);
2237                assert!(
2238                    e <= REF_TOL,
2239                    "case {i} d[{j}]: got {got:.17E} want {want:.17E} rel {e:.3E}"
2240                );
2241            }
2242            for k in 0..2 {
2243                let want = GTD7_REF[i][9 + k];
2244                let got = out.t[k];
2245                let e = rel_err(got, want);
2246                worst = worst.max(e);
2247                assert!(
2248                    e <= REF_TOL,
2249                    "case {i} t[{k}]: got {got:.17E} want {want:.17E} rel {e:.3E}"
2250                );
2251            }
2252        }
2253        assert!(worst <= REF_TOL, "worst relative error {worst:.3E}");
2254    }
2255
2256    #[test]
2257    fn gtd7d_total_density_matches_reference_oracle() {
2258        let (input, n_scalar, n_total) = reference_cases();
2259        for (i, inp) in input.iter().enumerate() {
2260            let mut flags = Flags::standard();
2261            if i >= n_scalar && i < n_total {
2262                flags = Flags::new({
2263                    let mut s = [1i32; 24];
2264                    s[0] = 0;
2265                    s[9] = -1;
2266                    s
2267                });
2268            }
2269            let out = gtd7d(inp, &flags).unwrap();
2270            let want = GTD7D_RHO_REF[i];
2271            let e = rel_err(out.d[5], want);
2272            assert!(
2273                e <= REF_TOL,
2274                "case {i} gtd7d rho: got {:.17E} want {want:.17E} rel {e:.3E}",
2275                out.d[5]
2276            );
2277        }
2278    }
2279
2280    #[test]
2281    fn nrlmsise00_metric_units() {
2282        // Metric convenience wrapper: sea-level total mass density in kg/m^3.
2283        let input = NrlmsiseInput {
2284            year: 0,
2285            doy: 172,
2286            sec: 29000.0,
2287            alt: 0.0,
2288            g_lat: 60.0,
2289            g_long: -70.0,
2290            lst: 16.0,
2291            f107a: 150.0,
2292            f107: 150.0,
2293            ap: 4.0,
2294            ap_array: None,
2295        };
2296        let out = nrlmsise00(&input).unwrap();
2297        // Reference case 10 RHO = 1.26106566E-03 g/cm^3 = 1.26106566 kg/m^3.
2298        // At sea level anomalous oxygen is zero, so gtd7d matches gtd7 here.
2299        assert!((out.density() - 1.26106566111855011).abs() < 1e-6);
2300        assert!(out.temperature_alt() > 270.0 && out.temperature_alt() < 290.0);
2301    }
2302
2303    #[test]
2304    fn density_decreases_with_altitude() {
2305        let make = |alt: f64| NrlmsiseInput {
2306            year: 0,
2307            doy: 172,
2308            sec: 29000.0,
2309            alt,
2310            g_lat: 60.0,
2311            g_long: -70.0,
2312            lst: 16.0,
2313            f107a: 150.0,
2314            f107: 150.0,
2315            ap: 4.0,
2316            ap_array: None,
2317        };
2318        let d0 = nrlmsise00(&make(0.0)).unwrap().density();
2319        let d200 = nrlmsise00(&make(200.0)).unwrap().density();
2320        let d400 = nrlmsise00(&make(400.0)).unwrap().density();
2321        let d800 = nrlmsise00(&make(800.0)).unwrap().density();
2322        assert!(d0 > d200 && d200 > d400 && d400 > d800);
2323    }
2324
2325    #[test]
2326    fn solar_activity_increases_thermospheric_density() {
2327        let make = |f107a: f64| NrlmsiseInput {
2328            year: 0,
2329            doy: 172,
2330            sec: 29000.0,
2331            alt: 400.0,
2332            g_lat: 60.0,
2333            g_long: -70.0,
2334            lst: 16.0,
2335            f107a,
2336            f107: f107a,
2337            ap: 4.0,
2338            ap_array: None,
2339        };
2340        assert!(
2341            nrlmsise00(&make(250.0)).unwrap().density()
2342                > nrlmsise00(&make(70.0)).unwrap().density()
2343        );
2344    }
2345
2346    #[test]
2347    fn local_solar_time_wraps() {
2348        assert!((local_solar_time(43200.0, 0.0) - 12.0).abs() < 0.001);
2349        assert!((local_solar_time(0.0, 0.0) - 0.0).abs() < 0.001);
2350        assert!((local_solar_time(0.0, 180.0) - 12.0).abs() < 0.001);
2351    }
2352
2353    fn sample_input() -> NrlmsiseInput {
2354        NrlmsiseInput {
2355            year: 0,
2356            doy: 172,
2357            sec: 29000.0,
2358            alt: 400.0,
2359            g_lat: 60.0,
2360            g_long: -70.0,
2361            lst: 16.0,
2362            f107a: 150.0,
2363            f107: 150.0,
2364            ap: 4.0,
2365            ap_array: None,
2366        }
2367    }
2368
2369    fn ap_history_flags() -> Flags {
2370        Flags::new({
2371            let mut s = [1i32; 24];
2372            s[0] = 0;
2373            s[9] = -1;
2374            s
2375        })
2376    }
2377
2378    #[test]
2379    fn ap_history_mode_without_array_is_rejected() {
2380        let input = sample_input(); // ap_array: None
2381        let flags = ap_history_flags();
2382        assert_eq!(gtd7(&input, &flags), Err(AtmosphereError::MissingApArray));
2383        assert_eq!(gtd7d(&input, &flags), Err(AtmosphereError::MissingApArray));
2384    }
2385
2386    #[test]
2387    fn ap_history_mode_with_array_succeeds() {
2388        let mut input = sample_input();
2389        input.ap_array = Some([100.0; 7]);
2390        assert!(gtd7(&input, &ap_history_flags()).is_ok());
2391    }
2392
2393    #[test]
2394    fn non_finite_inputs_are_rejected() {
2395        type Mutate = fn(&mut NrlmsiseInput);
2396        let cases: [(Mutate, &str); 5] = [
2397            (|i| i.alt = f64::NAN, "alt"),
2398            (|i| i.f107 = f64::INFINITY, "f107"),
2399            (|i| i.f107a = f64::NAN, "f107a"),
2400            (|i| i.ap = f64::INFINITY, "ap"),
2401            (|i| i.g_lat = f64::NAN, "g_lat"),
2402        ];
2403        for (mutate, name) in cases {
2404            let mut input = sample_input();
2405            mutate(&mut input);
2406            assert_eq!(
2407                gtd7(&input, &Flags::metric()),
2408                Err(AtmosphereError::NonFiniteInput(name)),
2409                "expected non-finite rejection for {name}"
2410            );
2411        }
2412    }
2413
2414    #[test]
2415    fn out_of_domain_inputs_are_rejected() {
2416        let below = {
2417            let mut i = sample_input();
2418            i.alt = -1.0;
2419            i
2420        };
2421        assert_eq!(
2422            gtd7(&below, &Flags::metric()),
2423            Err(AtmosphereError::OutOfDomain("alt"))
2424        );
2425
2426        let above = {
2427            let mut i = sample_input();
2428            i.alt = MAX_ALTITUDE_KM + 1.0;
2429            i
2430        };
2431        assert_eq!(
2432            gtd7(&above, &Flags::metric()),
2433            Err(AtmosphereError::OutOfDomain("alt"))
2434        );
2435
2436        let neg_f107 = {
2437            let mut i = sample_input();
2438            i.f107 = -1.0;
2439            i
2440        };
2441        assert_eq!(
2442            gtd7(&neg_f107, &Flags::metric()),
2443            Err(AtmosphereError::OutOfDomain("f107"))
2444        );
2445
2446        let neg_ap = {
2447            let mut i = sample_input();
2448            i.ap = -1.0;
2449            i
2450        };
2451        assert_eq!(
2452            gtd7(&neg_ap, &Flags::metric()),
2453            Err(AtmosphereError::OutOfDomain("ap"))
2454        );
2455    }
2456
2457    #[test]
2458    fn domain_boundaries_are_inclusive() {
2459        let sea_level = {
2460            let mut i = sample_input();
2461            i.alt = 0.0;
2462            i
2463        };
2464        assert!(gtd7(&sea_level, &Flags::metric()).is_ok());
2465
2466        let top = {
2467            let mut i = sample_input();
2468            i.alt = MAX_ALTITUDE_KM;
2469            i
2470        };
2471        assert!(gtd7(&top, &Flags::metric()).is_ok());
2472    }
2473
2474    #[test]
2475    fn nrlmsise00_uses_drag_effective_total_density() {
2476        // Above ~500 km anomalous oxygen is non-zero, so the drag-effective
2477        // total (gtd7d, used by nrlmsise00) must exceed the gtd7 total that
2478        // excludes it.
2479        let mut input = sample_input();
2480        input.alt = 800.0;
2481        let drag = nrlmsise00(&input).unwrap();
2482        let no_anom = gtd7(&input, &Flags::metric()).unwrap();
2483        assert!(drag.d[8] > 0.0, "expected non-zero anomalous oxygen");
2484        assert!(
2485            drag.density() > no_anom.density(),
2486            "nrlmsise00 (gtd7d) total {} should exceed gtd7 total {}",
2487            drag.density(),
2488            no_anom.density()
2489        );
2490    }
2491}