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;
fn tangent_basis(n: &Vector3<f64>) -> SMatrix<f64, 3, 2> {
let abs_n = Vector3::new(n[0].abs(), n[1].abs(), n[2].abs());
let axis = if abs_n[0] <= abs_n[1] && abs_n[0] <= abs_n[2] {
Vector3::x()
} else if abs_n[1] <= abs_n[2] {
Vector3::y()
} else {
Vector3::z()
};
let mut e1 = n.cross(&axis);
e1.normalize_mut();
let e2 = n.cross(&e1);
SMatrix::<f64, 3, 2>::from_columns(&[e1, e2])
}
pub struct BearingFactor {
measured_bearing: Vector3<f64>,
sqrt_information: SMatrix<f64, 2, 2>,
tangent_basis: SMatrix<f64, 3, 2>,
}
impl BearingFactor {
pub fn new(measured_bearing: Vector3<f64>, sqrt_information: SMatrix<f64, 2, 2>) -> Self {
let n = measured_bearing.normalize();
let basis = tangent_basis(&n);
Self {
measured_bearing: n,
sqrt_information,
tangent_basis: basis,
}
}
pub fn new_isotropic(measured_bearing: Vector3<f64>, sigma: f64) -> Self {
let sqrt_info = SMatrix::<f64, 2, 2>::identity() * (1.0 / sigma);
Self::new(measured_bearing, sqrt_info)
}
}
impl Factor for BearingFactor {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
debug_assert_eq!(params.len(), 2, "BearingFactor expects 2 parameter blocks");
debug_assert_eq!(params[0].len(), 7, "params[0] must be SE3 (7D)");
debug_assert_eq!(params[1].len(), 3, "params[1] must be 3D point");
let t_i = SE3::from_param_slice(params[0]);
let p_j = Vector3::new(params[1][0], params[1][1], params[1][2]);
let r_i = t_i.rotation_so3().rotation_matrix();
let t_i_pos = t_i.translation();
let p_body = r_i.transpose() * (p_j - t_i_pos);
let norm = p_body.norm();
if norm < 1e-16 {
residual[0] = 0.0;
residual[1] = 0.0;
if let Some(mut jac) = jacobian {
for row in 0..2 {
for col in 0..9 {
*jac.rb_mut().get_mut(row, col) = 0.0;
}
}
}
return;
}
let n_est = p_body / norm;
let e_3d = n_est - self.measured_bearing;
let e_2d = self.tangent_basis.transpose() * e_3d;
let weighted = self.sqrt_information * e_2d;
residual[0] = weighted[0];
residual[1] = weighted[1];
let Some(mut jac) = jacobian else {
return;
};
let dn_dp = (Matrix3::identity() - n_est * n_est.transpose()) / norm;
let prefix = self.sqrt_information * self.tangent_basis.transpose() * dn_dp;
let p_body_hat = skew(&p_body);
let j_rho = -prefix; let j_theta = prefix * p_body_hat;
let j_pj = prefix * r_i.transpose();
let mut j_full = SMatrix::<f64, 2, 9>::zeros();
j_full.fixed_view_mut::<2, 3>(0, 0).copy_from(&j_rho);
j_full.fixed_view_mut::<2, 3>(0, 3).copy_from(&j_theta);
j_full.fixed_view_mut::<2, 3>(0, 6).copy_from(&j_pj);
for row in 0..2 {
for col in 0..9 {
*jac.rb_mut().get_mut(row, col) = j_full[(row, col)];
}
}
}
fn residual_dim(&self) -> usize {
2
}
fn jacobian_shape(&self) -> (usize, usize) {
(2, 9)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
expect_block_sizes(
variables,
&[SE3::REP_SIZE, 3],
"BearingFactor expects [SE3 pose, 3D point]",
)
}
fn whitens_internally(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use apex_manifolds::Tangent;
use apex_manifolds::se3::SE3Tangent;
use nalgebra::{DMatrix, DVector, UnitQuaternion, Vector3};
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_bearing(pose: &[f64], point: &Vector3<f64>) -> Vector3<f64> {
let t_i = SE3::from_param_slice(pose);
let r_i = t_i.rotation_so3().rotation_matrix();
let t_pos = t_i.translation();
let p_body = r_i.transpose() * (point - t_pos);
p_body.normalize()
}
fn compute_residual(factor: &BearingFactor, pose: &[f64], point: &[f64]) -> Vec<f64> {
let mut residual = vec![0.0f64; factor.residual_dim()];
factor.linearize(&[pose, point], &mut residual, None);
residual
}
fn compute_with_jacobian(
factor: &BearingFactor,
pose: &[f64],
point: &[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, point], &mut residual, Some(jac_mut));
let jacobian = DMatrix::from_column_slice(rows, cols, &jac_buf);
(residual, jacobian)
}
#[test]
fn zero_residual() {
let q = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)),
0.5,
);
let pose = make_pose(1.0, 2.0, 0.5, q);
let point = Vector3::new(5.0, 3.0, 1.0);
let point_dv = DVector::from_iterator(3, point.iter().copied());
let bearing = compute_bearing(pose.as_slice(), &point);
let factor = BearingFactor::new_isotropic(bearing, 1.0);
let r = compute_residual(&factor, pose.as_slice(), point_dv.as_slice());
for (i, ri) in r.iter().enumerate().take(2) {
assert!(ri.abs() < 1e-10, "residual[{i}] = {} should be zero", ri);
}
}
#[test]
fn point_on_z_axis() {
let pose = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
let point = DVector::from_vec(vec![0.0, 0.0, 10.0]);
let bearing = Vector3::new(0.0, 0.0, 1.0);
let factor = BearingFactor::new_isotropic(bearing, 1.0);
let r = compute_residual(&factor, pose.as_slice(), point.as_slice());
for (i, ri) in r.iter().enumerate().take(2) {
assert!(ri.abs() < 1e-10, "residual[{i}] = {}", ri);
}
}
#[test]
fn finite_difference_jacobians() {
let q = UnitQuaternion::from_axis_angle(
&nalgebra::Unit::new_normalize(Vector3::new(0.3, 0.7, 0.1)),
0.4,
);
let pose = make_pose(1.0, -0.5, 2.0, q);
let point = Vector3::new(5.0, 3.0, 4.0);
let point_dv = DVector::from_iterator(3, point.iter().copied());
let bearing = compute_bearing(pose.as_slice(), &(point + Vector3::new(0.1, -0.05, 0.02)));
let factor = BearingFactor::new_isotropic(bearing, 0.5);
let (r0, jac) = compute_with_jacobian(&factor, pose.as_slice(), point_dv.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 pose_p = perturb_se3(pose.as_slice(), &tan);
let r_pert = compute_residual(&factor, pose_p.as_slice(), point_dv.as_slice());
for row in 0..2 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, col)]).abs();
assert!(
err < TOL,
"J_T_i[{row},{col}]: analytical={:.8} fd={:.8} err={err:.2e}",
jac[(row, col)],
fd
);
}
}
for col in 0..3 {
let mut point_p = point_dv.clone();
point_p[col] += EPS;
let r_pert = compute_residual(&factor, pose.as_slice(), point_p.as_slice());
for row in 0..2 {
let fd = (r_pert[row] - r0[row]) / EPS;
let err = (fd - jac[(row, 6 + col)]).abs();
assert!(
err < TOL,
"J_p_j[{row},{col}]: analytical={:.8} fd={:.8} err={err:.2e}",
jac[(row, 6 + col)],
fd
);
}
}
}
#[test]
fn tangent_basis_orthonormal() {
let dirs = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 1.0, 1.0).normalize(),
Vector3::new(-0.3, 0.7, 0.5).normalize(),
];
for n in &dirs {
let basis = tangent_basis(n);
let e1 = basis.column(0);
let e2 = basis.column(1);
assert!(e1.dot(n).abs() < 1e-12, "e1 not perpendicular to n");
assert!(e2.dot(n).abs() < 1e-12, "e2 not perpendicular to n");
assert!((e1.norm() - 1.0).abs() < 1e-12);
assert!((e2.norm() - 1.0).abs() < 1e-12);
assert!(e1.dot(&e2).abs() < 1e-12, "e1 and e2 not orthogonal");
}
}
#[test]
fn dimension_is_two() {
let factor = BearingFactor::new_isotropic(Vector3::z(), 1.0);
assert_eq!(factor.residual_dim(), 2);
}
}