Skip to content

Add SIM_verif_attach_mass RUN_09 (non-identity root orientation) (18→19/21) (#99)#631

Open
simnaut wants to merge 3 commits into
mainfrom
attach-mass-run09
Open

Add SIM_verif_attach_mass RUN_09 (non-identity root orientation) (18→19/21) (#99)#631
simnaut wants to merge 3 commits into
mainfrom
attach-mass-run09

Conversation

@simnaut
Copy link
Copy Markdown
Owner

@simnaut simnaut commented May 25, 2026

Summary

Adds RUN_09 — the first SIM_verif_attach_mass RUN with a non-identity, non-180° parent struct→body orientation (parent_mass_orientation_optionA): parent (StructCG inertia) + child1 (Body), offset attach [-1,0,0]. Takes attach_mass to 19/21.

In the course of validating RUN_09 it surfaced — and this PR fixes — a latent frame bug in the composite-inertia kernel.

Key fix — composite inertia is computed in the body frame

JEOD stores core_properties.inertia in the body frame (mass_properties_init.cc:103/119 rotate the StructCG/Struct specs struct→body at init) and computes the composite in the body frame — "the core and composite masses share a common body frame" (mass_calc_composite_inertia.cc), with the cm-to-cm offsets rotated by composite_properties.T_parent_this (mass_update.cc:101/107).

Our calc_composite_inertia accumulated in the struct frame, which is correct only when a body's own struct→body orientation is identity. calc_composite_inertia is rewritten to compute in the body frame (body-frame core unrotated, offsets rotated struct→body by t_parent_this, children rotated child-body→parent-body r = T·Sᵀ·T_childᵀ). The change is contained to astrodyn_dynamicscomposite_properties.inertia now genuinely holds the body-frame tensor that sync_body_mass_from_tree and the rotational integrator already expect, so no runner/Bevy consumer changes are needed.

Why this change is needed, and why it is correct

The bug

MassTree::recompute_composites produced composite_properties.inertia in the structural frame: it added core.inertia directly to struct-frame parallel-axis offsets (composite_wrt_pstr.position − cm). But core.inertia is defined body-frame (the field doc, the typed InertiaTensor<BodyFrame<V>>, and JEOD's init), and every consumer treats the composite as body-frame:

  • sync_body_mass_from_tree copies composite_properties into the integrated body's mass unchanged;
  • the rotational integrator evaluates Euler's equation in the body frame — torque_body = t_struct_body · torque, ω = ang_vel_body, α = I⁻¹(τ − ω×Iω) — so mass.inertia must be body-frame.

For a body whose struct and body frames differ by a non-trivial rotation T, the struct-frame composite and the body-frame composite differ by T·(·)·Tᵀ, so the integrator was fed a wrong-frame inertia.

Why it stayed invisible

The two frames coincide whenever T leaves the inertia invariant. Every non-identity orientation anywhere in the suite is Apollo's yaw_180 (180° about Z, R = diag(−1,−1,1)), and every Apollo body inertia is diagonal (Modified_data/mass/*.py), so R·diag·Rᵀ = diag exactly — and the stack's X-axis offsets keep the composite diagonal. Identity orientations are trivially invariant. So no existing scenario could observe the difference; RUN_09's general optionA rotation is the first that does.

Why the new computation is correct (JEOD-faithful)

The rewrite is a direct port of JEOD's body-frame composite:

  • Core inertia body-frame, unrotated — JEOD mass_calc_composite_inertia.cc ("common body frame"); composite_properties.T_parent_this == core_properties.T_parent_this (mass.cc:203).
  • Offsets rotated struct→body by composite_properties.t_parent_this — JEOD mass_update.cc:101/107 (Vector3::transform(T_parent_this, r_cm_cm_str, …)).
  • Child composite rotated child-body→parent-body by composite_wrt_pbdy.T_parent_this — JEOD mass_calc_composite_inertia.cc:78 (transpose_transform_matrix). For a direct child this rotation is r = T · Sᵀ · T_childᵀ (S = structure_point.t_parent_this, parent-struct→child-struct; T_child = child composite t_parent_this), derived from mass_attach.cc:519.

Three independent checks substantiate correctness:

  1. Exact identity reduction. When every t_parent_this is identity, r = Sᵀ and the offsets stay struct-frame, so the new code is the same expression as the old struct-frame sum (Sᵀ·I·S). Bit-for-bit no-op → the full existing suite (incl. all attach-mass identity RUNs and Apollo's yaw_180+diagonal) passes unchanged.
  2. Direct JEOD cross-validation. RUN_09 compares composite_properties.inertia directly, element-wise, against JEOD's C.M.P. Ib tensor from mass.out for the general optionA rotation.
  3. Frame-invariance through the pipeline. runner_attach_composite_inertia_is_body_frame builds a general-orientation composite, runs the full attach → sync_body_mass_from_tree → integrate path, and checks the pipeline's body-frame composite against an independent derivation S·(struct composite)·Sᵀ, plus a sensitivity guard asserting it is not the unrotated struct composite (so a regression that dropped the rotation fails loudly).

Changes

  • calc_composite_inertia body-frame rewrite (crates/astrodyn_dynamics/src/mass_body.rs).
  • RUN_09 built with a body-frame core (StructCG init rotation), comparing composite_properties.inertia directly against JEOD's C.M.P. Ib tensor.
  • New runner_attach_composite_inertia_is_body_frame analytical test.
  • Catalog MA.25 (composite inertia in the body frame) + // JEOD_INV source tag.
  • RUN_09 regen entry in generate_references.sh + committed attach_mass_09_mass.out.

Verification

tier3_sim_attach_mass (RUN_09 direct), the full dynamics + attach/detach + Apollo trajectory suite, the new analytical test, invariant_coverage, and the bevy↔runner parity wrappers — all green (full local run: 1111 passed). cargo fmt --check + clippy --tests -D warnings + rustdoc -D warnings clean.

Remaining (tracked in #99)

RUN_08/108 (child attached to two parents — topology resolution) and RUN_109 (named-point attach + non-identity root orientation).

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 25, 2026 20:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds JEOD cross-validation coverage for SIM_verif_attach_mass RUN_09, the first attach-mass scenario in this suite where the root parent has a non-identity struct→body orientation, and updates the reference-generation script and committed JEOD mass.out baseline accordingly.

Changes:

  • Add RUN_09 to generate_references.sh so its JEOD mass.out baseline can be regenerated alongside existing attach-mass runs.
  • Extend tier3_sim_attach_mass with build_run_09, parent_option_a(), and a check_body_rotated helper that rotates only the composite inertia into the body frame for comparison.
  • Commit new reference output attach_mass_09_mass.out.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
trick/generate_references.sh Adds RUN_09 to the SIM_verif_attach_mass reference generation group.
crates/astrodyn_verif_jeod/tests/tier3_sim_attach_mass.rs Adds RUN_09 test construction and a rotated-inertia comparison path to match JEOD’s mixed-frame print convention.
crates/astrodyn_verif_jeod/test_data/attach_mass_09_mass.out Adds JEOD baseline output for RUN_09 mass-tree print validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/astrodyn_verif_jeod/tests/tier3_sim_attach_mass.rs Outdated
Test User and others added 3 commits May 25, 2026 23:05
…19/21) (#99)

RUN_09 = parent (StructCG inertia + non-identity parent_mass_orientation_optionA
struct→body) + child1 (Body), offset attach [-1,0,0]. It's the first attach_mass
RUN with a non-identity root struct→body orientation.

Key finding — JEOD's mass print uses a MIXED frame convention for the
composite of an oriented body: the composite CoM (`C.M.P. CM vector`) stays in
the STRUCT frame, but the composite inertia (`C.M.P. Ib tensor`) is in the
BODY frame. Our `recompute_composites` keeps both in the struct frame, and the
two inertias are related by the single rotation `I_body = T_parent_this·I·Tᵀ`
(frame covariance of the composite combination). So no kernel change is needed:
`check_body_rotated` rotates only our composite inertia by `T_parent_this`
before comparing, leaving mass + CoM unrotated. Verified element-wise against
JEOD's `.out` (trace-preserving rotation; all 20 RUNs pass).

Adds `build_run_09`, the `parent_option_a` matrix (verbatim from JEOD
Modified_data), `check_body_rotated`, the RUN_09 regen entry, and the committed
`.out`. Tolerances at the existing JEOD `%20lf` print-precision floor.

Remaining attach_mass gaps (tracked in #99): RUN_08/108 (child attached to two
parents — topology resolution) and RUN_109 (named-point attach + the
non-identity root orientation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…accessor

RUN_09 previously sidestepped the non-identity root struct→body orientation:
it built the parent's mass tree with identity orientation and reconstructed
JEOD's body-frame composite inertia inside a bespoke `check_body_rotated` test
helper. That worked numerically (the rotation is exact) but kept the JEOD
`print_tree` frame convention in test code and never recorded the real
orientation on the body.

Move the conversion into production and make the scenario faithful:

- Add `MassTree::composite_inertia_in_body`, mirroring the struct→body
  rotation JEOD's `MassBody::print_tree` (`mass_print_body.cc`) applies to the
  `C.M.P. Ib tensor` line. A bit-exact no-op for identity-oriented bodies.
- Set the parent's real `t_parent_this` (optionA) in `build_run_09`. The
  struct-frame composite is unaffected (the kernel does not fold the root's own
  orientation into it), so this also exercises that invariant.
- Route `composite_errors` through the accessor, delete `check_body_rotated`,
  and collapse the RUN_09 block to the standard `validate_run` like every
  other run.
- Fix the module docstring count (17 → 18) flagged in review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JEOD stores `core_properties.inertia` in the body frame
(`mass_properties_init.cc:103/119` rotate the StructCG/Struct specs
struct→body at init) and computes the composite in the body frame —
"the core and composite masses share a common body frame"
(`mass_calc_composite_inertia.cc`), with the cm-to-cm offsets rotated by
`composite_properties.T_parent_this` (`mass_update.cc:101/107`). Our
`calc_composite_inertia` accumulated in the struct frame instead, which
is correct only when a body's own struct→body orientation is identity.

The divergence was masked everywhere: the sole non-identity orientation
in the test suite is Apollo's `yaw_180`, which is inertia-invariant on
its diagonal tensors. RUN_09 (the first general, non-180° orientation)
exposed it — and previously papered over it with a test-only rotation of
the struct-frame composite, which only matched because its core inertia
was stored struct-frame (contrary to JEOD).

Rewrite `calc_composite_inertia` to compute in the body frame: body-frame
core unrotated, parallel-axis offsets rotated struct→body by
`t_parent_this`, each child rotated child-body→parent-body
(`r = T·Sᵀ·T_childᵀ`). Reduces exactly to the prior struct-frame code
when every `t_parent_this` is identity, and is bit-identical for Apollo's
yaw_180+diagonal case. No consumer changes: `composite_properties.inertia`
now genuinely holds the body-frame tensor that `sync_body_mass_from_tree`
and the rotational integrator already expect.

- RUN_09 rebuilt with a body-frame core (the StructCG init rotation) and
  compares `composite_properties.inertia` directly against JEOD's
  `C.M.P. Ib tensor`; the test-only rotation accessor is removed.
- New `runner_attach_composite_inertia_is_body_frame` drives a general
  orientation through the full `attach → sync_body_mass_from_tree →
  integrate` pipeline, cross-checking the body-frame composite via an
  independent frame-invariance derivation (+ sensitivity guard + a
  torque-free propagation smoke).
- Catalog MA.25 (composite inertia in the body frame) + `// JEOD_INV`
  source tag.

Verified: tier3_sim_attach_mass (RUN_09 direct), full dynamics +
attach/detach + Apollo trajectory suite (365 tests), the new analytical
test, invariant_coverage, and the mass/attach/detach/Apollo/dyncomp
bevy↔runner parity wrappers (109 tests) — all green; fmt/clippy/rustdoc
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@simnaut simnaut force-pushed the attach-mass-run09 branch from 5612584 to 48fd667 Compare May 26, 2026 06:06
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