use rigidity_core::lie::{Se3, inverse_right_jacobian_se3};
use rigidity_core::nalgebra::{DMatrix, DVector, Matrix3, Matrix6, Vector3, Vector6};
use rigidity_core::observability::Conditioning;
#[derive(Debug, Clone, Copy)]
pub struct Edge {
pub from: usize,
pub to: usize,
pub measurement: Se3,
pub information: Matrix6<f64>,
}
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum GraphError {
#[error("edge {edge} names node {node}, and the graph has {nodes}")]
NoSuchNode {
edge: usize,
node: usize,
nodes: usize,
},
#[error("edge {edge} joins node {node} to itself")]
SelfLoop {
edge: usize,
node: usize,
},
#[error("the anchor is node {anchor}, and the graph has {nodes}")]
NoSuchAnchor {
anchor: usize,
nodes: usize,
},
#[error("the normal equations are singular at damping {damping:e}")]
Singular {
damping: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OptimiseParams {
pub anchor: usize,
pub max_iterations: usize,
pub step_tolerance: f64,
pub initial_damping: f64,
}
impl Default for OptimiseParams {
fn default() -> Self {
Self {
anchor: 0,
max_iterations: 100,
step_tolerance: 1e-10,
initial_damping: 1e-9,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Report {
pub iterations: usize,
pub converged: bool,
pub cost: [f64; 2],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Shape {
pub joined: usize,
pub closures: usize,
pub adrift: usize,
}
#[derive(Debug, Clone, Default)]
pub struct PoseGraph {
poses: Vec<Se3>,
edges: Vec<Edge>,
}
impl PoseGraph {
pub fn new(poses: Vec<Se3>) -> Self {
Self {
poses,
edges: Vec::new(),
}
}
pub fn poses(&self) -> &[Se3] {
&self.poses
}
pub fn edges(&self) -> &[Edge] {
&self.edges
}
pub fn push(&mut self, edge: Edge) -> Result<(), GraphError> {
let nodes = self.poses.len();
let index = self.edges.len();
for node in [edge.from, edge.to] {
if node >= nodes {
return Err(GraphError::NoSuchNode {
edge: index,
node,
nodes,
});
}
}
if edge.from == edge.to {
return Err(GraphError::SelfLoop {
edge: index,
node: edge.from,
});
}
self.edges.push(edge);
Ok(())
}
pub fn shape(&self, anchor: usize) -> Shape {
let nodes = self.poses.len();
let mut neighbours: Vec<Vec<usize>> = vec![Vec::new(); nodes];
for edge in &self.edges {
neighbours[edge.from].push(edge.to);
neighbours[edge.to].push(edge.from);
}
let touched: Vec<bool> = neighbours.iter().map(|list| !list.is_empty()).collect();
let mut seen = vec![false; nodes];
let mut components = 0;
for start in 0..nodes {
if !touched[start] || seen[start] {
continue;
}
components += 1;
let mut queue = vec![start];
seen[start] = true;
while let Some(node) = queue.pop() {
for next in &neighbours[node] {
if !seen[*next] {
seen[*next] = true;
queue.push(*next);
}
}
}
}
let mut reachable = vec![false; nodes];
if anchor < nodes {
reachable[anchor] = true;
let mut queue = vec![anchor];
while let Some(node) = queue.pop() {
for next in &neighbours[node] {
if !reachable[*next] {
reachable[*next] = true;
queue.push(*next);
}
}
}
}
let joined = touched.iter().filter(|t| **t).count();
Shape {
joined,
closures: (self.edges.len() + components).saturating_sub(joined),
adrift: (0..nodes)
.filter(|node| touched[*node] && !reachable[*node])
.count(),
}
}
pub fn residual(&self, edge: &Edge) -> Vector6<f64> {
self.error(edge).log()
}
fn error(&self, edge: &Edge) -> Se3 {
self.poses[edge.from].inverse() * self.poses[edge.to] * edge.measurement.inverse()
}
pub fn cost(&self) -> f64 {
self.edges
.iter()
.map(|edge| {
let r = self.residual(edge);
(r.transpose() * edge.information * r)[(0, 0)]
})
.sum()
}
pub fn optimise(&mut self, params: &OptimiseParams) -> Result<Report, GraphError> {
let nodes = self.poses.len();
if params.anchor >= nodes {
return Err(GraphError::NoSuchAnchor {
anchor: params.anchor,
nodes,
});
}
let before = self.cost();
let mut damping = params.initial_damping;
let mut iterations = 0;
let mut converged = false;
while iterations < params.max_iterations {
let (hessian, gradient) = self.normal_equations(params.anchor);
let mut step = None;
for _ in 0..12 {
let mut damped = hessian.clone();
for index in 0..damped.nrows() {
damped[(index, index)] += damping;
}
if let Some(cholesky) = damped.cholesky() {
step = Some(cholesky.solve(&(-&gradient)));
break;
}
damping *= 10.0;
}
let Some(step) = step else {
return Err(GraphError::Singular { damping });
};
let previous = self.poses.clone();
self.apply(&step, params.anchor);
let after = self.cost();
if after.is_finite() && after < self.cost_of(&previous) {
iterations += 1;
damping = (damping * 0.1).max(f64::MIN_POSITIVE);
if step.amax() < params.step_tolerance {
converged = true;
break;
}
} else {
self.poses = previous;
damping *= 10.0;
if damping > 1e12 {
converged = true;
break;
}
}
}
Ok(Report {
iterations,
converged,
cost: [before, self.cost()],
})
}
fn cost_of(&self, poses: &[Se3]) -> f64 {
let mut probe = self.clone();
probe.poses = poses.to_vec();
probe.cost()
}
fn normal_equations(&self, anchor: usize) -> (DMatrix<f64>, DVector<f64>) {
let free = self.poses.len() - 1;
let mut hessian = DMatrix::zeros(6 * free, 6 * free);
let mut gradient = DVector::zeros(6 * free);
let slot = |node: usize| -> Option<usize> {
match node.cmp(&anchor) {
std::cmp::Ordering::Less => Some(node),
std::cmp::Ordering::Equal => None,
std::cmp::Ordering::Greater => Some(node - 1),
}
};
for edge in &self.edges {
let error = self.error(edge);
let residual = error.log();
let lift = inverse_right_jacobian_se3(&residual);
let jacobian_from = -lift * error.inverse().adjoint();
let jacobian_to = lift * edge.measurement.adjoint();
let blocks = [(edge.from, jacobian_from), (edge.to, jacobian_to)];
for (node, jacobian) in blocks {
let Some(row) = slot(node) else { continue };
let weighted = jacobian.transpose() * edge.information;
let contribution = weighted * residual;
for axis in 0..6 {
gradient[6 * row + axis] += contribution[axis];
}
for (other, other_jacobian) in blocks {
let Some(column) = slot(other) else { continue };
let block = weighted * other_jacobian;
for r in 0..6 {
for c in 0..6 {
hessian[(6 * row + r, 6 * column + c)] += block[(r, c)];
}
}
}
}
}
(hessian, gradient)
}
fn apply(&mut self, step: &DVector<f64>, anchor: usize) {
let mut row = 0;
for (index, pose) in self.poses.iter_mut().enumerate() {
if index == anchor {
continue;
}
let delta = Vector6::new(
step[6 * row],
step[6 * row + 1],
step[6 * row + 2],
step[6 * row + 3],
step[6 * row + 4],
step[6 * row + 5],
);
*pose = *pose * Se3::exp(&delta);
row += 1;
}
}
}
pub fn calibrated_information(conditioning: &Conditioning, noise_sigma: f64) -> Matrix6<f64> {
let mut to_normalised = Matrix6::zeros();
for axis in 0..6 {
let mut basis = Vector6::zeros();
basis[axis] = 1.0;
to_normalised.set_column(axis, &conditioning.to_normalised(basis));
}
let spreads = conditioning.uncertainty(noise_sigma);
let mut normalised = Matrix6::zeros();
for (index, spread) in spreads.iter().enumerate() {
if !(spread.is_finite() && *spread > 0.0) {
continue;
}
let direction = conditioning.direction(index);
normalised += direction * direction.transpose() / (spread * spread);
}
to_normalised.transpose() * normalised * to_normalised
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EdgeReport {
pub edge: usize,
pub translation: f64,
pub rotation: f64,
pub cost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NodeReport {
pub node: usize,
pub covariance: Matrix6<f64>,
pub determined: bool,
}
impl NodeReport {
pub fn position_covariance(&self) -> Matrix3<f64> {
if !self.determined {
return Matrix3::from_diagonal_element(f64::INFINITY);
}
self.covariance.fixed_view::<3, 3>(0, 0).into()
}
pub fn position(&self) -> (f64, Vector3<f64>) {
if !self.determined {
return (f64::INFINITY, Vector3::new(1.0, 0.0, 0.0));
}
let eigen = self.position_covariance().symmetric_eigen();
let (index, value) = eigen.eigenvalues.iter().enumerate().fold(
(0usize, f64::NEG_INFINITY),
|best, (index, value)| {
if *value > best.1 {
(index, *value)
} else {
best
}
},
);
(
value.max(0.0).sqrt(),
eigen.eigenvectors.column(index).into(),
)
}
pub fn orientation(&self) -> f64 {
if !self.determined {
return f64::INFINITY;
}
self.covariance
.fixed_view::<3, 3>(3, 3)
.symmetric_eigen()
.eigenvalues
.iter()
.fold(0.0f64, |best, value| best.max(*value))
.max(0.0)
.sqrt()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnosis {
pub edges: Vec<EdgeReport>,
pub nodes: Vec<NodeReport>,
}
impl PoseGraph {
pub fn diagnose(&self, anchor: usize) -> Result<Diagnosis, GraphError> {
let nodes = self.poses.len();
if anchor >= nodes {
return Err(GraphError::NoSuchAnchor { anchor, nodes });
}
let edges = self
.edges
.iter()
.enumerate()
.map(|(index, edge)| {
let residual = self.residual(edge);
EdgeReport {
edge: index,
translation: residual.fixed_rows::<3>(0).norm(),
rotation: residual.fixed_rows::<3>(3).norm(),
cost: (residual.transpose() * edge.information * residual)[(0, 0)],
}
})
.collect();
let (hessian, _) = self.normal_equations(anchor);
let width = hessian.nrows();
let eigen = hessian.symmetric_eigen();
let largest = eigen
.eigenvalues
.iter()
.fold(0.0f64, |best, value| best.max(*value));
let cutoff = largest * 1e-12;
let mut covariance = DMatrix::zeros(width, width);
let mut unconstrained: DVector<f64> = DVector::zeros(width);
for (index, value) in eigen.eigenvalues.iter().enumerate() {
let vector = eigen.eigenvectors.column(index);
if *value > cutoff && largest > 0.0 {
covariance += (vector * vector.transpose()) / *value;
} else {
for row in 0..width {
unconstrained[row] += vector[row] * vector[row];
}
}
}
let mut reports = Vec::with_capacity(nodes.saturating_sub(1));
for node in 0..nodes {
if node == anchor {
continue;
}
let row = 6 * if node < anchor { node } else { node - 1 };
let determined = largest > 0.0 && (0..6).all(|axis| unconstrained[row + axis] <= 1e-9);
let mut block = Matrix6::zeros();
if determined {
for r in 0..6 {
for c in 0..6 {
block[(r, c)] = covariance[(row + r, row + c)];
}
}
}
reports.push(NodeReport {
node,
covariance: block,
determined,
});
}
Ok(Diagnosis {
edges,
nodes: reports,
})
}
}