1use collections::HashMap;
2use nalgebra::{Matrix3, Matrix6, Quaternion, UnitQuaternion, Vector3};
3
4#[cfg(feature = "visualization")]
5use rerun::external::glam::{Quat, Vec3};
6
7use std::{
8 collections, fmt,
9 fmt::{Display, Formatter},
10 io,
11 path::Path,
12};
13use thiserror::Error;
14use tracing::error;
15
16use crate::{
18 core::problem::VariableEnum,
19 manifold::{se2::SE2, se3::SE3},
20};
21
22pub mod g2o;
24pub mod toro;
25
26pub use g2o::G2oLoader;
28pub use toro::ToroLoader;
29
30#[derive(Error, Debug)]
32pub enum IoError {
33 #[error("IO error: {0}")]
34 Io(#[from] io::Error),
35
36 #[error("Parse error at line {line}: {message}")]
37 Parse { line: usize, message: String },
38
39 #[error("Unsupported vertex type: {0}")]
40 UnsupportedVertexType(String),
41
42 #[error("Unsupported edge type: {0}")]
43 UnsupportedEdgeType(String),
44
45 #[error("Invalid number format at line {line}: {value}")]
46 InvalidNumber { line: usize, value: String },
47
48 #[error("Missing required fields at line {line}")]
49 MissingFields { line: usize },
50
51 #[error("Duplicate vertex ID: {id}")]
52 DuplicateVertex { id: usize },
53
54 #[error("Invalid quaternion at line {line}: norm = {norm:.6}, expected ~1.0")]
55 InvalidQuaternion { line: usize, norm: f64 },
56
57 #[error("Unsupported file format: {0}")]
58 UnsupportedFormat(String),
59
60 #[error("Failed to create file '{path}': {reason}")]
61 FileCreationFailed { path: String, reason: String },
62}
63
64impl IoError {
65 #[must_use]
67 pub fn log(self) -> Self {
68 error!("{}", self);
69 self
70 }
71
72 #[must_use]
74 pub fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
75 error!("{} | Source: {:?}", self, source_error);
76 self
77 }
78}
79
80#[derive(Clone, PartialEq)]
81pub struct VertexSE2 {
82 pub id: usize,
83 pub pose: SE2,
84}
85impl VertexSE2 {
86 pub fn new(id: usize, x: f64, y: f64, theta: f64) -> Self {
87 Self {
88 id,
89 pose: SE2::from_xy_angle(x, y, theta),
90 }
91 }
92
93 pub fn from_vector(id: usize, vector: Vector3<f64>) -> Self {
94 Self {
95 id,
96 pose: SE2::from_xy_angle(vector[0], vector[1], vector[2]),
97 }
98 }
99
100 pub fn id(&self) -> usize {
101 self.id
102 }
103
104 pub fn x(&self) -> f64 {
105 self.pose.x()
106 }
107
108 pub fn y(&self) -> f64 {
109 self.pose.y()
110 }
111
112 pub fn theta(&self) -> f64 {
113 self.pose.angle()
114 }
115}
116
117impl Display for VertexSE2 {
118 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
119 write!(f, "VertexSE2 [ id: {}, pose: {} ]", self.id, self.pose)
120 }
121}
122
123impl VertexSE2 {
124 pub fn to_rerun_position_2d(&self, scale: f32) -> [f32; 2] {
134 [(self.x() as f32) * scale, (self.y() as f32) * scale]
135 }
136
137 #[cfg(feature = "visualization")]
148 pub fn to_rerun_position_3d(&self, scale: f32, height: f32) -> Vec3 {
149 Vec3::new((self.x() as f32) * scale, (self.y() as f32) * scale, height)
150 }
151}
152
153#[derive(Clone, PartialEq)]
155pub struct VertexSE3 {
156 pub id: usize,
157 pub pose: SE3,
158}
159
160impl VertexSE3 {
161 pub fn new(id: usize, translation: Vector3<f64>, rotation: UnitQuaternion<f64>) -> Self {
162 Self {
163 id,
164 pose: SE3::new(translation, rotation),
165 }
166 }
167
168 pub fn from_vector(id: usize, vector: [f64; 7]) -> Self {
169 let translation = Vector3::from([vector[0], vector[1], vector[2]]);
170 let rotation = UnitQuaternion::from_quaternion(Quaternion::from([
171 vector[3], vector[4], vector[5], vector[6],
172 ]));
173 Self::new(id, translation, rotation)
174 }
175
176 pub fn from_translation_quaternion(
177 id: usize,
178 translation: Vector3<f64>,
179 quaternion: Quaternion<f64>,
180 ) -> Self {
181 Self {
182 id,
183 pose: SE3::from_translation_quaternion(translation, quaternion),
184 }
185 }
186
187 pub fn id(&self) -> usize {
188 self.id
189 }
190
191 pub fn translation(&self) -> Vector3<f64> {
192 self.pose.translation()
193 }
194
195 pub fn rotation(&self) -> UnitQuaternion<f64> {
196 self.pose.rotation_quaternion()
197 }
198
199 pub fn x(&self) -> f64 {
200 self.pose.x()
201 }
202
203 pub fn y(&self) -> f64 {
204 self.pose.y()
205 }
206
207 pub fn z(&self) -> f64 {
208 self.pose.z()
209 }
210}
211
212impl Display for VertexSE3 {
213 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
214 write!(f, "VertexSE3 [ id: {}, pose: {} ]", self.id, self.pose)
215 }
216}
217
218impl VertexSE3 {
219 #[cfg(feature = "visualization")]
229 pub fn to_rerun_transform(&self, scale: f32) -> (Vec3, Quat) {
230 let trans = self.translation();
232 let position = Vec3::new(trans.x as f32, trans.y as f32, trans.z as f32) * scale;
233
234 let rot = self.rotation();
236 let nq = rot.as_ref();
237 let rotation = Quat::from_xyzw(nq.i as f32, nq.j as f32, nq.k as f32, nq.w as f32);
238
239 (position, rotation)
240 }
241}
242
243#[derive(Clone, PartialEq)]
245pub struct EdgeSE2 {
246 pub from: usize,
247 pub to: usize,
248 pub measurement: SE2, pub information: Matrix3<f64>, }
251
252impl EdgeSE2 {
253 pub fn new(
254 from: usize,
255 to: usize,
256 dx: f64,
257 dy: f64,
258 dtheta: f64,
259 information: Matrix3<f64>,
260 ) -> Self {
261 Self {
262 from,
263 to,
264 measurement: SE2::from_xy_angle(dx, dy, dtheta),
265 information,
266 }
267 }
268}
269
270impl Display for EdgeSE2 {
271 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
272 write!(
273 f,
274 "EdgeSE2 [ from: {}, to: {}, measurement: {}, information: {} ]",
275 self.from, self.to, self.measurement, self.information
276 )
277 }
278}
279
280#[derive(Clone, PartialEq)]
282pub struct EdgeSE3 {
283 pub from: usize,
284 pub to: usize,
285 pub measurement: SE3, pub information: Matrix6<f64>, }
288
289impl EdgeSE3 {
290 pub fn new(
291 from: usize,
292 to: usize,
293 translation: Vector3<f64>,
294 rotation: UnitQuaternion<f64>,
295 information: Matrix6<f64>,
296 ) -> Self {
297 Self {
298 from,
299 to,
300 measurement: SE3::new(translation, rotation),
301 information,
302 }
303 }
304}
305
306impl Display for EdgeSE3 {
307 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
308 write!(
309 f,
310 "EdgeSE3 [ from: {}, to: {}, measurement: {}, information: {} ]",
311 self.from, self.to, self.measurement, self.information
312 )
313 }
314}
315
316#[derive(Clone)]
318pub struct Graph {
319 pub vertices_se2: collections::HashMap<usize, VertexSE2>,
320 pub vertices_se3: collections::HashMap<usize, VertexSE3>,
321 pub edges_se2: Vec<EdgeSE2>,
322 pub edges_se3: Vec<EdgeSE3>,
323}
324
325impl Graph {
326 pub fn new() -> Self {
327 Self {
328 vertices_se2: collections::HashMap::new(),
329 vertices_se3: collections::HashMap::new(),
330 edges_se2: Vec::new(),
331 edges_se3: Vec::new(),
332 }
333 }
334
335 pub fn vertex_count(&self) -> usize {
336 self.vertices_se2.len() + self.vertices_se3.len()
337 }
338
339 pub fn edge_count(&self) -> usize {
340 self.edges_se2.len() + self.edges_se3.len()
341 }
342
343 pub fn from_optimized_variables(
355 variables: &HashMap<String, VariableEnum>,
356 original_edges: &Self,
357 ) -> Self {
358 use VariableEnum;
359
360 let mut graph = Graph::new();
361
362 graph.edges_se2 = original_edges.edges_se2.clone();
364 graph.edges_se3 = original_edges.edges_se3.clone();
365
366 for (var_name, var) in variables {
368 if let Some(id_str) = var_name.strip_prefix('x')
370 && let Ok(id) = id_str.parse::<usize>()
371 {
372 match var {
373 VariableEnum::SE2(v) => {
374 let vertex = VertexSE2 {
375 id,
376 pose: v.value.clone(),
377 };
378 graph.vertices_se2.insert(id, vertex);
379 }
380 VariableEnum::SE3(v) => {
381 let vertex = VertexSE3 {
382 id,
383 pose: v.value.clone(),
384 };
385 graph.vertices_se3.insert(id, vertex);
386 }
387 _ => {
388 }
391 }
392 }
393 }
394
395 graph
396 }
397}
398
399impl Default for Graph {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405impl Display for Graph {
406 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
407 write!(
408 f,
409 "Graph [[ vertices_se2: {} (count: {}), vertices_se3: {} (count: {}), edges_se2: {} (count: {}), edges_se3: {} (count: {}) ]]",
410 self.vertices_se2
411 .values()
412 .map(|v| format!("{}", v))
413 .collect::<Vec<_>>()
414 .join(", "),
415 self.vertices_se2.len(),
416 self.vertices_se3
417 .values()
418 .map(|v| format!("{}", v))
419 .collect::<Vec<_>>()
420 .join(", "),
421 self.vertices_se3.len(),
422 self.edges_se2
423 .iter()
424 .map(|e| format!("{}", e))
425 .collect::<Vec<_>>()
426 .join(", "),
427 self.edges_se2.len(),
428 self.edges_se3
429 .iter()
430 .map(|e| format!("{}", e))
431 .collect::<Vec<_>>()
432 .join(", "),
433 self.edges_se3.len()
434 )
435 }
436}
437
438pub trait GraphLoader {
440 fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError>;
442
443 fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError>;
445}
446
447pub fn load_graph<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
449 let path_ref = path.as_ref();
450 let extension = path_ref
451 .extension()
452 .and_then(|ext| ext.to_str())
453 .ok_or_else(|| {
454 IoError::UnsupportedFormat("No file extension".to_string())
455 .log_with_source(format!("File path: {:?}", path_ref))
456 })?;
457
458 match extension.to_lowercase().as_str() {
459 "g2o" => G2oLoader::load(path),
460 "graph" => ToroLoader::load(path),
461 _ => Err(
462 IoError::UnsupportedFormat(format!("Unsupported extension: {extension}"))
463 .log_with_source(format!("File path: {:?}", path_ref)),
464 ),
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use std::{error, io::Write};
472 use tempfile::NamedTempFile;
473
474 #[test]
475 fn test_load_simple_graph() -> Result<(), IoError> {
476 let mut temp_file = NamedTempFile::new().map_err(|e| {
477 IoError::FileCreationFailed {
478 path: "temp_file".to_string(),
479 reason: e.to_string(),
480 }
481 .log()
482 })?;
483 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
484 writeln!(temp_file, "VERTEX_SE2 1 1.0 0.0 0.0")?;
485 writeln!(temp_file, "# This is a comment")?;
486 writeln!(temp_file)?; writeln!(temp_file, "VERTEX_SE3:QUAT 2 0.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
488
489 let graph = G2oLoader::load(temp_file.path())?;
490
491 assert_eq!(graph.vertices_se2.len(), 2);
492 assert_eq!(graph.vertices_se3.len(), 1);
493 assert!(graph.vertices_se2.contains_key(&0));
494 assert!(graph.vertices_se2.contains_key(&1));
495 assert!(graph.vertices_se3.contains_key(&2));
496
497 Ok(())
498 }
499
500 #[test]
501 fn test_load_m3500() -> Result<(), Box<dyn error::Error>> {
502 let graph = G2oLoader::load("data/M3500.g2o")?;
503 assert!(!graph.vertices_se2.is_empty());
504 Ok(())
505 }
506
507 #[test]
508 fn test_load_parking_garage() -> Result<(), Box<dyn error::Error>> {
509 let graph = G2oLoader::load("data/parking-garage.g2o")?;
510 assert!(!graph.vertices_se3.is_empty());
511 Ok(())
512 }
513
514 #[test]
515 fn test_load_sphere2500() -> Result<(), Box<dyn error::Error>> {
516 let graph = G2oLoader::load("data/sphere2500.g2o")?;
517 assert!(!graph.vertices_se3.is_empty());
518 Ok(())
519 }
520
521 #[test]
522 fn test_duplicate_vertex_error() -> Result<(), io::Error> {
523 let mut temp_file = NamedTempFile::new()?;
524 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
525 writeln!(temp_file, "VERTEX_SE2 0 1.0 0.0 0.0")?; let result = G2oLoader::load(temp_file.path());
528 assert!(matches!(result, Err(IoError::DuplicateVertex { id: 0 })));
529
530 Ok(())
531 }
532
533 #[test]
534 fn test_toro_loader() -> Result<(), IoError> {
535 let mut temp_file = NamedTempFile::new().map_err(|e| {
536 IoError::FileCreationFailed {
537 path: "temp_file".to_string(),
538 reason: e.to_string(),
539 }
540 .log()
541 })?;
542 writeln!(temp_file, "VERTEX2 0 0.0 0.0 0.0")?;
543 writeln!(temp_file, "VERTEX2 1 1.0 0.0 0.0")?;
544
545 let graph = ToroLoader::load(temp_file.path()).map_err(|e| e.log())?;
546 assert_eq!(graph.vertices_se2.len(), 2);
547
548 Ok(())
549 }
550
551 #[test]
552 #[cfg(feature = "visualization")]
553 fn test_se3_to_rerun() {
554 let vertex = VertexSE3::new(0, Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
555
556 let (pos, rot) = vertex.to_rerun_transform(0.1);
557
558 assert!((pos.x - 0.1).abs() < 1e-6);
559 assert!((pos.y - 0.2).abs() < 1e-6);
560 assert!((pos.z - 0.3).abs() < 1e-6);
561 assert!((rot.w - 1.0).abs() < 1e-6);
562 }
563
564 #[test]
565 fn test_se2_to_rerun_2d() {
566 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
567
568 let pos = vertex.to_rerun_position_2d(0.1);
569
570 assert!((pos[0] - 1.0).abs() < 1e-6);
571 assert!((pos[1] - 2.0).abs() < 1e-6);
572 }
573
574 #[test]
575 #[cfg(feature = "visualization")]
576 fn test_se2_to_rerun_3d() {
577 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
578
579 let pos = vertex.to_rerun_position_3d(0.1, 5.0);
580
581 assert!((pos.x - 1.0).abs() < 1e-6);
582 assert!((pos.y - 2.0).abs() < 1e-6);
583 assert!((pos.z - 5.0).abs() < 1e-6);
584 }
585}