use rigidity_core::lie::{Se3, So3};
use rigidity_core::nalgebra::{Matrix6, Vector3, Vector6};
use rigidity_graph::{Edge, OptimiseParams, PoseGraph};
fn truth(stations: usize) -> Vec<Se3> {
let radius = 20.0;
(0..stations)
.map(|index| {
let angle = index as f64 / stations as f64 * std::f64::consts::TAU;
Se3::from_parts(
So3::exp(&Vector3::new(0.0, 0.0, angle)),
Vector3::new(radius * angle.cos(), radius * angle.sin(), 0.0),
)
})
.collect()
}
fn bias() -> Se3 {
Se3::exp(&Vector6::new(0.004, -0.002, 0.0, 0.0, 0.0, 0.0015))
}
fn information() -> Matrix6<f64> {
Matrix6::identity() * 1e6
}
fn survey(stations: usize, closed: bool) -> PoseGraph {
let truth = truth(stations);
let mut graph = PoseGraph::new(truth.clone());
let legs = if closed { stations } else { stations - 1 };
for from in 0..legs {
let to = (from + 1) % stations;
let exact = truth[from].inverse() * truth[to];
let measurement = if closed && from == stations - 1 {
exact
} else {
bias() * exact
};
graph
.push(Edge {
from,
to,
measurement,
information: information(),
})
.expect("the edge names real nodes");
}
graph
}
fn drift(graph: &PoseGraph, truth: &[Se3]) -> f64 {
graph
.poses()
.iter()
.zip(truth)
.map(|(a, b)| (a.translation() - b.translation()).norm())
.fold(0.0, f64::max)
}
#[test]
fn closing_the_loop_takes_the_drift_out() {
const STATIONS: usize = 12;
let truth = truth(STATIONS);
let params = OptimiseParams::default();
let mut open = survey(STATIONS, false);
let open_report = open.optimise(¶ms).expect("a chain solves");
let open_drift = drift(&open, &truth);
let mut closed = survey(STATIONS, true);
let closed_report = closed.optimise(¶ms).expect("a loop solves");
let closed_drift = drift(&closed, &truth);
println!(
"{STATIONS} stations\n open chain: drift {open_drift:.4} m \
(cost {:.3e} → {:.3e})\n closed: drift {closed_drift:.4} m \
(cost {:.3e} → {:.3e})",
open_report.cost[0], open_report.cost[1], closed_report.cost[0], closed_report.cost[1],
);
assert!(
open_report.cost[1] < 1e-12,
"a chain should reach zero cost, and reached {:e}",
open_report.cost[1]
);
assert!(
closed_report.cost[1] > open_report.cost[1],
"a closed loop cannot reach zero: its measurements disagree"
);
assert!(
closed_drift < 0.35 * open_drift,
"closing the loop cut the drift only from {open_drift} m to {closed_drift} m"
);
}
#[test]
fn a_duplicate_edge_is_not_a_closure() {
const STATIONS: usize = 12;
let truth = truth(STATIONS);
let params = OptimiseParams::default();
let mut open = survey(STATIONS, false);
open.optimise(¶ms).expect("a chain solves");
let open_drift = drift(&open, &truth);
let mut doubled = survey(STATIONS, false);
let exact = truth[0].inverse() * truth[1];
doubled
.push(Edge {
from: 0,
to: 1,
measurement: bias() * exact,
information: information(),
})
.expect("the edge names real nodes");
doubled.optimise(¶ms).expect("it solves");
let doubled_drift = drift(&doubled, &truth);
let change = (doubled_drift - open_drift).abs() / open_drift;
assert!(
change < 1e-6,
"a duplicate edge changed the drift by {change} relative, so the \
previous test may be measuring weight rather than closure"
);
}
#[test]
fn the_anchor_stays_exactly_where_it_was() {
const STATIONS: usize = 12;
let truth = truth(STATIONS);
for anchor in [0, 5, STATIONS - 1] {
let mut graph = survey(STATIONS, true);
graph
.optimise(&OptimiseParams {
anchor,
..OptimiseParams::default()
})
.expect("it solves");
let moved = (graph.poses()[anchor].matrix() - truth[anchor].matrix())
.abs()
.max();
assert_eq!(moved, 0.0, "anchor {anchor} moved by {moved}");
}
}
#[test]
fn the_shape_says_whether_anything_is_checked() {
const STATIONS: usize = 12;
let chain = survey(STATIONS, false).shape(0);
assert_eq!(chain.joined, STATIONS);
assert_eq!(chain.closures, 0, "a chain closes nothing");
assert_eq!(chain.adrift, 0);
let closed = survey(STATIONS, true).shape(0);
assert_eq!(closed.closures, 1, "one loop, one closure");
assert_eq!(closed.adrift, 0);
let mut across = survey(STATIONS, true);
let truth = truth(STATIONS);
across
.push(Edge {
from: 0,
to: STATIONS / 2,
measurement: truth[0].inverse() * truth[STATIONS / 2],
information: information(),
})
.expect("the edge names real nodes");
assert_eq!(across.shape(0).closures, 2);
}
#[test]
fn a_disconnected_survey_is_visible_in_advance() {
let truth = truth(4);
let mut graph = PoseGraph::new(truth.clone());
for (from, to) in [(0, 1), (2, 3)] {
graph
.push(Edge {
from,
to,
measurement: truth[from].inverse() * truth[to],
information: information(),
})
.expect("the edge names real nodes");
}
let shape = graph.shape(0);
assert_eq!(shape.joined, 4);
assert_eq!(shape.closures, 0);
assert_eq!(shape.adrift, 2, "the far pair is not joined to the anchor");
let mut orphaned = PoseGraph::new(truth.clone());
orphaned
.push(Edge {
from: 1,
to: 2,
measurement: truth[1].inverse() * truth[2],
information: information(),
})
.expect("the edge names real nodes");
assert_eq!(orphaned.shape(0).adrift, 2);
}
#[test]
fn a_scan_with_no_edges_is_not_counted_against_the_survey() {
let truth = truth(5);
let mut graph = PoseGraph::new(truth.clone());
for from in 0..2 {
graph
.push(Edge {
from,
to: from + 1,
measurement: truth[from].inverse() * truth[from + 1],
information: information(),
})
.expect("the edge names real nodes");
}
let shape = graph.shape(0);
assert_eq!(shape.joined, 3, "two of the five are in no edge");
assert_eq!(shape.adrift, 0);
assert_eq!(shape.closures, 0);
}