use apex_manifolds::LieGroup;
use apex_manifolds::se3::SE3;
use faer::prelude::ReborrowMut;
use nalgebra::{Matrix3, SMatrix, Vector3};
use crate::core::variable::ManifoldVariable;
use crate::factors::Factor;
use crate::factors::common::math::skew;
use crate::factors::common::validate::expect_block_sizes;
use crate::factors::imu::preintegration::ImuPreintegration;
use crate::factors::imu::types::{SpeedAndBias, SpeedAndBiasExt};
pub struct GpsFactor {
measurement: Vector3<f64>,
antenna_offset: Vector3<f64>,
sqrt_information: SMatrix<f64, 3, 3>,
}
impl GpsFactor {
pub fn new(
measurement: Vector3<f64>,
antenna_offset: Vector3<f64>,
sqrt_information: SMatrix<f64, 3, 3>,
) -> Self {
Self {
measurement,
antenna_offset,
sqrt_information,
}
}
pub fn new_isotropic(
measurement: Vector3<f64>,
antenna_offset: Vector3<f64>,
sigma: f64,
) -> Self {
let sqrt_info = SMatrix::<f64, 3, 3>::identity() * (1.0 / sigma);
Self::new(measurement, antenna_offset, sqrt_info)
}
}
impl Factor for GpsFactor {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
debug_assert_eq!(params.len(), 2, "GpsFactor expects 2 parameter blocks");
debug_assert_eq!(params[0].len(), 7, "params[0] must be SE3 (7D)");
debug_assert_eq!(params[1].len(), 7, "params[1] must be SE3 (7D)");
let t_ws = SE3::from_param_slice(params[0]);
let t_gw = SE3::from_param_slice(params[1]);
let c_ws: Matrix3<f64> = t_ws.rotation_so3().rotation_matrix();
let c_gw: Matrix3<f64> = t_gw.rotation_so3().rotation_matrix();
let pos_ws = t_ws.translation();
let pos_gw = t_gw.translation();
let t_antenna_w: Vector3<f64> = pos_ws + c_ws * self.antenna_offset;
let predicted: Vector3<f64> = c_gw * t_antenna_w + pos_gw;
let raw_err: Vector3<f64> = self.measurement - predicted;
let weighted_err = self.sqrt_information * raw_err;
for i in 0..3 {
residual[i] = weighted_err[i];
}
let Some(mut jac) = jacobian else {
return;
};
let r_sa_hat = skew(&self.antenna_offset);
let t_ant_hat = skew(&t_antenna_w);
let de_drho_ws = -(c_gw * c_ws);
let de_dtheta_ws = c_gw * c_ws * r_sa_hat;
let de_drho_gw = -c_gw;
let de_dtheta_gw = c_gw * t_ant_hat;
let mut j_raw = SMatrix::<f64, 3, 12>::zeros();
j_raw.fixed_view_mut::<3, 3>(0, 0).copy_from(&de_drho_ws);
j_raw.fixed_view_mut::<3, 3>(0, 3).copy_from(&de_dtheta_ws);
j_raw.fixed_view_mut::<3, 3>(0, 6).copy_from(&de_drho_gw);
j_raw.fixed_view_mut::<3, 3>(0, 9).copy_from(&de_dtheta_gw);
let j_weighted = self.sqrt_information * j_raw;
for row in 0..3 {
for col in 0..12 {
*jac.rb_mut().get_mut(row, col) = j_weighted[(row, col)];
}
}
}
fn residual_dim(&self) -> usize {
3
}
fn jacobian_shape(&self) -> (usize, usize) {
(3, 12)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
expect_block_sizes(
variables,
&[SE3::REP_SIZE, SE3::REP_SIZE],
"GpsFactor expects [SE3 T_WS, SE3 T_GW]",
)
}
fn whitens_internally(&self) -> bool {
true
}
}
pub struct GpsAsyncFactor {
measurement: Vector3<f64>,
antenna_offset: Vector3<f64>,
sqrt_information: SMatrix<f64, 3, 3>,
preintegration: ImuPreintegration,
}
impl GpsAsyncFactor {
pub fn new(
measurement: Vector3<f64>,
antenna_offset: Vector3<f64>,
sqrt_information: SMatrix<f64, 3, 3>,
preintegration: ImuPreintegration,
) -> Self {
Self {
measurement,
antenna_offset,
sqrt_information,
preintegration,
}
}
pub fn new_isotropic(
measurement: Vector3<f64>,
antenna_offset: Vector3<f64>,
sigma: f64,
preintegration: ImuPreintegration,
) -> Self {
let sqrt_info = SMatrix::<f64, 3, 3>::identity() * (1.0 / sigma);
Self::new(measurement, antenna_offset, sqrt_info, preintegration)
}
}
impl Factor for GpsAsyncFactor {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
debug_assert_eq!(params.len(), 3, "GpsAsyncFactor expects 3 parameter blocks");
debug_assert_eq!(params[0].len(), 7, "params[0] must be SE3 (7D)");
debug_assert_eq!(params[1].len(), 9, "params[1] must be SpeedAndBias (9D)");
debug_assert_eq!(params[2].len(), 7, "params[2] must be SE3 (7D)");
let preint = &self.preintegration;
let t_ws_k = SE3::from_param_slice(params[0]);
let sb_k = SpeedAndBias::from_column_slice(params[1]);
let t_gw = SE3::from_param_slice(params[2]);
let c_ws_k: Matrix3<f64> = t_ws_k.rotation_so3().rotation_matrix();
let c_gw: Matrix3<f64> = t_gw.rotation_so3().rotation_matrix();
let p_k = t_ws_k.translation();
let t_gw_pos = t_gw.translation();
let v_k = sb_k.velocity();
let delta_t = preint.t1() - preint.t0();
let delta_r: Matrix3<f64> = preint.delta_q().to_rotation_matrix().into_inner();
let b_g_ref = preint.speed_and_biases_ref().gyro_bias();
let b_a_ref = preint.speed_and_biases_ref().accel_bias();
let db_g = sb_k.gyro_bias() - b_g_ref;
let db_a = sb_k.accel_bias() - b_a_ref;
let delta_p_body = preint.acc_doubleintegral()
+ preint.dp_db_g() * db_g
+ preint.c_doubleintegral() * db_a;
let gravity = Vector3::new(0.0, 0.0, preint.imu_params().g);
let p_propagated =
p_k + v_k * delta_t - 0.5 * gravity * delta_t * delta_t + c_ws_k * delta_p_body;
let c_ws_g = c_ws_k * delta_r;
let t_antenna_w = p_propagated + c_ws_g * self.antenna_offset;
let predicted = c_gw * t_antenna_w + t_gw_pos;
let raw_err = self.measurement - predicted;
let weighted_err = self.sqrt_information * raw_err;
for i in 0..3 {
residual[i] = weighted_err[i];
}
let Some(mut jac) = jacobian else {
return;
};
let delta_r_r_sa = delta_r * self.antenna_offset;
let theta_arm = skew(&(delta_p_body + delta_r_r_sa));
let t_ant_hat = skew(&t_antenna_w);
let de_drho_ws = -(c_gw * c_ws_k);
let de_dtheta_ws = c_gw * c_ws_k * theta_arm;
let de_dv = -(c_gw * delta_t);
let de_dbg = -(c_gw * c_ws_k * preint.dp_db_g());
let de_dba = -(c_gw * c_ws_k * preint.c_doubleintegral());
let de_drho_gw = -c_gw;
let de_dtheta_gw = c_gw * t_ant_hat;
let mut j_raw = SMatrix::<f64, 3, 21>::zeros();
j_raw.fixed_view_mut::<3, 3>(0, 0).copy_from(&de_drho_ws);
j_raw.fixed_view_mut::<3, 3>(0, 3).copy_from(&de_dtheta_ws);
j_raw.fixed_view_mut::<3, 3>(0, 6).copy_from(&de_dv);
j_raw.fixed_view_mut::<3, 3>(0, 9).copy_from(&de_dbg);
j_raw.fixed_view_mut::<3, 3>(0, 12).copy_from(&de_dba);
j_raw.fixed_view_mut::<3, 3>(0, 15).copy_from(&de_drho_gw);
j_raw.fixed_view_mut::<3, 3>(0, 18).copy_from(&de_dtheta_gw);
let j_weighted = self.sqrt_information * j_raw;
for row in 0..3 {
for col in 0..21 {
*jac.rb_mut().get_mut(row, col) = j_weighted[(row, col)];
}
}
}
fn residual_dim(&self) -> usize {
3
}
fn jacobian_shape(&self) -> (usize, usize) {
(3, 21)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
expect_block_sizes(
variables,
&[SE3::REP_SIZE, 9, SE3::REP_SIZE],
"GpsAsyncFactor expects [SE3 T_WS, speed-and-bias (9D), SE3 T_GW]",
)
}
fn whitens_internally(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use apex_manifolds::LieGroup;
use apex_manifolds::Tangent;
use apex_manifolds::se3::{SE3, SE3Tangent};
use nalgebra::{DMatrix, DVector, UnitQuaternion, Vector3};
use crate::factors::imu::preintegration::ImuPreintegration;
use crate::factors::imu::types::{
ImuMeasurement, ImuParameters, ImuSensorReadings, SpeedAndBias,
};
fn identity_pose() -> DVector<f64> {
DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0])
}
fn make_pose(tx: f64, ty: f64, tz: f64, q: UnitQuaternion<f64>) -> DVector<f64> {
let q = q.quaternion();
DVector::from_vec(vec![tx, ty, tz, q.w, q.i, q.j, q.k])
}
fn perturb_se3(pose: &[f64], tangent: &[f64; 6]) -> DVector<f64> {
let se3 = SE3::from_param_slice(pose);
let tan = SE3Tangent::from_slice(tangent);
DVector::from_column_slice(se3.right_plus(&tan, None, None).as_param_slice())
}
fn compute_residual_gps(factor: &GpsFactor, pose_ws: &[f64], pose_gw: &[f64]) -> Vec<f64> {
let mut residual = vec![0.0f64; factor.residual_dim()];
factor.linearize(&[pose_ws, pose_gw], &mut residual, None);
residual
}
fn compute_with_jacobian_gps(
factor: &GpsFactor,
pose_ws: &[f64],
pose_gw: &[f64],
) -> (Vec<f64>, DMatrix<f64>) {
let (rows, cols) = factor.jacobian_shape();
let mut residual = vec![0.0f64; rows];
let mut jac_buf = vec![0.0f64; rows * cols];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
factor.linearize(&[pose_ws, pose_gw], &mut residual, Some(jac_mut));
let jacobian = DMatrix::from_column_slice(rows, cols, &jac_buf);
(residual, jacobian)
}
fn compute_residual_async(
factor: &GpsAsyncFactor,
pose_ws: &[f64],
sb: &[f64],
pose_gw: &[f64],
) -> Vec<f64> {
let mut residual = vec![0.0f64; factor.residual_dim()];
factor.linearize(&[pose_ws, sb, pose_gw], &mut residual, None);
residual
}
fn compute_with_jacobian_async(
factor: &GpsAsyncFactor,
pose_ws: &[f64],
sb: &[f64],
pose_gw: &[f64],
) -> (Vec<f64>, DMatrix<f64>) {
let (rows, cols) = factor.jacobian_shape();
let mut residual = vec![0.0f64; rows];
let mut jac_buf = vec![0.0f64; rows * cols];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
factor.linearize(&[pose_ws, sb, pose_gw], &mut residual, Some(jac_mut));
let jacobian = DMatrix::from_column_slice(rows, cols, &jac_buf);
(residual, jacobian)
}
#[test]
fn zero_residual_when_antenna_at_origin() {
let t_ws_pos = Vector3::new(1.0, 2.0, 3.0);
let pose_ws = make_pose(
t_ws_pos.x,
t_ws_pos.y,
t_ws_pos.z,
UnitQuaternion::identity(),
);
let pose_gw = identity_pose();
let measurement = t_ws_pos; let factor = GpsFactor::new_isotropic(measurement, Vector3::zeros(), 1.0);
let r = compute_residual_gps(&factor, pose_ws.as_slice(), pose_gw.as_slice());
for (i, ri) in r.iter().enumerate().take(3) {
assert!(ri.abs() < 1e-12, "residual[{i}] = {} should be zero", ri);
}
}
#[test]
fn zero_residual_with_antenna_offset() {
let t_ws = Vector3::new(0.5, -1.0, 2.0);
let r_sa = Vector3::new(0.1, 0.0, 0.05);
let q_ws = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)),
0.3,
);
let c_ws = q_ws.to_rotation_matrix().into_inner();
let q_gw = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(1.0, 0.0, 0.0)),
0.1,
);
let t_gw_pos = Vector3::new(100.0, 200.0, 0.0);
let c_gw = q_gw.to_rotation_matrix().into_inner();
let t_antenna_w = t_ws + c_ws * r_sa;
let measurement = c_gw * t_antenna_w + t_gw_pos;
let pose_ws = make_pose(t_ws.x, t_ws.y, t_ws.z, q_ws);
let pose_gw = make_pose(t_gw_pos.x, t_gw_pos.y, t_gw_pos.z, q_gw);
let factor = GpsFactor::new_isotropic(measurement, r_sa, 1.0);
let r = compute_residual_gps(&factor, pose_ws.as_slice(), pose_gw.as_slice());
for (i, ri) in r.iter().enumerate().take(3) {
assert!(ri.abs() < 1e-10, "residual[{i}] = {} should be zero", ri);
}
}
#[test]
fn finite_difference_jacobians() {
let t_ws = Vector3::new(1.0, -0.5, 0.3);
let r_sa = Vector3::new(0.15, 0.05, -0.02);
let q_ws = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
0.25,
);
let c_ws = q_ws.to_rotation_matrix().into_inner();
let q_gw = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)),
-0.15,
);
let t_gw_pos = Vector3::new(50.0, 30.0, -5.0);
let c_gw = q_gw.to_rotation_matrix().into_inner();
let t_antenna_w = t_ws + c_ws * r_sa;
let measurement = c_gw * t_antenna_w + t_gw_pos;
let pose_ws = make_pose(t_ws.x, t_ws.y, t_ws.z, q_ws);
let pose_gw = make_pose(t_gw_pos.x, t_gw_pos.y, t_gw_pos.z, q_gw);
let factor = GpsFactor::new_isotropic(measurement, r_sa, 0.5);
let (r0, jac) = compute_with_jacobian_gps(&factor, pose_ws.as_slice(), pose_gw.as_slice());
const EPS: f64 = 1e-6;
const TOL: f64 = 1e-4;
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let pose_ws_p = perturb_se3(pose_ws.as_slice(), &tan);
let r_pert = compute_residual_gps(&factor, pose_ws_p.as_slice(), pose_gw.as_slice());
for row in 0..3 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, col)]).abs();
assert!(
err < TOL,
"J_T_WS[{row},{col}]: analytical={:.6} fd={:.6} err={err:.2e}",
jac[(row, col)],
fd
);
}
}
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let pose_gw_p = perturb_se3(pose_gw.as_slice(), &tan);
let r_pert = compute_residual_gps(&factor, pose_ws.as_slice(), pose_gw_p.as_slice());
for row in 0..3 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, 6 + col)]).abs();
assert!(
err < TOL,
"J_T_GW[{row},{col}]: analytical={:.6} fd={:.6} err={err:.2e}",
jac[(row, 6 + col)],
fd
);
}
}
}
fn make_measurements(dt: f64) -> Vec<ImuMeasurement> {
let g = ImuParameters::default().g;
let n = 21usize;
let dt_step = dt / (n - 1) as f64;
(0..n)
.map(|i| ImuMeasurement {
timestamp: i as f64 * dt_step,
measurement: ImuSensorReadings {
gyroscopes: Vector3::zeros(),
accelerometers: Vector3::new(0.0, 0.0, g), },
})
.collect()
}
fn make_preint(dt: f64, v_world: Vector3<f64>) -> ImuPreintegration {
let params = ImuParameters::default();
let measurements = make_measurements(dt);
let mut sb = SpeedAndBias::zeros();
sb[0] = v_world.x;
sb[1] = v_world.y;
sb[2] = v_world.z;
ImuPreintegration::new(measurements, params, 0.0, dt, &sb)
}
fn perturb_sb(sb: &[f64], idx: usize, eps: f64) -> DVector<f64> {
let mut out = DVector::from_column_slice(sb);
out[idx] += eps;
out
}
#[test]
fn async_zero_residual() {
let dt = 0.1;
let v_k = Vector3::new(0.5, -0.2, 0.0);
let preint = make_preint(dt, v_k);
let t_ws_pos = Vector3::new(1.0, 2.0, 0.5);
let q_ws = UnitQuaternion::identity();
let r_sa = Vector3::zeros();
let params = ImuParameters::default();
let mut pose_at_gps = SE3::new(t_ws_pos, q_ws);
let mut sb_at_gps = {
let mut sb = SpeedAndBias::zeros();
sb[0] = v_k.x;
sb[1] = v_k.y;
sb[2] = v_k.z;
sb
};
ImuPreintegration::propagation(
&make_measurements(dt),
¶ms,
&mut pose_at_gps,
&mut sb_at_gps,
0.0,
dt,
);
let t_antenna_w =
pose_at_gps.translation() + pose_at_gps.rotation_so3().rotation_matrix() * r_sa;
let measurement = t_antenna_w;
let pose_ws = make_pose(t_ws_pos.x, t_ws_pos.y, t_ws_pos.z, q_ws);
let pose_gw = identity_pose();
let mut sb_vec = DVector::zeros(9);
sb_vec[0] = v_k.x;
sb_vec[1] = v_k.y;
sb_vec[2] = v_k.z;
let factor = GpsAsyncFactor::new_isotropic(measurement, r_sa, 1.0, preint);
let r = compute_residual_async(
&factor,
pose_ws.as_slice(),
sb_vec.as_slice(),
pose_gw.as_slice(),
);
for (i, ri) in r.iter().enumerate().take(3) {
assert!(
ri.abs() < 1e-6,
"async residual[{i}] = {} should be near zero",
ri
);
}
}
#[test]
fn async_finite_difference_jacobians() {
let dt = 0.05;
let v_k = Vector3::new(0.3, -0.1, 0.05);
let r_sa = Vector3::new(0.1, 0.0, 0.05);
let q_ws = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
0.2,
);
let t_ws_pos = Vector3::new(1.0, -0.5, 0.3);
let c_ws_k: nalgebra::Matrix3<f64> = q_ws.to_rotation_matrix().into_inner();
let q_gw = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)),
-0.1,
);
let t_gw_pos = Vector3::new(50.0, 30.0, -2.0);
let c_gw: nalgebra::Matrix3<f64> = q_gw.to_rotation_matrix().into_inner();
let preint = make_preint(dt, v_k);
let delta_p_body = *preint.acc_doubleintegral();
let delta_r: nalgebra::Matrix3<f64> = preint.delta_q().to_rotation_matrix().into_inner();
let p_propagated = t_ws_pos + v_k * dt + c_ws_k * delta_p_body;
let c_ws_g = c_ws_k * delta_r;
let t_antenna_w = p_propagated + c_ws_g * r_sa;
let measurement = c_gw * t_antenna_w + t_gw_pos;
let pose_ws = make_pose(t_ws_pos.x, t_ws_pos.y, t_ws_pos.z, q_ws);
let pose_gw = make_pose(t_gw_pos.x, t_gw_pos.y, t_gw_pos.z, q_gw);
let mut sb_vec = DVector::zeros(9);
sb_vec[0] = v_k.x;
sb_vec[1] = v_k.y;
sb_vec[2] = v_k.z;
let make_factor =
|| GpsAsyncFactor::new_isotropic(measurement, r_sa, 0.5, make_preint(dt, v_k));
let factor = make_factor();
let (r0, jac) = compute_with_jacobian_async(
&factor,
pose_ws.as_slice(),
sb_vec.as_slice(),
pose_gw.as_slice(),
);
const EPS: f64 = 1e-6;
const TOL: f64 = 1e-4;
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let pose_ws_p = perturb_se3(pose_ws.as_slice(), &tan);
let r_pert = compute_residual_async(
&make_factor(),
pose_ws_p.as_slice(),
sb_vec.as_slice(),
pose_gw.as_slice(),
);
for row in 0..3 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, col)]).abs();
assert!(
err < TOL,
"async J_T_WS[{row},{col}]: analytical={:.6} fd={:.6} err={err:.2e}",
jac[(row, col)],
fd
);
}
}
for col in 0..9 {
let sb_p = perturb_sb(sb_vec.as_slice(), col, EPS);
let r_pert = compute_residual_async(
&make_factor(),
pose_ws.as_slice(),
sb_p.as_slice(),
pose_gw.as_slice(),
);
for row in 0..3 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, 6 + col)]).abs();
assert!(
err < TOL,
"async J_sb[{row},{col}]: analytical={:.6} fd={:.6} err={err:.2e}",
jac[(row, 6 + col)],
fd
);
}
}
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let pose_gw_p = perturb_se3(pose_gw.as_slice(), &tan);
let r_pert = compute_residual_async(
&make_factor(),
pose_ws.as_slice(),
sb_vec.as_slice(),
pose_gw_p.as_slice(),
);
for row in 0..3 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, 15 + col)]).abs();
assert!(
err < TOL,
"async J_T_GW[{row},{col}]: analytical={:.6} fd={:.6} err={err:.2e}",
jac[(row, 15 + col)],
fd
);
}
}
}
}