use faer::prelude::ReborrowMut;
use nalgebra::{Matrix3, SMatrix, Vector3};
use apex_manifolds::LieGroup;
use apex_manifolds::se3::SE3;
use crate::core::variable::ManifoldVariable;
use crate::factors::Factor;
use crate::factors::common::math::skew;
use crate::factors::common::validate::expect_block_sizes;
pub trait DistanceField: Send + Sync {
fn query(&self, point: &Vector3<f64>) -> Option<(f64, Vector3<f64>)>;
}
pub struct IcpFactor<F: DistanceField> {
field: F,
point_b: Vector3<f64>,
sigma_measurement: f64,
}
impl<F: DistanceField> IcpFactor<F> {
pub fn new(field: F, point_b: Vector3<f64>, sigma_measurement: f64) -> Self {
Self {
field,
point_b,
sigma_measurement,
}
}
}
impl<F: DistanceField> Factor for IcpFactor<F> {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
debug_assert_eq!(params.len(), 2, "IcpFactor 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_wa = SE3::from_param_slice(params[0]);
let t_wb = SE3::from_param_slice(params[1]);
let c_wa: Matrix3<f64> = t_wa.rotation_so3().rotation_matrix();
let c_wb: Matrix3<f64> = t_wb.rotation_so3().rotation_matrix();
let t_wa_pos = t_wa.translation();
let t_wb_pos = t_wb.translation();
let p_w = c_wb * self.point_b + t_wb_pos;
let p_a = c_wa.transpose() * (p_w - t_wa_pos);
let zero_out = |residual: &mut [f64], jacobian: Option<faer::mat::MatMut<'_, f64>>| {
residual[0] = 0.0;
if let Some(mut jac) = jacobian {
for col in 0..12 {
*jac.rb_mut().get_mut(0, col) = 0.0;
}
}
};
let Some((field_val, gradient)) = self.field.query(&p_a) else {
zero_out(residual, jacobian);
return;
};
let grad_norm = gradient.norm();
if grad_norm < 1e-3 {
zero_out(residual, jacobian);
return;
}
let sqrt_info = 1.0 / self.sigma_measurement;
let weighted_error = sqrt_info * field_val / grad_norm;
residual[0] = weighted_error;
let Some(mut jac) = jacobian else {
return;
};
let de_dp_a: SMatrix<f64, 1, 3> =
SMatrix::<f64, 1, 3>::from_iterator((sqrt_info * gradient / grad_norm).iter().copied());
let c_wa_t = c_wa.transpose();
let c_wa_t_c_wb = c_wa_t * c_wb;
let mut dpa_dtwa = SMatrix::<f64, 3, 6>::zeros();
dpa_dtwa
.fixed_view_mut::<3, 3>(0, 0)
.copy_from(&(-Matrix3::identity()));
dpa_dtwa.fixed_view_mut::<3, 3>(0, 3).copy_from(&skew(&p_a));
let mut dpa_dtwb = SMatrix::<f64, 3, 6>::zeros();
dpa_dtwb
.fixed_view_mut::<3, 3>(0, 0)
.copy_from(&c_wa_t_c_wb);
dpa_dtwb
.fixed_view_mut::<3, 3>(0, 3)
.copy_from(&(-c_wa_t_c_wb * skew(&self.point_b)));
let j_twa: SMatrix<f64, 1, 6> = de_dp_a * dpa_dtwa;
let j_twb: SMatrix<f64, 1, 6> = de_dp_a * dpa_dtwb;
let mut j_full = SMatrix::<f64, 1, 12>::zeros();
j_full.fixed_view_mut::<1, 6>(0, 0).copy_from(&j_twa);
j_full.fixed_view_mut::<1, 6>(0, 6).copy_from(&j_twb);
for col in 0..12 {
*jac.rb_mut().get_mut(0, col) = j_full[(0, col)];
}
}
fn residual_dim(&self) -> usize {
1
}
fn jacobian_shape(&self) -> (usize, usize) {
(1, 12)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
expect_block_sizes(
variables,
&[SE3::REP_SIZE, SE3::REP_SIZE],
"IcpFactor expects [SE3 T_WA, SE3 T_WB]",
)
}
fn whitens_internally(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use apex_manifolds::LieGroup;
use apex_manifolds::Tangent;
use apex_manifolds::se3::SE3Tangent;
use nalgebra::{DMatrix, DVector, UnitQuaternion, Vector3};
struct PlanarField {
normal: Vector3<f64>,
offset: f64,
}
impl DistanceField for PlanarField {
fn query(&self, point: &Vector3<f64>) -> Option<(f64, Vector3<f64>)> {
let val = self.normal.dot(point) - self.offset;
Some((val, self.normal))
}
}
struct SphereField {
center: Vector3<f64>,
radius: f64,
}
impl DistanceField for SphereField {
fn query(&self, point: &Vector3<f64>) -> Option<(f64, Vector3<f64>)> {
let diff = point - self.center;
let dist = diff.norm();
if dist < 1e-16 {
return None;
}
Some((dist - self.radius, diff / dist))
}
}
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 identity_pose() -> DVector<f64> {
DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0])
}
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<F: DistanceField>(
factor: &IcpFactor<F>,
t_wa: &[f64],
t_wb: &[f64],
) -> Vec<f64> {
let mut residual = vec![0.0f64; factor.residual_dim()];
factor.linearize(&[t_wa, t_wb], &mut residual, None);
residual
}
fn compute_with_jacobian<F: DistanceField>(
factor: &IcpFactor<F>,
t_wa: &[f64],
t_wb: &[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(&[t_wa, t_wb], &mut residual, Some(jac_mut));
let jacobian = DMatrix::from_column_slice(rows, cols, &jac_buf);
(residual, jacobian)
}
#[test]
fn zero_residual_on_plane() {
let field = PlanarField {
normal: Vector3::z(),
offset: 5.0,
};
let t_wa = identity_pose();
let t_wb = identity_pose();
let point_b = Vector3::new(1.0, 2.0, 5.0);
let factor = IcpFactor::new(field, point_b, 1.0);
let r = compute_residual(&factor, t_wa.as_slice(), t_wb.as_slice());
assert!(r[0].abs() < 1e-10, "residual = {} should be zero", r[0]);
}
#[test]
fn nonzero_residual_off_plane() {
let field = PlanarField {
normal: Vector3::z(),
offset: 5.0,
};
let t_wa = identity_pose();
let t_wb = identity_pose();
let point_b = Vector3::new(1.0, 2.0, 7.0);
let factor = IcpFactor::new(field, point_b, 1.0);
let r = compute_residual(&factor, t_wa.as_slice(), t_wb.as_slice());
assert!((r[0] - 2.0).abs() < 1e-10, "residual = {}", r[0]);
}
#[test]
fn finite_difference_jacobians_planar() {
let field = PlanarField {
normal: Vector3::new(0.0, 0.3, 0.9).normalize(),
offset: 3.0,
};
let q_a = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
0.2,
);
let q_b = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)),
-0.15,
);
let t_wa = make_pose(1.0, -0.5, 0.3, q_a);
let t_wb = make_pose(2.0, 1.0, -0.2, q_b);
let point_b = Vector3::new(0.5, -0.3, 4.0);
let factor = IcpFactor::new(field, point_b, 0.5);
let (r0, jac) = compute_with_jacobian(&factor, t_wa.as_slice(), t_wb.as_slice());
const EPS: f64 = 1e-7;
const TOL: f64 = 1e-4;
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let t_wa_p = perturb_se3(t_wa.as_slice(), &tan);
let r_pert = compute_residual(&factor, t_wa_p.as_slice(), t_wb.as_slice());
let fd = (r_pert[0] - r0[0]) / EPS;
crate::factors::common::test_utils::assert_close(
jac[(0, col)],
fd,
TOL,
&format!("J_T_WA[0,{col}]"),
);
}
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let t_wb_p = perturb_se3(t_wb.as_slice(), &tan);
let r_pert = compute_residual(&factor, t_wa.as_slice(), t_wb_p.as_slice());
let fd = (r_pert[0] - r0[0]) / EPS;
crate::factors::common::test_utils::assert_close(
jac[(0, 6 + col)],
fd,
TOL,
&format!("J_T_WB[0,{col}]"),
);
}
}
#[test]
fn finite_difference_jacobians_sphere() {
let field = SphereField {
center: Vector3::new(2.0, 1.0, 3.0),
radius: 1.0,
};
let q_a = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(1.0, 0.0, 0.0)),
0.1,
);
let q_b = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 1.0, 0.0)),
-0.2,
);
let t_wa = make_pose(0.5, 0.0, 0.0, q_a);
let t_wb = make_pose(-0.5, 1.0, 0.5, q_b);
let point_b = Vector3::new(1.0, 0.5, 2.0);
let factor = IcpFactor::new(field, point_b, 0.3);
let (r0, jac) = compute_with_jacobian(&factor, t_wa.as_slice(), t_wb.as_slice());
const EPS: f64 = 1e-7;
const TOL: f64 = 1e-4;
for block in 0..2 {
let pose = if block == 0 { &t_wa } else { &t_wb };
let col_offset = block * 6;
for col in 0..6 {
let mut tan = [0.0f64; 6];
tan[col] = EPS;
let pose_p = perturb_se3(pose.as_slice(), &tan);
let field2 = SphereField {
center: Vector3::new(2.0, 1.0, 3.0),
radius: 1.0,
};
let factor2 = IcpFactor::new(field2, point_b, 0.3);
let r_pert = if block == 0 {
compute_residual(&factor2, pose_p.as_slice(), t_wb.as_slice())
} else {
compute_residual(&factor2, t_wa.as_slice(), pose_p.as_slice())
};
let fd = (r_pert[0] - r0[0]) / EPS;
let err = (fd - jac[(0, col_offset + col)]).abs();
assert!(
err < TOL,
"J_block{block}[0,{col}]: analytical={:.8} fd={:.8} err={err:.2e}",
jac[(0, col_offset + col)],
fd
);
}
}
}
#[test]
fn dimension_is_one() {
let field = PlanarField {
normal: Vector3::z(),
offset: 0.0,
};
let factor = IcpFactor::new(field, Vector3::zeros(), 1.0);
assert_eq!(factor.residual_dim(), 1);
}
}