Skip to content

feat(filter): equinoctial GVE + element-space orbital filtering - #148

Merged
montge merged 2 commits into
developfrom
feature/element-space-impl
Jul 17, 2026
Merged

feat(filter): equinoctial GVE + element-space orbital filtering#148
montge merged 2 commits into
developfrom
feature/element-space-impl

Conversation

@montge

@montge montge commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements element-space-orbital-filtering (#147 spec; 14/14 tasks) — the advanced-methods survey's space-accuracy centerpiece. All additive: existing models, trackers, and every calibrated benchmark unchanged.

thresh-core/orbital/gve — Gauss variational equations

  • Element rates for the stored direct equinoctial set (a, h, k, p, q, λ), transcribed from the fetched Stacey & D'Amico (arXiv:2105.06516) Appendix A — naming map documented, including the appendix's correction of a sign error in Roth's H̄
  • Perturbations enter as a time-aware inertial closure rotated into RSW internally (J2 = j2_acceleration alone; two-body secular analytic) — any force the propagation tier's config can express can later drive element filtering
  • Cross-formulation equivalence: the same J2 orbits through GVE-in-elements and the Cartesian path agree at measured sub-decimetre tolerances, both directions, LEO/MEO/eccentric-through-perigee — the transcription validated by an independent in-tree formulation sharing only the force closure

thresh-filter/models/equinoctial — the filter model

  • MotionModel over the 6D element state; mean longitude unwrapped in filter state space (periodicity owned by conversions) with proofs at the wrap boundary and across hundreds of accumulated radians — the classic element-filter bug made structurally impossible
  • Process noise = isotropic acceleration PSD mapped through the GVE input matrix (Stacey & D'Amico Eq. 5) — numerically the same physical Q the Cartesian model's CWNA encodes, so the demonstration cannot be won by tuning

The measured payoff

Seeded Monte-Carlo coast (7000 km / 51.6° LEO, 50 runs, bitwise-deterministic), position ANEES vs the 95% band [2.360, 3.716]:

coast (rev) Cartesian EKF equinoctial UKF
0.96 3.239 2.912
1.90 3.050 2.837
2.39 4.757 (out) 2.853 (in)
2.86 3.107 2.717
3.35 4.370 (out) 2.890 (in)
3.82 3.309 2.628

The Cartesian banana pushes the EKF out of band once per revolution as its major axis rotates; the equinoctial filter stays inside at all nine swept gaps. The folklore claim, now a recorded number. (The demonstration isolates coast-phase propagation via a shared warmup posterior — rationale and the measured element-space measurement-update trade-off recorded in design.md; that update path is the tracker-consumption follow-up's problem.)

Verification

  • Workspace tests green (incl. --features adsb, the feat(core): atmospheric measurement propagation — refraction + ionospheric delay biases with correction hooks #145 lesson), clippy -D warnings, fmt, rustdoc -Dwarnings, openspec 49/49
  • Benchmark invariance: all four calibrated scenarios digit-for-digit identical (empty diff)
  • Adversarially reviewed (4 lenses, with the GVE module given a term-by-term re-fetch of its source after its implementing agent returned a corrupted report): 6 findings → 2 confirmed & fixed (the citation named "Sullivan" where the fetched paper's authors are Stacey & D'Amico — precisely the filled-from-memory fingerprint the provenance rule exists to catch; and a 21× minute-conversion slip in design prose), 4 refuted — including refutations of the fairness challenges against the demonstration

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added equinoctial-element orbital propagation with J2 perturbation support.
    • Added an equinoctial orbital motion model for sigma-point filtering.
    • Added position measurement conversion and acceleration-based process-noise modeling.
    • Added continuous mean-longitude handling to avoid artifacts when crossing angle boundaries.
  • Tests

    • Added validation for propagation accuracy, covariance behavior, determinism, and coast-gap filtering performance.
  • Documentation

    • Updated implementation records and marked the element-space orbital filtering plan complete.

…ent-space-orbital-filtering)

Completes the element-space-orbital-filtering change (14/14 tasks) — the
advanced-methods survey's space-accuracy centerpiece. All additive:
existing models, trackers, and every calibrated benchmark unchanged.

- thresh-core/orbital/gve: Gauss variational equations for the stored
  direct equinoctial set (a, h, k, p, q, lambda), transcribed from the
  fetched Stacey & D'Amico arXiv:2105.06516 Appendix A (naming map
  documented; the appendix's corrected Roth H-bar sign); perturbing
  acceleration as a time-aware inertial closure rotated into RSW
  internally (J2 enters as j2_acceleration alone; two-body secular
  analytic); sub-stepped RK4; near-singular (e,i ~ 1e-8) finite;
  cross-formulation equivalence vs the Cartesian two-body+J2 path in
  both directions across LEO/MEO/eccentric-through-perigee at measured
  sub-decimetre tolerances — the transcription validated by an
  independent in-tree formulation sharing only the force closure
- thresh-filter/models/equinoctial: MotionModel over the 6D element
  state (KeplerJ2 conventions); mean longitude UNWRAPPED in filter state
  space with wrap-straddle and hundreds-of-radians proofs; elements->ECI
  position measurement mapping bitwise-equal to the existing conversion;
  process noise = isotropic acceleration PSD through the GVE input
  matrix (Stacey & D'Amico Eq. 5) — the same physical Q the Cartesian
  model encodes, so demonstration fairness is by construction
- The measured payoff (thresh-eval/tests/orbital_coast_gap.rs, seeded,
  50 runs, bitwise-deterministic): coasting a 7000 km LEO, the Cartesian
  EKF's position ANEES exits the 95% band once per revolution as the
  banana rotates (4.757 at 2.39 rev vs band [2.360, 3.716]; again 4.370
  at 3.35 rev) while the equinoctial UKF stays inside at every one of
  nine swept gaps (2.58-2.91) — the element-space consistency claim,
  measured. Demonstration isolates coast-phase propagation via a shared
  warmup posterior (divergence + rationale recorded in design.md)
- Benchmark invariance: all four calibrated scenarios digit-for-digit
  identical to the pre-change baseline

Adversarially reviewed (4 lenses incl. a term-by-term re-fetch of the
GVE source): 6 findings, 2 confirmed and fixed (the fetched paper is by
Stacey & D'Amico, not "Sullivan" — the exact filled-from-memory citation
fingerprint the provenance rule exists to catch; a 21x minute-conversion
slip in design prose), 4 refuted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 02:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@montge, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e71fa602-f10e-4f57-8e5e-ea74996bae79

📥 Commits

Reviewing files that changed from the base of the PR and between 78bc4d6 and 87b26a2.

📒 Files selected for processing (3)
  • crates/thresh-core/src/orbital/gve.rs
  • crates/thresh-eval/tests/orbital_coast_gap.rs
  • crates/thresh-filter/src/models/equinoctial.rs
📝 Walkthrough

Walkthrough

Changes

Equinoctial orbital filtering

Layer / File(s) Summary
GVE propagation and orbital exports
crates/thresh-core/src/orbital/gve.rs, crates/thresh-core/src/orbital/mod.rs
Adds RSW projection, J2 perturbation acceleration, equinoctial GVE rates, deterministic RK4 propagation, and numerical validation tests.
Equinoctial motion model and noise mapping
crates/thresh-filter/src/models/equinoctial.rs, crates/thresh-filter/src/models.rs, openspec/changes/element-space-orbital-filtering/design.md
Adds the six-state equinoctial motion model, Cartesian position conversion, inertial process noise, unwrapped mean longitude handling, and resolved design records.
Coast-gap filtering evaluation
crates/thresh-eval/tests/orbital_coast_gap.rs, openspec/changes/element-space-orbital-filtering/tasks.md
Adds seeded EKF/UKF coast-gap NEES comparison, crossover assertions, determinism checks, and completed task statuses.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Filter as EquinoctialModel
  participant GVE as propagate_equinoctial
  participant Rates as equinoctial_element_rates
  participant Conversion as equinoctial_position
  Filter->>GVE: predict six equinoctial elements
  GVE->>Rates: evaluate RK4 stage rates
  Rates-->>GVE: return element derivatives
  GVE-->>Filter: return propagated state
  Filter->>Conversion: map elements to ECI position
  Conversion-->>Filter: return position measurement
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: equinoctial GVE plus element-space orbital filtering.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/element-space-impl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.65991% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/thresh-filter/src/models/equinoctial.rs 95.21% 12 Missing ⚠️
crates/thresh-core/src/orbital/gve.rs 99.23% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/thresh-core/src/orbital/gve.rs`:
- Around line 689-706: Extend gve_vs_cartesian_gap while preserving the existing
element-born path, then add a Cartesian-born case that initializes r0 and v0
with keplerian_to_cartesian, converts that state to equinoctial elements for
propagate_equinoctial, and runs the same three-regime assertions. Ensure both
initialization paths execute and are compared against the Cartesian propagator.
- Around line 350-355: Update substep_count to validate or guard max_step_s
before division so zero or invalid step sizes cannot produce a saturated
usize::MAX count, and calculate the count from dt.abs() so negative intervals
receive the same sub-stepping as positive intervals. Preserve the minimum
one-step behavior for zero-duration propagation and the existing usize return
contract.

In `@crates/thresh-eval/tests/orbital_coast_gap.rs`:
- Around line 401-445: Extend the sweep in the crossover-checking test to retain
each equinoctial UKF verdict, then assert after the loop that all nine recorded
verdicts are Consistent. Also assert the contractual Cartesian crossing
checkpoint indices if those two crossings are expected, while preserving the
existing crossover value and bound assertions.

In `@crates/thresh-filter/src/models/equinoctial.rs`:
- Around line 226-236: Update Equinoctial process-noise computation around
process_noise and inertial_input_matrix so Γ is evaluated from the propagated
nominal state rather than the unchanged reference_elements on every call.
Advance or otherwise maintain the nominal reference across each interval before
constructing Q, while preserving the existing PSD scaling; add coverage
demonstrating Q changes as orbital longitude changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54d06e50-c39a-421e-a33b-b55c4cfa07c4

📥 Commits

Reviewing files that changed from the base of the PR and between e057b65 and 78bc4d6.

📒 Files selected for processing (7)
  • crates/thresh-core/src/orbital/gve.rs
  • crates/thresh-core/src/orbital/mod.rs
  • crates/thresh-eval/tests/orbital_coast_gap.rs
  • crates/thresh-filter/src/models.rs
  • crates/thresh-filter/src/models/equinoctial.rs
  • openspec/changes/element-space-orbital-filtering/design.md
  • openspec/changes/element-space-orbital-filtering/tasks.md

Comment thread crates/thresh-core/src/orbital/gve.rs Outdated
Comment thread crates/thresh-core/src/orbital/gve.rs
Comment thread crates/thresh-eval/tests/orbital_coast_gap.rs
Comment on lines +226 to +236
fn process_noise(&self, dt: f64) -> DMatrix<f64> {
// Eq. (5) of Stacey & D'Amico with Φ ≈ I over the interval and the
// isotropic inertial acceleration PSD Q̃ᴵ = σ_accel²·I₃ (their Eq. 3):
// Q ≈ Γᴵ·(σ²I₃)·Γᴵᵀ·Δt = σ²·(Γᴵ Γᴵᵀ)·Δt.
// Symmetric positive-semidefinite (Gram form; rank ≤ 3 — added to the
// full-rank predicted covariance, so the sum stays PD). Γᴵ is frozen
// at the reference elements (module docs). This is the same physical
// PSD KeplerJ2 assumes (its Q is Eq. 5's closed form in Cartesian).
let g = inertial_input_matrix(&self.reference_elements, self.gravity.mu);
let scale = self.sigma_accel * self.sigma_accel * dt;
(&g * g.transpose()) * scale

@coderabbitai coderabbitai Bot Jul 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Advance the process-noise linearization with the nominal state.

Every call evaluates Γ at the unchanged reference_elements, although Γ depends on orbital phase. The coast test consequently injects the same element-space covariance orientation for four revolutions—and supplies that reference from truth[0].

The cited SNC model treats this as a small-interval approximation; longer propagation requires evaluating the state and covariance contributions across subintervals. (arxiv.org) Make process noise state-aware, or advance a nominal reference before computing each interval’s Q, and add a test showing Q changes with longitude.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/thresh-filter/src/models/equinoctial.rs` around lines 226 - 236,
Update Equinoctial process-noise computation around process_noise and
inertial_input_matrix so Γ is evaluated from the propagated nominal state rather
than the unchanged reference_elements on every call. Advance or otherwise
maintain the nominal reference across each interval before constructing Q, while
preserving the existing PSD scaling; add coverage demonstrating Q changes as
orbital longitude changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 87b26a2EquinoctialModel::re_anchored(state) added and the demo re-anchors Γ at the current mean each coast step; the model docs state the small-interval approximation and the re-anchoring contract for long arcs. Demo numbers unchanged at recorded precision (Q is small; its orientation now honestly tracks phase).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- current commit ---'
git rev-parse --short HEAD

printf '%s\n' '--- equinoctial model outline ---'
ast-grep outline crates/thresh-filter/src/models/equinoctial.rs --items all --type function,method --match 're_anchored|process_noise|inertial_input_matrix|test|noise'

printf '%s\n' '--- re-anchoring and process-noise references ---'
rg -n -C 5 're_anchored\s*\(|process_noise\s*\(|inertial_input_matrix\s*\(|reference_elements' \
  crates/thresh-filter/src/models/equinoctial.rs crates/thresh-eval/tests/orbital_coast_gap.rs

printf '%s\n' '--- target implementation sections ---'
sed -n '80,270p' crates/thresh-filter/src/models/equinoctial.rs

printf '%s\n' '--- target test sections ---'
sed -n '270,525p' crates/thresh-filter/src/models/equinoctial.rs

printf '%s\n' '--- coast demo re-anchoring contexts ---'
rg -n -C 10 're_anchored\s*\(' crates/thresh-eval/tests/orbital_coast_gap.rs

Length of output: 36698


@montge, the long-arc behavior is now correctly re-anchored: coast_run builds step_model from the current ukf.x before every coast-step prediction, and the API documentation makes the small-interval contract explicit.

One requested piece is still missing: the unit tests do not yet demonstrate that re_anchored causes process_noise(dt) to change with orbital longitude. Please add that focused regression test; then this thread can be resolved.


✏️ Learnings added
Learnt from: montge
Repo: Chaffgold/thresh PR: 148
File: crates/thresh-filter/src/models/equinoctial.rs:241-251
Timestamp: 2026-07-17T02:59:58.708Z
Learning: In `crates/thresh-filter/src/models/equinoctial.rs`, `EquinoctialModel::process_noise` linearizes the Gauss input matrix at `reference_elements`. For long arcs, callers should use `EquinoctialModel::re_anchored(&current_filter_mean)` before each predict step so the element-space process-noise orientation follows orbital phase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

…weep asserts, phase-tracking Q

- gve.rs: substep count uses |dt| (backward propagation splits correctly)
  and propagate_equinoctial validates max_step_s (finite, > 0)
- gve.rs: genuinely Cartesian-born equivalence direction added — (r0, v0)
  from keplerian_to_cartesian, GVE elements born FROM that state via
  cartesian_to_keplerian, so no shared elements-first initialization can
  cancel a conversion defect; measured gap 0.138 m over 3 revs is
  initial-condition conditioning (near-circular round trip ~mm delta-a
  drifting 3*pi per rev), documented, tolerance 1.0 m vs km-scale for a
  real defect
- orbital_coast_gap.rs: the full stated result is now asserted — the
  equinoctial UKF in-band at ALL nine gaps and both recorded Cartesian
  excursions (checkpoints 5 and 7), not just one crossover
- EquinoctialModel: re_anchored(state) helper (Clone+Copy derived); the
  SNC input matrix Gamma depends on orbital phase, so the demo re-anchors
  per coast step and the model documents the small-interval approximation
  and the re-anchoring contract for long arcs; demo numbers unchanged at
  recorded precision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@montge
montge merged commit 6996b84 into develop Jul 17, 2026
28 checks passed
montge added a commit that referenced this pull request Jul 17, 2026
… completed change (#149)

Implementation merged in #148 (14/14 tasks). Syncs the two delta specs
into main specs — gauss-variational-equations and
equinoctial-filter-model — then moves the change to
openspec/changes/archive/2026-07-17-element-space-orbital-filtering.

openspec validate --all --strict --no-interactive: 50/50 post-archive.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants