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