Skip to content

Commit 05c7163

Browse files
authored
Merge pull request #228 from jbrodovsky/copilot/implement-geophysical-jacobians
Implement geophysical measurement Jacobians for EKF updates
2 parents 8b91fac + 9a3d014 commit 05c7163

4 files changed

Lines changed: 164 additions & 35 deletions

File tree

core/src/kalman.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,11 @@ impl NavigationFilter for ExtendedKalmanFilter {
885885
};
886886

887887
// Compute measurement Jacobian for 9-state system
888-
let h_9state = if measurement
888+
// First check if the measurement provides its own Jacobian (for geophysical measurements)
889+
let h_9state = if let Some(jacobian) = measurement.get_jacobian(&self.mean_state) {
890+
// Measurement provides its own Jacobian (e.g., geophysical anomaly measurements)
891+
jacobian
892+
} else if measurement
889893
.as_any()
890894
.downcast_ref::<GPSPositionMeasurement>()
891895
.is_some()

core/src/linearize.rs

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -632,21 +632,26 @@ pub fn relative_altitude_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
632632

633633
/// Compute measurement Jacobian (H) for gravity anomaly measurement
634634
///
635-
/// **Note**: This is a placeholder function for future use. Currently, the EKF computes
636-
/// measurement Jacobians internally during the update step. This function provides a
637-
/// template structure for when custom Jacobian computation is integrated into the EKF.
635+
/// **Note**: This is a placeholder function that returns zeros. Geophysical measurements
636+
/// (gravity anomaly and magnetic anomaly) now provide their own Jacobians via the
637+
/// `MeasurementModel::get_jacobian()` trait method. The EKF update function automatically
638+
/// detects and uses these measurement-provided Jacobians.
638639
///
639640
/// Gravity anomaly measurements depend on latitude and longitude (position) to query the
640-
/// geophysical map. The partial derivatives ∂z/∂lat and ∂z/∂lon would be computed
641-
/// numerically by the measurement model based on map gradients.
641+
/// geophysical map. The partial derivatives ∂z/∂lat and ∂z/∂lon are computed numerically
642+
/// by the `GravityMeasurement` struct in the `strapdown-geonav` crate based on map gradients.
642643
///
643644
/// # Arguments
644645
///
645-
/// * `_state` - Current navigation state (reserved for future use)
646+
/// * `_state` - Current navigation state (unused, this is a placeholder)
646647
///
647648
/// # Returns
648649
///
649-
/// 1×9 measurement Jacobian matrix H template for gravity anomaly (currently zeros)
650+
/// 1×9 measurement Jacobian matrix H filled with zeros (placeholder)
651+
///
652+
/// # See Also
653+
///
654+
/// For actual gravity anomaly Jacobian computation, see `geonav::GravityMeasurement::get_jacobian_internal()`.
650655
///
651656
/// # Example
652657
///
@@ -660,34 +665,35 @@ pub fn relative_altitude_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
660665
/// assert_eq!(h.ncols(), 9);
661666
/// ```
662667
pub fn gravity_anomaly_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
663-
// Gravity anomaly depends on position (lat, lon) through map lookup
664-
// The partial derivatives ∂z/∂lat and ∂z/∂lon are computed numerically
665-
// by the measurement model based on map gradients
666-
667-
// These will be filled in by the measurement model with numerical derivatives
668-
// from the geophysical map interpolation
669-
// h[(0, 0)] = ∂(anomaly)/∂(lat) - computed from map gradient
670-
// h[(0, 1)] = ∂(anomaly)/∂(lon) - computed from map gradient
668+
// NOTE: This is a placeholder. Real gravity anomaly Jacobians are provided
669+
// by the GravityMeasurement struct in the geonav crate via the
670+
// MeasurementModel::get_jacobian() trait method. The EKF will automatically
671+
// use the measurement-provided Jacobian when available.
671672
DMatrix::<f64>::zeros(1, 9)
672673
}
673674

674675
/// Compute measurement Jacobian (H) for magnetic anomaly measurement
675676
///
676-
/// **Note**: This is a placeholder function for future use. Currently, the EKF computes
677-
/// measurement Jacobians internally during the update step. This function provides a
678-
/// template structure for when custom Jacobian computation is integrated into the EKF.
677+
/// **Note**: This is a placeholder function that returns zeros. Geophysical measurements
678+
/// (gravity anomaly and magnetic anomaly) now provide their own Jacobians via the
679+
/// `MeasurementModel::get_jacobian()` trait method. The EKF update function automatically
680+
/// detects and uses these measurement-provided Jacobians.
679681
///
680682
/// Magnetic anomaly measurements depend on latitude and longitude (position) to query the
681-
/// geophysical map. The partial derivatives ∂z/∂lat and ∂z/∂lon would be computed
682-
/// numerically by the measurement model based on map gradients.
683+
/// geophysical map. The partial derivatives ∂z/∂lat and ∂z/∂lon are computed numerically
684+
/// by the `MagneticAnomalyMeasurement` struct in the `strapdown-geonav` crate based on map gradients.
683685
///
684686
/// # Arguments
685687
///
686-
/// * `_state` - Current navigation state (reserved for future use)
688+
/// * `_state` - Current navigation state (unused, this is a placeholder)
687689
///
688690
/// # Returns
689691
///
690-
/// 1×9 measurement Jacobian matrix H template for magnetic anomaly (currently zeros)
692+
/// 1×9 measurement Jacobian matrix H filled with zeros (placeholder)
693+
///
694+
/// # See Also
695+
///
696+
/// For actual magnetic anomaly Jacobian computation, see `geonav::MagneticAnomalyMeasurement::get_jacobian_internal()`.
691697
///
692698
/// # Example
693699
///
@@ -701,14 +707,10 @@ pub fn gravity_anomaly_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
701707
/// assert_eq!(h.ncols(), 9);
702708
/// ```
703709
pub fn magnetic_anomaly_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
704-
// Magnetic anomaly depends on position (lat, lon) through map lookup
705-
// The partial derivatives ∂z/∂lat and ∂z/∂lon are computed numerically
706-
// by the measurement model based on map gradients
707-
708-
// These will be filled in by the measurement model with numerical derivatives
709-
// from the geophysical map interpolation
710-
// h[(0, 0)] = ∂(anomaly)/∂(lat) - computed from map gradient
711-
// h[(0, 1)] = ∂(anomaly)/∂(lon) - computed from map gradient
710+
// NOTE: This is a placeholder. Real magnetic anomaly Jacobians are provided
711+
// by the MagneticAnomalyMeasurement struct in the geonav crate via the
712+
// MeasurementModel::get_jacobian() trait method. The EKF will automatically
713+
// use the measurement-provided Jacobian when available.
712714
DMatrix::<f64>::zeros(1, 9)
713715
}
714716

core/src/measurements.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,42 @@ pub trait MeasurementModel: Any {
7272
///
7373
/// Expected measurement given the state
7474
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64>;
75+
76+
/// Optionally provide the measurement Jacobian for EKF updates.
77+
///
78+
/// This method allows measurements to provide their own linearized measurement
79+
/// model (Jacobian matrix H) for Extended Kalman Filter updates. If not provided
80+
/// (returns None), the EKF will fall back to using a default Jacobian based on
81+
/// the measurement type.
82+
///
83+
/// For geophysical measurements (gravity anomaly, magnetic anomaly), this method
84+
/// computes numerical gradients from the geophysical map to create the Jacobian.
85+
///
86+
/// # Arguments
87+
///
88+
/// * `state` - Current state estimate vector [lat, lon, alt, v_n, v_e, v_d, roll, pitch, yaw]
89+
///
90+
/// # Returns
91+
///
92+
/// Optional Jacobian matrix H (measurement_dim × 9) for EKF updates. Returns None
93+
/// if the measurement does not provide its own Jacobian.
94+
///
95+
/// # Example
96+
///
97+
/// ```rust
98+
/// use strapdown::measurements::MeasurementModel;
99+
/// use nalgebra::{DVector, DMatrix};
100+
///
101+
/// // For a measurement that provides its own Jacobian:
102+
/// // let h_matrix = measurement.get_jacobian(&state).unwrap();
103+
///
104+
/// // For a measurement that doesn't provide a Jacobian:
105+
/// // let h_matrix_opt = measurement.get_jacobian(&state);
106+
/// // assert!(h_matrix_opt.is_none());
107+
/// ```
108+
fn get_jacobian(&self, _state: &DVector<f64>) -> Option<DMatrix<f64>> {
109+
None // Default implementation returns None
110+
}
75111
}
76112

77113
/// GPS position measurement model

geonav/src/lib.rs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,10 @@ impl MeasurementModel for GravityMeasurement {
579579
.unwrap_or(f64::NAN);
580580
DVector::from_vec(vec![map_value])
581581
}
582+
583+
fn get_jacobian(&self, state: &DVector<f64>) -> Option<DMatrix<f64>> {
584+
Some(self.get_jacobian_internal(state))
585+
}
582586
}
583587

584588
impl GravityMeasurement {
@@ -594,7 +598,7 @@ impl GravityMeasurement {
594598
/// # Returns
595599
///
596600
/// 1×9 Jacobian matrix H for gravity anomaly measurement
597-
pub fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
601+
pub fn get_jacobian_internal(&self, state: &DVector<f64>) -> DMatrix<f64> {
598602
let mut h = DMatrix::<f64>::zeros(1, 9);
599603

600604
let lat = state[0];
@@ -679,6 +683,10 @@ impl MeasurementModel for MagneticAnomalyMeasurement {
679683
.unwrap_or(f64::NAN);
680684
DVector::from_vec(vec![map_value])
681685
}
686+
687+
fn get_jacobian(&self, state: &DVector<f64>) -> Option<DMatrix<f64>> {
688+
Some(self.get_jacobian_internal(state))
689+
}
682690
}
683691

684692
impl MagneticAnomalyMeasurement {
@@ -694,7 +702,7 @@ impl MagneticAnomalyMeasurement {
694702
/// # Returns
695703
///
696704
/// 1×9 Jacobian matrix H for magnetic anomaly measurement
697-
pub fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
705+
pub fn get_jacobian_internal(&self, state: &DVector<f64>) -> DMatrix<f64> {
698706
let mut h = DMatrix::<f64>::zeros(1, 9);
699707

700708
let lat = state[0];
@@ -1697,7 +1705,7 @@ mod tests {
16971705
0.0, // attitude
16981706
]);
16991707

1700-
let jacobian = measurement.get_jacobian(&state);
1708+
let jacobian = measurement.get_jacobian_internal(&state);
17011709

17021710
// Jacobian should be 1x9
17031711
assert_eq!(jacobian.nrows(), 1);
@@ -1737,7 +1745,7 @@ mod tests {
17371745
0.0, // attitude
17381746
]);
17391747

1740-
let jacobian = measurement.get_jacobian(&state);
1748+
let jacobian = measurement.get_jacobian_internal(&state);
17411749

17421750
// Jacobian should be 1x9
17431751
assert_eq!(jacobian.nrows(), 1);
@@ -1749,4 +1757,83 @@ mod tests {
17491757
assert_approx_eq!(jacobian[(0, j)], 0.0, 1e-10);
17501758
}
17511759
}
1760+
1761+
#[test]
1762+
fn test_gravity_measurement_trait_jacobian() {
1763+
// Test that the MeasurementModel trait method works correctly
1764+
let map = Rc::new(create_test_gravity_map());
1765+
let measurement: Box<dyn strapdown::measurements::MeasurementModel> =
1766+
Box::new(GravityMeasurement {
1767+
map: map.clone(),
1768+
noise_std: 100.0,
1769+
gravity_observed: 9.8,
1770+
latitude: 41.0_f64.to_radians(),
1771+
altitude: 100.0,
1772+
north_velocity: 0.0,
1773+
east_velocity: 0.0,
1774+
});
1775+
1776+
let state = DVector::from_vec(vec![
1777+
41.0_f64.to_radians(),
1778+
-73.0_f64.to_radians(),
1779+
100.0,
1780+
0.0,
1781+
0.0,
1782+
0.0,
1783+
0.0,
1784+
0.0,
1785+
0.0,
1786+
]);
1787+
1788+
// Test that get_jacobian returns Some
1789+
let jacobian_opt = measurement.get_jacobian(&state);
1790+
assert!(
1791+
jacobian_opt.is_some(),
1792+
"Gravity measurement should provide Jacobian via trait"
1793+
);
1794+
1795+
let jacobian = jacobian_opt.unwrap();
1796+
assert_eq!(jacobian.nrows(), 1);
1797+
assert_eq!(jacobian.ncols(), 9);
1798+
}
1799+
1800+
#[test]
1801+
fn test_magnetic_measurement_trait_jacobian() {
1802+
// Test that the MeasurementModel trait method works correctly
1803+
let map = Rc::new(create_test_magnetic_map());
1804+
let measurement: Box<dyn strapdown::measurements::MeasurementModel> =
1805+
Box::new(MagneticAnomalyMeasurement {
1806+
map: map.clone(),
1807+
noise_std: 100.0,
1808+
mag_obs: 48000.0,
1809+
latitude: 41.0,
1810+
longitude: -73.0,
1811+
altitude: 100.0,
1812+
year: 2023,
1813+
day: 216,
1814+
});
1815+
1816+
let state = DVector::from_vec(vec![
1817+
41.0_f64.to_radians(),
1818+
-73.0_f64.to_radians(),
1819+
100.0,
1820+
0.0,
1821+
0.0,
1822+
0.0,
1823+
0.0,
1824+
0.0,
1825+
0.0,
1826+
]);
1827+
1828+
// Test that get_jacobian returns Some
1829+
let jacobian_opt = measurement.get_jacobian(&state);
1830+
assert!(
1831+
jacobian_opt.is_some(),
1832+
"Magnetic measurement should provide Jacobian via trait"
1833+
);
1834+
1835+
let jacobian = jacobian_opt.unwrap();
1836+
assert_eq!(jacobian.nrows(), 1);
1837+
assert_eq!(jacobian.ncols(), 9);
1838+
}
17521839
}

0 commit comments

Comments
 (0)