use nalgebra::{DMatrix, DVector};
use crate::core::{
CoreError, CoreResult, corrector::Corrector, loss_functions::LossFunction, variable::Variable,
};
use crate::factors::Factor;
use apex_manifolds::{LieGroup, Tangent};
pub struct ResidualBlock {
pub residual_block_id: usize,
pub residual_row_start_idx: usize,
pub variable_key_list: Vec<String>,
pub factor: Box<dyn Factor + Send>,
pub loss_func: Option<Box<dyn LossFunction + Send>>,
}
impl ResidualBlock {
pub fn new(
residual_block_id: usize,
residual_row_start_idx: usize,
variable_key_size_list: &[&str],
factor: Box<dyn Factor + Send>,
loss_func: Option<Box<dyn LossFunction + Send>>,
) -> Self {
ResidualBlock {
residual_block_id,
residual_row_start_idx,
variable_key_list: variable_key_size_list
.iter()
.map(|s| s.to_string())
.collect(),
factor,
loss_func,
}
}
pub fn residual_and_jacobian<M>(
&self,
variables: &[&Variable<M>],
) -> CoreResult<(DVector<f64>, DMatrix<f64>)>
where
M: LieGroup + Clone + Into<DVector<f64>>,
M::TangentVector: Tangent<M>,
{
let param_vec: Vec<_> = variables.iter().map(|v| v.value.clone().into()).collect();
let (mut residual, jacobian_opt) = self.factor.linearize(¶m_vec, true);
let mut jacobian = jacobian_opt.ok_or_else(|| {
CoreError::FactorLinearization(
"Factor returned None for Jacobian when compute_jacobian=true".to_string(),
)
.log()
})?;
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};
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn test_residual_block_creation() -> TestResult {
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(0, 0, &["x0", "x1"], factor, loss);
assert_eq!(block.residual_block_id, 0);
assert_eq!(block.residual_row_start_idx, 0);
assert_eq!(block.variable_key_list, vec!["x0", "x1"]);
assert!(block.loss_func.is_some());
Ok(())
}
#[test]
fn test_residual_block_without_loss() -> TestResult {
let factor = Box::new(PriorFactor {
data: dvector![0.0, 0.0, 0.0],
});
let block = ResidualBlock::new(1, 3, &["x0"], factor, None);
assert_eq!(block.residual_block_id, 1);
assert_eq!(block.residual_row_start_idx, 3);
assert_eq!(block.variable_key_list, vec!["x0"]);
assert!(block.loss_func.is_none());
Ok(())
}
#[test]
fn test_residual_and_jacobian_se2_between_factor() -> TestResult {
let dx = 1.0;
let dy = 0.5;
let dtheta = 0.1;
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(dx, dy, dtheta)));
let block = ResidualBlock::new(0, 0, &["x0", "x1"], 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 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(0, 0, &["x0", "x1"], 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 factor_no_loss = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.0)));
let block_no_loss = ResidualBlock::new(0, 0, &["x0", "x1"], factor_no_loss, None);
let (residual_no_loss, jacobian_no_loss) =
block_no_loss.residual_and_jacobian(&variables)?;
let residual_diff = (residual_with_loss - residual_no_loss).norm();
assert!(
residual_diff > 1e-10,
"Loss function should modify residuals"
);
let jacobian_diff = (jacobian_with_loss - jacobian_no_loss).norm();
assert!(
jacobian_diff > 1e-10,
"Loss function should modify Jacobian"
);
Ok(())
}
#[test]
fn test_residual_block_se3_between_factor() -> TestResult {
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.clone(),
});
let block = ResidualBlock::new(0, 0, &["x0"], 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 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> = factors
.into_iter()
.enumerate()
.map(
|(i, factor)| -> Result<ResidualBlock, Box<dyn std::error::Error>> {
Ok(ResidualBlock::new(
i,
i * 3, if i == 2 { &["x0"] } else { &["x0", "x1"] },
factor,
if i == 1 {
Some(Box::new(HuberLoss::new(0.5)?))
} else {
None
},
))
},
)
.collect::<Result<Vec<_>, _>>()?;
for (i, block) in blocks.iter().enumerate() {
assert_eq!(block.residual_block_id, i);
assert_eq!(block.residual_row_start_idx, i * 3);
if i == 2 {
assert_eq!(block.variable_key_list.len(), 1);
assert!(block.loss_func.is_none());
} else {
assert_eq!(block.variable_key_list.len(), 2);
assert_eq!(block.loss_func.is_some(), i == 1);
}
}
Ok(())
}
#[test]
fn test_residual_block_variable_ordering() -> TestResult {
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1.0, 0.0, 0.1)));
let block = ResidualBlock::new(0, 0, &["pose_2", "pose_1", "pose_0"], factor, None);
let expected_order = vec!["pose_2", "pose_1", "pose_0"];
assert_eq!(block.variable_key_list, expected_order);
Ok(())
}
#[test]
fn test_residual_block_numerical_stability() -> TestResult {
let factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(1e-8, 1e-8, 1e-8)));
let block = ResidualBlock::new(0, 0, &["x0", "x1"], 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 factor = Box::new(BetweenFactor::new(SE2::from_xy_angle(100.0, -200.0, 1.5)));
let block = ResidualBlock::new(0, 0, &["x0", "x1"], 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 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(
0,
0,
&["x0", "x1"],
factor1,
Some(Box::new(HuberLoss::new(0.1)?)),
);
let block_without_loss = ResidualBlock::new(0, 0, &["x0", "x1"], 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(())
}
}