use nalgebra::{DMatrix, DVector};
use crate::math::jacobian::{DifferenceMethod, PerturbationStrategy};
use crate::time::Epoch;
use crate::utils::errors::BraheError;
#[allow(dead_code)]
pub fn measurement_jacobian_numerical(
model: &dyn MeasurementModel,
epoch: &Epoch,
state: &DVector<f64>,
params: Option<&DVector<f64>>,
method: DifferenceMethod,
perturbation: PerturbationStrategy,
) -> Result<DMatrix<f64>, BraheError> {
let m = model.measurement_dim();
let n = state.len();
let mut h_matrix = DMatrix::zeros(m, n);
let offsets: DVector<f64> = match perturbation {
PerturbationStrategy::Adaptive {
scale_factor,
min_value,
} => {
let sqrt_eps = f64::EPSILON.sqrt();
let base_offset = scale_factor * sqrt_eps;
state.map(|x| base_offset * x.abs().max(min_value))
}
PerturbationStrategy::Fixed(offset) => DVector::from_element(n, offset),
PerturbationStrategy::Percentage(pct) => state.map(|x| x.abs() * pct),
};
match method {
DifferenceMethod::Central => {
for j in 0..n {
let mut state_plus = state.clone();
state_plus[j] += offsets[j];
let h_plus = model.predict(epoch, &state_plus, params)?;
let mut state_minus = state.clone();
state_minus[j] -= offsets[j];
let h_minus = model.predict(epoch, &state_minus, params)?;
for i in 0..m {
h_matrix[(i, j)] = (h_plus[i] - h_minus[i]) / (2.0 * offsets[j]);
}
}
}
DifferenceMethod::Forward => {
let f0 = model.predict(epoch, state, params)?;
for j in 0..n {
let mut state_plus = state.clone();
state_plus[j] += offsets[j];
let fp = model.predict(epoch, &state_plus, params)?;
for i in 0..m {
h_matrix[(i, j)] = (fp[i] - f0[i]) / offsets[j];
}
}
}
DifferenceMethod::Backward => {
let f0 = model.predict(epoch, state, params)?;
for j in 0..n {
let mut state_minus = state.clone();
state_minus[j] -= offsets[j];
let fm = model.predict(epoch, &state_minus, params)?;
for i in 0..m {
h_matrix[(i, j)] = (f0[i] - fm[i]) / offsets[j];
}
}
}
}
Ok(h_matrix)
}
pub trait MeasurementModel: Send + Sync {
fn predict(
&self,
epoch: &Epoch,
state: &DVector<f64>,
params: Option<&DVector<f64>>,
) -> Result<DVector<f64>, BraheError>;
fn jacobian(
&self,
epoch: &Epoch,
state: &DVector<f64>,
params: Option<&DVector<f64>>,
) -> Result<DMatrix<f64>, BraheError> {
let m = self.measurement_dim();
let n = state.len();
let mut h_matrix = DMatrix::zeros(m, n);
let sqrt_eps = f64::EPSILON.sqrt();
for j in 0..n {
let h_j = sqrt_eps * state[j].abs().max(1.0);
let mut state_plus = state.clone();
state_plus[j] += h_j;
let h_plus = self.predict(epoch, &state_plus, params)?;
let mut state_minus = state.clone();
state_minus[j] -= h_j;
let h_minus = self.predict(epoch, &state_minus, params)?;
for i in 0..m {
h_matrix[(i, j)] = (h_plus[i] - h_minus[i]) / (2.0 * h_j);
}
}
Ok(h_matrix)
}
fn noise_covariance(&self) -> DMatrix<f64>;
fn measurement_dim(&self) -> usize;
fn name(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::estimation::InertialPositionMeasurementModel;
use crate::time::TimeSystem;
use approx::assert_abs_diff_eq;
fn test_epoch() -> Epoch {
Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC)
}
fn test_state() -> DVector<f64> {
DVector::from_vec(vec![6878.0e3, 1000.0e3, 500.0e3, 0.0, 7500.0, 100.0])
}
fn expected_jacobian() -> DMatrix<f64> {
let mut h = DMatrix::zeros(3, 6);
h[(0, 0)] = 1.0;
h[(1, 1)] = 1.0;
h[(2, 2)] = 1.0;
h
}
#[test]
fn test_numerical_jacobian_central_difference() {
let model = InertialPositionMeasurementModel::new(10.0);
let epoch = test_epoch();
let state = test_state();
let expected = expected_jacobian();
let h = measurement_jacobian_numerical(
&model,
&epoch,
&state,
None,
DifferenceMethod::Central,
PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
)
.unwrap();
assert_eq!(h.nrows(), 3);
assert_eq!(h.ncols(), 6);
assert_abs_diff_eq!(h, expected, epsilon = 1e-8);
}
#[test]
fn test_numerical_jacobian_forward_difference() {
let model = InertialPositionMeasurementModel::new(10.0);
let epoch = test_epoch();
let state = test_state();
let expected = expected_jacobian();
let h = measurement_jacobian_numerical(
&model,
&epoch,
&state,
None,
DifferenceMethod::Forward,
PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
)
.unwrap();
assert_abs_diff_eq!(h, expected, epsilon = 1e-8);
}
#[test]
fn test_numerical_jacobian_backward_difference() {
let model = InertialPositionMeasurementModel::new(10.0);
let epoch = test_epoch();
let state = test_state();
let expected = expected_jacobian();
let h = measurement_jacobian_numerical(
&model,
&epoch,
&state,
None,
DifferenceMethod::Backward,
PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
)
.unwrap();
assert_abs_diff_eq!(h, expected, epsilon = 1e-8);
}
#[test]
fn test_numerical_jacobian_perturbation_strategies() {
let model = InertialPositionMeasurementModel::new(10.0);
let epoch = test_epoch();
let state = test_state();
let expected = expected_jacobian();
let h_fixed = measurement_jacobian_numerical(
&model,
&epoch,
&state,
None,
DifferenceMethod::Central,
PerturbationStrategy::Fixed(1.0),
)
.unwrap();
assert_abs_diff_eq!(h_fixed, expected, epsilon = 1e-8);
let nonzero_state =
DVector::from_vec(vec![6878.0e3, 1000.0e3, 500.0e3, 100.0, 7500.0, 100.0]);
let h_pct = measurement_jacobian_numerical(
&model,
&epoch,
&nonzero_state,
None,
DifferenceMethod::Central,
PerturbationStrategy::Percentage(1e-6),
)
.unwrap();
assert_abs_diff_eq!(h_pct, expected, epsilon = 1e-8);
let h_adaptive = measurement_jacobian_numerical(
&model,
&epoch,
&state,
None,
DifferenceMethod::Central,
PerturbationStrategy::Adaptive {
scale_factor: 2.0,
min_value: 0.1,
},
)
.unwrap();
assert_abs_diff_eq!(h_adaptive, expected, epsilon = 1e-8);
}
}