apsis-rs/validation/tools/generate_nutation_table.py

144 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Generate the Rust source for the IAU 2000A nutation series.
The series is the MHB2000 model. It has a luni-solar part and a planetary
part, both long tables of integer argument multipliers and real
coefficients, so they are transcribed mechanically from the reference
rather than by hand.
The reference is the ERFA library, a derivative of the IAU SOFA library
maintained by the International Astronomical Union. Its source is the
authoritative statement of the series.
Field order is the one the reference uses, which is not the order the
coefficients are listed in the paper:
luni-solar: multipliers l, l', F, D, Om; then sp, spt, cp, ce, cet, se
planetary: multipliers l, F, D, Om, Me, Ve, E, Ma, Ju, Sa, Ur, Ne, pa;
then sp, cp, se, ce
The two series use different argument sets and different coefficient
groupings for the obliquity, so the generated Rust types keep them separate.
Fetch the reference with:
curl -O https://raw.githubusercontent.com/liberfa/erfa/master/src/nut00a.c
Usage:
python validation/tools/generate_nutation_table.py path/to/nut00a.c
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
def extract_array(source: str, name: str) -> str:
"""Returns the body of a C array initializer."""
start = source.index(name + "[] = {")
brace = source.index("{", start)
depth = 0
index = brace
while index < len(source):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
break
index += 1
return source[brace : index + 1]
def parse_terms(body: str) -> list[list[str]]:
"""Returns the numeric terms of an array of brace groups."""
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
terms = []
for group in re.findall(r"\{([^{}]*)\}", body):
values = group.replace("\n", " ").strip()
if values:
terms.append([value.strip() for value in values.split(",")])
return terms
def main(argv: list[str]) -> int:
if len(argv) < 2:
print(__doc__, file=sys.stderr)
return 2
source = Path(argv[1]).read_text()
lunisolar = parse_terms(extract_array(source, "xls"))
planetary = parse_terms(extract_array(source, "xpl"))
if any(len(term) != 11 for term in lunisolar):
print("unexpected luni-solar row width", file=sys.stderr)
return 1
if any(len(term) != 17 for term in planetary):
print("unexpected planetary row width", file=sys.stderr)
return 1
print("//! The IAU 2000A nutation series.")
print("//!")
print("//! The table is generated from the reference implementation of the")
print("//! model by `validation/tools/generate_nutation_table.py` and is")
print("//! reproduced without change.")
print("//!")
print("//! The field order follows the reference, which is not the order the")
print("//! coefficients are listed in the paper.")
print("//!")
print("//! Source: the MHB2000 model of Mathews, Herring and Buffett, adopted by")
print("//! the International Astronomical Union in 2000 and distributed as part")
print("//! of the IAU SOFA library, as implemented by ERFA.")
print()
print("use super::{PlanetaryTerm, SeriesTerm};")
print()
print("/// The luni-solar terms of the series.")
print("///")
print("/// The multipliers apply to the arguments `l`, `l'`, `F`, `D`, and `Om`")
print("/// in that order. The coefficients are the longitude sine, its time")
print("/// derivative, the longitude cosine, the obliquity cosine, its time")
print("/// derivative, and the obliquity sine, in units of 0.1 microarcsecond.")
print(f"pub static LUNI_SOLAR: [SeriesTerm; {len(lunisolar)}] = [")
for term in lunisolar:
multipliers = ", ".join(term[:5])
coefficients = ", ".join(
value if "." in value or "e" in value.lower() else value + ".0"
for value in term[5:]
)
print(
f" SeriesTerm {{ argument: [{multipliers}], "
f"coefficients: [{coefficients}] }},"
)
print("];")
print()
print("/// The planetary terms of the series.")
print("///")
print("/// The multipliers apply to the arguments `l`, `F`, `D`, `Om`, the mean")
print("/// longitudes of Mercury through Uranus, the mean longitude of Neptune,")
print("/// and the general accumulated precession, in that order. The")
print("/// coefficients are the longitude sine, the longitude cosine, the")
print("/// obliquity sine, and the obliquity cosine, in units of 0.1")
print("/// microarcsecond.")
print(f"pub static PLANETARY: [PlanetaryTerm; {len(planetary)}] = [")
for term in planetary:
multipliers = ", ".join(term[:13])
coefficients = ", ".join(
value if "." in value or "e" in value.lower() else value + ".0"
for value in term[13:]
)
print(
f" PlanetaryTerm {{ argument: [{multipliers}], "
f"coefficients: [{coefficients}] }},"
)
print("];")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))