use apex_io::{G2oLoader, GraphLoader, ODOMETRY_DATA_DIR_2D, ODOMETRY_DATA_DIR_3D};
use apex_solver::JacobianMode;
use apex_solver::ManifoldType;
use apex_solver::core::loss_functions::HuberLoss;
use apex_solver::core::problem::Problem;
use apex_solver::factors::{BetweenFactor, PriorFactor};
use apex_solver::optimizer::OptimizationStatus;
use apex_solver::optimizer::levenberg_marquardt::{LevenbergMarquardt, LevenbergMarquardtConfig};
use nalgebra::dvector;
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug)]
struct TestResult {
vertices: usize,
edges: usize,
final_cost: f64,
improvement_pct: f64,
iterations: usize,
elapsed_time: Duration,
status: OptimizationStatus,
success: bool,
}
fn run_se3_optimization(
dataset_name: &str,
max_iterations: usize,
use_prior: bool,
) -> Result<TestResult, Box<dyn std::error::Error>> {
let dataset_path = format!("{}/{}.g2o", ODOMETRY_DATA_DIR_3D, dataset_name);
let graph = G2oLoader::load(&dataset_path)?;
let num_vertices = graph.vertices_se3.len();
let num_edges = graph.edges_se3.len();
let mut problem = Problem::new(JacobianMode::Sparse);
let mut initial_values = HashMap::new();
let mut vertex_ids: Vec<_> = graph.vertices_se3.keys().cloned().collect();
vertex_ids.sort();
for &id in &vertex_ids {
if let Some(vertex) = graph.vertices_se3.get(&id) {
let var_name = format!("x{}", id);
let quat = vertex.pose.rotation_quaternion();
let trans = vertex.pose.translation();
let se3_data = dvector![trans.x, trans.y, trans.z, quat.w, quat.i, quat.j, quat.k];
initial_values.insert(var_name, (ManifoldType::SE3, se3_data));
}
}
if use_prior
&& let Some(&first_id) = vertex_ids.first()
&& let Some(first_vertex) = graph.vertices_se3.get(&first_id)
{
let var_name = format!("x{}", first_id);
let quat = first_vertex.pose.rotation_quaternion();
let trans = first_vertex.pose.translation();
let prior_value = dvector![trans.x, trans.y, trans.z, quat.w, quat.i, quat.j, quat.k];
let prior_factor = PriorFactor {
data: prior_value.clone(),
};
let huber_loss = HuberLoss::new(1.0)?;
problem.add_residual_block(
&[&var_name],
Box::new(prior_factor),
Some(Box::new(huber_loss)),
);
}
for edge in &graph.edges_se3 {
let id0 = format!("x{}", edge.from);
let id1 = format!("x{}", edge.to);
let relative_pose = edge.measurement.clone();
let between_factor = BetweenFactor::new(relative_pose);
problem.add_residual_block(&[&id0, &id1], Box::new(between_factor), None);
}
let config = LevenbergMarquardtConfig::new()
.with_max_iterations(max_iterations)
.with_cost_tolerance(1e-4)
.with_parameter_tolerance(1e-4)
.with_damping(1e-3);
let mut solver = LevenbergMarquardt::with_config(config);
let start_time = Instant::now();
let result = solver.optimize(&problem, &initial_values)?;
let elapsed_time = start_time.elapsed();
let improvement_pct = if result.initial_cost > 0.0 {
((result.initial_cost - result.final_cost) / result.initial_cost) * 100.0
} else {
0.0
};
let success = matches!(
result.status,
OptimizationStatus::Converged
| OptimizationStatus::CostToleranceReached
| OptimizationStatus::ParameterToleranceReached
| OptimizationStatus::GradientToleranceReached
);
let test_result = TestResult {
vertices: num_vertices,
edges: num_edges,
final_cost: result.final_cost,
improvement_pct,
iterations: result.iterations,
elapsed_time,
status: result.status.clone(),
success,
};
Ok(test_result)
}
fn run_se2_optimization(
dataset_name: &str,
max_iterations: usize,
use_prior: bool,
) -> Result<TestResult, Box<dyn std::error::Error>> {
let dataset_path = format!("{}/{}.g2o", ODOMETRY_DATA_DIR_2D, dataset_name);
let graph = G2oLoader::load(&dataset_path)?;
let num_vertices = graph.vertices_se2.len();
let num_edges = graph.edges_se2.len();
let mut problem = Problem::new(JacobianMode::Sparse);
let mut initial_values = HashMap::new();
let mut vertex_ids: Vec<_> = graph.vertices_se2.keys().cloned().collect();
vertex_ids.sort();
for &id in &vertex_ids {
if let Some(vertex) = graph.vertices_se2.get(&id) {
let var_name = format!("x{}", id);
let pose = &vertex.pose;
let se2_data = dvector![pose.x(), pose.y(), pose.angle()];
initial_values.insert(var_name, (ManifoldType::SE2, se2_data));
}
}
if use_prior
&& let Some(&first_id) = vertex_ids.first()
&& let Some(first_vertex) = graph.vertices_se2.get(&first_id)
{
let var_name = format!("x{}", first_id);
let pose = &first_vertex.pose;
let prior_value = dvector![pose.x(), pose.y(), pose.angle()];
let prior_factor = PriorFactor {
data: prior_value.clone(),
};
let huber_loss = HuberLoss::new(1.0)?;
problem.add_residual_block(
&[&var_name],
Box::new(prior_factor),
Some(Box::new(huber_loss)),
);
}
for edge in &graph.edges_se2 {
let id0 = format!("x{}", edge.from);
let id1 = format!("x{}", edge.to);
let between_factor = BetweenFactor::new(edge.measurement.clone());
problem.add_residual_block(&[&id0, &id1], Box::new(between_factor), None);
}
let config = LevenbergMarquardtConfig::new()
.with_max_iterations(max_iterations)
.with_cost_tolerance(1e-4)
.with_parameter_tolerance(1e-4)
.with_damping(1e-3);
let mut solver = LevenbergMarquardt::with_config(config);
let start_time = Instant::now();
let result = solver.optimize(&problem, &initial_values)?;
let elapsed_time = start_time.elapsed();
let improvement_pct = if result.initial_cost > 0.0 {
((result.initial_cost - result.final_cost) / result.initial_cost) * 100.0
} else {
0.0
};
let success = matches!(
result.status,
OptimizationStatus::Converged
| OptimizationStatus::CostToleranceReached
| OptimizationStatus::ParameterToleranceReached
| OptimizationStatus::GradientToleranceReached
);
let test_result = TestResult {
vertices: num_vertices,
edges: num_edges,
final_cost: result.final_cost,
improvement_pct,
iterations: result.iterations,
elapsed_time,
status: result.status.clone(),
success,
};
Ok(test_result)
}
#[test]
fn test_intel_se2_converges() -> Result<(), Box<dyn std::error::Error>> {
apex_io::ensure_odometry_dataset("intel")?;
let result = run_se2_optimization("intel", 100, true)?;
assert_eq!(
result.vertices, 1228,
"intel.g2o should have 1228 vertices, got {}",
result.vertices
);
assert_eq!(
result.edges, 1483,
"intel.g2o should have 1483 edges, got {}",
result.edges
);
assert!(
result.success,
"Optimization did not converge. Status: {:?}, Iterations: {}, Final cost: {}",
result.status, result.iterations, result.final_cost
);
assert!(
result.improvement_pct > 85.0,
"Cost improvement too low: {:.2}% (expected >85%)",
result.improvement_pct
);
assert!(
result.iterations < 100,
"Hit maximum iterations: {}",
result.iterations
);
assert!(
result.final_cost.is_finite(),
"Final cost is not finite: {}",
result.final_cost
);
assert!(
result.elapsed_time.as_secs() < 5,
"Optimization took too long: {:?}",
result.elapsed_time
);
Ok(())
}
#[test]
fn test_sphere2500_se3_converges() -> Result<(), Box<dyn std::error::Error>> {
apex_io::ensure_odometry_dataset("sphere2500")?;
let result = run_se3_optimization("sphere2500", 100, true)?;
assert_eq!(
result.vertices, 2500,
"sphere2500.g2o should have 2500 vertices, got {}",
result.vertices
);
assert_eq!(
result.edges, 4949,
"sphere2500.g2o should have 4949 edges, got {}",
result.edges
);
assert!(
result.success,
"Optimization did not converge. Status: {:?}, Iterations: {}, Final cost: {}",
result.status, result.iterations, result.final_cost
);
assert!(
result.improvement_pct > 99.0,
"Cost improvement too low: {:.2}% (expected >99.0%)",
result.improvement_pct
);
assert!(
result.iterations < 20,
"Should complete in < 20 iterations: {}",
result.iterations
);
assert!(
result.final_cost.is_finite(),
"Final cost is not finite: {}",
result.final_cost
);
assert!(
result.elapsed_time.as_secs() < 30,
"Optimization took too long: {:?}",
result.elapsed_time
);
Ok(())
}
#[test]
fn test_parking_garage_se3_converges() -> Result<(), Box<dyn std::error::Error>> {
apex_io::ensure_odometry_dataset("parking-garage")?;
let result = run_se3_optimization("parking-garage", 100, true)?;
assert_eq!(
result.vertices, 1661,
"parking-garage.g2o should have 1661 vertices, got {}",
result.vertices
);
assert_eq!(
result.edges, 6275,
"parking-garage.g2o should have 6275 edges, got {}",
result.edges
);
assert!(
result.success,
"Optimization did not converge. Status: {:?}, Iterations: {}, Final cost: {}",
result.status, result.iterations, result.final_cost
);
assert!(
result.improvement_pct > 99.00,
"Cost improvement too low: {:.2}% (expected >99.00%)",
result.improvement_pct
);
assert!(
result.iterations < 20,
"Should complete in < 20 iterations: {}",
result.iterations
);
assert!(
result.final_cost.is_finite(),
"Final cost is not finite: {}",
result.final_cost
);
assert!(
result.elapsed_time.as_secs() < 20,
"Optimization took too long: {:?}",
result.elapsed_time
);
Ok(())
}