Skip to content

Physics-equation catalog with JEOD source traceability #518

@simnaut

Description

@simnaut

Motivation

CLAUDE.md's JEOD Convention Rule exists because of a real near-miss: an agent guessed the time_periapsis → mean anomaly formula as M = 2π − n·t instead of M = n·t, producing 11,668 km of error against NASA flight data. The rule today reads:

When JEOD uses a field name whose meaning could be ambiguous (e.g., time_periapsis — is it time to periapsis or time since periapsis?), always read the JEOD C++ source to determine the convention. Do not guess or reason by analogy.

It's a cultural rule. An AI-assisted PR that ports a formula without reading the .cc can satisfy fmt + clippy + most tests and still be silently wrong — exactly the failure shape discussed for issue #517. The fix discussed there (a generic "every new formula needs a .cc:line citation" lint) was deferred because the citation grammar wasn't tractable.

This issue proposes a more tractable shape: a physics-equation catalog that mirrors the existing docs/JEOD_invariants.md// JEOD_INV:tests/invariant_coverage.rs mechanism, but for formulas instead of guard conditions. The catalog itself is the citation grammar.

Proposal

docs/JEOD_equations.md — a parallel catalog

One row per non-trivial formula we port from JEOD. Each row carries:

  • TagSection.Item, e.g. KE.03 (Kepler/anomaly conversions), RO.07 (rotation), GR.12 (gravity gradient), AT.02 (atmosphere), TM.05 (time scales), RF.04 (frame transforms).
  • Equation / quantity — short prose name (e.g. "mean anomaly from time-since-periapsis").
  • Formula — written in unambiguous form (LaTeX or plain ASCII), including sign and direction conventions explicitly.
  • JEOD sourcemodels/.../*.cc:line reference, with the relevant function name. Example: models/dynamics/body_action/src/dyn_body_init_orbit.cc:218 dyn_body_init_orbit::apply().
  • Our implementation — crate path + function (e.g. crates/astrodyn_orbital/src/elements.rs:mean_anomaly_from_time_since_periapsis).
  • Convention notes — anything that bit the original port or could bite a future one. Sign conventions, hand of rotation, scalar-first vs scalar-last, time-scale, frame.
  • Divergence — where our Rust differs from JEOD's C++ (e.g. "we divide by mass at runtime; JEOD precomputes inverse_mass"). Parallel to the existing invariant catalog's divergence column.

Source-side tags

Mirror the JEOD_INV pattern:

// JEOD_EQ: KE.03 — M = n·(t − t_peri); never the 2π complement form
fn mean_anomaly_from_time_since_periapsis(n: f64, t_since_peri: f64) -> f64 {
    n * t_since_peri
}

The tag goes at the formula site, not at the function signature, so derivations that compose two catalog entries can carry both tags inline.

tests/equation_coverage.rs — bidirectional CI gate

Parallel to tests/invariant_coverage.rs:

  1. Every row in JEOD_equations.md must have at least one // JEOD_EQ: XX.YY tag in the workspace source.
  2. Every // JEOD_EQ: XX.YY tag must reference a catalog row.
  3. Every row's JEOD source citation must be syntactically well-formed (models/.../*.cc:<line> or models/.../*.hh:<line>); the test does not attempt to verify the citation against an actual JEOD checkout (CI runs without $JEOD_HOME, per CLAUDE.md), but the format check rules out garbage.

This is the citation grammar: rows in the catalog are the allowed citations, and tags are the allowed call sites.

Why a catalog and not a free-form citation lint

A "require .cc:line in any new formula" lint has two failure modes that the catalog avoids:

  • What counts as a "formula"? A v.normalize() call doesn't need a citation; a quaternion-from-Euler-angles construction does. A regex can't tell them apart; a catalog row makes the boundary explicit ("if it's in the catalog, it needs a tag; if it's not in the catalog, an author who thinks it should be opens a PR adding the row").
  • Citation rot. A free-form .cc:line comment goes stale silently when JEOD reorganizes. A central catalog can be re-grepped/re-verified in one place during a JEOD bump, the same way extract_* binaries are.

This shape also matches what already works: JEOD_INV solves an analogous problem and the team is fluent in the pattern.

Scope discipline

The catalog must not explode into an enumeration of every line of math. Inclusion criterion: the formula has a convention choice that could be guessed wrong and produce silently-wrong physics that passes shallow tests.

Candidates on day one (non-exhaustive — actual numbering chosen during implementation):

  • Anomaly conversions (Kepler equation iteration, M ↔ E ↔ ν, sign of time_periapsis)
  • Quaternion ↔ Euler / DCM conversions (scalar position, left-vs-right transformation)
  • RNP composition order (TIRS → ITRS, polar motion sign)
  • LVLH and NED frame definitions (z-down vs z-radial-out, hand)
  • Gravity-gradient torque sign and frame
  • Atmosphere lookup interpolation rule (log-density vs linear, altitude reference)
  • Time-scale offsets (TAI − UTC = leap seconds, sign of UT1−UTC)
  • Geodetic ↔ geocentric latitude conversion (which iteration, convergence criterion)

What is not a catalog candidate: arithmetic plumbing (a + b), unit conversions handled by the typed-quantity facade, library calls into anise/glam that don't encode a JEOD-specific choice.

Bootstrap path

  1. Land docs/JEOD_equations.md with section headings and ~5 seed rows drawn from the canonical near-miss (KE.* for anomaly conversions, RO.* for the quaternion convention covered in CLAUDE.md's "Quaternion Convention" section).
  2. Tag the existing implementation sites for those seeds.
  3. Land tests/equation_coverage.rs with both directions enforced. CI now refuses untagged catalog rows and orphan tags.
  4. Backfill incrementally: when porting or reviewing a new formula, the author adds a row + tag. No big-bang sweep — the catalog grows when the cost of guessing would be high.
  5. Once stable, update CLAUDE.md's JEOD Convention Rule section to point at the catalog as the enforcement mechanism, and the JEOD Invariant Tracking section to note the equation-catalog sibling.

Acceptance

  • docs/JEOD_equations.md exists with seed rows for at least the time_periapsis formula and the quaternion convention.
  • tests/equation_coverage.rs passes locally and in CI, enforcing bidirectional consistency.
  • CLAUDE.md's JEOD Convention Rule section references the catalog.
  • At least one PR after merge demonstrates the workflow end-to-end: a new ported formula lands with a new catalog row and a new // JEOD_EQ: tag together.

Relationship to #517

This issue is the deferred recommendation 5 from #517. Independent of #517's four items — can land before, after, or in parallel. Shares the same underlying motivation (make AI-assisted port correctness structural) but a different mechanism (traceability instead of structural guard).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions