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 toro;
22
23pub use bal::{BalCamera, BalDataset, BalLoader, BalObservation, BalPoint};
25pub use g2o::G2oLoader;
26pub use toro::ToroLoader;
27
28#[derive(Error, Debug)]
30pub enum IoError {
31 #[error("IO error: {0}")]
32 Io(#[from] io::Error),
33
34 #[error("Parse error at line {line}: {message}")]
35 Parse { line: usize, message: String },
36
37 #[error("Unsupported vertex type: {0}")]
38 UnsupportedVertexType(String),
39
40 #[error("Unsupported edge type: {0}")]
41 UnsupportedEdgeType(String),
42
43 #[error("Invalid number format at line {line}: {value}")]
44 InvalidNumber { line: usize, value: String },
45
46 #[error("Missing required fields at line {line}")]
47 MissingFields { line: usize },
48
49 #[error("Duplicate vertex ID: {id}")]
50 DuplicateVertex { id: usize },
51
52 #[error("Invalid quaternion at line {line}: norm = {norm:.6}, expected ~1.0")]
53 InvalidQuaternion { line: usize, norm: f64 },
54
55 #[error("Unsupported file format: {0}")]
56 UnsupportedFormat(String),
57
58 #[error("Failed to create file '{path}': {reason}")]
59 FileCreationFailed { path: String, reason: String },
60}
61
62impl IoError {
63 #[must_use]
65 pub fn log(self) -> Self {
66 error!("{}", self);
67 self
68 }
69
70 #[must_use]
72 pub fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
73 error!("{} | Source: {:?}", self, source_error);
74 self
75 }
76}
77
78#[derive(Clone, PartialEq)]
79pub struct VertexSE2 {
80 pub id: usize,
81 pub pose: SE2,
82}
83impl VertexSE2 {
84 pub fn new(id: usize, x: f64, y: f64, theta: f64) -> Self {
85 Self {
86 id,
87 pose: SE2::from_xy_angle(x, y, theta),
88 }
89 }
90
91 pub fn from_vector(id: usize, vector: Vector3<f64>) -> Self {
92 Self {
93 id,
94 pose: SE2::from_xy_angle(vector[0], vector[1], vector[2]),
95 }
96 }
97
98 pub fn id(&self) -> usize {
99 self.id
100 }
101
102 pub fn x(&self) -> f64 {
103 self.pose.x()
104 }
105
106 pub fn y(&self) -> f64 {
107 self.pose.y()
108 }
109
110 pub fn theta(&self) -> f64 {
111 self.pose.angle()
112 }
113}
114
115impl Display for VertexSE2 {
116 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117 write!(f, "VertexSE2 [ id: {}, pose: {} ]", self.id, self.pose)
118 }
119}
120
121impl VertexSE2 {
122 pub fn to_rerun_position_2d(&self, scale: f32) -> [f32; 2] {
132 [(self.x() as f32) * scale, (self.y() as f32) * scale]
133 }
134
135 #[cfg(feature = "visualization")]
146 pub fn to_rerun_position_3d(&self, scale: f32, height: f32) -> Vec3 {
147 Vec3::new((self.x() as f32) * scale, (self.y() as f32) * scale, height)
148 }
149}
150
151#[derive(Clone, PartialEq)]
153pub struct VertexSE3 {
154 pub id: usize,
155 pub pose: SE3,
156}
157
158impl VertexSE3 {
159 pub fn new(id: usize, translation: Vector3<f64>, rotation: UnitQuaternion<f64>) -> Self {
160 Self {
161 id,
162 pose: SE3::new(translation, rotation),
163 }
164 }
165
166 pub fn from_vector(id: usize, vector: [f64; 7]) -> Self {
167 let translation = Vector3::from([vector[0], vector[1], vector[2]]);
168 let rotation = UnitQuaternion::from_quaternion(Quaternion::from([
169 vector[3], vector[4], vector[5], vector[6],
170 ]));
171 Self::new(id, translation, rotation)
172 }
173
174 pub fn from_translation_quaternion(
175 id: usize,
176 translation: Vector3<f64>,
177 quaternion: Quaternion<f64>,
178 ) -> Self {
179 Self {
180 id,
181 pose: SE3::from_translation_quaternion(translation, quaternion),
182 }
183 }
184
185 pub fn id(&self) -> usize {
186 self.id
187 }
188
189 pub fn translation(&self) -> Vector3<f64> {
190 self.pose.translation()
191 }
192
193 pub fn rotation(&self) -> UnitQuaternion<f64> {
194 self.pose.rotation_quaternion()
195 }
196
197 pub fn x(&self) -> f64 {
198 self.pose.x()
199 }
200
201 pub fn y(&self) -> f64 {
202 self.pose.y()
203 }
204
205 pub fn z(&self) -> f64 {
206 self.pose.z()
207 }
208}
209
210impl Display for VertexSE3 {
211 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
212 write!(f, "VertexSE3 [ id: {}, pose: {} ]", self.id, self.pose)
213 }
214}
215
216impl VertexSE3 {
217 #[cfg(feature = "visualization")]
227 pub fn to_rerun_transform(&self, scale: f32) -> (Vec3, Quat) {
228 let trans = self.translation();
230 let position = Vec3::new(trans.x as f32, trans.y as f32, trans.z as f32) * scale;
231
232 let rot = self.rotation();
234 let nq = rot.as_ref();
235 let rotation = Quat::from_xyzw(nq.i as f32, nq.j as f32, nq.k as f32, nq.w as f32);
236
237 (position, rotation)
238 }
239}
240
241#[derive(Clone, PartialEq)]
243pub struct EdgeSE2 {
244 pub from: usize,
245 pub to: usize,
246 pub measurement: SE2, pub information: Matrix3<f64>, }
249
250impl EdgeSE2 {
251 pub fn new(
252 from: usize,
253 to: usize,
254 dx: f64,
255 dy: f64,
256 dtheta: f64,
257 information: Matrix3<f64>,
258 ) -> Self {
259 Self {
260 from,
261 to,
262 measurement: SE2::from_xy_angle(dx, dy, dtheta),
263 information,
264 }
265 }
266}
267
268impl Display for EdgeSE2 {
269 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
270 write!(
271 f,
272 "EdgeSE2 [ from: {}, to: {}, measurement: {}, information: {} ]",
273 self.from, self.to, self.measurement, self.information
274 )
275 }
276}
277
278#[derive(Clone, PartialEq)]
280pub struct EdgeSE3 {
281 pub from: usize,
282 pub to: usize,
283 pub measurement: SE3, pub information: Matrix6<f64>, }
286
287impl EdgeSE3 {
288 pub fn new(
289 from: usize,
290 to: usize,
291 translation: Vector3<f64>,
292 rotation: UnitQuaternion<f64>,
293 information: Matrix6<f64>,
294 ) -> Self {
295 Self {
296 from,
297 to,
298 measurement: SE3::new(translation, rotation),
299 information,
300 }
301 }
302}
303
304impl Display for EdgeSE3 {
305 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
306 write!(
307 f,
308 "EdgeSE3 [ from: {}, to: {}, measurement: {}, information: {} ]",
309 self.from, self.to, self.measurement, self.information
310 )
311 }
312}
313
314#[derive(Clone)]
316pub struct Graph {
317 pub vertices_se2: collections::HashMap<usize, VertexSE2>,
318 pub vertices_se3: collections::HashMap<usize, VertexSE3>,
319 pub edges_se2: Vec<EdgeSE2>,
320 pub edges_se3: Vec<EdgeSE3>,
321}
322
323impl Graph {
324 pub fn new() -> Self {
325 Self {
326 vertices_se2: collections::HashMap::new(),
327 vertices_se3: collections::HashMap::new(),
328 edges_se2: Vec::new(),
329 edges_se3: Vec::new(),
330 }
331 }
332
333 pub fn vertex_count(&self) -> usize {
334 self.vertices_se2.len() + self.vertices_se3.len()
335 }
336
337 pub fn edge_count(&self) -> usize {
338 self.edges_se2.len() + self.edges_se3.len()
339 }
340
341 }
344
345impl Default for Graph {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351impl Display for Graph {
352 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
353 write!(
354 f,
355 "Graph [[ vertices_se2: {} (count: {}), vertices_se3: {} (count: {}), edges_se2: {} (count: {}), edges_se3: {} (count: {}) ]]",
356 self.vertices_se2
357 .values()
358 .map(|v| format!("{}", v))
359 .collect::<Vec<_>>()
360 .join(", "),
361 self.vertices_se2.len(),
362 self.vertices_se3
363 .values()
364 .map(|v| format!("{}", v))
365 .collect::<Vec<_>>()
366 .join(", "),
367 self.vertices_se3.len(),
368 self.edges_se2
369 .iter()
370 .map(|e| format!("{}", e))
371 .collect::<Vec<_>>()
372 .join(", "),
373 self.edges_se2.len(),
374 self.edges_se3
375 .iter()
376 .map(|e| format!("{}", e))
377 .collect::<Vec<_>>()
378 .join(", "),
379 self.edges_se3.len()
380 )
381 }
382}
383
384pub trait GraphLoader {
386 fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError>;
388
389 fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError>;
391}
392
393pub fn load_graph<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
395 let path_ref = path.as_ref();
396 let extension = path_ref
397 .extension()
398 .and_then(|ext| ext.to_str())
399 .ok_or_else(|| {
400 IoError::UnsupportedFormat("No file extension".to_string())
401 .log_with_source(format!("File path: {:?}", path_ref))
402 })?;
403
404 match extension.to_lowercase().as_str() {
405 "g2o" => G2oLoader::load(path),
406 "graph" => ToroLoader::load(path),
407 _ => Err(
408 IoError::UnsupportedFormat(format!("Unsupported extension: {extension}"))
409 .log_with_source(format!("File path: {:?}", path_ref)),
410 ),
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use std::{error, io::Write};
418 use tempfile::NamedTempFile;
419
420 #[test]
421 fn test_load_simple_graph() -> 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, "VERTEX_SE2 0 0.0 0.0 0.0")?;
430 writeln!(temp_file, "VERTEX_SE2 1 1.0 0.0 0.0")?;
431 writeln!(temp_file, "# This is a comment")?;
432 writeln!(temp_file)?; writeln!(temp_file, "VERTEX_SE3:QUAT 2 0.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
434
435 let graph = G2oLoader::load(temp_file.path())?;
436
437 assert_eq!(graph.vertices_se2.len(), 2);
438 assert_eq!(graph.vertices_se3.len(), 1);
439 assert!(graph.vertices_se2.contains_key(&0));
440 assert!(graph.vertices_se2.contains_key(&1));
441 assert!(graph.vertices_se3.contains_key(&2));
442
443 Ok(())
444 }
445
446 #[test]
447 fn test_load_m3500() -> Result<(), Box<dyn error::Error>> {
448 let graph = G2oLoader::load("../../data/odometry/M3500.g2o")?;
449 assert!(!graph.vertices_se2.is_empty());
450 Ok(())
451 }
452
453 #[test]
454 fn test_load_sphere2500() -> Result<(), Box<dyn error::Error>> {
455 let graph = G2oLoader::load("../../data/odometry/sphere2500.g2o")?;
456 assert!(!graph.vertices_se3.is_empty());
457 Ok(())
458 }
459
460 #[test]
461 fn test_duplicate_vertex_error() -> Result<(), io::Error> {
462 let mut temp_file = NamedTempFile::new()?;
463 writeln!(temp_file, "VERTEX_SE2 0 0.0 0.0 0.0")?;
464 writeln!(temp_file, "VERTEX_SE2 0 1.0 0.0 0.0")?; let result = G2oLoader::load(temp_file.path());
467 assert!(matches!(result, Err(IoError::DuplicateVertex { id: 0 })));
468
469 Ok(())
470 }
471
472 #[test]
473 fn test_toro_loader() -> Result<(), IoError> {
474 let mut temp_file = NamedTempFile::new().map_err(|e| {
475 IoError::FileCreationFailed {
476 path: "temp_file".to_string(),
477 reason: e.to_string(),
478 }
479 .log()
480 })?;
481 writeln!(temp_file, "VERTEX2 0 0.0 0.0 0.0")?;
482 writeln!(temp_file, "VERTEX2 1 1.0 0.0 0.0")?;
483
484 let graph = ToroLoader::load(temp_file.path()).map_err(|e| e.log())?;
485 assert_eq!(graph.vertices_se2.len(), 2);
486
487 Ok(())
488 }
489
490 #[test]
491 #[cfg(feature = "visualization")]
492 fn test_se3_to_rerun() {
493 let vertex = VertexSE3::new(0, Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
494
495 let (pos, rot) = vertex.to_rerun_transform(0.1);
496
497 assert!((pos.x - 0.1).abs() < 1e-6);
498 assert!((pos.y - 0.2).abs() < 1e-6);
499 assert!((pos.z - 0.3).abs() < 1e-6);
500 assert!((rot.w - 1.0).abs() < 1e-6);
501 }
502
503 #[test]
504 fn test_se2_to_rerun_2d() {
505 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
506
507 let pos = vertex.to_rerun_position_2d(0.1);
508
509 assert!((pos[0] - 1.0).abs() < 1e-6);
510 assert!((pos[1] - 2.0).abs() < 1e-6);
511 }
512
513 #[test]
514 #[cfg(feature = "visualization")]
515 fn test_se2_to_rerun_3d() {
516 let vertex = VertexSE2::new(0, 10.0, 20.0, 0.5);
517
518 let pos = vertex.to_rerun_position_3d(0.1, 5.0);
519
520 assert!((pos.x - 1.0).abs() < 1e-6);
521 assert!((pos.y - 2.0).abs() < 1e-6);
522 assert!((pos.z - 5.0).abs() < 1e-6);
523 }
524}