use nalgebra::{DMatrix, DVector};
use crate::core::{
CoreResult, FactorKey, VarKey, corrector::Corrector, loss_functions::LossFunction,
variable::Variable,
};
use crate::factors::Factor;
use apex_manifolds::{LieGroup, Tangent};
pub struct ResidualBlock {
pub residual_block_id: FactorKey,
pub residual_row_start_idx: usize,
pub variable_keys: Vec<VarKey>,
pub factor: Box<dyn Factor + Send>,
pub loss_func: Option<Box<dyn LossFunction + Send>>,
}
impl ResidualBlock {
pub fn new(
residual_block_id: FactorKey,
residual_row_start_idx: usize,
variable_keys: &[VarKey],
factor: Box<dyn Factor + Send>,
loss_func: Option<Box<dyn LossFunction + Send>>,
) -> Self {
ResidualBlock {
residual_block_id,
residual_row_start_idx,
variable_keys: variable_keys.to_vec(),
factor,
loss_func,
}
}
pub fn residual_and_jacobian<M>(
&self,
variables: &[&Variable<M>],
) -> CoreResult<(DVector<f64>, DMatrix<f64>)>
where
M: LieGroup + Clone,
M::TangentVector: Tangent<M>,
{
let param_owned: Vec<M> = variables.iter().map(|v| v.value.clone()).collect();
let param_slices: Vec<&[f64]> = param_owned.iter().map(|v| v.as_param_slice()).collect();
let res_dim = self.factor.residual_dim();
let (jac_rows, jac_cols) = self.factor.jacobian_shape();
let mut residual_buf = vec![0.0f64; res_dim];
let mut jacobian_buf = vec![0.0f64; jac_rows * jac_cols];
let jac_mut =
faer::mat::MatMut::from_column_major_slice_mut(&mut jacobian_buf, jac_rows, jac_cols);
self.factor
.linearize(¶m_slices, &mut residual_buf, Some(jac_mut));
let mut residual = DVector::from_vec(residual_buf);
let mut jacobian = DMatrix::from_column_slice(jac_rows, jac_cols, &jacobian_buf);
if let Some(loss_func) = self.loss_func.as_ref() {
let squared_norm = residual.norm_squared();
let corrector = Corrector::new(loss_func.as_ref(), squared_norm);
corrector.correct_jacobian(&residual, &mut jacobian);
corrector.correct_residuals(&mut residual);
}
Ok((residual, jacobian))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{
loss_functions::{HuberLoss, LossFunction},
variable::Variable,
};
use crate::factors::{BetweenFactor, PriorFactor};
use apex_manifolds::{se2::SE2, se3::SE3};
use nalgebra::{Quaternion, dvector, vector};
use slotmap::SlotMap;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn make_keys(n_vars: usize) -> (FactorKey, Vec<VarKey>) {
let mut fac_sm: SlotMap<FactorKey, ()> = SlotMap::with_key();
let fk = fac_sm.insert(());
let mut var_sm: SlotMap<VarKey, ()> = SlotMap::with_key();
let keys = (0..n_vars).map(|_| var_sm.insert(())).collect();
(fk, keys)
}
#[test]
fn test_residual_block_creation() -> TestResult {
let (fk, keys) = make_keys(2);
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1)));
let loss = Some(Box::new(HuberLoss::new(1.0)?) as Box<dyn LossFunction + Send>);
let block = ResidualBlock::new(fk, 0, &keys, factor, loss);
assert_eq!(block.residual_block_id, fk);
assert_eq!(block.residual_row_start_idx, 0);
assert_eq!(block.variable_keys, keys);
assert!(block.loss_func.is_some());
Ok(())
}
#[test]
fn test_residual_block_without_loss() -> TestResult {
let (fk, keys) = make_keys(1);
let factor = Box::new(PriorFactor {
data: dvector![0.0, 0.0, 0.0],
});
let block = ResidualBlock::new(fk, 3, &keys, factor, None);
assert_eq!(block.residual_block_id, fk);
assert_eq!(block.residual_row_start_idx, 3);
assert_eq!(block.variable_keys, keys);
assert!(block.loss_func.is_none());
Ok(())
}
#[test]
fn test_residual_and_jacobian_se2_between_factor() -> TestResult {
let (fk, keys) = make_keys(2);
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.5, 0.1)));
let block = ResidualBlock::new(fk, 0, &keys, factor, None);
let var0 = Variable::new(SE2::from_xy_angle(0.0, 0.0, 0.0));
let var1 = Variable::new(SE2::from_xy_angle(1.0, 0.5, 0.1));
let variables = vec![&var0, &var1];
let (residual, jacobian) = block.residual_and_jacobian(&variables)?;
assert_eq!(residual.len(), 3);
assert_eq!(jacobian.nrows(), 3);
assert_eq!(jacobian.ncols(), 6);
assert!(
residual.norm() < 1e-10,
"Residual norm: {}",
residual.norm()
);
assert!(jacobian.norm() > 1e-10, "Jacobian should not be near zero");
Ok(())
}
#[test]
fn test_residual_and_jacobian_with_huber_loss() -> TestResult {
let (fk, keys) = make_keys(2);
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.0)));
let loss = Some(Box::new(HuberLoss::new(1.0)?) as Box<dyn LossFunction + Send>);
let block = ResidualBlock::new(fk, 0, &keys, factor, loss);
let var0 = Variable::new(SE2::from_xy_angle(0.0, 0.0, 0.0));
let var1 = Variable::new(SE2::from_xy_angle(5.0, 5.0, 2.0));
let variables = vec![&var0, &var1];
let (residual_with_loss, jacobian_with_loss) = block.residual_and_jacobian(&variables)?;
let (fk2, keys2) = make_keys(2);
let factor_no_loss = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.0)));
let block_no_loss = ResidualBlock::new(fk2, 0, &keys2, factor_no_loss, None);
let (residual_no_loss, jacobian_no_loss) =
block_no_loss.residual_and_jacobian(&variables)?;
assert!((residual_with_loss - residual_no_loss).norm() > 1e-10);
assert!((jacobian_with_loss - jacobian_no_loss).norm() > 1e-10);
Ok(())
}
#[test]
fn test_residual_block_se3_between_factor() -> TestResult {
let (fk, keys) = make_keys(1);
let se3_data = dvector![1.0, 0.5, 0.2, 1.0, 0.0, 0.0, 0.0];
let factor = Box::new(PriorFactor { data: se3_data });
let block = ResidualBlock::new(fk, 0, &keys, factor, None);
let var0 = Variable::new(SE3::from_translation_quaternion(
vector![1.0, 0.5, 0.2],
Quaternion::new(1.0, 0.0, 0.0, 0.0),
));
let variables = vec![&var0];
let (residual, jacobian) = block.residual_and_jacobian(&variables)?;
assert_eq!(residual.len(), 7);
assert_eq!(jacobian.nrows(), 7);
assert!(jacobian.ncols() == 6 || jacobian.ncols() == 7);
Ok(())
}
#[test]
fn test_multiple_residual_blocks_different_ids() -> TestResult {
let mut fac_sm: SlotMap<FactorKey, ()> = SlotMap::with_key();
let mut var_sm: SlotMap<VarKey, ()> = SlotMap::with_key();
let k0 = var_sm.insert(());
let k1 = var_sm.insert(());
let configs: Vec<(FactorKey, usize, Vec<VarKey>, bool)> = vec![
(fac_sm.insert(()), 0, vec![k0, k1], false),
(fac_sm.insert(()), 3, vec![k0, k1], true),
(fac_sm.insert(()), 6, vec![k0], false),
];
let factors: Vec<Box<dyn Factor + Send>> = vec![
Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1))),
Box::new(BetweenFactor::new(SE2::from_xy_angle(0.8, 0.2, -0.05))),
Box::new(PriorFactor {
data: dvector![0.0, 0.0, 0.0],
}),
];
let blocks: Vec<ResidualBlock> = configs
.iter()
.zip(factors)
.map(|((fk, row, keys, has_loss), factor)| -> Result<ResidualBlock, Box<dyn std::error::Error>> {
Ok(ResidualBlock::new(
*fk,
*row,
keys,
factor,
if *has_loss { Some(Box::new(HuberLoss::new(0.5)?)) } else { None },
))
})
.collect::<Result<Vec<_>, _>>()?;
for (i, (block, (fk, row, keys, has_loss))) in blocks.iter().zip(configs.iter()).enumerate()
{
assert_eq!(block.residual_block_id, *fk);
assert_eq!(block.residual_row_start_idx, *row);
assert_eq!(block.variable_keys.len(), keys.len(), "block {i}");
assert_eq!(block.loss_func.is_some(), *has_loss, "block {i}");
}
Ok(())
}
#[test]
fn test_residual_block_variable_ordering() -> TestResult {
let mut fac_sm: SlotMap<FactorKey, ()> = SlotMap::with_key();
let mut var_sm: SlotMap<VarKey, ()> = SlotMap::with_key();
let fk = fac_sm.insert(());
let k2 = var_sm.insert(());
let k1 = var_sm.insert(());
let k0 = var_sm.insert(());
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1)));
let block = ResidualBlock::new(fk, 0, &[k2, k1, k0], factor, None);
assert_eq!(block.variable_keys, vec![k2, k1, k0]);
Ok(())
}
#[test]
fn test_residual_block_numerical_stability() -> TestResult {
let (fk, keys) = make_keys(2);
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1e-8, 1e-8, 1e-8)));
let block = ResidualBlock::new(fk, 0, &keys, factor, None);
let var0 = Variable::new(SE2::from_xy_angle(0.0, 0.0, 0.0));
let var1 = Variable::new(SE2::from_xy_angle(1e-8, 1e-8, 1e-8));
let variables = vec![&var0, &var1];
let (residual, jacobian) = block.residual_and_jacobian(&variables)?;
assert!(residual.iter().all(|&x| x.is_finite()));
assert!(jacobian.iter().all(|&x| x.is_finite()));
assert!(residual.norm() < 1e-6);
Ok(())
}
#[test]
fn test_residual_block_large_values() -> TestResult {
let (fk, keys) = make_keys(2);
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(100.0, -200.0, 1.5)));
let block = ResidualBlock::new(fk, 0, &keys, factor, None);
let var0 = Variable::new(SE2::from_xy_angle(0.0, 0.0, 0.0));
let var1 = Variable::new(SE2::from_xy_angle(100.0, -200.0, 1.5));
let variables = vec![&var0, &var1];
let (residual, jacobian) = block.residual_and_jacobian(&variables)?;
assert!(residual.iter().all(|&x| x.is_finite()));
assert!(jacobian.iter().all(|&x| x.is_finite()));
assert!(residual.norm() < 1e-10);
Ok(())
}
#[test]
fn test_residual_block_loss_function_switching() -> TestResult {
let (fk1, keys1) = make_keys(2);
let (fk2, keys2) = make_keys(2);
let factor1 = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1)));
let factor2 = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1)));
let block_with_loss = ResidualBlock::new(
fk1,
0,
&keys1,
factor1,
Some(Box::new(HuberLoss::new(0.1)?)),
);
let block_without_loss = ResidualBlock::new(fk2, 0, &keys2, factor2, None);
let var0 = Variable::new(SE2::from_xy_angle(0.0, 0.0, 0.0));
let var1 = Variable::new(SE2::from_xy_angle(2.0, 1.0, 0.2));
let variables = vec![&var0, &var1];
let (res_with, jac_with) = block_with_loss.residual_and_jacobian(&variables)?;
let (res_without, jac_without) = block_without_loss.residual_and_jacobian(&variables)?;
assert!((res_with.clone() - res_without.clone()).norm() > 1e-6);
assert!((jac_with.clone() - jac_without.clone()).norm() > 1e-6);
assert!(res_with.norm() < res_without.norm());
Ok(())
}
}