1pub mod error;
9pub mod g2o;
10pub mod toro;
11
12use std::{
13 collections, fmt,
14 fmt::{Display, Formatter},
15};
16
17use nalgebra::{Matrix3, Matrix6, Quaternion, UnitQuaternion, Vector3};
18
19use apex_manifolds::{se2::SE2, se3::SE3};
20
21pub use error::IoError;
22
23#[derive(Clone, PartialEq)]
24pub struct VertexSE2 {
25 pub id: usize,
26 pub pose: SE2,
27}
28impl VertexSE2 {
29 pub fn new(id: usize, x: f64, y: f64, theta: f64) -> Self {
30 Self {
31 id,
32 pose: SE2::from_xy_angle(x, y, theta),
33 }
34 }
35
36 pub fn from_vector(id: usize, vector: Vector3<f64>) -> Self {
37 Self {
38 id,
39 pose: SE2::from_xy_angle(vector[0], vector[1], vector[2]),
40 }
41 }
42
43 pub fn id(&self) -> usize {
44 self.id
45 }
46
47 pub fn x(&self) -> f64 {
48 self.pose.x()
49 }
50
51 pub fn y(&self) -> f64 {
52 self.pose.y()
53 }
54
55 pub fn theta(&self) -> f64 {
56 self.pose.angle()
57 }
58}
59
60impl Display for VertexSE2 {
61 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
62 write!(f, "VertexSE2 [ id: {}, pose: {} ]", self.id, self.pose)
63 }
64}
65
66impl VertexSE2 {
67 pub fn to_rerun_position_2d(&self, scale: f32) -> [f32; 2] {
77 [(self.x() as f32) * scale, (self.y() as f32) * scale]
78 }
79
80 #[cfg(feature = "visualization")]
91 pub fn to_rerun_position_3d(&self, scale: f32, height: f32) -> rerun::external::glam::Vec3 {
92 rerun::external::glam::Vec3::new(
93 (self.x() as f32) * scale,
94 (self.y() as f32) * scale,
95 height,
96 )
97 }
98}
99
100#[derive(Clone, PartialEq)]
102pub struct VertexSE3 {
103 pub id: usize,
104 pub pose: SE3,
105}
106
107impl VertexSE3 {
108 pub fn new(id: usize, translation: Vector3<f64>, rotation: UnitQuaternion<f64>) -> Self {
109 Self {
110 id,
111 pose: SE3::new(translation, rotation),
112 }
113 }
114
115 pub fn from_vector(id: usize, vector: [f64; 7]) -> Self {
116 let translation = Vector3::from([vector[0], vector[1], vector[2]]);
117 let rotation = UnitQuaternion::from_quaternion(Quaternion::from([
118 vector[3], vector[4], vector[5], vector[6],
119 ]));
120 Self::new(id, translation, rotation)
121 }
122
123 pub fn from_translation_quaternion(
124 id: usize,
125 translation: Vector3<f64>,
126 quaternion: Quaternion<f64>,
127 ) -> Self {
128 Self {
129 id,
130 pose: SE3::from_translation_quaternion(translation, quaternion),
131 }
132 }
133
134 pub fn id(&self) -> usize {
135 self.id
136 }
137
138 pub fn translation(&self) -> Vector3<f64> {
139 self.pose.translation()
140 }
141
142 pub fn rotation(&self) -> UnitQuaternion<f64> {
143 self.pose.rotation_quaternion()
144 }
145
146 pub fn x(&self) -> f64 {
147 self.pose.x()
148 }
149
150 pub fn y(&self) -> f64 {
151 self.pose.y()
152 }
153
154 pub fn z(&self) -> f64 {
155 self.pose.z()
156 }
157}
158
159impl Display for VertexSE3 {
160 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
161 write!(f, "VertexSE3 [ id: {}, pose: {} ]", self.id, self.pose)
162 }
163}
164
165impl VertexSE3 {
166 #[cfg(feature = "visualization")]
176 pub fn to_rerun_transform(
177 &self,
178 scale: f32,
179 ) -> (rerun::external::glam::Vec3, rerun::external::glam::Quat) {
180 let trans = self.translation();
182 let position =
183 rerun::external::glam::Vec3::new(trans.x as f32, trans.y as f32, trans.z as f32)
184 * scale;
185
186 let rot = self.rotation();
188 let nq = rot.as_ref();
189 let rotation = rerun::external::glam::Quat::from_xyzw(
190 nq.i as f32,
191 nq.j as f32,
192 nq.k as f32,
193 nq.w as f32,
194 );
195
196 (position, rotation)
197 }
198}
199
200#[derive(Clone, PartialEq)]
202pub struct EdgeSE2 {
203 pub from: usize,
204 pub to: usize,
205 pub measurement: SE2, pub information: Matrix3<f64>, }
208
209impl EdgeSE2 {
210 pub fn new(
211 from: usize,
212 to: usize,
213 dx: f64,
214 dy: f64,
215 dtheta: f64,
216 information: Matrix3<f64>,
217 ) -> Self {
218 Self {
219 from,
220 to,
221 measurement: SE2::from_xy_angle(dx, dy, dtheta),
222 information,
223 }
224 }
225}
226
227impl Display for EdgeSE2 {
228 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
229 write!(
230 f,
231 "EdgeSE2 [ from: {}, to: {}, measurement: {}, information: {} ]",
232 self.from, self.to, self.measurement, self.information
233 )
234 }
235}
236
237#[derive(Clone, PartialEq)]
239pub struct EdgeSE3 {
240 pub from: usize,
241 pub to: usize,
242 pub measurement: SE3, pub information: Matrix6<f64>, }
245
246impl EdgeSE3 {
247 pub fn new(
248 from: usize,
249 to: usize,
250 translation: Vector3<f64>,
251 rotation: UnitQuaternion<f64>,
252 information: Matrix6<f64>,
253 ) -> Self {
254 Self {
255 from,
256 to,
257 measurement: SE3::new(translation, rotation),
258 information,
259 }
260 }
261}
262
263impl Display for EdgeSE3 {
264 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
265 write!(
266 f,
267 "EdgeSE3 [ from: {}, to: {}, measurement: {}, information: {} ]",
268 self.from, self.to, self.measurement, self.information
269 )
270 }
271}
272
273#[derive(Clone)]
275pub struct Graph {
276 pub vertices_se2: collections::HashMap<usize, VertexSE2>,
277 pub vertices_se3: collections::HashMap<usize, VertexSE3>,
278 pub edges_se2: Vec<EdgeSE2>,
279 pub edges_se3: Vec<EdgeSE3>,
280}
281
282impl Graph {
283 pub fn new() -> Self {
284 Self {
285 vertices_se2: collections::HashMap::new(),
286 vertices_se3: collections::HashMap::new(),
287 edges_se2: Vec::new(),
288 edges_se3: Vec::new(),
289 }
290 }
291
292 pub fn vertex_count(&self) -> usize {
293 self.vertices_se2.len() + self.vertices_se3.len()
294 }
295
296 pub fn edge_count(&self) -> usize {
297 self.edges_se2.len() + self.edges_se3.len()
298 }
299
300 }
303
304impl Default for Graph {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310impl Display for Graph {
311 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
312 write!(
313 f,
314 "Graph [[ vertices_se2: {} (count: {}), vertices_se3: {} (count: {}), edges_se2: {} (count: {}), edges_se3: {} (count: {}) ]]",
315 self.vertices_se2
316 .values()
317 .map(|v| format!("{v}"))
318 .collect::<Vec<_>>()
319 .join(", "),
320 self.vertices_se2.len(),
321 self.vertices_se3
322 .values()
323 .map(|v| format!("{v}"))
324 .collect::<Vec<_>>()
325 .join(", "),
326 self.vertices_se3.len(),
327 self.edges_se2
328 .iter()
329 .map(|e| format!("{e}"))
330 .collect::<Vec<_>>()
331 .join(", "),
332 self.edges_se2.len(),
333 self.edges_se3
334 .iter()
335 .map(|e| format!("{e}"))
336 .collect::<Vec<_>>()
337 .join(", "),
338 self.edges_se3.len()
339 )
340 }
341}
342
343pub trait GraphLoader {
345 fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError>;
347
348 fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError>;
350}
351
352use std::path::Path;
353
354pub fn load_graph<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
356 let path_ref = path.as_ref();
357 let extension = path_ref
358 .extension()
359 .and_then(|ext| ext.to_str())
360 .ok_or_else(|| {
361 IoError::UnsupportedFormat("No file extension".to_string())
362 .log_with_source(format!("File path: {:?}", path_ref))
363 })?;
364
365 match extension.to_lowercase().as_str() {
366 "g2o" => g2o::G2oLoader::load(path),
367 "graph" => toro::ToroLoader::load(path),
368 _ => Err(
369 IoError::UnsupportedFormat(format!("Unsupported extension: {extension}"))
370 .log_with_source(format!("File path: {:?}", path_ref)),
371 ),
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use nalgebra::{Matrix3, Matrix6, Quaternion, UnitQuaternion, Vector3};
379 use std::{error, io, io::Write};
380 use tempfile::NamedTempFile;
381
382 #[test]
383 fn test_load_simple_graph() -> Result<(), IoError> {
384 let mut temp_file = NamedTempFile::new().map_err(|e| {
385 IoError::FileCreationFailed {
386 path: "temp_file".to_string(),
387 reason: e.to_string(),
388 }
389 .log()
390 })?;
391 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
392 writeln!(temp_file, "VERTEX_SE2 1 1.0 0.0 0.0")?;
393 writeln!(temp_file, "# This is a comment")?;
394 writeln!(temp_file)?; writeln!(temp_file, "VERTEX_SE3:QUAT 2 0.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
396
397 let graph = g2o::G2oLoader::load(temp_file.path())?;
398
399 assert_eq!(graph.vertices_se2.len(), 2);
400 assert_eq!(graph.vertices_se3.len(), 1);
401 assert!(graph.vertices_se2.contains_key(&0));
402 assert!(graph.vertices_se2.contains_key(&1));
403 assert!(graph.vertices_se3.contains_key(&2));
404
405 Ok(())
406 }
407
408 #[test]
409 fn test_duplicate_vertex_error() -> Result<(), io::Error> {
410 let mut temp_file = NamedTempFile::new()?;
411 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
412 writeln!(temp_file, "VERTEX_SE2 0 1.0 0.0 0.0")?; let result = g2o::G2oLoader::load(temp_file.path());
415 assert!(matches!(result, Err(IoError::DuplicateVertex { id: 0 })));
416
417 Ok(())
418 }
419
420 #[test]
421 fn test_toro_loader() -> Result<(), IoError> {
422 let mut temp_file = NamedTempFile::new().map_err(|e| {
423 IoError::FileCreationFailed {
424 path: "temp_file".to_string(),
425 reason: e.to_string(),
426 }
427 .log()
428 })?;
429 writeln!(temp_file, "VERTEX2 0 0.0 0.0 0.0")?;
430 writeln!(temp_file, "VERTEX2 1 1.0 0.0 0.0")?;
431
432 let graph = toro::ToroLoader::load(temp_file.path()).map_err(|e| e.log())?;
433 assert_eq!(graph.vertices_se2.len(), 2);
434
435 Ok(())
436 }
437
438 #[test]
439 #[cfg(feature = "visualization")]
440 fn test_se3_to_rerun() {
441 let vertex = VertexSE3::new(0, Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
442
443 let (pos, rot) = vertex.to_rerun_transform(0.1);
444
445 assert!((pos.x - 0.1).abs() < 1e-6);
446 assert!((pos.y - 0.2).abs() < 1e-6);
447 assert!((pos.z - 0.3).abs() < 1e-6);
448 assert!((rot.w - 1.0).abs() < 1e-6);
449 }
450
451 #[test]
452 fn test_se2_to_rerun_2d() {
453 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
454
455 let pos = vertex.to_rerun_position_2d(0.1);
456
457 assert!((pos[0] - 1.0).abs() < 1e-6);
458 assert!((pos[1] - 2.0).abs() < 1e-6);
459 }
460
461 #[test]
462 #[cfg(feature = "visualization")]
463 fn test_se2_to_rerun_3d() {
464 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
465
466 let pos = vertex.to_rerun_position_3d(0.1, 5.0);
467
468 assert!((pos.x - 1.0).abs() < 1e-6);
469 assert!((pos.y - 2.0).abs() < 1e-6);
470 assert!((pos.z - 5.0).abs() < 1e-6);
471 }
472
473 #[test]
478 fn test_vertex_se2_from_vector() {
479 let v = VertexSE2::from_vector(5, Vector3::new(1.0, 2.0, 0.5));
480 assert_eq!(v.id(), 5);
481 assert!((v.x() - 1.0).abs() < 1e-12);
482 assert!((v.y() - 2.0).abs() < 1e-12);
483 assert!((v.theta() - 0.5).abs() < 1e-12);
484 }
485
486 #[test]
487 fn test_vertex_se3_from_vector() {
488 let arr = [1.0f64, 2.0, 3.0, 0.0, 0.0, 0.0, 1.0];
490 let v = VertexSE3::from_vector(7, arr);
491 assert_eq!(v.id(), 7);
492 assert!((v.x() - 1.0).abs() < 1e-10);
493 assert!((v.y() - 2.0).abs() < 1e-10);
494 assert!((v.z() - 3.0).abs() < 1e-10);
495 }
496
497 #[test]
498 fn test_vertex_se3_from_translation_quaternion() {
499 let trans = Vector3::new(1.0, 2.0, 3.0);
500 let quat = Quaternion::new(1.0, 0.0, 0.0, 0.0);
502 let v = VertexSE3::from_translation_quaternion(3, trans, quat);
503 assert_eq!(v.id(), 3);
504 assert!((v.x() - 1.0).abs() < 1e-10);
505 assert!((v.y() - 2.0).abs() < 1e-10);
506 assert!((v.z() - 3.0).abs() < 1e-10);
507 }
508
509 #[test]
514 fn test_vertex_se2_display() {
515 let v = VertexSE2::new(0, 1.0, 2.0, 0.5);
516 let s = format!("{v}");
517 assert!(
518 s.contains("VertexSE2"),
519 "Display should contain 'VertexSE2': {s}"
520 );
521 assert!(s.contains('0'), "Display should contain id: {s}");
522 }
523
524 #[test]
525 fn test_vertex_se3_display() {
526 let v = VertexSE3::new(1, Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
527 let s = format!("{v}");
528 assert!(
529 s.contains("VertexSE3"),
530 "Display should contain 'VertexSE3': {s}"
531 );
532 assert!(s.contains('1'), "Display should contain id: {s}");
533 }
534
535 #[test]
536 fn test_edge_se2_display() {
537 let e = EdgeSE2::new(0, 1, 1.0, 0.0, 0.0, Matrix3::identity());
538 let s = format!("{e}");
539 assert!(
540 s.contains("EdgeSE2"),
541 "Display should contain 'EdgeSE2': {s}"
542 );
543 }
544
545 #[test]
546 fn test_edge_se3_display() {
547 let e = EdgeSE3::new(
548 0,
549 1,
550 Vector3::zeros(),
551 UnitQuaternion::identity(),
552 Matrix6::identity(),
553 );
554 let s = format!("{e}");
555 assert!(
556 s.contains("EdgeSE3"),
557 "Display should contain 'EdgeSE3': {s}"
558 );
559 }
560
561 #[test]
562 fn test_graph_display() {
563 let mut g = Graph::new();
564 g.vertices_se2.insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
565 let s = format!("{g}");
566 assert!(s.contains("Graph"), "Display should contain 'Graph': {s}");
567 assert!(s.contains("count: 1"), "Display should show count: {s}");
568 }
569
570 #[test]
575 fn test_graph_default_is_empty() {
576 let g = Graph::default();
577 assert_eq!(g.vertex_count(), 0);
578 assert_eq!(g.edge_count(), 0);
579 }
580
581 #[test]
582 fn test_graph_vertex_and_edge_counts() {
583 let mut g = Graph::new();
584 g.vertices_se2.insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
585 g.vertices_se3.insert(
586 1,
587 VertexSE3::new(1, Vector3::zeros(), UnitQuaternion::identity()),
588 );
589 g.edges_se2
590 .push(EdgeSE2::new(0, 1, 0.0, 0.0, 0.0, Matrix3::identity()));
591 assert_eq!(g.vertex_count(), 2);
592 assert_eq!(g.edge_count(), 1);
593 }
594
595 #[test]
600 fn test_load_graph_unsupported_extension() {
601 let result = load_graph("fake_path.xyz");
602 assert!(
603 matches!(result, Err(IoError::UnsupportedFormat(_))),
604 "unknown extension should return UnsupportedFormat"
605 );
606 }
607
608 #[test]
609 fn test_load_graph_no_extension() {
610 let result = load_graph("/tmp/no_extension_file");
611 assert!(
612 matches!(result, Err(IoError::UnsupportedFormat(_))),
613 "path with no extension should return UnsupportedFormat"
614 );
615 }
616
617 #[test]
618 fn test_load_graph_toro_extension() -> Result<(), Box<dyn error::Error>> {
619 let mut f = NamedTempFile::new()?;
620 writeln!(f, "VERTEX2 0 0.0 0.0 0.0")?;
621 writeln!(f, "VERTEX2 1 1.0 0.0 0.0")?;
622 f.flush()?;
623 let toro_path = f.path().with_extension("graph");
625 std::fs::copy(f.path(), &toro_path)?;
626 let graph = load_graph(&toro_path)?;
627 std::fs::remove_file(&toro_path)?;
628 assert_eq!(graph.vertices_se2.len(), 2);
629 Ok(())
630 }
631
632 #[test]
633 fn test_io_error_log_returns_self() {
634 let err = IoError::UnsupportedFormat("xyz".to_string());
635 let returned = err.log();
636 assert!(matches!(returned, IoError::UnsupportedFormat(_)));
637 }
638
639 #[test]
640 fn test_io_error_log_with_source() {
641 let err = IoError::UnsupportedFormat("abc".to_string());
642 let source = std::io::Error::other("source");
643 let returned = err.log_with_source(source);
644 assert!(matches!(returned, IoError::UnsupportedFormat(_)));
645 }
646
647 #[test]
648 fn test_vertex_se2_theta() {
649 use std::f64::consts::PI;
650 let v = VertexSE2::new(0, 1.0, 2.0, PI / 4.0);
651 assert!((v.theta() - PI / 4.0).abs() < 1e-10);
652 }
653
654 #[test]
655 fn test_edge_se3_new() {
656 let t = Vector3::new(1.0, 2.0, 3.0);
657 let r = UnitQuaternion::identity();
658 let info = Matrix6::identity();
659 let e = EdgeSE3::new(0, 1, t, r, info);
660 assert_eq!(e.from, 0);
661 assert_eq!(e.to, 1);
662 }
663
664 #[test]
665 fn test_vertex_se3_new() {
666 let t = Vector3::new(1.0, 2.0, 3.0);
667 let r = UnitQuaternion::identity();
668 let v = VertexSE3::new(5, t, r);
669 assert_eq!(v.id, 5);
670 assert!((v.translation() - t).norm() < 1e-10);
671 }
672}