Files
nrlmsise00/readme.md
T

8.6 KiB

nrlmsise00

The NRLMSISE-00 empirical atmosphere model, generic over its scalar type.

Evaluated at f64 it gives density and temperature. Evaluated at a forward-mode dual number it gives density and its exact derivatives, which is the reason this crate exists: density falls by a factor of e about every fifty kilometers, so a drag partial that drops its dependence on position is badly wrong rather than slightly wrong. Two other Rust implementations exist and both are f64 only.

use nrlmsise00::{Input, MagneticActivity, Model};

let model = Model::new();
let output = model.gtd7d(&Input::consistent(
    172,                                   // day of year
    29000.0,                               // seconds of universal time
    400.0,                                 // altitude, km
    60.0,                                  // latitude, degrees north
    -70.0,                                 // longitude, degrees east
    150.0,                                 // 81-day average F10.7, centered
    150.0,                                 // previous day's F10.7
    MagneticActivity::Daily { ap: 4.0 },
));

// SI at the boundary: kg/m^3 and m^-3. Local solar time comes out at 3.4 h here, since
// `consistent` derives it from the universal time and the longitude.
assert!((output.total_mass - 2.400e-12).abs() < 0.001e-12);
assert!((output.temperature - 1098.2).abs() < 0.1);

And the derivative, through the model rather than around it:

# #[cfg(feature = "autodiff")]
# fn main() {
use num_dual::{Dual64, DualNum};
use nrlmsise00::{Input, MagneticActivity, Model, Scalar};

let model = Model::new();
let altitude = Dual64::from(400.0).derivative();
let output = model.gtd7d(&Input::consistent(
    172,
    Dual64::lift(29000.0),
    altitude,
    Dual64::lift(60.0),
    Dual64::lift(-70.0),
    Dual64::lift(150.0),
    Dual64::lift(150.0),
    MagneticActivity::Daily { ap: Dual64::lift(4.0) },
));

// d(rho)/dz, in kg/m^3 per km, and the scale height it implies.
let scale_height = -output.total_mass.re / output.total_mass.eps;
assert!((scale_height - 55.8).abs() < 0.1);
# }
# #[cfg(not(feature = "autodiff"))]
# fn main() {}

gtd7 or gtd7d

They differ in one number. gtd7's total mass density sums helium, atomic oxygen, molecular nitrogen, molecular oxygen, argon, hydrogen and atomic nitrogen. gtd7d also includes anomalous oxygen, which is hot atomic oxygen and is 18.6% of the mass at 1000 km in the model's own standard case. A drag calculation wants gtd7d. The individual number densities are identical either way.

How closely it matches the reference

At f64, bit for bit, over every case there is to compare against:

What Cases Values compared
The standard test cases 17 187
A wide input grid, fifty altitudes against sixty scenarios under eight switch settings 2880 31680
The individual helpers, expansions and profiles 819 several thousand

The comparisons are exact rather than approximate because an approximate one would not notice a term dropped from an expansion whose contribution happens to be small where it is sampled. The fixtures hold the reference's own output at seventeen significant digits and are regenerated by the scripts in scripts/, which fetch Dominik Brodowski's C translation of the NRL Fortran, run it, and discard the source: what is committed is numbers, which are facts about the model rather than anyone's code.

At a dual type the value agrees to about 1e-14 rather than exactly, because num-dual computes a division as a reciprocal and a multiply. See the scalar module docs.

Where it deliberately differs from the reference

The reference keeps its working state in file-scope statics, so two of its answers depend on what was asked before them. Both are fixed here, and both are fixed by computing a value the reference reads without having written rather than by changing any arithmetic:

  • At exactly 32.5 km, it splines through temperature nodes its own call never filled.
  • With switch 15 off, below 72.5 km, it scales every species from a mixed nitrogen density its own call never computed.

These nodes and that density do not depend on altitude, so filling them reproduces what a preceding call would have left, which is why the grid fixture still matches at 32.5 km in sixty scenarios. Nothing else departs from the reference's control flow.

It is not continuous in altitude

Worth knowing before integrating through it. Each species' mixed correction is dropped above a fitted altitude by a test on the altitude alone, and the model's three vertical regions are joined by switches rather than smoothly, so the density steps. Sweeping 72.5 to 1000 km at ten meters and comparing each interval against the model's own derivative finds eight places where it does, and no others:

Altitude Total mass density What happens there
72.5 km 1.0e-3 the join between the model's two halves
123.435 km 1.5e-3 za, the join between the Bates profile and the spline below it
160 km 7.1e-5 molecular nitrogen's mixed correction is dropped
240 km 2.3e-6 argon's
250 km 1.9e-5 molecular oxygen's
300 km 5.3e-5 atomic oxygen's, and the lower thermosphere temperature variation
428.475 km 6.9e-7 atomic nitrogen's turbopause blend saturates, a step of about exp(-10)
450 km 3.5e-5 atomic nitrogen's mixed correction is dropped

Individual species move more than the total does — up to 3e-3 — because the species that steps is usually not the one carrying the mass. Above 500 km there are no jumps at all.

The temperature is continuous at all of these but za, where the exospheric temperature steps by nearly two fifths: 1027.3 K to 1426.4 K in the standard case. That is the reference's documented behavior rather than an artifact, its header saying the exospheric temperature "is set to global average for altitudes below 120 km", and it is what makes 123.435 km the largest discontinuity in the model above 72.5 km.

What is not implemented

ghp7, the reference's pressure-level entry point, which answers at a given pressure rather than a given altitude. It is a secant iteration wrapped around gtd7, so a caller who wants it can write it against this crate; nothing in the model needs it.

Units and switches

The crate answers in SI by default — m^-3, kg/m^3 and K — using the model's own metric switch, so the conversion happens in one place and is the reference's own. Units::Native gives the cm^-3 and g/cm^3 the published cases are quoted in.

The twenty-three switches are named rather than indexed, and each has three states: the reference's 2 keeps a term's cross terms while dropping the term itself. The reference's switch 9 does two jobs, one of which is to select between a daily ap value and a seven-element history; that is a statement about the data, so it lives on the input as MagneticActivity and the mode cannot disagree with the values.

Drivers

The solar and geomagnetic values the model reads are measurements, and this crate does not own them: f107, f107_average and the ap history are plain numbers on the input, so a caller with its own record passes them straight in and pays nothing for the convenience it is not using.

The drivers feature, on by default, is not implemented yet. It will add a compiled-in record of those measurements and a lookup by date, for callers who would rather not source their own. The feature is declared now so that turning it on later is not a change to what default means.

The model is not this crate's work

NRLMSISE-00 is the work of Mike Picone, Alan Hedin and Doug Drob at the US Naval Research Laboratory, published as Picone et al. (2002), NRLMSISE-00 empirical model of the atmosphere: Statistical comparisons and scientific issues, J. Geophys. Res. 107(A12). It is believed to be in the public domain as a work of the US Government under 17 U.S.C. 105; NRL attached no explicit license to it. The coefficients compiled into this crate are the model's own.

MSIS is a registered trademark of the Government of the United States of America, as represented by the Secretary of the Navy. It is used here only to identify which model this implements, which is nominative fair use; this crate is not endorsed by or affiliated with NRL or the US Navy.

Later versions of the model — NRLMSIS 2.0 and 2.1 — are not here and cannot be, on these terms: they are licensed for academic and non-commercial use only and their methods are covered by US Patent 10,641,925. See nrlmsis2/readme.md in the repository.

License

MIT, for this implementation.