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