1use nalgebra::{Matrix3, Matrix6, Quaternion, UnitQuaternion, Vector3};
2
3#[cfg(feature = "visualization")]
4use rerun::external::glam::{Quat, Vec3};
5
6use std::{
7 collections, fmt,
8 fmt::{Display, Formatter},
9 io,
10 path::Path,
11};
12use thiserror::Error;
13use tracing::error;
14
15use apex_manifolds::{se2::SE2, se3::SE3};
17
18pub mod bal;
20pub mod g2o;
21pub mod logger;
22pub mod toro;
23pub mod utils;
24
25pub mod rosbag;
26
27#[cfg(feature = "dds")]
28pub mod dds;
29
30pub use logger::init_logger;
31pub use utils::{DatasetRegistry, ensure_ba_dataset, ensure_odometry_dataset};
32
33pub const ODOMETRY_DATA_DIR: &str = "data/odometry";
35
36pub const ODOMETRY_DATA_DIR_2D: &str = "data/odometry/2d";
38
39pub const ODOMETRY_DATA_DIR_3D: &str = "data/odometry/3d";
41
42pub const BUNDLE_ADJUSTMENT_DATA_DIR: &str = "data/bundle_adjustment";
44
45pub use bal::{BalCamera, BalDataset, BalLoader, BalObservation, BalPoint};
47pub use g2o::G2oLoader;
48pub use toro::ToroLoader;
49
50#[derive(Error, Debug)]
52pub enum IoError {
53 #[error("IO error: {0}")]
54 Io(#[from] io::Error),
55
56 #[error("Parse error at line {line}: {message}")]
57 Parse { line: usize, message: String },
58
59 #[error("Unsupported vertex type: {0}")]
60 UnsupportedVertexType(String),
61
62 #[error("Unsupported edge type: {0}")]
63 UnsupportedEdgeType(String),
64
65 #[error("Invalid number format at line {line}: {value}")]
66 InvalidNumber { line: usize, value: String },
67
68 #[error("Missing required fields at line {line}")]
69 MissingFields { line: usize },
70
71 #[error("Duplicate vertex ID: {id}")]
72 DuplicateVertex { id: usize },
73
74 #[error("Invalid quaternion at line {line}: norm = {norm:.6}, expected ~1.0")]
75 InvalidQuaternion { line: usize, norm: f64 },
76
77 #[error("Unsupported file format: {0}")]
78 UnsupportedFormat(String),
79
80 #[error("Failed to create file '{path}': {reason}")]
81 FileCreationFailed { path: String, reason: String },
82}
83
84impl IoError {
85 pub fn log(self) -> Self {
87 error!("{}", self);
88 self
89 }
90
91 pub fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
93 error!("{} | Source: {:?}", self, source_error);
94 self
95 }
96}
97
98#[derive(Clone, PartialEq)]
99pub struct VertexSE2 {
100 pub id: usize,
101 pub pose: SE2,
102}
103impl VertexSE2 {
104 pub fn new(id: usize, x: f64, y: f64, theta: f64) -> Self {
105 Self {
106 id,
107 pose: SE2::from_xy_angle(x, y, theta),
108 }
109 }
110
111 pub fn from_vector(id: usize, vector: Vector3<f64>) -> Self {
112 Self {
113 id,
114 pose: SE2::from_xy_angle(vector[0], vector[1], vector[2]),
115 }
116 }
117
118 pub fn id(&self) -> usize {
119 self.id
120 }
121
122 pub fn x(&self) -> f64 {
123 self.pose.x()
124 }
125
126 pub fn y(&self) -> f64 {
127 self.pose.y()
128 }
129
130 pub fn theta(&self) -> f64 {
131 self.pose.angle()
132 }
133}
134
135impl Display for VertexSE2 {
136 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
137 write!(f, "VertexSE2 [ id: {}, pose: {} ]", self.id, self.pose)
138 }
139}
140
141impl VertexSE2 {
142 pub fn to_rerun_position_2d(&self, scale: f32) -> [f32; 2] {
152 [(self.x() as f32) * scale, (self.y() as f32) * scale]
153 }
154
155 #[cfg(feature = "visualization")]
166 pub fn to_rerun_position_3d(&self, scale: f32, height: f32) -> Vec3 {
167 Vec3::new((self.x() as f32) * scale, (self.y() as f32) * scale, height)
168 }
169}
170
171#[derive(Clone, PartialEq)]
173pub struct VertexSE3 {
174 pub id: usize,
175 pub pose: SE3,
176}
177
178impl VertexSE3 {
179 pub fn new(id: usize, translation: Vector3<f64>, rotation: UnitQuaternion<f64>) -> Self {
180 Self {
181 id,
182 pose: SE3::new(translation, rotation),
183 }
184 }
185
186 pub fn from_vector(id: usize, vector: [f64; 7]) -> Self {
187 let translation = Vector3::from([vector[0], vector[1], vector[2]]);
188 let rotation = UnitQuaternion::from_quaternion(Quaternion::from([
189 vector[3], vector[4], vector[5], vector[6],
190 ]));
191 Self::new(id, translation, rotation)
192 }
193
194 pub fn from_translation_quaternion(
195 id: usize,
196 translation: Vector3<f64>,
197 quaternion: Quaternion<f64>,
198 ) -> Self {
199 Self {
200 id,
201 pose: SE3::from_translation_quaternion(translation, quaternion),
202 }
203 }
204
205 pub fn id(&self) -> usize {
206 self.id
207 }
208
209 pub fn translation(&self) -> Vector3<f64> {
210 self.pose.translation()
211 }
212
213 pub fn rotation(&self) -> UnitQuaternion<f64> {
214 self.pose.rotation_quaternion()
215 }
216
217 pub fn x(&self) -> f64 {
218 self.pose.x()
219 }
220
221 pub fn y(&self) -> f64 {
222 self.pose.y()
223 }
224
225 pub fn z(&self) -> f64 {
226 self.pose.z()
227 }
228}
229
230impl Display for VertexSE3 {
231 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
232 write!(f, "VertexSE3 [ id: {}, pose: {} ]", self.id, self.pose)
233 }
234}
235
236impl VertexSE3 {
237 #[cfg(feature = "visualization")]
247 pub fn to_rerun_transform(&self, scale: f32) -> (Vec3, Quat) {
248 let trans = self.translation();
250 let position = Vec3::new(trans.x as f32, trans.y as f32, trans.z as f32) * scale;
251
252 let rot = self.rotation();
254 let nq = rot.as_ref();
255 let rotation = Quat::from_xyzw(nq.i as f32, nq.j as f32, nq.k as f32, nq.w as f32);
256
257 (position, rotation)
258 }
259}
260
261#[derive(Clone, PartialEq)]
263pub struct EdgeSE2 {
264 pub from: usize,
265 pub to: usize,
266 pub measurement: SE2, pub information: Matrix3<f64>, }
269
270impl EdgeSE2 {
271 pub fn new(
272 from: usize,
273 to: usize,
274 dx: f64,
275 dy: f64,
276 dtheta: f64,
277 information: Matrix3<f64>,
278 ) -> Self {
279 Self {
280 from,
281 to,
282 measurement: SE2::from_xy_angle(dx, dy, dtheta),
283 information,
284 }
285 }
286}
287
288impl Display for EdgeSE2 {
289 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
290 write!(
291 f,
292 "EdgeSE2 [ from: {}, to: {}, measurement: {}, information: {} ]",
293 self.from, self.to, self.measurement, self.information
294 )
295 }
296}
297
298#[derive(Clone, PartialEq)]
300pub struct EdgeSE3 {
301 pub from: usize,
302 pub to: usize,
303 pub measurement: SE3, pub information: Matrix6<f64>, }
306
307impl EdgeSE3 {
308 pub fn new(
309 from: usize,
310 to: usize,
311 translation: Vector3<f64>,
312 rotation: UnitQuaternion<f64>,
313 information: Matrix6<f64>,
314 ) -> Self {
315 Self {
316 from,
317 to,
318 measurement: SE3::new(translation, rotation),
319 information,
320 }
321 }
322}
323
324impl Display for EdgeSE3 {
325 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
326 write!(
327 f,
328 "EdgeSE3 [ from: {}, to: {}, measurement: {}, information: {} ]",
329 self.from, self.to, self.measurement, self.information
330 )
331 }
332}
333
334#[derive(Clone)]
336pub struct Graph {
337 pub vertices_se2: collections::HashMap<usize, VertexSE2>,
338 pub vertices_se3: collections::HashMap<usize, VertexSE3>,
339 pub edges_se2: Vec<EdgeSE2>,
340 pub edges_se3: Vec<EdgeSE3>,
341}
342
343impl Graph {
344 pub fn new() -> Self {
345 Self {
346 vertices_se2: collections::HashMap::new(),
347 vertices_se3: collections::HashMap::new(),
348 edges_se2: Vec::new(),
349 edges_se3: Vec::new(),
350 }
351 }
352
353 pub fn vertex_count(&self) -> usize {
354 self.vertices_se2.len() + self.vertices_se3.len()
355 }
356
357 pub fn edge_count(&self) -> usize {
358 self.edges_se2.len() + self.edges_se3.len()
359 }
360
361 }
364
365impl Default for Graph {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371impl Display for Graph {
372 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
373 write!(
374 f,
375 "Graph [[ vertices_se2: {} (count: {}), vertices_se3: {} (count: {}), edges_se2: {} (count: {}), edges_se3: {} (count: {}) ]]",
376 self.vertices_se2
377 .values()
378 .map(|v| format!("{}", v))
379 .collect::<Vec<_>>()
380 .join(", "),
381 self.vertices_se2.len(),
382 self.vertices_se3
383 .values()
384 .map(|v| format!("{}", v))
385 .collect::<Vec<_>>()
386 .join(", "),
387 self.vertices_se3.len(),
388 self.edges_se2
389 .iter()
390 .map(|e| format!("{}", e))
391 .collect::<Vec<_>>()
392 .join(", "),
393 self.edges_se2.len(),
394 self.edges_se3
395 .iter()
396 .map(|e| format!("{}", e))
397 .collect::<Vec<_>>()
398 .join(", "),
399 self.edges_se3.len()
400 )
401 }
402}
403
404pub trait GraphLoader {
406 fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError>;
408
409 fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError>;
411}
412
413pub fn load_graph<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
415 let path_ref = path.as_ref();
416 let extension = path_ref
417 .extension()
418 .and_then(|ext| ext.to_str())
419 .ok_or_else(|| {
420 IoError::UnsupportedFormat("No file extension".to_string())
421 .log_with_source(format!("File path: {:?}", path_ref))
422 })?;
423
424 match extension.to_lowercase().as_str() {
425 "g2o" => G2oLoader::load(path),
426 "graph" => ToroLoader::load(path),
427 _ => Err(
428 IoError::UnsupportedFormat(format!("Unsupported extension: {extension}"))
429 .log_with_source(format!("File path: {:?}", path_ref)),
430 ),
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use nalgebra::{Matrix3, Matrix6, Quaternion, UnitQuaternion, Vector3};
438 use std::{error, io::Write};
439 use tempfile::NamedTempFile;
440
441 #[test]
442 fn test_load_simple_graph() -> Result<(), IoError> {
443 let mut temp_file = NamedTempFile::new().map_err(|e| {
444 IoError::FileCreationFailed {
445 path: "temp_file".to_string(),
446 reason: e.to_string(),
447 }
448 .log()
449 })?;
450 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
451 writeln!(temp_file, "VERTEX_SE2 1 1.0 0.0 0.0")?;
452 writeln!(temp_file, "# This is a comment")?;
453 writeln!(temp_file)?; writeln!(temp_file, "VERTEX_SE3:QUAT 2 0.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
455
456 let graph = G2oLoader::load(temp_file.path())?;
457
458 assert_eq!(graph.vertices_se2.len(), 2);
459 assert_eq!(graph.vertices_se3.len(), 1);
460 assert!(graph.vertices_se2.contains_key(&0));
461 assert!(graph.vertices_se2.contains_key(&1));
462 assert!(graph.vertices_se3.contains_key(&2));
463
464 Ok(())
465 }
466
467 #[test]
468 fn test_load_m3500() -> Result<(), Box<dyn error::Error>> {
469 let path = utils::ensure_odometry_dataset("M3500")?;
470 let graph = G2oLoader::load(&path)?;
471 assert!(!graph.vertices_se2.is_empty());
472 Ok(())
473 }
474
475 #[test]
476 fn test_load_sphere2500() -> Result<(), Box<dyn error::Error>> {
477 let path = utils::ensure_odometry_dataset("sphere2500")?;
478 let graph = G2oLoader::load(&path)?;
479 assert!(!graph.vertices_se3.is_empty());
480 Ok(())
481 }
482
483 #[test]
484 fn test_duplicate_vertex_error() -> Result<(), io::Error> {
485 let mut temp_file = NamedTempFile::new()?;
486 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
487 writeln!(temp_file, "VERTEX_SE2 0 1.0 0.0 0.0")?; let result = G2oLoader::load(temp_file.path());
490 assert!(matches!(result, Err(IoError::DuplicateVertex { id: 0 })));
491
492 Ok(())
493 }
494
495 #[test]
496 fn test_toro_loader() -> Result<(), IoError> {
497 let mut temp_file = NamedTempFile::new().map_err(|e| {
498 IoError::FileCreationFailed {
499 path: "temp_file".to_string(),
500 reason: e.to_string(),
501 }
502 .log()
503 })?;
504 writeln!(temp_file, "VERTEX2 0 0.0 0.0 0.0")?;
505 writeln!(temp_file, "VERTEX2 1 1.0 0.0 0.0")?;
506
507 let graph = ToroLoader::load(temp_file.path()).map_err(|e| e.log())?;
508 assert_eq!(graph.vertices_se2.len(), 2);
509
510 Ok(())
511 }
512
513 #[test]
514 #[cfg(feature = "visualization")]
515 fn test_se3_to_rerun() {
516 let vertex = VertexSE3::new(0, Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
517
518 let (pos, rot) = vertex.to_rerun_transform(0.1);
519
520 assert!((pos.x - 0.1).abs() < 1e-6);
521 assert!((pos.y - 0.2).abs() < 1e-6);
522 assert!((pos.z - 0.3).abs() < 1e-6);
523 assert!((rot.w - 1.0).abs() < 1e-6);
524 }
525
526 #[test]
527 fn test_se2_to_rerun_2d() {
528 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
529
530 let pos = vertex.to_rerun_position_2d(0.1);
531
532 assert!((pos[0] - 1.0).abs() < 1e-6);
533 assert!((pos[1] - 2.0).abs() < 1e-6);
534 }
535
536 #[test]
537 #[cfg(feature = "visualization")]
538 fn test_se2_to_rerun_3d() {
539 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
540
541 let pos = vertex.to_rerun_position_3d(0.1, 5.0);
542
543 assert!((pos.x - 1.0).abs() < 1e-6);
544 assert!((pos.y - 2.0).abs() < 1e-6);
545 assert!((pos.z - 5.0).abs() < 1e-6);
546 }
547
548 #[test]
553 fn test_vertex_se2_from_vector() {
554 let v = VertexSE2::from_vector(5, Vector3::new(1.0, 2.0, 0.5));
555 assert_eq!(v.id(), 5);
556 assert!((v.x() - 1.0).abs() < 1e-12);
557 assert!((v.y() - 2.0).abs() < 1e-12);
558 assert!((v.theta() - 0.5).abs() < 1e-12);
559 }
560
561 #[test]
562 fn test_vertex_se3_from_vector() {
563 let arr = [1.0f64, 2.0, 3.0, 0.0, 0.0, 0.0, 1.0];
565 let v = VertexSE3::from_vector(7, arr);
566 assert_eq!(v.id(), 7);
567 assert!((v.x() - 1.0).abs() < 1e-10);
568 assert!((v.y() - 2.0).abs() < 1e-10);
569 assert!((v.z() - 3.0).abs() < 1e-10);
570 }
571
572 #[test]
573 fn test_vertex_se3_from_translation_quaternion() {
574 let trans = Vector3::new(1.0, 2.0, 3.0);
575 let quat = Quaternion::new(1.0, 0.0, 0.0, 0.0);
577 let v = VertexSE3::from_translation_quaternion(3, trans, quat);
578 assert_eq!(v.id(), 3);
579 assert!((v.x() - 1.0).abs() < 1e-10);
580 assert!((v.y() - 2.0).abs() < 1e-10);
581 assert!((v.z() - 3.0).abs() < 1e-10);
582 }
583
584 #[test]
589 fn test_vertex_se2_display() {
590 let v = VertexSE2::new(0, 1.0, 2.0, 0.5);
591 let s = format!("{v}");
592 assert!(
593 s.contains("VertexSE2"),
594 "Display should contain 'VertexSE2': {s}"
595 );
596 assert!(s.contains('0'), "Display should contain id: {s}");
597 }
598
599 #[test]
600 fn test_vertex_se3_display() {
601 let v = VertexSE3::new(1, Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
602 let s = format!("{v}");
603 assert!(
604 s.contains("VertexSE3"),
605 "Display should contain 'VertexSE3': {s}"
606 );
607 assert!(s.contains('1'), "Display should contain id: {s}");
608 }
609
610 #[test]
611 fn test_edge_se2_display() {
612 let e = EdgeSE2::new(0, 1, 1.0, 0.0, 0.0, Matrix3::identity());
613 let s = format!("{e}");
614 assert!(
615 s.contains("EdgeSE2"),
616 "Display should contain 'EdgeSE2': {s}"
617 );
618 }
619
620 #[test]
621 fn test_edge_se3_display() {
622 let e = EdgeSE3::new(
623 0,
624 1,
625 Vector3::zeros(),
626 UnitQuaternion::identity(),
627 Matrix6::identity(),
628 );
629 let s = format!("{e}");
630 assert!(
631 s.contains("EdgeSE3"),
632 "Display should contain 'EdgeSE3': {s}"
633 );
634 }
635
636 #[test]
637 fn test_graph_display() {
638 let mut g = Graph::new();
639 g.vertices_se2.insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
640 let s = format!("{g}");
641 assert!(s.contains("Graph"), "Display should contain 'Graph': {s}");
642 assert!(s.contains("count: 1"), "Display should show count: {s}");
643 }
644
645 #[test]
650 fn test_graph_default_is_empty() {
651 let g = Graph::default();
652 assert_eq!(g.vertex_count(), 0);
653 assert_eq!(g.edge_count(), 0);
654 }
655
656 #[test]
657 fn test_graph_vertex_and_edge_counts() {
658 let mut g = Graph::new();
659 g.vertices_se2.insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
660 g.vertices_se3.insert(
661 1,
662 VertexSE3::new(1, Vector3::zeros(), UnitQuaternion::identity()),
663 );
664 g.edges_se2
665 .push(EdgeSE2::new(0, 1, 0.0, 0.0, 0.0, Matrix3::identity()));
666 assert_eq!(g.vertex_count(), 2);
667 assert_eq!(g.edge_count(), 1);
668 }
669
670 #[test]
675 fn test_load_graph_unsupported_extension() {
676 let result = load_graph("fake_path.xyz");
677 assert!(
678 matches!(result, Err(IoError::UnsupportedFormat(_))),
679 "unknown extension should return UnsupportedFormat"
680 );
681 }
682
683 #[test]
684 fn test_load_graph_no_extension() {
685 let result = load_graph("/tmp/no_extension_file");
686 assert!(
687 matches!(result, Err(IoError::UnsupportedFormat(_))),
688 "path with no extension should return UnsupportedFormat"
689 );
690 }
691
692 #[test]
693 fn test_load_graph_toro_extension() -> Result<(), Box<dyn error::Error>> {
694 let mut f = NamedTempFile::new()?;
695 writeln!(f, "VERTEX2 0 0.0 0.0 0.0")?;
696 writeln!(f, "VERTEX2 1 1.0 0.0 0.0")?;
697 f.flush()?;
698 let toro_path = f.path().with_extension("graph");
700 std::fs::copy(f.path(), &toro_path)?;
701 let graph = load_graph(&toro_path)?;
702 std::fs::remove_file(&toro_path)?;
703 assert_eq!(graph.vertices_se2.len(), 2);
704 Ok(())
705 }
706
707 #[test]
708 fn test_io_error_log_returns_self() {
709 let err = IoError::UnsupportedFormat("xyz".to_string());
710 let returned = err.log();
711 assert!(matches!(returned, IoError::UnsupportedFormat(_)));
712 }
713
714 #[test]
715 fn test_io_error_log_with_source() {
716 let err = IoError::UnsupportedFormat("abc".to_string());
717 let source = std::io::Error::other("source");
718 let returned = err.log_with_source(source);
719 assert!(matches!(returned, IoError::UnsupportedFormat(_)));
720 }
721
722 #[test]
723 fn test_vertex_se2_theta() {
724 use std::f64::consts::PI;
725 let v = VertexSE2::new(0, 1.0, 2.0, PI / 4.0);
726 assert!((v.theta() - PI / 4.0).abs() < 1e-10);
727 }
728
729 #[test]
730 fn test_edge_se3_new() {
731 let t = Vector3::new(1.0, 2.0, 3.0);
732 let r = UnitQuaternion::identity();
733 let info = Matrix6::identity();
734 let e = EdgeSE3::new(0, 1, t, r, info);
735 assert_eq!(e.from, 0);
736 assert_eq!(e.to, 1);
737 }
738
739 #[test]
740 fn test_vertex_se3_new() {
741 let t = Vector3::new(1.0, 2.0, 3.0);
742 let r = UnitQuaternion::identity();
743 let v = VertexSE3::new(5, t, r);
744 assert_eq!(v.id, 5);
745 assert!((v.translation() - t).norm() < 1e-10);
746 }
747}