Skip to content

Commit ee25423

Browse files
authored
Merge pull request #234 from jbrodovsky/copilot/fix-rbpf-length-mismatch
Fix filter output length mismatch: store initial state before event processing
2 parents 4d9afc4 + f0cee7c commit ee25423

3 files changed

Lines changed: 160 additions & 31 deletions

File tree

core/src/sim.rs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,14 +1914,20 @@ pub fn run_closed_loop<F: NavigationFilter>(
19141914
let start_time = stream.start_time;
19151915
let mut results: Vec<NavigationResult> = Vec::with_capacity(stream.events.len());
19161916
let total = stream.events.len();
1917-
let mut last_ts: Option<DateTime<Utc>> = None;
19181917
let mut monitor = HealthMonitor::new(health_limits.unwrap_or_default());
19191918

19201919
info!(
19211920
"Starting closed-loop navigation filter with {} events",
19221921
total
19231922
);
19241923

1924+
// Store the initial state (before processing any events)
1925+
let mean = filter.get_estimate();
1926+
let cov = filter.get_certainty();
1927+
results.push(NavigationResult::from((&start_time, &mean, &cov)));
1928+
debug!("Initial filter state at {}: {:?}", start_time, mean);
1929+
let mut last_ts = start_time;
1930+
19251931
for (i, event) in stream.events.into_iter().enumerate() {
19261932
// Print detailed progress every 100 iterations or at key milestones
19271933
if i % 10 == 0 || i == total {
@@ -1987,23 +1993,13 @@ pub fn run_closed_loop<F: NavigationFilter>(
19871993
}
19881994
}
19891995

1990-
// If timestamp changed, or it's the last event, record the previous state
1991-
if Some(ts) != last_ts {
1992-
if let Some(prev_ts) = last_ts {
1993-
let mean = filter.get_estimate();
1994-
let cov = filter.get_certainty();
1995-
results.push(NavigationResult::from((&prev_ts, &mean, &cov)));
1996-
debug!("Filter state at {}: {:?}", ts, mean);
1997-
}
1998-
last_ts = Some(ts);
1999-
}
2000-
2001-
// If this is the last event, also push
2002-
if i == total - 1 {
1996+
// Store state when timestamp changes
1997+
if ts != last_ts {
20031998
let mean = filter.get_estimate();
20041999
let cov = filter.get_certainty();
2005-
debug!("Filter state at {}: {:?}", ts, mean);
20062000
results.push(NavigationResult::from((&ts, &mean, &cov)));
2001+
debug!("Filter state at {}: {:?}", ts, mean);
2002+
last_ts = ts;
20072003
}
20082004
}
20092005
debug!("Closed-loop simulation complete");

core/tests/integration_tests.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,3 +1918,126 @@ fn test_rbpf_with_degraded_gnss() {
19181918
assert!(result.altitude.is_finite());
19191919
}
19201920
}
1921+
1922+
/// Test that filter output length matches input data length
1923+
///
1924+
/// This test verifies that all filter implementations (UKF, EKF, ESKF, dead reckoning)
1925+
/// produce output with the same number of records as the input data. This is critical for
1926+
/// downstream analysis tools that expect aligned data streams.
1927+
#[test]
1928+
fn test_filter_output_length_matches_input() {
1929+
let manifest_dir = env!("CARGO_MANIFEST_DIR");
1930+
let test_data_path = Path::new(manifest_dir).join("tests/test_data.csv");
1931+
let records = load_test_data(&test_data_path);
1932+
1933+
assert!(
1934+
!records.is_empty(),
1935+
"Test data should contain at least one record"
1936+
);
1937+
1938+
let input_length = records.len();
1939+
println!("Testing with {} input records", input_length);
1940+
1941+
// Test dead reckoning
1942+
let dr_results = dead_reckoning(&records);
1943+
assert_eq!(
1944+
dr_results.len(),
1945+
input_length,
1946+
"Dead reckoning output length {} should match input length {}",
1947+
dr_results.len(),
1948+
input_length
1949+
);
1950+
println!(
1951+
"✓ Dead reckoning: {} outputs for {} inputs",
1952+
dr_results.len(),
1953+
input_length
1954+
);
1955+
1956+
// Create initial state from first record
1957+
let initial_state = create_initial_state(&records[0]);
1958+
let imu_biases = vec![0.0; 6]; // Zero initial bias estimates
1959+
let initial_covariance = DEFAULT_INITIAL_COVARIANCE.to_vec();
1960+
let process_noise = DMatrix::from_diagonal(&DVector::from_vec(DEFAULT_PROCESS_NOISE.to_vec()));
1961+
let degradation = GnssDegradationConfig::default();
1962+
1963+
// Test UKF
1964+
let mut ukf = UnscentedKalmanFilter::new(
1965+
initial_state.clone(),
1966+
imu_biases.clone(),
1967+
None, // No measurement bias
1968+
initial_covariance.clone(),
1969+
process_noise.clone(),
1970+
1e-3, // alpha
1971+
2.0, // beta
1972+
0.0, // kappa
1973+
);
1974+
1975+
let event_stream = build_event_stream(&records, &degradation);
1976+
let ukf_results = run_closed_loop(&mut ukf, event_stream, None)
1977+
.expect("UKF closed loop should complete successfully");
1978+
1979+
assert_eq!(
1980+
ukf_results.len(),
1981+
input_length,
1982+
"UKF output length {} should match input length {}",
1983+
ukf_results.len(),
1984+
input_length
1985+
);
1986+
println!(
1987+
"✓ UKF: {} outputs for {} inputs",
1988+
ukf_results.len(),
1989+
input_length
1990+
);
1991+
1992+
// Test EKF
1993+
let mut ekf = ExtendedKalmanFilter::new(
1994+
initial_state.clone(),
1995+
imu_biases.clone(),
1996+
initial_covariance.clone(),
1997+
process_noise.clone(),
1998+
true,
1999+
);
2000+
2001+
let event_stream = build_event_stream(&records, &degradation);
2002+
let ekf_results = run_closed_loop(&mut ekf, event_stream, None)
2003+
.expect("EKF closed loop should complete successfully");
2004+
2005+
assert_eq!(
2006+
ekf_results.len(),
2007+
input_length,
2008+
"EKF output length {} should match input length {}",
2009+
ekf_results.len(),
2010+
input_length
2011+
);
2012+
println!(
2013+
"✓ EKF: {} outputs for {} inputs",
2014+
ekf_results.len(),
2015+
input_length
2016+
);
2017+
2018+
// Test ESKF
2019+
let mut eskf =
2020+
ErrorStateKalmanFilter::new(initial_state, imu_biases, initial_covariance, process_noise);
2021+
2022+
let event_stream = build_event_stream(&records, &degradation);
2023+
let eskf_results = run_closed_loop(&mut eskf, event_stream, None)
2024+
.expect("ESKF closed loop should complete successfully");
2025+
2026+
assert_eq!(
2027+
eskf_results.len(),
2028+
input_length,
2029+
"ESKF output length {} should match input length {}",
2030+
eskf_results.len(),
2031+
input_length
2032+
);
2033+
println!(
2034+
"✓ ESKF: {} outputs for {} inputs",
2035+
eskf_results.len(),
2036+
input_length
2037+
);
2038+
2039+
println!(
2040+
"\n✅ All filters produce output length matching input length: {}",
2041+
input_length
2042+
);
2043+
}

sim/src/main.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -833,9 +833,17 @@ fn process_file(
833833

834834
let start_time = event_stream.start_time;
835835
let mut results = Vec::with_capacity(event_stream.events.len());
836-
let mut last_ts: Option<chrono::DateTime<chrono::Utc>> = None;
837836
let mut monitor = HealthMonitor::new(HealthLimits::default());
838837

838+
// Store the initial state (before processing any events)
839+
let (mean, cov) = rbpf.estimate();
840+
results.push(NavigationResult::from_particle_filter(
841+
&start_time,
842+
&mean,
843+
&cov,
844+
));
845+
let mut last_ts = start_time;
846+
839847
for event in event_stream.events.into_iter() {
840848
let elapsed_s = match &event {
841849
Event::Imu { elapsed_s, .. } => *elapsed_s,
@@ -874,13 +882,10 @@ fn process_file(
874882
return Err(e.into());
875883
}
876884

877-
if Some(ts) != last_ts {
878-
if let Some(prev_ts) = last_ts {
879-
results.push(NavigationResult::from_particle_filter(
880-
&prev_ts, &mean, &cov,
881-
));
882-
}
883-
last_ts = Some(ts);
885+
// Store state when timestamp changes
886+
if ts != last_ts {
887+
results.push(NavigationResult::from_particle_filter(&ts, &mean, &cov));
888+
last_ts = ts;
884889
}
885890
}
886891

@@ -1714,9 +1719,17 @@ fn run_particle_filter(args: &ParticleFilterSimArgs) -> Result<(), Box<dyn Error
17141719

17151720
let start_time = event_stream.start_time;
17161721
let mut results = Vec::with_capacity(event_stream.events.len());
1717-
let mut last_ts: Option<chrono::DateTime<chrono::Utc>> = None;
17181722
let mut monitor = HealthMonitor::new(HealthLimits::default());
17191723

1724+
// Store the initial state (before processing any events)
1725+
let (mean, cov) = rbpf.estimate();
1726+
results.push(NavigationResult::from_particle_filter(
1727+
&start_time,
1728+
&mean,
1729+
&cov,
1730+
));
1731+
let mut last_ts = start_time;
1732+
17201733
for event in event_stream.events.into_iter() {
17211734
let elapsed_s = match &event {
17221735
Event::Imu { elapsed_s, .. } => *elapsed_s,
@@ -1755,13 +1768,10 @@ fn run_particle_filter(args: &ParticleFilterSimArgs) -> Result<(), Box<dyn Error
17551768
return Err(e.into());
17561769
}
17571770

1758-
if Some(ts) != last_ts {
1759-
if let Some(prev_ts) = last_ts {
1760-
results.push(NavigationResult::from_particle_filter(
1761-
&prev_ts, &mean, &cov,
1762-
));
1763-
}
1764-
last_ts = Some(ts);
1771+
// Store state when timestamp changes
1772+
if ts != last_ts {
1773+
results.push(NavigationResult::from_particle_filter(&ts, &mean, &cov));
1774+
last_ts = ts;
17651775
}
17661776
}
17671777

0 commit comments

Comments
 (0)