use nalgebra::{Matrix3, Matrix6, Vector3, Vector6};
use super::series;
use super::so3::hat;
pub fn left_jacobian_so3(phi: &Vector3<f64>) -> Matrix3<f64> {
let theta = phi.norm();
let hat_phi = hat(phi);
Matrix3::identity()
+ hat_phi * series::one_minus_cos_over_theta_sq(theta)
+ hat_phi * hat_phi * series::theta_minus_sin_over_theta_cubed(theta)
}
pub fn inverse_left_jacobian_so3(phi: &Vector3<f64>) -> Matrix3<f64> {
let theta = phi.norm();
let hat_phi = hat(phi);
Matrix3::identity() - hat_phi * 0.5
+ hat_phi * hat_phi * series::inverse_left_jacobian_coef(theta)
}
fn q_block(rho: &Vector3<f64>, phi: &Vector3<f64>) -> Matrix3<f64> {
let theta = phi.norm();
let (p, r) = (hat(phi), hat(rho));
let (pr, rp) = (p * r, r * p);
let prp = pr * p;
0.5 * r
+ series::theta_minus_sin_over_theta_cubed(theta) * (pr + rp + prp)
+ series::q_second_coef(theta) * (p * pr + rp * p - 3.0 * prp)
+ series::q_third_coef(theta) * (prp * p + p * prp)
}
pub fn left_jacobian_se3(xi: &Vector6<f64>) -> Matrix6<f64> {
let rho = Vector3::new(xi[0], xi[1], xi[2]);
let phi = Vector3::new(xi[3], xi[4], xi[5]);
let j = left_jacobian_so3(&phi);
let mut out = Matrix6::zeros();
out.fixed_view_mut::<3, 3>(0, 0).copy_from(&j);
out.fixed_view_mut::<3, 3>(0, 3)
.copy_from(&q_block(&rho, &phi));
out.fixed_view_mut::<3, 3>(3, 3).copy_from(&j);
out
}
pub fn inverse_left_jacobian_se3(xi: &Vector6<f64>) -> Matrix6<f64> {
let rho = Vector3::new(xi[0], xi[1], xi[2]);
let phi = Vector3::new(xi[3], xi[4], xi[5]);
let inverse = inverse_left_jacobian_so3(&phi);
let mut out = Matrix6::zeros();
out.fixed_view_mut::<3, 3>(0, 0).copy_from(&inverse);
out.fixed_view_mut::<3, 3>(0, 3)
.copy_from(&(-inverse * q_block(&rho, &phi) * inverse));
out.fixed_view_mut::<3, 3>(3, 3).copy_from(&inverse);
out
}
pub fn right_jacobian_se3(xi: &Vector6<f64>) -> Matrix6<f64> {
left_jacobian_se3(&(-xi))
}
pub fn inverse_right_jacobian_se3(xi: &Vector6<f64>) -> Matrix6<f64> {
inverse_left_jacobian_se3(&(-xi))
}
#[cfg(test)]
mod se3_tests {
use super::*;
use crate::lie::Se3;
fn samples() -> Vec<Vector6<f64>> {
let mut out = Vec::new();
let near_pi = std::f64::consts::PI - 1e-3;
for theta in [0.0, 1e-6, 1e-3, 0.1, 0.499, 0.501, 1.0, 2.0, 3.0, near_pi] {
for (index, axis) in [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 2.0, -3.0).normalize(),
]
.into_iter()
.enumerate()
{
let phi = axis * theta;
let rho = Vector3::new(0.3, -1.7, 2.5) * (1.0 + index as f64);
out.push(Vector6::new(rho[0], rho[1], rho[2], phi[0], phi[1], phi[2]));
}
}
out
}
#[test]
fn the_left_jacobian_is_the_derivative_of_exp() {
const STEP: f64 = 1e-6;
for xi in samples() {
let jacobian = left_jacobian_se3(&xi);
let base = Se3::exp(&xi);
for axis in 0..6 {
let mut delta = Vector6::zeros();
delta[axis] = STEP;
let moved = Se3::exp(&(xi + delta));
let measured = (moved * base.inverse()).log();
let predicted = jacobian * delta;
let error = (measured - predicted).norm() / STEP;
assert!(
error < 1e-5,
"ξ = {xi:?}, axis {axis}: the Jacobian is off by {error} per unit step"
);
}
}
}
#[test]
fn the_right_jacobian_is_the_derivative_on_the_other_side() {
const STEP: f64 = 1e-6;
for xi in samples() {
let jacobian = right_jacobian_se3(&xi);
let base = Se3::exp(&xi);
for axis in 0..6 {
let mut delta = Vector6::zeros();
delta[axis] = STEP;
let moved = Se3::exp(&(xi + delta));
let measured = (base.inverse() * moved).log();
let predicted = jacobian * delta;
let error = (measured - predicted).norm() / STEP;
assert!(
error < 1e-5,
"ξ = {xi:?}, axis {axis}: off by {error} per unit step"
);
}
}
}
#[test]
fn the_inverse_undoes_the_jacobian() {
for xi in samples() {
let product = left_jacobian_se3(&xi) * inverse_left_jacobian_se3(&xi);
let error = (product - Matrix6::identity()).abs().max();
assert!(error < 1e-12, "ξ = {xi:?}: J·J⁻¹ is off by {error}");
let product = inverse_right_jacobian_se3(&xi) * right_jacobian_se3(&xi);
let error = (product - Matrix6::identity()).abs().max();
assert!(
error < 1e-12,
"ξ = {xi:?}: the right pair is off by {error}"
);
}
}
#[test]
fn without_rotation_the_answer_is_known_exactly() {
let rho = Vector3::new(0.4, -2.0, 7.5);
let xi = Vector6::new(rho[0], rho[1], rho[2], 0.0, 0.0, 0.0);
let jacobian = left_jacobian_se3(&xi);
assert!(
(jacobian.fixed_view::<3, 3>(0, 0) - Matrix3::identity())
.abs()
.max()
< 1e-15
);
assert!(
(jacobian.fixed_view::<3, 3>(3, 3) - Matrix3::identity())
.abs()
.max()
< 1e-15
);
assert!(
(jacobian.fixed_view::<3, 3>(0, 3) - 0.5 * hat(&rho))
.abs()
.max()
< 1e-15
);
assert_eq!(jacobian.fixed_view::<3, 3>(3, 0), Matrix3::zeros());
}
#[test]
fn q_branches_agree_at_threshold() {
let theta = series::THETA_SMALL_Q;
for (name, taylor, trig, scale) in [
(
"second",
series::taylor_q_second_coef(theta),
series::trig_q_second_coef(theta),
1.0 / 24.0,
),
(
"third",
series::taylor_q_third_coef(theta),
series::trig_q_third_coef(theta),
1.0 / 120.0,
),
] {
let relative = (taylor - trig).abs() / scale;
assert!(
relative < 1e-11,
"the {name} coefficient's branches differ by {relative} relative at the threshold"
);
}
}
}