use crate::core::scalar::ControlScalar;
use crate::navigation::dead_reckoning::NavigationError;
use heapless::Vec as HVec;
pub struct PoseGraph<S: ControlScalar, const NODES: usize> {
poses: [[S; 3]; NODES],
edges: HVec<(usize, usize, [S; 3], S), 256>,
}
pub use crate::navigation::dead_reckoning::NavigationError as PoseGraphError;
impl<S: ControlScalar, const NODES: usize> PoseGraph<S, NODES> {
pub fn new() -> Self {
Self {
poses: [[S::ZERO; 3]; NODES],
edges: HVec::new(),
}
}
pub fn set_pose(&mut self, node: usize, pose: [S; 3]) -> Result<(), NavigationError> {
if node >= NODES {
return Err(NavigationError::InvalidNode);
}
self.poses[node] = pose;
Ok(())
}
pub fn add_edge(
&mut self,
from: usize,
to: usize,
meas: [S; 3],
info: S,
) -> Result<(), NavigationError> {
if from >= NODES || to >= NODES {
return Err(NavigationError::InvalidNode);
}
self.edges
.push((from, to, meas, info))
.map_err(|_| NavigationError::TooManyEdges)
}
pub fn optimize_sequential(&mut self) -> Result<S, NavigationError> {
if self.edges.is_empty() {
return Err(NavigationError::SingularSystem);
}
let mut seq_edges: HVec<(usize, [S; 3], S), 256> = HVec::new();
for &(from, to, meas, info) in &self.edges {
if to == from + 1 {
let _ = seq_edges.push((from, meas, info));
}
}
let n = seq_edges.len();
for i in 1..n {
let mut j = i;
while j > 0 && seq_edges[j - 1].0 > seq_edges[j].0 {
seq_edges.swap(j - 1, j);
j -= 1;
}
}
for &(from, meas, _info) in &seq_edges {
if from + 1 < NODES {
self.poses[from + 1] = Self::compose_poses(self.poses[from], meas);
}
}
for &(from, to, meas, _info) in &self.edges {
if to < from {
let predicted = Self::compose_poses(self.poses[from], meas);
let actual = self.poses[to];
let err_x = actual[0] - predicted[0];
let err_y = actual[1] - predicted[1];
let err_theta = Self::wrap_angle(actual[2] - predicted[2]);
let span = (from - to) as f64;
if span < 1.0 {
continue;
}
let span_s = S::from_f64(span + 1.0);
for node in to..=from {
let weight = S::from_f64((node - to) as f64) / span_s;
self.poses[node][0] += err_x * weight;
self.poses[node][1] += err_y * weight;
self.poses[node][2] =
Self::wrap_angle(self.poses[node][2] + err_theta * weight);
}
}
}
let mut total = S::ZERO;
for &(from, to, meas, info) in &self.edges {
let predicted = Self::compose_poses(self.poses[from], meas);
let actual = self.poses[to];
let ex = actual[0] - predicted[0];
let ey = actual[1] - predicted[1];
let eth = Self::wrap_angle(actual[2] - predicted[2]);
total += info * (ex * ex + ey * ey + eth * eth);
}
Ok(total)
}
pub fn pose(&self, node: usize) -> Result<[S; 3], NavigationError> {
if node >= NODES {
return Err(NavigationError::InvalidNode);
}
Ok(self.poses[node])
}
fn compose_poses(base: [S; 3], rel: [S; 3]) -> [S; 3] {
let th = base[2];
let cos_th = S::from_f64(libm::cos(th.to_f64()));
let sin_th = S::from_f64(libm::sin(th.to_f64()));
let x = base[0] + cos_th * rel[0] - sin_th * rel[1];
let y = base[1] + sin_th * rel[0] + cos_th * rel[1];
let theta = Self::wrap_angle(base[2] + rel[2]);
[x, y, theta]
}
fn wrap_angle(a: S) -> S {
let mut v = a.to_f64();
use core::f64::consts::PI;
while v > PI {
v -= 2.0 * PI;
}
while v <= -PI {
v += 2.0 * PI;
}
S::from_f64(v)
}
}
impl<S: ControlScalar, const NODES: usize> Default for PoseGraph<S, NODES> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sequential_chain_integrates() {
let mut pg = PoseGraph::<f64, 4>::new();
pg.add_edge(0, 1, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.add_edge(1, 2, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.add_edge(2, 3, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.optimize_sequential().unwrap();
let p1 = pg.pose(1).unwrap();
let p2 = pg.pose(2).unwrap();
let p3 = pg.pose(3).unwrap();
assert!((p1[0] - 1.0).abs() < 1e-9, "p1.x: {}", p1[0]);
assert!((p2[0] - 2.0).abs() < 1e-9, "p2.x: {}", p2[0]);
assert!((p3[0] - 3.0).abs() < 1e-9, "p3.x: {}", p3[0]);
}
#[test]
fn loop_closure_distributes_error() {
let mut pg = PoseGraph::<f64, 4>::new();
pg.add_edge(0, 1, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.add_edge(1, 2, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.add_edge(2, 3, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.add_edge(3, 0, [-2.5, 0.0, 0.0], 1.0).unwrap();
pg.optimize_sequential().unwrap();
let p3 = pg.pose(3).unwrap();
assert!(
p3[0] < 3.0,
"loop closure should pull node 3 below 3.0: got {}",
p3[0]
);
assert!(
p3[0] > 1.0,
"node 3 should not move past node 1: got {}",
p3[0]
);
}
#[test]
fn invalid_node_returns_error() {
let mut pg = PoseGraph::<f64, 3>::new();
let result = pg.set_pose(5, [0.0, 0.0, 0.0]);
assert_eq!(result.unwrap_err(), NavigationError::InvalidNode);
}
#[test]
fn consistent_graph_zero_residual() {
let mut pg = PoseGraph::<f64, 3>::new();
pg.set_pose(0, [0.0, 0.0, 0.0]).unwrap();
pg.set_pose(1, [1.0, 0.0, 0.0]).unwrap();
pg.set_pose(2, [2.0, 0.0, 0.0]).unwrap();
pg.add_edge(0, 1, [1.0, 0.0, 0.0], 1.0).unwrap();
pg.add_edge(1, 2, [1.0, 0.0, 0.0], 1.0).unwrap();
let residual = pg.optimize_sequential().unwrap();
assert!(
residual < 1e-9,
"consistent graph residual should be ~0: {}",
residual
);
}
#[test]
fn no_edges_returns_singular_error() {
let mut pg = PoseGraph::<f64, 3>::new();
let result = pg.optimize_sequential();
assert_eq!(result.unwrap_err(), NavigationError::SingularSystem);
}
#[test]
fn pose_query_invalid_node() {
let pg = PoseGraph::<f64, 3>::new();
assert_eq!(pg.pose(10).unwrap_err(), NavigationError::InvalidNode);
}
#[test]
fn add_edge_invalid_node_returns_error() {
let mut pg = PoseGraph::<f64, 3>::new();
let result = pg.add_edge(0, 10, [1.0, 0.0, 0.0], 1.0);
assert_eq!(result.unwrap_err(), NavigationError::InvalidNode);
}
}