use super::BetweenFactor;
use crate::core::variable::ManifoldVariable;
use crate::factors::Factor;
use crate::factors::common::validate::expect_block_sizes;
use apex_manifolds::LieGroup;
use faer::prelude::ReborrowMut;
use nalgebra::DVector;
#[derive(Debug, Clone)]
pub struct PriorFactor<T>
where
T: LieGroup + Clone + Send + Sync,
{
prior: T,
between: BetweenFactor<T>,
}
impl<T> PriorFactor<T>
where
T: LieGroup + Clone + Send + Sync,
{
pub fn new(prior: T) -> Self {
let identity = prior.between(&prior, None, None);
Self {
prior,
between: BetweenFactor::new(identity),
}
}
}
impl<T> Factor for PriorFactor<T>
where
T: LieGroup + Clone + Send + Sync,
{
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
self.between.linearize_wrt_second(
self.prior.as_param_slice(),
params[0],
residual,
jacobian,
);
}
fn residual_dim(&self) -> usize {
self.prior.tangent_dim()
}
fn jacobian_shape(&self) -> (usize, usize) {
let dof = self.prior.tangent_dim();
(dof, dof)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
expect_block_sizes(
variables,
&[self.prior.as_param_slice().len()],
"PriorFactor expects one variable of the prior's own group",
)
}
}
#[derive(Debug, Clone)]
pub struct EuclideanPriorFactor {
pub data: DVector<f64>,
}
impl EuclideanPriorFactor {
pub fn new(data: DVector<f64>) -> Self {
Self { data }
}
}
impl Factor for EuclideanPriorFactor {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
let n = self.data.len();
for i in 0..n {
residual[i] = params[0][i] - self.data[i];
}
if let Some(mut jac) = jacobian {
for i in 0..n {
*jac.rb_mut().get_mut(i, i) = 1.0;
}
}
}
fn residual_dim(&self) -> usize {
self.data.len()
}
fn jacobian_shape(&self) -> (usize, usize) {
let n = self.data.len();
(n, n)
}
fn validate_variables(&self, variables: &[&dyn ManifoldVariable]) -> Result<(), String> {
for variable in variables {
if variable.manifold_type_name() != "Rn" {
return Err(format!(
"EuclideanPriorFactor operates on ambient parameters and is restricted to Rn \
variables, but was registered on a {} variable. Use PriorFactor<T> for \
Lie-group anchoring.",
variable.manifold_type_name()
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::factors::pose::BetweenFactor;
use apex_manifolds::se2::{SE2, SE2Tangent};
use apex_manifolds::se3::{SE3, SE3Tangent};
use nalgebra::{Quaternion, Vector3};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn fd_jacobian_se3(factor: &PriorFactor<SE3>, x: &SE3) -> Vec<Vec<f64>> {
let dof = 6;
let eps = 1e-6;
let mut fd = vec![vec![0.0; dof]; factor.residual_dim()];
for k in 0..dof {
let mut rho = Vector3::zeros();
let mut theta = Vector3::zeros();
if k < 3 {
rho[k] = eps;
} else {
theta[k - 3] = eps;
}
let step_p = SE3Tangent::new(rho, theta);
let step_m = SE3Tangent::new(-rho, -theta);
let x_p = x.plus(&step_p, None, None);
let x_m = x.plus(&step_m, None, None);
let mut rp = vec![0.0; factor.residual_dim()];
let mut rm = vec![0.0; factor.residual_dim()];
factor.linearize(&[x_p.as_param_slice()], &mut rp, None);
factor.linearize(&[x_m.as_param_slice()], &mut rm, None);
for i in 0..factor.residual_dim() {
fd[i][k] = (rp[i] - rm[i]) / (2.0 * eps);
}
}
fd
}
fn fd_jacobian_se2(factor: &PriorFactor<SE2>, x: &SE2) -> Vec<Vec<f64>> {
let dof = 3;
let eps = 1e-6;
let mut fd = vec![vec![0.0; dof]; factor.residual_dim()];
for k in 0..dof {
let mut c = [0.0f64; 3];
c[k] = eps;
let step_p = SE2Tangent::new(c[0], c[1], c[2]);
let step_m = SE2Tangent::new(-c[0], -c[1], -c[2]);
let x_p = x.plus(&step_p, None, None);
let x_m = x.plus(&step_m, None, None);
let mut rp = vec![0.0; factor.residual_dim()];
let mut rm = vec![0.0; factor.residual_dim()];
factor.linearize(&[x_p.as_param_slice()], &mut rp, None);
factor.linearize(&[x_m.as_param_slice()], &mut rm, None);
for i in 0..factor.residual_dim() {
fd[i][k] = (rp[i] - rm[i]) / (2.0 * eps);
}
}
fd
}
#[test]
fn prior_factor_se3_jacobian_matches_central_difference() -> TestResult {
let prior = SE3::from_translation_quaternion(
Vector3::new(0.2, -0.4, 0.7),
Quaternion::new(0.9, 0.1, -0.2, 0.3),
);
let x = SE3::from_translation_quaternion(
Vector3::new(1.0, 0.5, -0.3),
Quaternion::new(0.8, 0.2, 0.1, -0.4),
);
let factor = PriorFactor::new(prior.clone());
let (rows, cols) = factor.jacobian_shape();
let mut jac_buf = vec![0.0; rows * cols];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
let mut res_buf = vec![0.0; rows];
factor.linearize(&[x.as_param_slice()], &mut res_buf, Some(jac_mut));
let fd = fd_jacobian_se3(&factor, &x);
for i in 0..rows {
for k in 0..cols {
let analytic = jac_buf[k * rows + i]; assert!(
(analytic - fd[i][k]).abs() < 1e-5,
"SE3 prior J[{i}][{k}]: analytic {analytic} vs FD {}",
fd[i][k]
);
}
}
Ok(())
}
#[test]
fn prior_factor_se2_jacobian_matches_central_difference() -> TestResult {
let prior = SE2::from_xy_angle(0.7, -0.2, 1.2);
let x = SE2::from_xy_angle(-0.3, 0.9, -2.5);
let factor = PriorFactor::new(prior);
let (rows, cols) = factor.jacobian_shape();
let mut jac_buf = vec![0.0; rows * cols];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, rows, cols);
let mut res_buf = vec![0.0; rows];
factor.linearize(&[x.as_param_slice()], &mut res_buf, Some(jac_mut));
let fd = fd_jacobian_se2(&factor, &x);
for i in 0..rows {
for k in 0..cols {
let analytic = jac_buf[k * rows + i];
assert!(
(analytic - fd[i][k]).abs() < 1e-5,
"SE2 prior J[{i}][{k}]: analytic {analytic} vs FD {}",
fd[i][k]
);
}
}
Ok(())
}
#[test]
fn prior_factor_matches_between_with_identity_measurement() -> TestResult {
let prior = SE3::from_translation_quaternion(
Vector3::new(0.2, -0.4, 0.7),
Quaternion::new(0.9, 0.1, -0.2, 0.3),
);
let x = SE3::from_translation_quaternion(
Vector3::new(1.0, 0.5, -0.3),
Quaternion::new(0.8, 0.2, 0.1, -0.4),
);
let mut rp = vec![0.0; 6];
PriorFactor::<SE3>::new(prior.clone()).linearize(&[x.as_param_slice()], &mut rp, None);
let mut rb = vec![0.0; 6];
let between = BetweenFactor::new(SE3::identity());
between.linearize(&[prior.as_param_slice(), x.as_param_slice()], &mut rb, None);
for i in 0..6 {
assert!(
(rp[i] - rb[i]).abs() < 1e-12,
"row {i}: {} vs {}",
rp[i],
rb[i]
);
}
Ok(())
}
#[test]
fn prior_factor_se2_wraps_angle() -> TestResult {
let prior = SE2::from_xy_angle(0.0, 0.0, 3.1);
let x = SE2::from_xy_angle(0.0, 0.0, -3.1);
let mut r = vec![0.0; 3];
PriorFactor::<SE2>::new(prior).linearize(&[x.as_param_slice()], &mut r, None);
assert!(
r[2].abs() < 0.1,
"angle residual must wrap: got {} (expected ≈ -0.083)",
r[2]
);
Ok(())
}
#[test]
fn euclidean_prior_rejects_non_rn_variables() -> TestResult {
use crate::core::variable::{ManifoldVariable, Variable};
use apex_manifolds::se3::SE3;
use slotmap::SlotMap;
let mut variables: SlotMap<crate::core::VarKey, Box<dyn ManifoldVariable>> =
SlotMap::with_key();
let key = variables.insert(Box::new(Variable::new(SE3::identity())));
let factor = EuclideanPriorFactor::new(DVector::from_element(7, 0.0));
let err = factor
.validate_variables(&[variables.get(key).ok_or("key missing")?.as_ref()])
.err()
.ok_or("expected rejection")?;
assert!(err.contains("Rn"), "rejection must name Rn: {err}");
Ok(())
}
}