Skip to content

Commit 4d9afc4

Browse files
authored
Merge pull request #239 from jbrodovsky/jbrodovsky/issue238
Fix EKF altitude bounds for magnetic field calculations
2 parents 914d6c7 + d917f23 commit 4d9afc4

3 files changed

Lines changed: 132 additions & 173 deletions

File tree

core/src/sim.rs

Lines changed: 62 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,37 +2066,47 @@ pub fn print_ukf(ukf: &UnscentedKalmanFilter, record: &TestDataRecord) {
20662066
ukf.get_certainty()[(14, 14)]
20672067
);
20682068
}
2069+
2070+
/// Configuration parameters for UKF initialization.
2071+
///
2072+
/// This struct groups together optional parameters for initializing an Unscented Kalman Filter,
2073+
/// reducing the number of function arguments and making it easier to specify custom configurations.
2074+
#[derive(Debug, Clone, Default)]
2075+
pub struct UkfConfig {
2076+
/// Optional vector of f64 representing the initial attitude covariance (default is a small value).
2077+
pub attitude_covariance: Option<Vec<f64>>,
2078+
/// Optional vector of f64 representing the initial IMU biases (default is a small value).
2079+
pub imu_biases: Option<Vec<f64>>,
2080+
/// Optional vector of f64 representing the IMU biases covariance.
2081+
pub imu_biases_covariance: Option<Vec<f64>>,
2082+
/// Optional vector of f64 for any additional states (not used in the canonical UKF, but can be useful for custom implementations).
2083+
pub other_states: Option<Vec<f64>>,
2084+
/// Optional vector of f64 for other states covariance.
2085+
pub other_states_covariance: Option<Vec<f64>>,
2086+
/// Optional process noise diagonal vector.
2087+
pub process_noise_diagonal: Option<Vec<f64>>,
2088+
/// Optional UKF alpha parameter (sigma-point spread).
2089+
pub ukf_alpha: Option<f64>,
2090+
/// Optional UKF beta parameter (prior distribution).
2091+
pub ukf_beta: Option<f64>,
2092+
/// Optional UKF kappa parameter (secondary spread control).
2093+
pub ukf_kappa: Option<f64>,
2094+
}
2095+
20692096
/// Helper function to initialize a UKF for closed-loop mode.
20702097
///
2071-
/// This function sets up the Unscented Kalman Filter (UKF) with initial pose, attitude covariance, and IMU biases based on
2072-
/// the provided `TestDataRecord`. It initializes the UKF with position, velocity, attitude, and covariance matrices.
2073-
/// Optional parameters for attitude covariance and IMU biases can be provided to customize the filter's initial state.
2098+
/// This function sets up the Unscented Kalman Filter (UKF) with initial pose and configuration parameters.
2099+
/// It initializes the UKF with position, velocity, attitude, and covariance matrices.
20742100
///
20752101
/// # Arguments
20762102
///
20772103
/// * `initial_pose` - A `TestDataRecord` containing the initial pose information.
2078-
/// * `attitude_covariance` - Optional vector of f64 representing the initial attitude covariance (default is a small value).
2079-
/// * `imu_biases` - Optional vector of f64 representing the initial IMU biases (default is a small value).
2080-
/// * `other_states` - Optional vector of f64 for any additional states (not used in the canonical UKF, but can be useful for custom implementations).
2081-
/// * `ukf_alpha` - Optional UKF alpha parameter (sigma-point spread).
2082-
/// * `ukf_beta` - Optional UKF beta parameter (prior distribution).
2083-
/// * `ukf_kappa` - Optional UKF kappa parameter (secondary spread control).
2104+
/// * `config` - A `UkfConfig` struct containing optional configuration parameters.
20842105
///
20852106
/// # Returns
20862107
///
2087-
/// * `UKF` - An instance of the Unscented Kalman Filter initialized with the provided parameters.
2088-
pub fn initialize_ukf(
2089-
initial_pose: TestDataRecord,
2090-
attitude_covariance: Option<Vec<f64>>,
2091-
imu_biases: Option<Vec<f64>>,
2092-
imu_biases_covariance: Option<Vec<f64>>,
2093-
other_states: Option<Vec<f64>>,
2094-
other_states_covariance: Option<Vec<f64>>,
2095-
process_noise_diagonal: Option<Vec<f64>>,
2096-
ukf_alpha: Option<f64>,
2097-
ukf_beta: Option<f64>,
2098-
ukf_kappa: Option<f64>,
2099-
) -> UnscentedKalmanFilter {
2108+
/// * `UnscentedKalmanFilter` - An instance of the Unscented Kalman Filter initialized with the provided parameters.
2109+
pub fn initialize_ukf(initial_pose: TestDataRecord, config: UkfConfig) -> UnscentedKalmanFilter {
21002110
let initial_state = InitialState {
21012111
latitude: initial_pose.latitude,
21022112
longitude: initial_pose.longitude,
@@ -2122,7 +2132,7 @@ pub fn initialize_ukf(
21222132
in_degrees: true,
21232133
is_enu: true,
21242134
};
2125-
let process_noise_diagonal = match process_noise_diagonal {
2135+
let process_noise_diagonal = match config.process_noise_diagonal {
21262136
Some(pn) => pn,
21272137
None => DEFAULT_PROCESS_NOISE.to_vec(),
21282138
};
@@ -2138,14 +2148,14 @@ pub fn initialize_ukf(
21382148
initial_pose.speed_accuracy.powf(2.0),
21392149
];
21402150
// extend the covariance diagonal if attitude covariance is provided
2141-
match attitude_covariance {
2151+
match config.attitude_covariance {
21422152
Some(att_cov) => covariance_diagonal.extend(att_cov),
21432153
None => covariance_diagonal.extend(vec![1e-9; 3]), // Default values if not provided
21442154
}
21452155
// extend the covariance diagonal if imu biases are provided
2146-
let imu_biases = match imu_biases {
2156+
let imu_biases = match config.imu_biases {
21472157
Some(imu_biases) => {
2148-
covariance_diagonal.extend(match imu_biases_covariance {
2158+
covariance_diagonal.extend(match config.imu_biases_covariance {
21492159
Some(imu_cov) => imu_cov,
21502160
None => vec![1e-3; 6], // Default covariance if not provided
21512161
});
@@ -2157,9 +2167,9 @@ pub fn initialize_ukf(
21572167
}
21582168
};
21592169
// extend the covariance diagonal if other states are provided
2160-
let other_states = match other_states {
2170+
let other_states = match config.other_states {
21612171
Some(other_states) => {
2162-
covariance_diagonal.extend(match other_states_covariance {
2172+
covariance_diagonal.extend(match config.other_states_covariance {
21632173
Some(other_cov) => other_cov,
21642174
None => vec![1e-3; other_states.len()], // Default covariance if not provided
21652175
});
@@ -2193,9 +2203,9 @@ pub fn initialize_ukf(
21932203
other_states,
21942204
covariance_diagonal,
21952205
process_noise,
2196-
ukf_alpha.unwrap_or(1e-3),
2197-
ukf_beta.unwrap_or(2.0),
2198-
ukf_kappa.unwrap_or(0.0),
2206+
config.ukf_alpha.unwrap_or(1e-3),
2207+
config.ukf_beta.unwrap_or(2.0),
2208+
config.ukf_kappa.unwrap_or(0.0),
21992209
)
22002210
}
22012211

@@ -3718,18 +3728,7 @@ mod tests {
37183728
});
37193729

37203730
// Initialize UKF
3721-
let mut ukf = initialize_ukf(
3722-
rec.clone(),
3723-
None,
3724-
None,
3725-
None,
3726-
None,
3727-
None,
3728-
None,
3729-
None,
3730-
None,
3731-
None,
3732-
);
3731+
let mut ukf = initialize_ukf(rec.clone(), UkfConfig::default());
37333732

37343733
// Create a minimal EventStream with one IMU event
37353734
let imu_data = IMUData {
@@ -3784,30 +3783,15 @@ mod tests {
37843783
grav_y: 0.0,
37853784
grav_x: 0.0,
37863785
};
3787-
let ukf = initialize_ukf(
3788-
rec.clone(),
3789-
None,
3790-
None,
3791-
None,
3792-
None,
3793-
None,
3794-
None,
3795-
None,
3796-
None,
3797-
None,
3798-
);
3786+
let ukf = initialize_ukf(rec.clone(), UkfConfig::default());
37993787
assert!(!ukf.get_estimate().is_empty());
38003788
let ukf2 = initialize_ukf(
38013789
rec,
3802-
Some(vec![0.1, 0.2, 0.3]),
3803-
Some(vec![0.4, 0.5, 0.6, 0.7, 0.8, 0.9]),
3804-
None,
3805-
None,
3806-
None,
3807-
None,
3808-
None,
3809-
None,
3810-
None,
3790+
UkfConfig {
3791+
attitude_covariance: Some(vec![0.1, 0.2, 0.3]),
3792+
imu_biases: Some(vec![0.4, 0.5, 0.6, 0.7, 0.8, 0.9]),
3793+
..Default::default()
3794+
},
38113795
);
38123796
assert!(!ukf2.get_estimate().is_empty());
38133797
}
@@ -3997,18 +3981,7 @@ mod tests {
39973981
yaw: 0.3,
39983982
..Default::default()
39993983
};
4000-
let ukf = initialize_ukf(
4001-
rec.clone(),
4002-
None,
4003-
None,
4004-
None,
4005-
None,
4006-
None,
4007-
None,
4008-
None,
4009-
None,
4010-
None,
4011-
);
3984+
let ukf = initialize_ukf(rec.clone(), UkfConfig::default());
40123985
let timestamp = Utc::now();
40133986
let nav_result = NavigationResult::from((&timestamp, &ukf));
40143987

@@ -4062,18 +4035,7 @@ mod tests {
40624035
yaw: 0.3,
40634036
..Default::default()
40644037
};
4065-
let ukf = initialize_ukf(
4066-
rec.clone(),
4067-
None,
4068-
None,
4069-
None,
4070-
None,
4071-
None,
4072-
None,
4073-
None,
4074-
None,
4075-
None,
4076-
);
4038+
let ukf = initialize_ukf(rec.clone(), UkfConfig::default());
40774039
// Just ensure it doesn't panic
40784040
print_ukf(&ukf, &rec);
40794041
}
@@ -4094,7 +4056,7 @@ mod tests {
40944056
yaw: f64::NAN,
40954057
..Default::default()
40964058
};
4097-
let ukf = initialize_ukf(rec, None, None, None, None, None, None, None, None, None);
4059+
let ukf = initialize_ukf(rec, UkfConfig::default());
40984060
let estimate = ukf.get_estimate();
40994061
// Should default NaN angles to 0.0
41004062
assert!(estimate[6].abs() < 1e-6); // roll
@@ -4121,15 +4083,12 @@ mod tests {
41214083
};
41224084
let ukf = initialize_ukf(
41234085
rec,
4124-
Some(vec![1e-4, 2e-4, 3e-4]),
4125-
Some(vec![0.01, 0.02, 0.03, 0.001, 0.002, 0.003]),
4126-
Some(vec![1e-5; 6]),
4127-
None,
4128-
None,
4129-
None,
4130-
None,
4131-
None,
4132-
None,
4086+
UkfConfig {
4087+
attitude_covariance: Some(vec![1e-4, 2e-4, 3e-4]),
4088+
imu_biases: Some(vec![0.01, 0.02, 0.03, 0.001, 0.002, 0.003]),
4089+
imu_biases_covariance: Some(vec![1e-5; 6]),
4090+
..Default::default()
4091+
},
41334092
);
41344093
let estimate = ukf.get_estimate();
41354094
assert_eq!(estimate.len(), 15);
@@ -4155,15 +4114,10 @@ mod tests {
41554114
let custom_noise = vec![1e-5; 15];
41564115
let ukf = initialize_ukf(
41574116
rec,
4158-
None,
4159-
None,
4160-
None,
4161-
None,
4162-
None,
4163-
Some(custom_noise),
4164-
None,
4165-
None,
4166-
None,
4117+
UkfConfig {
4118+
process_noise_diagonal: Some(custom_noise),
4119+
..Default::default()
4120+
},
41674121
);
41684122
assert!(!ukf.get_estimate().is_empty());
41694123
}
@@ -4651,18 +4605,7 @@ mod tests {
46514605
..Default::default()
46524606
};
46534607

4654-
let mut ukf = initialize_ukf(
4655-
rec.clone(),
4656-
None,
4657-
None,
4658-
None,
4659-
None,
4660-
None,
4661-
None,
4662-
None,
4663-
None,
4664-
None,
4665-
);
4608+
let mut ukf = initialize_ukf(rec.clone(), UkfConfig::default());
46664609

46674610
let stream = EventStream {
46684611
start_time: rec.time,

geonav/src/lib.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ use strapdown::{IMUData, NavigationFilter, StrapdownState};
5252
/// Conversion factor from radians to degrees (180/π)
5353
const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
5454

55+
/// World Magnetic Model valid altitude range (meters)
56+
/// The WMM is typically valid from -1km below sea level to ~850km above
57+
const WMM_MIN_ALTITUDE_M: f64 = -1000.0;
58+
const WMM_MAX_ALTITUDE_M: f64 = 850000.0;
59+
5560
//================= Map Information ========================================================================
5661
/// Resolution values for bathymetric or terrain relief maps
5762
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -671,8 +676,11 @@ pub struct MagneticAnomalyMeasurement {
671676
}
672677
impl GeophysicalAnomalyMeasurementModel for MagneticAnomalyMeasurement {
673678
fn get_anomaly(&self) -> f64 {
679+
// Clamp altitude to valid WMM range to prevent errors
680+
let alt_clamped = self.altitude.clamp(WMM_MIN_ALTITUDE_M, WMM_MAX_ALTITUDE_M);
681+
674682
let magnetic_field = GeomagneticField::new(
675-
Length::new::<meter>(self.altitude as f32),
683+
Length::new::<meter>(alt_clamped as f32),
676684
Angle::new::<degree>(self.latitude as f32),
677685
Angle::new::<degree>(self.longitude as f32),
678686
Date::from_ordinal_date(self.year, self.day).unwrap(),
@@ -700,16 +708,27 @@ impl MeasurementModel for MagneticAnomalyMeasurement {
700708
// Return the observed magnetic anomaly as the measurement vector.
701709
// Use provided state if available (for per-particle updates), otherwise fallback to stored state.
702710
let anomaly = if let Some((lat_deg, lon_deg, alt)) = self.extract_state_inputs(state) {
711+
// Clamp altitude to valid WMM range to prevent errors
712+
let alt_clamped = alt.clamp(WMM_MIN_ALTITUDE_M, WMM_MAX_ALTITUDE_M);
713+
714+
if (alt - alt_clamped).abs() > 1.0 {
715+
log::warn!(
716+
"Altitude {} m out of WMM bounds, clamped to {} m",
717+
alt,
718+
alt_clamped
719+
);
720+
}
721+
703722
let magnetic_field = GeomagneticField::new(
704-
Length::new::<meter>(alt as f32),
723+
Length::new::<meter>(alt_clamped as f32),
705724
Angle::new::<degree>(lat_deg as f32),
706725
Angle::new::<degree>(lon_deg as f32),
707726
Date::from_ordinal_date(self.year, self.day).unwrap(),
708727
)
709728
.unwrap_or_else(|e| {
710729
panic!(
711-
"Failed to create GeomagneticField at lat={}, lon={}, alt={}: {:?}",
712-
lat_deg, lon_deg, alt, e
730+
"Failed to create GeomagneticField at lat={}, lon={}, alt={} (clamped: {}): {:?}",
731+
lat_deg, lon_deg, alt, alt_clamped, e
713732
)
714733
});
715734
self.mag_obs - magnetic_field.f().value as f64
@@ -898,7 +917,7 @@ pub fn build_event_stream(
898917
r1.horizontal_accuracy.max(1e-3)
899918
};
900919
let vert_std = if r1.vertical_accuracy.is_nan() {
901-
1000.0
920+
20.0
902921
} else {
903922
r1.vertical_accuracy.max(1e-3)
904923
};

0 commit comments

Comments
 (0)