use crate::backend::tiny_solver_manifolds::UnitVector3Manifold;
use crate::backend::{
BackendSolution, BackendSolveOptions, LinearSolverKind, OptimBackend, SolveReport,
};
use crate::factors::laserline::{
laser_line_dist_normalized_generic, laser_plane_pixel_residual_generic,
};
use crate::factors::reprojection_model::{
reproj_residual_pinhole4_dist5_handeye_generic,
reproj_residual_pinhole4_dist5_handeye_robot_delta_generic,
reproj_residual_pinhole4_dist5_scheimpflug2_se3_generic,
reproj_residual_pinhole4_dist5_se3_generic, reproj_residual_pinhole4_dist5_two_se3_generic,
reproj_residual_pinhole4_se3_generic,
};
use crate::ir::{FactorKind, ManifoldKind, ProblemIR, RobustLoss};
use anyhow::{Result, anyhow, ensure};
use nalgebra::DVector;
use std::collections::HashMap;
use std::sync::Arc;
use tiny_solver::factors::Factor;
use tiny_solver::loss_functions::{ArctanLoss, CauchyLoss, HuberLoss, Loss};
use tiny_solver::manifold::se3::SE3Manifold;
use tiny_solver::manifold::so3::QuaternionManifold;
use tiny_solver::optimizer::{Optimizer, OptimizerOptions};
use tiny_solver::problem::Problem;
use tiny_solver::{LevenbergMarquardtOptimizer, linear::sparse::LinearSolverType};
#[derive(Debug, Clone, Copy)]
pub struct TinySolverBackend;
impl TinySolverBackend {
fn compile(
&self,
ir: &ProblemIR,
initial: &HashMap<String, DVector<f64>>,
) -> Result<(Problem, HashMap<String, DVector<f64>>)> {
ir.validate()?;
let mut problem = Problem::new();
for param in &ir.params {
let init = initial.get(¶m.name).ok_or_else(|| {
anyhow!(
"initial values missing parameter {} (id {:?})",
param.name,
param.id
)
})?;
ensure!(
init.len() == param.dim,
"initial dimension mismatch for {}: expected {}, got {}",
param.name,
param.dim,
init.len()
);
let mut set_manifold = true;
match param.manifold {
ManifoldKind::Euclidean => {}
ManifoldKind::SE3 => {
if !param.fixed.is_empty() {
if param.fixed.is_all_fixed(param.dim) {
set_manifold = false;
} else {
return Err(anyhow!(
"tiny-solver cannot partially fix SE3 manifold {}",
param.name
));
}
}
if set_manifold {
problem.set_variable_manifold(¶m.name, Arc::new(SE3Manifold));
}
}
ManifoldKind::SO3 => {
if !param.fixed.is_empty() {
if param.fixed.is_all_fixed(param.dim) {
set_manifold = false;
} else {
return Err(anyhow!(
"tiny-solver cannot partially fix SO3 manifold {}",
param.name
));
}
}
if set_manifold {
problem.set_variable_manifold(¶m.name, Arc::new(QuaternionManifold));
}
}
ManifoldKind::S2 => {
if !param.fixed.is_empty() {
if param.fixed.is_all_fixed(param.dim) {
set_manifold = false;
} else {
return Err(anyhow!(
"tiny-solver cannot partially fix S2 manifold {}",
param.name
));
}
}
if set_manifold {
problem.set_variable_manifold(¶m.name, Arc::new(UnitVector3Manifold));
}
}
}
for idx in param.fixed.iter() {
problem.fix_variable(¶m.name, idx);
}
if let Some(bounds) = ¶m.bounds {
for bound in bounds {
problem.set_variable_bounds(¶m.name, bound.idx, bound.lower, bound.upper);
}
}
}
for residual in &ir.residuals {
let (factor, loss) = compile_factor(residual)?;
let param_names: Vec<String> = residual
.params
.iter()
.map(|id| ir.params[id.0].name.clone())
.collect();
let param_refs: Vec<&str> = param_names.iter().map(|s| s.as_str()).collect();
problem.add_residual_block(residual.residual_dim, ¶m_refs, factor, loss);
}
Ok((problem, initial.clone()))
}
}
impl OptimBackend for TinySolverBackend {
fn solve(
&self,
ir: &ProblemIR,
initial: &HashMap<String, DVector<f64>>,
opts: &BackendSolveOptions,
) -> Result<BackendSolution> {
let (problem, initial_map) = self.compile(ir, initial)?;
let optimizer = LevenbergMarquardtOptimizer::default();
let options = to_optimizer_options(opts);
let solution = optimizer
.optimize(&problem, &initial_map, Some(options))
.ok_or_else(|| anyhow!("tiny-solver failed to converge"))?;
let param_blocks = problem.initialize_parameter_blocks(&solution);
let residuals = problem.compute_residuals(¶m_blocks, true);
let final_cost = 0.5 * residuals.as_ref().squared_norm_l2();
Ok(BackendSolution {
params: solution,
solve_report: SolveReport { final_cost },
})
}
}
fn to_optimizer_options(opts: &BackendSolveOptions) -> OptimizerOptions {
let mut options = OptimizerOptions {
max_iteration: opts.max_iters,
verbosity_level: opts.verbosity,
..OptimizerOptions::default()
};
if let Some(solver) = opts.linear_solver {
options.linear_solver_type = match solver {
LinearSolverKind::SparseCholesky => LinearSolverType::SparseCholesky,
LinearSolverKind::SparseQR => LinearSolverType::SparseQR,
};
}
if let Some(v) = opts.min_abs_decrease {
options.min_abs_error_decrease_threshold = v;
}
if let Some(v) = opts.min_rel_decrease {
options.min_rel_error_decrease_threshold = v;
}
if let Some(v) = opts.min_error {
options.min_error_threshold = v;
}
options
}
fn compile_loss(loss: RobustLoss) -> Result<Option<Box<dyn Loss + Send>>> {
match loss {
RobustLoss::None => Ok(None),
RobustLoss::Huber { scale } => {
ensure!(scale > 0.0, "Huber scale must be positive");
Ok(Some(Box::new(HuberLoss::new(scale))))
}
RobustLoss::Cauchy { scale } => {
ensure!(scale > 0.0, "Cauchy scale must be positive");
Ok(Some(Box::new(CauchyLoss::new(scale))))
}
RobustLoss::Arctan { scale } => {
ensure!(scale > 0.0, "Arctan scale must be positive");
Ok(Some(Box::new(ArctanLoss::new(scale))))
}
}
}
type CompiledFactor = (
Box<dyn tiny_solver::factors::FactorImpl + Send>,
Option<Box<dyn Loss + Send>>,
);
fn compile_factor(residual: &crate::ir::ResidualBlock) -> Result<CompiledFactor> {
let loss = compile_loss(residual.loss)?;
match &residual.factor {
FactorKind::ReprojPointPinhole4 { pw, uv, w } => {
let factor = TinyReprojPointFactor {
pw: *pw,
uv: *uv,
w: *w,
};
Ok((Box::new(factor), loss))
}
FactorKind::ReprojPointPinhole4Dist5 { pw, uv, w } => {
let factor = TinyReprojPointDistFactor {
pw: *pw,
uv: *uv,
w: *w,
};
Ok((Box::new(factor), loss))
}
FactorKind::ReprojPointPinhole4Dist5Scheimpflug2 { pw, uv, w } => {
let factor = TinyReprojPointDistScheimpflugFactor {
pw: *pw,
uv: *uv,
w: *w,
};
Ok((Box::new(factor), loss))
}
FactorKind::ReprojPointPinhole4Dist5TwoSE3 { pw, uv, w } => {
let factor = TinyReprojPointDistTwoSE3Factor {
pw: *pw,
uv: *uv,
w: *w,
};
Ok((Box::new(factor), loss))
}
FactorKind::ReprojPointPinhole4Dist5HandEye {
pw,
uv,
w,
base_to_gripper_se3,
mode,
} => {
let factor = TinyReprojPointDistHandEyeFactor {
pw: *pw,
uv: *uv,
w: *w,
robot_se3: *base_to_gripper_se3,
mode: *mode,
};
Ok((Box::new(factor), loss))
}
FactorKind::ReprojPointPinhole4Dist5HandEyeRobotDelta {
pw,
uv,
w,
base_to_gripper_se3,
mode,
} => {
let factor = TinyReprojPointDistHandEyeDeltaFactor {
pw: *pw,
uv: *uv,
w: *w,
robot_se3: *base_to_gripper_se3,
mode: *mode,
};
Ok((Box::new(factor), loss))
}
FactorKind::Se3TangentPrior { sqrt_info } => {
let factor = TinySe3TangentPriorFactor {
sqrt_info: *sqrt_info,
};
Ok((Box::new(factor), loss))
}
FactorKind::LaserPlanePixel { laser_pixel, w } => {
let factor = TinyLaserPlanePixelFactor {
laser_pixel: *laser_pixel,
w: *w,
};
Ok((Box::new(factor), loss))
}
FactorKind::LaserLineDist2D { laser_pixel, w } => {
let factor = TinyLaserLineDist2DFactor {
laser_pixel: *laser_pixel,
w: *w,
};
Ok((Box::new(factor), loss))
}
other => Err(anyhow!("factor kind {:?} not supported", other)),
}
}
#[derive(Debug, Clone)]
struct TinyReprojPointFactor {
pw: [f64; 3],
uv: [f64; 2],
w: f64,
}
impl<T: nalgebra::RealField> Factor<T> for TinyReprojPointFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(params.len(), 2, "expected [cam, pose] parameter blocks");
let r = reproj_residual_pinhole4_se3_generic(
params[0].as_view(),
params[1].as_view(),
self.pw,
self.uv,
self.w,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinyReprojPointDistFactor {
pw: [f64; 3],
uv: [f64; 2],
w: f64,
}
impl<T: nalgebra::RealField> Factor<T> for TinyReprojPointDistFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
3,
"expected [cam, dist, pose] parameter blocks"
);
let r = reproj_residual_pinhole4_dist5_se3_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), self.pw,
self.uv,
self.w,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinyReprojPointDistScheimpflugFactor {
pw: [f64; 3],
uv: [f64; 2],
w: f64,
}
impl<T: nalgebra::RealField> Factor<T> for TinyReprojPointDistScheimpflugFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
4,
"expected [cam, dist, sensor, pose] parameter blocks"
);
let r = reproj_residual_pinhole4_dist5_scheimpflug2_se3_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), params[3].as_view(), self.pw,
self.uv,
self.w,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinyReprojPointDistTwoSE3Factor {
pw: [f64; 3],
uv: [f64; 2],
w: f64,
}
impl<T: nalgebra::RealField> Factor<T> for TinyReprojPointDistTwoSE3Factor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
4,
"expected [cam, dist, extr, pose] parameter blocks"
);
let obs = crate::factors::reprojection_model::ObservationData {
pw: self.pw,
uv: self.uv,
w: self.w,
};
let r = reproj_residual_pinhole4_dist5_two_se3_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), params[3].as_view(), &obs,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinyReprojPointDistHandEyeFactor {
pw: [f64; 3],
uv: [f64; 2],
w: f64,
robot_se3: [f64; 7],
mode: crate::ir::HandEyeMode,
}
impl<T: nalgebra::RealField> Factor<T> for TinyReprojPointDistHandEyeFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
5,
"expected [cam, dist, extr, handeye, target] parameter blocks"
);
let obs = crate::factors::reprojection_model::ObservationData {
pw: self.pw,
uv: self.uv,
w: self.w,
};
let robot_data = crate::factors::reprojection_model::RobotPoseData {
robot_se3: self.robot_se3,
mode: self.mode,
};
let r = reproj_residual_pinhole4_dist5_handeye_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), params[3].as_view(), params[4].as_view(), &robot_data,
&obs,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinyReprojPointDistHandEyeDeltaFactor {
pw: [f64; 3],
uv: [f64; 2],
w: f64,
robot_se3: [f64; 7],
mode: crate::ir::HandEyeMode,
}
impl<T: nalgebra::RealField> Factor<T> for TinyReprojPointDistHandEyeDeltaFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
6,
"expected [cam, dist, extr, handeye, target, robot_delta] parameter blocks"
);
let data = crate::factors::reprojection_model::HandEyeRobotDeltaData {
robot: crate::factors::reprojection_model::RobotPoseData {
robot_se3: self.robot_se3,
mode: self.mode,
},
obs: crate::factors::reprojection_model::ObservationData {
pw: self.pw,
uv: self.uv,
w: self.w,
},
};
let r = reproj_residual_pinhole4_dist5_handeye_robot_delta_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), params[3].as_view(), params[4].as_view(), params[5].as_view(), &data,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinySe3TangentPriorFactor {
sqrt_info: [f64; 6],
}
impl<T: nalgebra::RealField> Factor<T> for TinySe3TangentPriorFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(params.len(), 1, "expected [robot_delta] parameter blocks");
DVector::from_fn(6, |idx, _| {
let w = T::from_f64(self.sqrt_info[idx]).unwrap();
params[0][idx].clone() * w
})
}
}
#[derive(Debug, Clone)]
struct TinyLaserPlanePixelFactor {
laser_pixel: [f64; 2],
w: f64,
}
impl<T: nalgebra::RealField> Factor<T> for TinyLaserPlanePixelFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
6,
"expected [cam, dist, sensor, pose, plane_normal, plane_distance] parameter blocks"
);
let r = laser_plane_pixel_residual_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), params[3].as_view(), params[4].as_view(), params[5].as_view(), self.laser_pixel,
self.w,
);
DVector::from_row_slice(r.as_slice())
}
}
#[derive(Debug, Clone)]
struct TinyLaserLineDist2DFactor {
laser_pixel: [f64; 2],
w: f64,
}
impl<T: nalgebra::RealField> Factor<T> for TinyLaserLineDist2DFactor {
fn residual_func(&self, params: &[DVector<T>]) -> DVector<T> {
debug_assert_eq!(
params.len(),
6,
"expected [cam, dist, sensor, pose, plane_normal, plane_distance] parameter blocks"
);
let r = laser_line_dist_normalized_generic(
params[0].as_view(), params[1].as_view(), params[2].as_view(), params[3].as_view(), params[4].as_view(), params[5].as_view(), self.laser_pixel,
self.w,
);
DVector::from_row_slice(r.as_slice())
}
}