Skip to content

Commit 412415b

Browse files
authored
Phase 3 — fixture and pipeline hygiene (#10)
Closes C-2, L-1, L-2, L-3, L-5 from FINAL_CLEANUP.md. Verifies M-7 is already complete from Phase 2. - C-2 (pipeline/mod.rs): replace the four opaque FAR_FIELD_* ECI Cartesian constants in the far-field test fixture with a Keplerian derivation — iss_like_elements() + 50 km SMA offset + 0.2 rad mean anomaly offset. δa/a ≈ 7.4e-3 crosses the default roe_threshold of 5e-3, classifying the pair as FarField. δλ is deliberately excluded from dimensionless_norm per Koenig Sec. V, so a pure in-track offset would always classify as Proximity regardless of separation — the SMA bump is the lightest physical component that crosses the threshold. Mirrors the existing proximity_input() idiom. - L-1 (validation/trajectory.rs:249): pre-size eclipse_samples to chief_results.len() when earth_frame.is_some(); leave it as Vec::new() when eclipse validation is disabled so the disabled path stays zero-allocation. - L-2 (validation/trajectory.rs:1071): flip f64::from(u32::try_from(5 + j).unwrap()) to f64::from(5_u32 + u32::try_from(j).unwrap()) so the arithmetic stays in u32 rather than casting a usize sum. - L-3 (validation/statistics.rs): drop four narrative comments that restated what the code does — mechanical "compare candidates" at the binary search, a three-line test-value table, two "leg excluded" / "no input" test narrations. Keep the load-bearing comments (time- sorted input invariant, sum/sum-sq recovery math, +0.0 bit-pattern empty-input contract, all-zero summaries contract). - L-5 (validation/trajectory.rs:1833, 1945): normalize two #[ignore] strings from capital-R "Requires MetaAlmanac" to lowercase-r "requires MetaAlmanac" so all 25 MetaAlmanac ignore sites use the same canonical string. SPICE-kernel-specific strings in nyx_bridge/almanac.rs describe a different prerequisite and are intentionally left alone. - M-7: verified as already complete — Phase 2 named both 709.0 call sites as const MID_COAST_BURN_ELAPSED_S. Verification: 586 passed / 0 failed / 0 ignored across the workspace with --include-ignored (unchanged from Phase 2 baseline). Clippy, docs, and wasm-pack build all clean. rpo-cli mission/validate/mc JSON outputs are byte-identical to the pre-Phase-3 baseline (modulo non-deterministic elapsed_wall_s in MC).
1 parent 5c47988 commit 412415b

3 files changed

Lines changed: 36 additions & 40 deletions

File tree

rpo-nyx/src/pipeline/mod.rs

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -213,40 +213,39 @@ mod tests {
213213
/// cascade will exceed it.
214214
const CASCADE_DV_CHANGE_TOL_KM_S: f64 = 1e-10;
215215

216-
// --- Far-field test fixture state vectors (ECI) -----------------------
217-
// ISS-like orbit, chief and deputy separated by ~1600 km so the
218-
// classification path exits the proximity regime and exercises the
219-
// full Lambert solve + propagation pipeline.
220-
221-
/// Far-field fixture: chief position in ECI (km).
222-
const FAR_FIELD_CHIEF_POSITION_ECI_KM: [f64; 3] = [5876.261, 3392.661, 0.0];
223-
224-
/// Far-field fixture: chief velocity in ECI (km/s).
225-
const FAR_FIELD_CHIEF_VELOCITY_ECI_KM_S: [f64; 3] = [-2.380_512, 4.123_167, 6.006_917];
226-
227-
/// Far-field fixture: deputy position in ECI (km).
228-
const FAR_FIELD_DEPUTY_POSITION_ECI_KM: [f64; 3] =
229-
[5_199.839_421, 4_281.648_523, 1_398.070_066];
230-
231-
/// Far-field fixture: deputy velocity in ECI (km/s).
232-
const FAR_FIELD_DEPUTY_VELOCITY_ECI_KM_S: [f64; 3] = [-3.993_103, 2.970_313, 5.764_540];
233-
234216
/// Build a minimal `PipelineInput` from the standard far-field test scenario.
217+
///
218+
/// Chief is an ISS-like orbit; deputy is offset by `FAR_FIELD_SMA_OFFSET_KM`
219+
/// in semi-major axis and `FAR_FIELD_PHASE_OFFSET_RAD` in mean anomaly so
220+
/// that `dimensionless_norm(roe) = δa/a ≈ 7.4e-3` crosses the default
221+
/// `ProximityConfig::roe_threshold` of 5e-3, classifying the pair as
222+
/// `FarField` and exercising the full Lambert solve + propagation pipeline.
223+
///
224+
/// Note: δλ (along-track phase) is deliberately excluded from
225+
/// `dimensionless_norm` per Koenig Sec. V, so a pure in-track offset would
226+
/// always classify as Proximity no matter how large. δa is the lightest
227+
/// component that crosses the threshold with a physically sensible value.
235228
fn far_field_input() -> PipelineInput {
236229
use hifitime::Epoch;
237-
use nalgebra::Vector3;
230+
use rpo_core::elements::keplerian_conversions::keplerian_to_state;
231+
use rpo_core::test_helpers::iss_like_elements;
238232

239-
let chief = StateVector {
240-
epoch: Epoch::from_gregorian_str("2024-01-01T00:00:00 UTC").unwrap(),
241-
position_eci_km: Vector3::from(FAR_FIELD_CHIEF_POSITION_ECI_KM),
242-
velocity_eci_km_s: Vector3::from(FAR_FIELD_CHIEF_VELOCITY_ECI_KM_S),
243-
};
233+
// Altitude offset producing δa ≈ 7.4e-3 for an ISS-like chief (a ≈ 6778 km).
234+
// Comfortably above the default 5e-3 FarField threshold without entering
235+
// a regime where Lambert needs multi-rev handling.
236+
const FAR_FIELD_SMA_OFFSET_KM: f64 = 50.0;
237+
// In-track phase shift giving the deputy a physically interpretable
238+
// separation from the chief at t = 0. ~0.2 rad ≈ 1.4 Mm along-track.
239+
const FAR_FIELD_PHASE_OFFSET_RAD: f64 = 0.2;
244240

245-
let deputy = StateVector {
246-
epoch: Epoch::from_gregorian_str("2024-01-01T00:00:00 UTC").unwrap(),
247-
position_eci_km: Vector3::from(FAR_FIELD_DEPUTY_POSITION_ECI_KM),
248-
velocity_eci_km_s: Vector3::from(FAR_FIELD_DEPUTY_VELOCITY_ECI_KM_S),
249-
};
241+
let epoch = Epoch::from_gregorian_str("2024-01-01T00:00:00 UTC").unwrap();
242+
let chief_ke = iss_like_elements();
243+
let mut deputy_ke = chief_ke;
244+
deputy_ke.a_km += FAR_FIELD_SMA_OFFSET_KM;
245+
deputy_ke.mean_anomaly_rad += FAR_FIELD_PHASE_OFFSET_RAD;
246+
247+
let chief = keplerian_to_state(&chief_ke, epoch).unwrap();
248+
let deputy = keplerian_to_state(&deputy_ke, epoch).unwrap();
250249

251250
PipelineInput {
252251
chief,

rpo-nyx/src/validation/statistics.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ pub(super) fn find_closest_analytical_ric(trajectory: &[PropagatedState], elapse
2424
// Trajectory is time-sorted by construction; binary search for insertion point
2525
let idx = trajectory.partition_point(|s| s.elapsed_s < elapsed_s);
2626

27-
// Compare the two candidates bracketing the insertion point
2827
let best = if idx == 0 {
2928
&trajectory[0]
3029
} else if idx >= trajectory.len() {
@@ -249,10 +248,6 @@ mod tests {
249248
}
250249

251250
/// Verify `compute_leg_summaries` with mixed pre/post-COLA points across legs.
252-
///
253-
/// Leg 0: errors [1.0, 3.0, 2.0], no post-COLA → max=3.0, mean=2.0, rms=sqrt(14/3)
254-
/// Leg 1: errors [1.0(pre), 2.0(pre), 10.0(post)] → max=2.0, mean=1.5, 1 excluded
255-
/// Leg 2: all post-COLA → `num_points=0`, 2 excluded
256251
#[test]
257252
fn leg_summaries_mixed_cola() {
258253
let make = |pos_err: f64, vel_err: f64, post_cola: bool| ValidationPoint {
@@ -297,7 +292,6 @@ mod tests {
297292
assert_eq!(summaries[1].num_points, 2);
298293
assert_eq!(summaries[1].num_post_cola_excluded, 1);
299294

300-
// Leg 2: all excluded — max starts at +0.0 and never updates.
301295
assert_eq!(summaries[2].max_position_error_km.to_bits(), 0_u64);
302296
assert_eq!(summaries[2].num_points, 0);
303297
assert_eq!(summaries[2].num_post_cola_excluded, 2);
@@ -317,7 +311,6 @@ mod tests {
317311
assert_eq!(summaries.len(), 1);
318312
assert_eq!(summaries[0].num_points, 0);
319313
assert_eq!(summaries[0].num_post_cola_excluded, 0);
320-
// No input means no update to `max_position_error_km`; stays at +0.0.
321314
assert_eq!(summaries[0].max_position_error_km.to_bits(), 0_u64);
322315
}
323316
}

rpo-nyx/src/validation/trajectory.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,11 @@ fn build_leg_comparison_points(
246246
) -> Result<LegComparisonOutput, ValidationError> {
247247
let mut points = Vec::with_capacity(chief_results.len());
248248
let mut safety_pairs = Vec::with_capacity(chief_results.len());
249-
let mut eclipse_samples = Vec::new();
249+
let mut eclipse_samples = if earth_frame.is_some() {
250+
Vec::with_capacity(chief_results.len())
251+
} else {
252+
Vec::new()
253+
};
250254

251255
for (idx, (chief_sample, deputy_sample)) in
252256
chief_results.iter().zip(deputy_results.iter()).enumerate()
@@ -1068,7 +1072,7 @@ mod tests {
10681072
let post_cola_distances = [3.0, 1.5, 0.8, 2.0, 4.0];
10691073
for (j, &d) in post_cola_distances.iter().enumerate() {
10701074
deputy.push(make_timed_state(
1071-
f64::from(u32::try_from(5 + j).unwrap()) * 100.0,
1075+
f64::from(5_u32 + u32::try_from(j).unwrap()) * 100.0,
10721076
Vector3::new(d, 0.0, 0.0),
10731077
));
10741078
}
@@ -1826,7 +1830,7 @@ mod tests {
18261830
/// guarantees pre-COLA and post-COLA paths see the same sampling grid and
18271831
/// the same physical trajectory in the pre-burn window.
18281832
#[test]
1829-
#[ignore = "Requires MetaAlmanac (network on first run)"]
1833+
#[ignore = "requires MetaAlmanac (network on first run)"]
18301834
fn propagate_leg_segment_1_is_impulse_independent() {
18311835
use test_scenario::{
18321836
iss_formation_roe, leg_propagation_ctx_from_scenario,
@@ -1938,7 +1942,7 @@ mod tests {
19381942
/// the sampling artifact where post-COLA looked worse than pre-COLA in
19391943
/// the validate.md Safety Comparison table.
19401944
#[test]
1941-
#[ignore = "Requires MetaAlmanac (network on first run)"]
1945+
#[ignore = "requires MetaAlmanac (network on first run)"]
19421946
fn pre_cola_and_post_cola_pre_burn_window_match_within_sampling_tolerance() {
19431947
use rpo_core::mission::config::MissionConfig;
19441948
use rpo_core::mission::types::Waypoint;

0 commit comments

Comments
 (0)