1use nalgebra;
2use std::{collections, fmt, path};
3use thiserror;
4
5use crate::{
7 core::problem,
8 manifold::{se2::SE2, se3::SE3},
9};
10
11pub mod g2o;
13pub mod toro;
14
15pub use g2o::G2oLoader;
17pub use toro::ToroLoader;
18
19#[derive(thiserror::Error, Debug)]
21pub enum ApexSolverIoError {
22 #[error("IO error: {0}")]
23 Io(#[from] std::io::Error),
24
25 #[error("Parse error at line {line}: {message}")]
26 Parse { line: usize, message: String },
27
28 #[error("Unsupported vertex type: {0}")]
29 UnsupportedVertexType(String),
30
31 #[error("Unsupported edge type: {0}")]
32 UnsupportedEdgeType(String),
33
34 #[error("Invalid number format at line {line}: {value}")]
35 InvalidNumber { line: usize, value: String },
36
37 #[error("Missing required fields at line {line}")]
38 MissingFields { line: usize },
39
40 #[error("Duplicate vertex ID: {id}")]
41 DuplicateVertex { id: usize },
42
43 #[error("Invalid quaternion at line {line}: norm = {norm:.6}, expected ~1.0")]
44 InvalidQuaternion { line: usize, norm: f64 },
45
46 #[error("Unsupported file format: {0}")]
47 UnsupportedFormat(String),
48}
49
50#[derive(Clone, PartialEq)]
51pub struct VertexSE2 {
52 pub id: usize,
53 pub pose: SE2,
54}
55impl VertexSE2 {
56 pub fn new(id: usize, x: f64, y: f64, theta: f64) -> Self {
57 Self {
58 id,
59 pose: SE2::from_xy_angle(x, y, theta),
60 }
61 }
62
63 pub fn from_vector(id: usize, vector: nalgebra::Vector3<f64>) -> Self {
64 Self {
65 id,
66 pose: SE2::from_xy_angle(vector[0], vector[1], vector[2]),
67 }
68 }
69
70 pub fn id(&self) -> usize {
71 self.id
72 }
73
74 pub fn x(&self) -> f64 {
75 self.pose.x()
76 }
77
78 pub fn y(&self) -> f64 {
79 self.pose.y()
80 }
81
82 pub fn theta(&self) -> f64 {
83 self.pose.angle()
84 }
85}
86
87impl fmt::Display for VertexSE2 {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "VertexSE2 [ id: {}, pose: {} ]", self.id, self.pose)
90 }
91}
92
93impl VertexSE2 {
94 pub fn to_rerun_position_2d(&self, scale: f32) -> [f32; 2] {
102 [(self.x() as f32) * scale, (self.y() as f32) * scale]
103 }
104
105 pub fn to_rerun_position_3d(&self, scale: f32, height: f32) -> rerun::external::glam::Vec3 {
114 rerun::external::glam::Vec3::new(
115 (self.x() as f32) * scale,
116 (self.y() as f32) * scale,
117 height,
118 )
119 }
120}
121
122#[derive(Clone, PartialEq)]
124pub struct VertexSE3 {
125 pub id: usize,
126 pub pose: SE3,
127}
128
129impl VertexSE3 {
130 pub fn new(
131 id: usize,
132 translation: nalgebra::Vector3<f64>,
133 rotation: nalgebra::UnitQuaternion<f64>,
134 ) -> Self {
135 Self {
136 id,
137 pose: SE3::new(translation, rotation),
138 }
139 }
140
141 pub fn from_vector(id: usize, vector: [f64; 7]) -> Self {
142 let translation = nalgebra::Vector3::from([vector[0], vector[1], vector[2]]);
143 let rotation = nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion::from([
144 vector[3], vector[4], vector[5], vector[6],
145 ]));
146 Self::new(id, translation, rotation)
147 }
148
149 pub fn from_translation_quaternion(
150 id: usize,
151 translation: nalgebra::Vector3<f64>,
152 quaternion: nalgebra::Quaternion<f64>,
153 ) -> Self {
154 Self {
155 id,
156 pose: SE3::from_translation_quaternion(translation, quaternion),
157 }
158 }
159
160 pub fn id(&self) -> usize {
161 self.id
162 }
163
164 pub fn translation(&self) -> nalgebra::Vector3<f64> {
165 self.pose.translation()
166 }
167
168 pub fn rotation(&self) -> nalgebra::UnitQuaternion<f64> {
169 self.pose.rotation_quaternion()
170 }
171
172 pub fn x(&self) -> f64 {
173 self.pose.x()
174 }
175
176 pub fn y(&self) -> f64 {
177 self.pose.y()
178 }
179
180 pub fn z(&self) -> f64 {
181 self.pose.z()
182 }
183}
184
185impl fmt::Display for VertexSE3 {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(f, "VertexSE3 [ id: {}, pose: {} ]", self.id, self.pose)
188 }
189}
190
191impl VertexSE3 {
192 pub fn to_rerun_transform(
200 &self,
201 scale: f32,
202 ) -> (rerun::external::glam::Vec3, rerun::external::glam::Quat) {
203 let trans = self.translation();
205 let position =
206 rerun::external::glam::Vec3::new(trans.x as f32, trans.y as f32, trans.z as f32)
207 * scale;
208
209 let rot = self.rotation();
211 let nq = rot.as_ref();
212 let rotation = rerun::external::glam::Quat::from_xyzw(
213 nq.i as f32,
214 nq.j as f32,
215 nq.k as f32,
216 nq.w as f32,
217 );
218
219 (position, rotation)
220 }
221}
222
223#[derive(Clone, PartialEq)]
225pub struct EdgeSE2 {
226 pub from: usize,
227 pub to: usize,
228 pub measurement: SE2, pub information: nalgebra::Matrix3<f64>, }
231
232impl EdgeSE2 {
233 pub fn new(
234 from: usize,
235 to: usize,
236 dx: f64,
237 dy: f64,
238 dtheta: f64,
239 information: nalgebra::Matrix3<f64>,
240 ) -> Self {
241 Self {
242 from,
243 to,
244 measurement: SE2::from_xy_angle(dx, dy, dtheta),
245 information,
246 }
247 }
248}
249
250impl fmt::Display for EdgeSE2 {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 write!(
253 f,
254 "EdgeSE2 [ from: {}, to: {}, measurement: {}, information: {} ]",
255 self.from, self.to, self.measurement, self.information
256 )
257 }
258}
259
260#[derive(Clone, PartialEq)]
262pub struct EdgeSE3 {
263 pub from: usize,
264 pub to: usize,
265 pub measurement: SE3, pub information: nalgebra::Matrix6<f64>, }
268
269impl EdgeSE3 {
270 pub fn new(
271 from: usize,
272 to: usize,
273 translation: nalgebra::Vector3<f64>,
274 rotation: nalgebra::UnitQuaternion<f64>,
275 information: nalgebra::Matrix6<f64>,
276 ) -> Self {
277 Self {
278 from,
279 to,
280 measurement: SE3::new(translation, rotation),
281 information,
282 }
283 }
284}
285
286impl fmt::Display for EdgeSE3 {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 write!(
289 f,
290 "EdgeSE3 [ from: {}, to: {}, measurement: {}, information: {} ]",
291 self.from, self.to, self.measurement, self.information
292 )
293 }
294}
295
296#[derive(Clone)]
298pub struct Graph {
299 pub vertices_se2: collections::HashMap<usize, VertexSE2>,
300 pub vertices_se3: collections::HashMap<usize, VertexSE3>,
301 pub edges_se2: Vec<EdgeSE2>,
302 pub edges_se3: Vec<EdgeSE3>,
303}
304
305impl Graph {
306 pub fn new() -> Self {
307 Self {
308 vertices_se2: collections::HashMap::new(),
309 vertices_se3: collections::HashMap::new(),
310 edges_se2: Vec::new(),
311 edges_se3: Vec::new(),
312 }
313 }
314
315 pub fn vertex_count(&self) -> usize {
316 self.vertices_se2.len() + self.vertices_se3.len()
317 }
318
319 pub fn edge_count(&self) -> usize {
320 self.edges_se2.len() + self.edges_se3.len()
321 }
322
323 pub fn from_optimized_variables(
335 variables: &collections::HashMap<String, problem::VariableEnum>,
336 original_edges: &Self,
337 ) -> Self {
338 use problem::VariableEnum;
339
340 let mut graph = Graph::new();
341
342 graph.edges_se2 = original_edges.edges_se2.clone();
344 graph.edges_se3 = original_edges.edges_se3.clone();
345
346 for (var_name, var) in variables {
348 if let Some(id_str) = var_name.strip_prefix('x')
350 && let Ok(id) = id_str.parse::<usize>()
351 {
352 match var {
353 VariableEnum::SE2(v) => {
354 let vertex = VertexSE2 {
355 id,
356 pose: v.value.clone(),
357 };
358 graph.vertices_se2.insert(id, vertex);
359 }
360 VariableEnum::SE3(v) => {
361 let vertex = VertexSE3 {
362 id,
363 pose: v.value.clone(),
364 };
365 graph.vertices_se3.insert(id, vertex);
366 }
367 _ => {
368 }
371 }
372 }
373 }
374
375 graph
376 }
377}
378
379impl Default for Graph {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385impl fmt::Display for Graph {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 write!(
388 f,
389 "Graph [[ vertices_se2: {} (count: {}), vertices_se3: {} (count: {}), edges_se2: {} (count: {}), edges_se3: {} (count: {}) ]]",
390 self.vertices_se2
391 .values()
392 .map(|v| format!("{}", v))
393 .collect::<Vec<_>>()
394 .join(", "),
395 self.vertices_se2.len(),
396 self.vertices_se3
397 .values()
398 .map(|v| format!("{}", v))
399 .collect::<Vec<_>>()
400 .join(", "),
401 self.vertices_se3.len(),
402 self.edges_se2
403 .iter()
404 .map(|e| format!("{}", e))
405 .collect::<Vec<_>>()
406 .join(", "),
407 self.edges_se2.len(),
408 self.edges_se3
409 .iter()
410 .map(|e| format!("{}", e))
411 .collect::<Vec<_>>()
412 .join(", "),
413 self.edges_se3.len()
414 )
415 }
416}
417
418pub trait GraphLoader {
420 fn load<P: AsRef<path::Path>>(path: P) -> Result<Graph, ApexSolverIoError>;
422
423 fn write<P: AsRef<path::Path>>(graph: &Graph, path: P) -> Result<(), ApexSolverIoError>;
425}
426
427pub fn load_graph<P: AsRef<path::Path>>(path: P) -> Result<Graph, ApexSolverIoError> {
429 let path_ref = path.as_ref();
430 let extension = path_ref
431 .extension()
432 .and_then(|ext| ext.to_str())
433 .ok_or_else(|| ApexSolverIoError::UnsupportedFormat("No file extension".to_string()))?;
434
435 match extension.to_lowercase().as_str() {
436 "g2o" => G2oLoader::load(path),
437 "graph" => ToroLoader::load(path),
438 _ => Err(ApexSolverIoError::UnsupportedFormat(format!(
439 "Unsupported extension: {extension}"
440 ))),
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use std::io::Write;
448 use tempfile::NamedTempFile;
449
450 #[test]
451 fn test_load_simple_graph() -> Result<(), ApexSolverIoError> {
452 let mut temp_file = NamedTempFile::new().unwrap();
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 std::error::Error>> {
472 let graph = G2oLoader::load("data/M3500.g2o")?;
473 println!(
474 "M3500 loaded: {} vertices, {} edges",
475 graph.vertex_count(),
476 graph.edge_count()
477 );
478 assert!(!graph.vertices_se2.is_empty());
479 Ok(())
480 }
481
482 #[test]
483 fn test_load_parking_garage() -> Result<(), Box<dyn std::error::Error>> {
484 let graph = G2oLoader::load("data/parking-garage.g2o")?;
485 println!(
486 "Parking garage loaded: {} vertices, {} edges",
487 graph.vertex_count(),
488 graph.edge_count()
489 );
490 assert!(!graph.vertices_se3.is_empty());
491 Ok(())
492 }
493
494 #[test]
495 fn test_load_sphere2500() -> Result<(), Box<dyn std::error::Error>> {
496 let graph = G2oLoader::load("data/sphere2500.g2o")?;
497 println!(
498 "Sphere2500 loaded: {} vertices, {} edges",
499 graph.vertex_count(),
500 graph.edge_count()
501 );
502 assert!(!graph.vertices_se3.is_empty());
503 Ok(())
504 }
505
506 #[test]
507 fn test_duplicate_vertex_error() -> Result<(), std::io::Error> {
508 let mut temp_file = NamedTempFile::new()?;
509 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
510 writeln!(temp_file, "VERTEX_SE2 0 1.0 0.0 0.0")?; let result = G2oLoader::load(temp_file.path());
513 assert!(matches!(
514 result,
515 Err(ApexSolverIoError::DuplicateVertex { id: 0 })
516 ));
517
518 Ok(())
519 }
520
521 #[test]
522 fn test_toro_loader() -> Result<(), std::io::Error> {
523 let mut temp_file = NamedTempFile::new()?;
524 writeln!(temp_file, "VERTEX2 0 0.0 0.0 0.0")?;
525 writeln!(temp_file, "VERTEX2 1 1.0 0.0 0.0")?;
526
527 let graph = ToroLoader::load(temp_file.path()).unwrap();
528 assert_eq!(graph.vertices_se2.len(), 2);
529
530 Ok(())
531 }
532
533 #[test]
534 fn test_se3_to_rerun() {
535 let vertex = VertexSE3::new(
536 0,
537 nalgebra::Vector3::new(1.0, 2.0, 3.0),
538 nalgebra::UnitQuaternion::identity(),
539 );
540
541 let (pos, rot) = vertex.to_rerun_transform(0.1);
542
543 assert!((pos.x - 0.1).abs() < 1e-6);
544 assert!((pos.y - 0.2).abs() < 1e-6);
545 assert!((pos.z - 0.3).abs() < 1e-6);
546 assert!((rot.w - 1.0).abs() < 1e-6);
547 }
548
549 #[test]
550 fn test_se2_to_rerun_2d() {
551 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
552
553 let pos = vertex.to_rerun_position_2d(0.1);
554
555 assert!((pos[0] - 1.0).abs() < 1e-6);
556 assert!((pos[1] - 2.0).abs() < 1e-6);
557 }
558
559 #[test]
560 fn test_se2_to_rerun_3d() {
561 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
562
563 let pos = vertex.to_rerun_position_3d(0.1, 5.0);
564
565 assert!((pos.x - 1.0).abs() < 1e-6);
566 assert!((pos.y - 2.0).abs() < 1e-6);
567 assert!((pos.z - 5.0).abs() < 1e-6);
568 }
569}