Files
nrlmsise00/scripts/generate-grid-cases.sh
T

172 lines
8.0 KiB
Bash
Executable File

#!/bin/sh
# Generates tests/fixtures/grid-cases.csv: a wide sweep of the model's inputs, to catch
# what the seventeen standard cases happen not to exercise and to hold a performance
# change to the same answers.
#
# The seventeen vary one input at a time from one baseline. That leaves most of the input
# space untouched: no case combines a storm with low flux, none is in the southern
# hemisphere in winter, none sits just either side of the 72.5 and 120 km seams between the
# model's three vertical regions, and only two use the ap history. This sweeps a dense
# ladder of altitudes against a spread of the other inputs, and against eight switch
# settings.
#
# Fetches Dominik Brodowski's C translation of the NRL Fortran, runs it, and throws the
# source away. What is committed is the numbers, which are facts about the model rather than
# anyone's code. The model itself is the work of Picone, Hedin and Drob at the US Naval
# Research Laboratory. Needs a C compiler and network access.
#
# Usage: scripts/generate-grid-cases.sh
set -e
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
out="$root/tests/fixtures/grid-cases.csv"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
base="https://raw.githubusercontent.com/magnific0/nrlmsise-00/master"
echo "fetching the reference from $base"
for f in nrlmsise-00.c nrlmsise-00.h nrlmsise-00_data.c; do
curl -sS --fail --max-time 120 -L "$base/$f" -o "$work/$f"
done
digest=$(cat "$work/nrlmsise-00.c" "$work/nrlmsise-00_data.c" | sha256sum | cut -d' ' -f1)
echo "source digest $digest"
cat > "$work/driver.c" <<'DRIVER'
/* Writes the grid as CSV: the inputs at the digits they need, the outputs at seventeen
* significant digits. Scenarios are drawn from a fixed linear congruential sequence, so
* the same grid comes out every time without a table of it having to be written down. */
#include <stdio.h>
#include <math.h>
#include "nrlmsise-00.h"
/* The altitude ladder. Dense through the lower thermosphere, where the model switches
* between its three vertical regions, and either side of every altitude at which a
* species' mixed correction is dropped. */
static const double ALTS[] = {
0.0, 0.5, 2.0, 5.0, 10.0, 20.0, 32.4, 32.5, 32.6, 45.0, 55.0, 62.5, 65.0, 70.0,
72.4, 72.5, 72.6, 80.0, 90.0, 100.0, 110.0, 119.0, 122.0, 123.0, 125.0, 140.0,
159.0, 161.0, 180.0, 199.0, 201.0, 220.0, 239.0, 241.0, 249.0, 251.0, 280.0,
299.0, 301.0, 319.0, 321.0, 360.0, 400.0, 449.0, 451.0, 500.0, 600.0, 700.0,
850.0, 1000.0
};
#define NALTS ((int)(sizeof ALTS / sizeof *ALTS))
/* How many scenarios of the other inputs to sweep the ladder against. */
#define NSCENARIOS 60
static unsigned long seed = 20261002UL;
/* A fixed sequence, so the grid is reproducible without being written down. */
static double next_unit(void) {
seed = seed * 6364136223846793005UL + 1442695040888963407UL;
return (double)((seed >> 33) & 0x7FFFFF) / (double)0x800000;
}
static double between(double low, double high) {
return low + (high - low) * next_unit();
}
int main(void) {
struct nrlmsise_output output;
struct nrlmsise_input input;
struct nrlmsise_flags flags;
struct ap_array aph;
int scenario, i, j, set;
char switches[25];
printf("switches,day_of_year,seconds_of_day,altitude_km,latitude_deg,longitude_deg,"
"local_solar_time_h,f107_average,f107,ap_daily,ap1,ap2,ap3,ap4,ap5,ap6,ap7,"
"he,o,n2,o2,ar,total_mass,h,n,anomalous_o,exospheric_temperature,temperature\n");
for (scenario = 0; scenario < NSCENARIOS; scenario++) {
/* Eight switch settings, cycled across the scenarios: everything on in each of
* the two magnetic modes, then one family at a time turned off or set to cross
* terms only. */
set = scenario % 8;
for (i = 0; i < 24; i++) flags.switches[i] = 1;
flags.switches[0] = 0;
switch (set) {
case 0: flags.switches[9] = -1; break;
case 1: break;
case 2: flags.switches[9] = 0; break;
case 3: flags.switches[9] = -1; flags.switches[15] = 0; break;
case 4: flags.switches[9] = -1; flags.switches[7] = 0;
flags.switches[8] = 0; flags.switches[14] = 0; break;
case 5: flags.switches[9] = -1; flags.switches[3] = 2;
flags.switches[5] = 2; flags.switches[6] = 2; break;
case 6: flags.switches[9] = -1; flags.switches[10] = 0; break;
case 7: flags.switches[9] = -1; flags.switches[16] = 0;
flags.switches[17] = 0; flags.switches[18] = 0;
flags.switches[20] = 0; flags.switches[22] = 0; break;
}
/* gtd7 resolves the switches itself; these are the raw values. */
for (i = 0; i < 24; i++) {
if (i == 9) {
switches[i] = flags.switches[i] == -1 ? 'h'
: (char)('0' + flags.switches[i]);
} else {
switches[i] = (char)('0' + flags.switches[i]);
}
}
switches[24] = '\0';
input.year = 0;
input.doy = 1 + (int)between(0.0, 365.0);
input.sec = between(0.0, 86400.0);
input.g_lat = between(-90.0, 90.0);
input.g_long = between(-180.0, 180.0);
/* Consistent with the universal time and the longitude, as the model asks, except
* where a scenario deliberately breaks it to exercise the independent inputs. */
input.lst = input.sec / 3600.0 + input.g_long / 15.0;
if (scenario % 7 == 3) input.lst = between(0.0, 24.0);
input.f107A = between(65.0, 250.0);
input.f107 = between(65.0, 300.0);
input.ap = between(0.0, 300.0);
for (i = 0; i < 7; i++) aph.a[i] = between(0.0, 300.0);
input.ap_a = &aph;
for (j = 0; j < NALTS; j++) {
input.alt = ALTS[j];
/* With switch 15 off, the reference reads a mixed nitrogen density that its
* own call never wrote, so below 72.5 km its answer depends on which call
* last happened to write that static. There is nothing to validate against
* there, so those rows are left out; the same switch setting above 72.5 km,
* where the value is unused, is kept. */
if (flags.switches[15] == 0 && input.alt < 72.5) continue;
gtd7(&input, &flags, &output);
printf("%s,%d,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g",
switches, input.doy, input.sec, input.alt, input.g_lat, input.g_long,
input.lst, input.f107A, input.f107, input.ap);
for (i = 0; i < 7; i++) printf(",%.17g", aph.a[i]);
for (i = 0; i < 9; i++) printf(",%.17e", output.d[i]);
printf(",%.17e,%.17e\n", output.t[0], output.t[1]);
}
}
return 0;
}
DRIVER
cc -O2 -o "$work/driver" "$work/nrlmsise-00.c" "$work/nrlmsise-00_data.c" "$work/driver.c" -lm
mkdir -p "$(dirname "$out")"
{
echo "# The NRLMSISE-00 model over a wide sweep of its inputs, for validating a port."
echo "# model: NRLMSISE-00, by Picone, Hedin and Drob at the US Naval Research Laboratory"
echo "# reference: $base, Dominik Brodowski's C translation of the NRL Fortran"
echo "# reference_sha256: $digest"
echo "# generated: $(date -u +%Y-%m-%d) by scripts/generate-grid-cases.sh"
echo "# units: the model's own, since switch 0 is left at its standard value."
echo "# altitude km, angles degrees, number densities cm^-3, total mass g/cm^3, K."
echo "# switches: the reference's 24, in order, as 0, 1 or 2; index 9 is h for the ap"
echo "# history mode, which is the reference's -1."
echo "# Rows with switch 15 off and an altitude below 72.5 km are absent: there the"
echo "# reference reads a density its own call never wrote, so its answer depends on"
echo "# what was asked before it and there is nothing to compare against."
"$work/driver"
} > "$out"
python3 -c "
rows = [l for l in open('$out') if not l.startswith('#')]
print(f'wrote tests/fixtures/grid-cases.csv: {len(rows) - 1} cases, {len(open(\"$out\").read()) / 1e6:.2f} MB')
"