diff --git a/crates/astrodyn_verif_jeod/src/bin/extract_body_init.rs b/crates/astrodyn_verif_jeod/src/bin/extract_body_init.rs index 132f9f4e..08bf9f38 100644 --- a/crates/astrodyn_verif_jeod/src/bin/extract_body_init.rs +++ b/crates/astrodyn_verif_jeod/src/bin/extract_body_init.rs @@ -73,6 +73,7 @@ const SCENARIOS: &[Scenario] = &[ reference_inertial: true, orbit_inits: &[ "trans_Orbit_inertial_body_set01", + "trans_Orbit_inertial_body_set02", "trans_Orbit_pfix_body_set01", ], trans_states: &["trans_TransState_inertial_body"], diff --git a/crates/astrodyn_verif_jeod/src/run_verification/sim_orbinit_docker.rs b/crates/astrodyn_verif_jeod/src/run_verification/sim_orbinit_docker.rs index 837bfd70..b2962e05 100644 --- a/crates/astrodyn_verif_jeod/src/run_verification/sim_orbinit_docker.rs +++ b/crates/astrodyn_verif_jeod/src/run_verification/sim_orbinit_docker.rs @@ -13,13 +13,19 @@ //! parity wrapper drives bit-identity at the same propagation depth as //! the matching tier3 runner test. //! -//! Five recipes correspond to JEOD's RUN list: +//! The recipes correspond to JEOD's RUN list: //! * RUN_0001 — ISS orbital elements (set01) in `Earth.inertial`; +//! * RUN_0002 — ISS orbital elements (set02, mean anomaly) in `Earth.inertial`; //! * RUN_0101 — STS-114 orbital elements (set01) in `Earth.inertial`; +//! * RUN_0102 — STS-114 orbital elements (set02, mean anomaly) in `Earth.inertial`; //! * RUN_0201 — ISS orbital elements (set01) in `Earth.pfix`; //! * RUN_0301 — STS-114 orbital elements (set01) in `Earth.pfix`; //! * RUN_0401 — STS-114 direct Cartesian state in `Earth.inertial`. //! +//! Both parameterizations resolve to [`init_from_mean_anomaly`]; set01 +//! derives `M = t_peri·√(μ/a³)` from the fixture's `time_periapsis`, while +//! set02 reads the fixture's `mean_anomaly` field directly. +//! //! The orbital-element-to-Cartesian conversion is the substance of this //! test; it runs inside every scenario factory rather than being //! pre-baked, so the parity wrapper exercises `init_from_mean_anomaly` @@ -154,6 +160,35 @@ fn orbital_element_state(vehicle: &str, init_name: &str, mu_earth: f64) -> Trans } } +/// Materialize a JEOD set02 (`SmaEccIncAscnodeArgperMeanAnomaly`) fixture +/// into an inertial-frame translational state. Unlike set01, the mean +/// anomaly is supplied directly by the deck (fixture field `mean_anomaly`, +/// stored in radians by `extract_body_init`), so this is exactly +/// [`init_from_mean_anomaly`] with no time-periapsis derivation. The set02 +/// decks are `Earth.inertial` only. +fn mean_anomaly_element_state(vehicle: &str, init_name: &str, mu_earth: f64) -> TranslationalState { + let init = load_orbital_init(vehicle, init_name); + let mean_anomaly = init.mean_anomaly.unwrap_or_else(|| { + panic!("{vehicle}/{init_name}: set02 expected mean_anomaly in the fixture") + }); + assert_eq!( + init.reference_frame.as_str(), + "Earth.inertial", + "{vehicle}/{init_name}: set02 recipe only supports Earth.inertial frames, \ + got '{}' — add frame handling if a pfix set02 RUN is introduced", + init.reference_frame, + ); + init_from_mean_anomaly( + init.semi_major_axis, + init.eccentricity, + init.inclination, + init.ascending_node, + init.arg_periapsis, + mean_anomaly, + mu_earth, + ) +} + /// Materialize a JEOD direct-Cartesian fixture (RUN_0401 only) into an /// inertial-frame translational state. The fixture is a pass-through: /// `position`/`velocity` arrays are taken verbatim. @@ -244,6 +279,24 @@ fn build_run_0401(_init: &InitialConditions) -> SimulationBuilder { build_orbinit_docker(mu, state) } +/// RUN_0002: ISS set02 (mean-anomaly) elements from the committed +/// `iss.json` fixture (`trans_Orbit_inertial_body_set02`), in +/// `Earth.inertial`. +fn build_run_0002(_init: &InitialConditions) -> SimulationBuilder { + let mu = load_mu_earth(); + let state = mean_anomaly_element_state("ISS", "trans_Orbit_inertial_body_set02", mu); + build_orbinit_docker(mu, state) +} + +/// RUN_0102: STS-114 set02 (mean-anomaly) elements from the committed +/// `sts_114.json` fixture (`trans_Orbit_inertial_body_set02`), in +/// `Earth.inertial`. +fn build_run_0102(_init: &InitialConditions) -> SimulationBuilder { + let mu = load_mu_earth(); + let state = mean_anomaly_element_state("STS_114", "trans_Orbit_inertial_body_set02", mu); + build_orbinit_docker(mu, state) +} + /// RUN_0001: ISS orbital elements (set01) in `Earth.inertial`. pub fn run_0001() -> VerificationCase { VerificationCase { @@ -260,6 +313,38 @@ pub fn run_0001() -> VerificationCase { } } +/// RUN_0002: ISS orbital elements (set02, mean-anomaly) in `Earth.inertial`. +pub fn run_0002() -> VerificationCase { + VerificationCase { + name: "tier3_orbinit_docker_run_0002", + scenario: build_run_0002, + reference: CsvReference::SyntheticTimes { + dt: DT_S, + num_steps: NUM_STEPS, + }, + duration: Time::new::(0.0), + tolerances: synthetic_tolerances(), + extras: None, + pre_step: None, + } +} + +/// RUN_0102: STS-114 orbital elements (set02, mean-anomaly) in `Earth.inertial`. +pub fn run_0102() -> VerificationCase { + VerificationCase { + name: "tier3_orbinit_docker_run_0102", + scenario: build_run_0102, + reference: CsvReference::SyntheticTimes { + dt: DT_S, + num_steps: NUM_STEPS, + }, + duration: Time::new::(0.0), + tolerances: synthetic_tolerances(), + extras: None, + pre_step: None, + } +} + /// RUN_0101: STS-114 orbital elements (set01) in `Earth.inertial`. pub fn run_0101() -> VerificationCase { VerificationCase { diff --git a/crates/astrodyn_verif_jeod/test_data/body_init/iss.json b/crates/astrodyn_verif_jeod/test_data/body_init/iss.json index f6b8b44b..a96923e0 100644 --- a/crates/astrodyn_verif_jeod/test_data/body_init/iss.json +++ b/crates/astrodyn_verif_jeod/test_data/body_init/iss.json @@ -4,7 +4,7 @@ "source": "models/dynamics/body_action/verif/SIM_orbinit/Modified_data/ISS/", "jeod_version": "5.4", "jeod_commit": "27893108bbde8bb162b3213a30d57af89acd1c76", - "generated_utc": "unknown (pre-#503 baseline)", + "generated_utc": "2026-05-25T07:10:54Z", "note": "Body initialization vectors. Regenerate with: cargo run -p astrodyn_verif_jeod --bin extract_body_init -- --jeod-home $JEOD_HOME", "reference_inertial": {"position": [1244540.53, 5655938.85, 3425643.22], "velocity": [-6003.833051, -1469.496044, 4590.511776]}, "orbital_inits": [ @@ -13,8 +13,8 @@ "semi_major_axis": 6732901.20152, "eccentricity": 0.0012907335, "inclination": 0.9018194918388728, - "ascending_node": 0.8675755493238396, - "arg_periapsis": 1.755494852217414, + "ascending_node": 0.8675755493238397, + "arg_periapsis": 1.7554948522174143, "time_periapsis": 4581.96167293, "mean_anomaly": null, "true_anomaly": null, @@ -26,8 +26,8 @@ "semi_major_axis": 6732901.20152, "eccentricity": 0.0012907335, "inclination": 0.9018194918388728, - "ascending_node": 0.8675755493238396, - "arg_periapsis": 1.755494852217414, + "ascending_node": 0.8675755493238397, + "arg_periapsis": 1.7554948522174143, "time_periapsis": null, "mean_anomaly": 5.236209017533277, "true_anomaly": null, @@ -39,8 +39,8 @@ "semi_major_axis": 6732901.20152, "eccentricity": 0.0012907335, "inclination": 0.9018194918388728, - "ascending_node": 0.8675755493238396, - "arg_periapsis": 1.755494852217414, + "ascending_node": 0.8675755493238397, + "arg_periapsis": 1.7554948522174143, "time_periapsis": null, "mean_anomaly": null, "true_anomaly": 5.2339718836974285, @@ -51,7 +51,7 @@ "name": "trans_Orbit_pfix_body_set01", "semi_major_axis": 6732901.205250001, "eccentricity": 0.00129073426, - "inclination": 0.9014390876767299, + "inclination": 0.90143908767673, "ascending_node": 5.429530680701693, "arg_periapsis": 1.7559744228809693, "time_periapsis": 4581.96222432, diff --git a/crates/astrodyn_verif_jeod/test_data/body_init/sts_114.json b/crates/astrodyn_verif_jeod/test_data/body_init/sts_114.json index 09c46de4..28f8336a 100644 --- a/crates/astrodyn_verif_jeod/test_data/body_init/sts_114.json +++ b/crates/astrodyn_verif_jeod/test_data/body_init/sts_114.json @@ -4,7 +4,7 @@ "source": "models/dynamics/body_action/verif/SIM_orbinit/Modified_data/STS_114/", "jeod_version": "5.4", "jeod_commit": "27893108bbde8bb162b3213a30d57af89acd1c76", - "generated_utc": "unknown (pre-#503 baseline)", + "generated_utc": "2026-05-25T07:10:54Z", "note": "Body initialization vectors. Regenerate with: cargo run -p astrodyn_verif_jeod --bin extract_body_init -- --jeod-home $JEOD_HOME", "reference_inertial": {"position": [1244471.94, 5655811.8, 3425518.88], "velocity": [-6003.553468, -1469.321965, 4590.57723]}, "orbital_inits": [ @@ -13,7 +13,7 @@ "semi_major_axis": 6732163.59764, "eccentricity": 0.00122446354, "inclination": 0.9018261209658911, - "ascending_node": 0.8675961322535078, + "ascending_node": 0.867596132253508, "arg_periapsis": 1.801291139680453, "time_periapsis": 4541.07695545, "mean_anomaly": null, @@ -21,12 +21,25 @@ "planet_name": "Earth", "reference_frame": "Earth.inertial" }, + { + "name": "trans_Orbit_inertial_body_set02", + "semi_major_axis": 6732163.59764, + "eccentricity": 0.00122446354, + "inclination": 0.9018261209658911, + "ascending_node": 0.867596132253508, + "arg_periapsis": 1.801291139680453, + "time_periapsis": null, + "mean_anomaly": 5.19033936505041, + "true_anomaly": null, + "planet_name": "Earth", + "reference_frame": "Earth.inertial" + }, { "name": "trans_Orbit_pfix_body_set01", "semi_major_axis": 6732163.61166, "eccentricity": 0.00122446412, - "inclination": 0.9014457086530604, - "ascending_node": 5.429551274731655, + "inclination": 0.9014457086530605, + "ascending_node": 5.429551274731656, "arg_periapsis": 1.8017698046402992, "time_periapsis": 4541.07829767, "mean_anomaly": null, diff --git a/crates/astrodyn_verif_jeod/test_data/orbinit_0002_orbinit.csv b/crates/astrodyn_verif_jeod/test_data/orbinit_0002_orbinit.csv new file mode 100644 index 00000000..6dcaeb66 --- /dev/null +++ b/crates/astrodyn_verif_jeod/test_data/orbinit_0002_orbinit.csv @@ -0,0 +1,2 @@ +sys.exec.out.time {s},target.dyn_body.composite_body.state.trans.position[0] {m},target.dyn_body.composite_body.state.trans.position[1] {m},target.dyn_body.composite_body.state.trans.position[2] {m},target.dyn_body.composite_body.state.trans.velocity[0] {m/s},target.dyn_body.composite_body.state.trans.velocity[1] {m/s},target.dyn_body.composite_body.state.trans.velocity[2] {m/s} + 0, 1244540.530006297, 5655938.850008326, 3425643.219975276, -6003.833092393274, -1469.496054158121, 4590.511807712143 diff --git a/crates/astrodyn_verif_jeod/test_data/orbinit_0102_orbinit.csv b/crates/astrodyn_verif_jeod/test_data/orbinit_0102_orbinit.csv new file mode 100644 index 00000000..f17d68f5 --- /dev/null +++ b/crates/astrodyn_verif_jeod/test_data/orbinit_0102_orbinit.csv @@ -0,0 +1,2 @@ +sys.exec.out.time {s},target.dyn_body.composite_body.state.trans.position[0] {m},target.dyn_body.composite_body.state.trans.position[1] {m},target.dyn_body.composite_body.state.trans.position[2] {m},target.dyn_body.composite_body.state.trans.velocity[0] {m/s},target.dyn_body.composite_body.state.trans.velocity[1] {m/s},target.dyn_body.composite_body.state.trans.velocity[2] {m/s} + 0, 1244471.939980413, 5655811.799992709, 3425518.880036598, -6003.553509436046, -1469.321975147157, 4590.577261632054 diff --git a/crates/astrodyn_verif_jeod/tests/tier3_sim_orbinit_docker.rs b/crates/astrodyn_verif_jeod/tests/tier3_sim_orbinit_docker.rs index cf3a8b4e..74354186 100644 --- a/crates/astrodyn_verif_jeod/tests/tier3_sim_orbinit_docker.rs +++ b/crates/astrodyn_verif_jeod/tests/tier3_sim_orbinit_docker.rs @@ -165,13 +165,13 @@ fn assert_orbinit_match( fn tier3_orbinit_docker_run0001_iss_inertial() { // RUN_0001: ISS, SmaEccIncAscnodeArgperTimeperi, reference=Earth.inertial. // No frame rotation required — recipe output is already in inertial. - // Observed: pos=3.76e-9 m, vel=3.43e-12 m/s (5% above → listed). + // Observed: pos=6.25e-9 m, vel=6.19e-12 m/s (5% above → listed). assert_orbinit_match( sim_orbinit_docker::run_0001(), "orbinit_0001_orbinit.csv", "RUN_0001 (ISS inertial set01)", - 3.95e-9, - 3.61e-12, + 6.56e-9, + 6.50e-12, ); } @@ -181,13 +181,43 @@ fn tier3_orbinit_docker_run0001_iss_inertial() { #[test] fn tier3_orbinit_docker_run0101_sts_inertial() { - // Observed: pos=1.04e-9 m, vel=1.83e-12 m/s (5% above → listed). + // Observed: pos=1.04e-9 m, vel=2.27e-13 m/s (5% above → listed). assert_orbinit_match( sim_orbinit_docker::run_0101(), "orbinit_0101_orbinit.csv", "RUN_0101 (STS-114 inertial set01)", 1.10e-9, - 1.93e-12, + 2.39e-13, + ); +} + +// ─────────────────────────────────────────────────────────────────────────── +// RUN_0002 / RUN_0102: set02 (mean-anomaly parameterization), inertial frame. +// Exercises `init_from_mean_anomaly` directly (distinct from set01's +// time-periapsis → mean-anomaly derivation). Tolerances are 1.05× observed. +// ─────────────────────────────────────────────────────────────────────────── + +#[test] +fn tier3_orbinit_docker_run0002_iss_inertial() { + // Observed: pos=3.26e-9 m, vel=3.40e-12 m/s (5% above → listed). + assert_orbinit_match( + sim_orbinit_docker::run_0002(), + "orbinit_0002_orbinit.csv", + "RUN_0002 (ISS inertial set02, mean anomaly)", + 3.42e-9, + 3.57e-12, + ); +} + +#[test] +fn tier3_orbinit_docker_run0102_sts_inertial() { + // Observed: pos=1.68e-9 m, vel=2.33e-12 m/s (5% above → listed). + assert_orbinit_match( + sim_orbinit_docker::run_0102(), + "orbinit_0102_orbinit.csv", + "RUN_0102 (STS-114 inertial set02, mean anomaly)", + 1.76e-9, + 2.45e-12, ); } diff --git a/crates/astrodyn_verif_jeod_fixtures/src/orbital_init.rs b/crates/astrodyn_verif_jeod_fixtures/src/orbital_init.rs index a8d688e7..d7e41e11 100644 --- a/crates/astrodyn_verif_jeod_fixtures/src/orbital_init.rs +++ b/crates/astrodyn_verif_jeod_fixtures/src/orbital_init.rs @@ -18,7 +18,6 @@ //! binary; runtime test paths never call them. use regex::Regex; -use std::f64::consts::PI; use crate::body_init_fixtures::{ load_vehicle_bundle, BodyInitFixtureError, OrbitalInitRecord, TransStateRecord, @@ -33,7 +32,7 @@ use crate::body_init_fixtures::{ /// - `key = value` (bare numeric) /// /// Unit conversions applied automatically: -/// - `"degree"` -> radians (multiply by PI/180) +/// - `"degree"` -> radians (via `f64::to_radians`) /// - `"km"` -> meters (multiply by 1000) /// - `"s"` -> seconds (no conversion) #[derive(Debug, Clone)] @@ -171,12 +170,12 @@ pub fn parse_orbital_init_py(content: &str) -> Result semi_major_axis = Some(val * 1000.0), // assume km "eccentricity" => eccentricity = Some(val), - "inclination" => inclination = Some(val * PI / 180.0), // assume degrees - "ascending_node" => ascending_node = Some(val * PI / 180.0), - "arg_periapsis" => arg_periapsis = Some(val * PI / 180.0), + "inclination" => inclination = Some(val.to_radians()), // assume degrees + "ascending_node" => ascending_node = Some(val.to_radians()), + "arg_periapsis" => arg_periapsis = Some(val.to_radians()), "time_periapsis" => time_periapsis = Some(val), - "mean_anomaly" => mean_anomaly = Some(val * PI / 180.0), - "true_anomaly" => true_anomaly = Some(val * PI / 180.0), + "mean_anomaly" => mean_anomaly = Some(val.to_radians()), + "true_anomaly" => true_anomaly = Some(val.to_radians()), _ => {} } continue; @@ -218,7 +217,7 @@ pub fn parse_orbital_init_py(content: &str) -> Result Result { match unit { - "degree" => Ok(val * PI / 180.0), + "degree" => Ok(val.to_radians()), "km" => Ok(val * 1000.0), "s" => Ok(val), "m" => Ok(val), @@ -372,8 +371,7 @@ vehicle.set01.subject.orbit_frame_name = "Earth.inertial" let rec = parse_orbital_init_py(py).unwrap(); assert!((rec.semi_major_axis - 6_732_901.201_52).abs() < 1e-6); assert!((rec.eccentricity - 0.00129073350).abs() < 1e-12); - let deg2rad = PI / 180.0; - assert!((rec.inclination - 51.670450765 * deg2rad).abs() < 1e-12); + assert!((rec.inclination - 51.670450765_f64.to_radians()).abs() < 1e-12); assert_eq!(rec.planet_name, "Earth"); assert_eq!(rec.reference_frame, "Earth.inertial"); assert!((rec.time_periapsis.unwrap() - 4581.96167293).abs() < 1e-9); diff --git a/crates/astrodyn_verif_parity/tests/bevy_parity_orbinit_docker.rs b/crates/astrodyn_verif_parity/tests/bevy_parity_orbinit_docker.rs index 243f12b7..c663fbab 100644 --- a/crates/astrodyn_verif_parity/tests/bevy_parity_orbinit_docker.rs +++ b/crates/astrodyn_verif_parity/tests/bevy_parity_orbinit_docker.rs @@ -28,6 +28,16 @@ fn bevy_parity_orbinit_docker_run_0101() { sim_orbinit_docker::run_0101().run_and_assert_parity::(); } +#[test] +fn bevy_parity_orbinit_docker_run_0002() { + sim_orbinit_docker::run_0002().run_and_assert_parity::(); +} + +#[test] +fn bevy_parity_orbinit_docker_run_0102() { + sim_orbinit_docker::run_0102().run_and_assert_parity::(); +} + #[test] fn bevy_parity_orbinit_docker_run_0201() { sim_orbinit_docker::run_0201().run_and_assert_parity::(); diff --git a/trick/generate_references.sh b/trick/generate_references.sh index 2eb96234..e4ec9c09 100755 --- a/trick/generate_references.sh +++ b/trick/generate_references.sh @@ -696,6 +696,9 @@ run_orbinit_group() { "SET_test/RUN_0201:orbinit_0201:orbinit_0201_orbinit.csv" "SET_test/RUN_0301:orbinit_0301:orbinit_0301_orbinit.csv" "SET_test/RUN_0401:orbinit_0401:orbinit_0401_orbinit.csv" + # set02 mean-anomaly parameterization (ISS + STS, inertial) + "SET_test/RUN_0002:orbinit_0002:orbinit_0002_orbinit.csv" + "SET_test/RUN_0102:orbinit_0102:orbinit_0102_orbinit.csv" ) local needs_build=0 for entry in "${RUNS[@]}"; do