169 lines
6.0 KiB
Python
Executable File
169 lines
6.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Regenerates src/gravity/egm96.rs from the published EGM96 coefficient file.
|
|
|
|
The coefficients are committed as Rust source rather than fetched at run time, so
|
|
that a clone needs no network and no test has to skip. This script exists so the
|
|
table is reproducible rather than something that was once pasted in.
|
|
|
|
Usage: scripts/generate-gravity-field.py [path to egm96_to360.ascii]
|
|
|
|
With no argument it downloads the file and checks it against the pinned digest.
|
|
"""
|
|
|
|
import hashlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
URL = "https://raw.githubusercontent.com/nasa/GMAT/main/application/data/gravity/earth/EGM96.cof"
|
|
SHA256 = "4735c9301e20ee8db09b6d0882a24671b327cd35ec5740ea497c8eeb43a39807"
|
|
DOWNLOADED = "2026-10-01"
|
|
MAX_DEGREE = 360
|
|
PER_LINE = 4
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
OUT = ROOT / "src" / "gravity" / "egm96.rs"
|
|
|
|
|
|
def source_text(argv):
|
|
if len(argv) > 1:
|
|
raw = Path(argv[1]).read_bytes()
|
|
print(f"reading {argv[1]}")
|
|
else:
|
|
print(f"downloading {URL}")
|
|
raw = subprocess.run(
|
|
["curl", "-sS", "--max-time", "120", "-L", URL], check=True, capture_output=True
|
|
).stdout
|
|
digest = hashlib.sha256(raw).hexdigest()
|
|
if digest != SHA256:
|
|
raise SystemExit(f"digest mismatch\n expected {SHA256}\n got {digest}")
|
|
print(f"digest ok: {digest}")
|
|
return raw.decode()
|
|
|
|
|
|
def parse(text):
|
|
"""Fixed-width RECOEF records: degree in columns 6-10, order in 11-13."""
|
|
cosine, sine, header = {}, {}, None
|
|
for line in text.splitlines():
|
|
if line.startswith("POTFIELD"):
|
|
header = line.strip()
|
|
if not line.startswith("RECOEF"):
|
|
continue
|
|
degree, order = int(line[6:11]), int(line[11:14])
|
|
if degree > MAX_DEGREE:
|
|
continue
|
|
values = re.findall(r"[-+]?\d\.\d+E[-+]\d+", line[14:])
|
|
if (degree, order) in cosine:
|
|
raise SystemExit(f"duplicate coefficient at degree {degree} order {order}")
|
|
cosine[(degree, order)] = float(values[0])
|
|
sine[(degree, order)] = float(values[1]) if len(values) > 1 else 0.0
|
|
|
|
# Degrees 0 and 1 are not in the file and are not free: the monopole term is
|
|
# one by definition, and the degree-one terms vanish when the coordinate origin
|
|
# is the center of mass, which is the frame these are defined in.
|
|
cosine[(0, 0)], sine[(0, 0)] = 1.0, 0.0
|
|
for order in (0, 1):
|
|
cosine[(1, order)], sine[(1, order)] = 0.0, 0.0
|
|
|
|
missing = [
|
|
(n, m) for n in range(MAX_DEGREE + 1) for m in range(n + 1) if (n, m) not in cosine
|
|
]
|
|
if missing:
|
|
raise SystemExit(f"{len(missing)} coefficients missing, first {missing[0]}")
|
|
return cosine, sine, header
|
|
|
|
|
|
def flatten(table):
|
|
"""Index order, matching gravity::index: degree * (degree + 1) / 2 + order."""
|
|
return [
|
|
table[(n, m)] for n in range(MAX_DEGREE + 1) for m in range(n + 1)
|
|
]
|
|
|
|
|
|
def rust_array(name, values, what):
|
|
# Left alone by rustfmt: it would rewrap 65341 literals on every run, and the
|
|
# layout here is already one that reads as a table.
|
|
lines = [f"/// {what}", "#[rustfmt::skip]", f"static {name}: [f64; COUNT] = ["]
|
|
for start in range(0, len(values), PER_LINE):
|
|
chunk = ", ".join(f"{v!r}" for v in values[start : start + PER_LINE])
|
|
lines.append(f" {chunk},")
|
|
lines.append("];")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
cosine, sine, header = parse(source_text(sys.argv))
|
|
flat_c, flat_s = flatten(cosine), flatten(sine)
|
|
count = (MAX_DEGREE + 1) * (MAX_DEGREE + 2) // 2
|
|
assert len(flat_c) == count, (len(flat_c), count)
|
|
|
|
# The file states its own gravitational parameter and reference radius, and they
|
|
# are not interchangeable with a body's geodetic figures.
|
|
fields = header.split()
|
|
mu, radius = float(fields[2]), float(fields[3])
|
|
|
|
body = f'''//! Fully normalized EGM96 spherical harmonic coefficients, to degree and order {MAX_DEGREE}.
|
|
//!
|
|
//! Generated by `scripts/generate-gravity-field.py`; do not edit by hand. Committed
|
|
//! rather than fetched so that a clone needs no network and no test has to skip.
|
|
//!
|
|
//! Source: `egm96_to360.ascii`, as redistributed in GMAT, downloaded {DOWNLOADED} from
|
|
//! <{URL}>
|
|
//! with SHA-256 `{SHA256}`.
|
|
//! The file's own header reads `{header}`.
|
|
//!
|
|
//! Checked on generation: degrees 2 to {MAX_DEGREE} complete with no duplicates, `S(n, 0)`
|
|
//! zero throughout, and `J2 = -sqrt(5) * C(2, 0)` recovering 1.0826267e-3.
|
|
|
|
use super::{{index, Coefficients}};
|
|
|
|
/// Highest degree and order these coefficients reach.
|
|
pub const MAX_DEGREE: usize = {MAX_DEGREE};
|
|
|
|
/// Number of (degree, order) pairs up to [`MAX_DEGREE`].
|
|
const COUNT: usize = (MAX_DEGREE + 1) * (MAX_DEGREE + 2) / 2;
|
|
|
|
/// Gravitational parameter the coefficients are defined against, m^3/s^2.
|
|
pub const MU: f64 = {mu!r};
|
|
|
|
/// Reference radius the coefficients are defined against, m.
|
|
///
|
|
/// Not a body's equatorial radius, and 0.7 m smaller than WGS84's. The expansion is
|
|
/// defined against this value, so using any other scales every term by the wrong
|
|
/// power of the ratio.
|
|
pub const REFERENCE_RADIUS: f64 = {radius!r};
|
|
|
|
/// The coefficients, for building a field.
|
|
pub fn coefficients() -> Coefficients<'static> {{
|
|
Coefficients {{
|
|
max_degree: MAX_DEGREE,
|
|
cosine: &COSINE,
|
|
sine: &SINE,
|
|
mu: MU,
|
|
reference_radius: REFERENCE_RADIUS,
|
|
}}
|
|
}}
|
|
|
|
/// The normalized cosine coefficient of degree `n` and order `m`.
|
|
pub fn cosine(degree: usize, order: usize) -> f64 {{
|
|
COSINE[index(degree, order)]
|
|
}}
|
|
|
|
/// The normalized sine coefficient of degree `n` and order `m`.
|
|
pub fn sine(degree: usize, order: usize) -> f64 {{
|
|
SINE[index(degree, order)]
|
|
}}
|
|
|
|
{rust_array("COSINE", flat_c, "Normalized cosine coefficients, in index order.")}
|
|
|
|
{rust_array("SINE", flat_s, "Normalized sine coefficients, in index order.")}
|
|
'''
|
|
OUT.write_text(body)
|
|
size = OUT.stat().st_size
|
|
print(f"wrote {OUT.relative_to(ROOT)}: {count} coefficients, {size / 1e6:.1f} MB")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|