apex_solver/io/
g2o.rs

1use crate::io::{EdgeSE2, EdgeSE3, Graph, GraphLoader, IoError, VertexSE2, VertexSE3};
2use memmap2;
3use rayon::prelude::*;
4use std::collections::HashMap;
5use std::{fs::File, io::Write, path::Path};
6
7/// High-performance G2O file loader
8pub struct G2oLoader;
9
10impl GraphLoader for G2oLoader {
11    fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
12        let path_ref = path.as_ref();
13        let file = File::open(path_ref).map_err(|e| {
14            IoError::Io(e).log_with_source(format!("Failed to open G2O file: {:?}", path_ref))
15        })?;
16        let mmap = unsafe {
17            memmap2::Mmap::map(&file).map_err(|e| {
18                IoError::Io(e)
19                    .log_with_source(format!("Failed to memory-map G2O file: {:?}", path_ref))
20            })?
21        };
22        let content = std::str::from_utf8(&mmap).map_err(|e| {
23            IoError::Parse {
24                line: 0,
25                message: format!("Invalid UTF-8: {e}"),
26            }
27            .log()
28        })?;
29
30        Self::parse_content(content)
31    }
32
33    fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError> {
34        let path_ref = path.as_ref();
35        let mut file = File::create(path_ref).map_err(|e| {
36            IoError::Io(e).log_with_source(format!("Failed to create G2O file: {:?}", path_ref))
37        })?;
38
39        // Write header comment
40        writeln!(file, "# G2O file written by Apex Solver")
41            .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O header"))?;
42        writeln!(
43            file,
44            "# Timestamp: {}",
45            chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
46        )
47        .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O timestamp"))?;
48        writeln!(
49            file,
50            "# SE2 vertices: {}, SE3 vertices: {}, SE2 edges: {}, SE3 edges: {}",
51            graph.vertices_se2.len(),
52            graph.vertices_se3.len(),
53            graph.edges_se2.len(),
54            graph.edges_se3.len()
55        )
56        .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O statistics"))?;
57        writeln!(file)
58            .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O header newline"))?;
59
60        // Write SE2 vertices (sorted by ID)
61        let mut se2_ids: Vec<_> = graph.vertices_se2.keys().collect();
62        se2_ids.sort();
63
64        for id in se2_ids {
65            let vertex = &graph.vertices_se2[id];
66            writeln!(
67                file,
68                "VERTEX_SE2 {} {:.17e} {:.17e} {:.17e}",
69                vertex.id,
70                vertex.x(),
71                vertex.y(),
72                vertex.theta()
73            )
74            .map_err(|e| {
75                IoError::Io(e).log_with_source(format!("Failed to write SE2 vertex {}", vertex.id))
76            })?;
77        }
78
79        // Write SE3 vertices (sorted by ID)
80        let mut se3_ids: Vec<_> = graph.vertices_se3.keys().collect();
81        se3_ids.sort();
82
83        for id in se3_ids {
84            let vertex = &graph.vertices_se3[id];
85            let trans = vertex.translation();
86            let quat = vertex.rotation();
87            writeln!(
88                file,
89                "VERTEX_SE3:QUAT {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
90                vertex.id, trans.x, trans.y, trans.z, quat.i, quat.j, quat.k, quat.w
91            )
92            .map_err(|e| {
93                IoError::Io(e).log_with_source(format!("Failed to write SE3 vertex {}", vertex.id))
94            })?;
95        }
96
97        // Write SE2 edges
98        for edge in &graph.edges_se2 {
99            let meas = &edge.measurement;
100            let info = &edge.information;
101
102            // G2O SE2 information matrix order: i11, i12, i22, i33, i13, i23
103            writeln!(
104                file,
105                "EDGE_SE2 {} {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
106                edge.from,
107                edge.to,
108                meas.x(),
109                meas.y(),
110                meas.angle(),
111                info[(0, 0)],
112                info[(0, 1)],
113                info[(1, 1)],
114                info[(2, 2)],
115                info[(0, 2)],
116                info[(1, 2)]
117            )
118            .map_err(|e| {
119                IoError::Io(e).log_with_source(format!(
120                    "Failed to write SE2 edge {} -> {}",
121                    edge.from, edge.to
122                ))
123            })?;
124        }
125
126        // Write SE3 edges
127        for edge in &graph.edges_se3 {
128            let trans = edge.measurement.translation();
129            let quat = edge.measurement.rotation_quaternion();
130            let info = &edge.information;
131
132            // Write EDGE_SE3:QUAT with full 6x6 upper triangular information matrix (21 values)
133            write!(
134                file,
135                "EDGE_SE3:QUAT {} {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
136                edge.from, edge.to, trans.x, trans.y, trans.z, quat.i, quat.j, quat.k, quat.w
137            )
138            .map_err(|e| {
139                IoError::Io(e).log_with_source(format!(
140                    "Failed to write SE3 edge {} -> {}",
141                    edge.from, edge.to
142                ))
143            })?;
144
145            // Write upper triangular information matrix (21 values)
146            for i in 0..6 {
147                for j in i..6 {
148                    write!(file, " {:.17e}", info[(i, j)]).map_err(|e| {
149                        IoError::Io(e).log_with_source(format!(
150                            "Failed to write SE3 edge {} -> {} information matrix",
151                            edge.from, edge.to
152                        ))
153                    })?;
154                }
155            }
156            writeln!(file).map_err(|e| {
157                IoError::Io(e).log_with_source(format!(
158                    "Failed to write SE3 edge {} -> {} newline",
159                    edge.from, edge.to
160                ))
161            })?;
162        }
163
164        Ok(())
165    }
166}
167
168impl G2oLoader {
169    /// Parse G2O content with performance optimizations
170    fn parse_content(content: &str) -> Result<Graph, IoError> {
171        let lines: Vec<&str> = content.lines().collect();
172        let minimum_lines_for_parallel = 1000;
173
174        // Pre-allocate collections based on estimated size
175        let estimated_vertices = lines.len() / 4;
176        let estimated_edges = estimated_vertices * 3;
177        let mut graph = Graph {
178            vertices_se2: HashMap::with_capacity(estimated_vertices),
179            vertices_se3: HashMap::with_capacity(estimated_vertices),
180            edges_se2: Vec::with_capacity(estimated_edges),
181            edges_se3: Vec::with_capacity(estimated_edges),
182        };
183
184        // For large files, use parallel processing
185        if lines.len() > minimum_lines_for_parallel {
186            Self::parse_parallel(&lines, &mut graph)?;
187        } else {
188            Self::parse_sequential(&lines, &mut graph)?;
189        }
190
191        Ok(graph)
192    }
193
194    /// Sequential parsing for smaller files
195    fn parse_sequential(lines: &[&str], graph: &mut Graph) -> Result<(), IoError> {
196        for (line_num, line) in lines.iter().enumerate() {
197            Self::parse_line(line, line_num + 1, graph)?;
198        }
199        Ok(())
200    }
201
202    /// Parallel parsing for larger files
203    fn parse_parallel(lines: &[&str], graph: &mut Graph) -> Result<(), IoError> {
204        // Collect parse results in parallel
205        let results: Result<Vec<_>, IoError> = lines
206            .par_iter()
207            .enumerate()
208            .map(|(line_num, line)| Self::parse_line_to_enum(line, line_num + 1))
209            .collect();
210
211        let parsed_items = results?;
212
213        // Sequential insertion to avoid concurrent modification
214        for item in parsed_items.into_iter().flatten() {
215            match item {
216                ParsedItem::VertexSE2(vertex) => {
217                    let id = vertex.id;
218                    if graph.vertices_se2.insert(id, vertex).is_some() {
219                        return Err(IoError::DuplicateVertex { id });
220                    }
221                }
222                ParsedItem::VertexSE3(vertex) => {
223                    let id = vertex.id;
224                    if graph.vertices_se3.insert(id, vertex).is_some() {
225                        return Err(IoError::DuplicateVertex { id });
226                    }
227                }
228                ParsedItem::EdgeSE2(edge) => {
229                    graph.edges_se2.push(edge);
230                }
231                ParsedItem::EdgeSE3(edge) => {
232                    graph.edges_se3.push(*edge);
233                }
234            }
235        }
236
237        Ok(())
238    }
239
240    /// Parse a single line (for sequential processing)
241    fn parse_line(line: &str, line_num: usize, graph: &mut Graph) -> Result<(), IoError> {
242        let line = line.trim();
243
244        // Skip empty lines and comments
245        if line.is_empty() || line.starts_with('#') {
246            return Ok(());
247        }
248
249        let parts: Vec<&str> = line.split_whitespace().collect();
250        if parts.is_empty() {
251            return Ok(());
252        }
253
254        match parts[0] {
255            "VERTEX_SE2" => {
256                let vertex = Self::parse_vertex_se2(&parts, line_num)?;
257                let id = vertex.id;
258                if graph.vertices_se2.insert(id, vertex).is_some() {
259                    return Err(IoError::DuplicateVertex { id });
260                }
261            }
262            "VERTEX_SE3:QUAT" => {
263                let vertex = Self::parse_vertex_se3(&parts, line_num)?;
264                let id = vertex.id;
265                if graph.vertices_se3.insert(id, vertex).is_some() {
266                    return Err(IoError::DuplicateVertex { id });
267                }
268            }
269            "EDGE_SE2" => {
270                let edge = Self::parse_edge_se2(&parts, line_num)?;
271                graph.edges_se2.push(edge);
272            }
273            "EDGE_SE3:QUAT" => {
274                let edge = Self::parse_edge_se3(&parts, line_num)?;
275                graph.edges_se3.push(edge);
276            }
277            _ => {
278                // Skip unknown types silently for compatibility
279            }
280        }
281
282        Ok(())
283    }
284
285    /// Parse a single line to enum (for parallel processing)
286    fn parse_line_to_enum(line: &str, line_num: usize) -> Result<Option<ParsedItem>, IoError> {
287        let line = line.trim();
288
289        // Skip empty lines and comments
290        if line.is_empty() || line.starts_with('#') {
291            return Ok(None);
292        }
293
294        let parts: Vec<&str> = line.split_whitespace().collect();
295        if parts.is_empty() {
296            return Ok(None);
297        }
298
299        let item = match parts[0] {
300            "VERTEX_SE2" => Some(ParsedItem::VertexSE2(Self::parse_vertex_se2(
301                &parts, line_num,
302            )?)),
303            "VERTEX_SE3:QUAT" => Some(ParsedItem::VertexSE3(Self::parse_vertex_se3(
304                &parts, line_num,
305            )?)),
306            "EDGE_SE2" => Some(ParsedItem::EdgeSE2(Self::parse_edge_se2(&parts, line_num)?)),
307            "EDGE_SE3:QUAT" => Some(ParsedItem::EdgeSE3(Box::new(Self::parse_edge_se3(
308                &parts, line_num,
309            )?))),
310            _ => None, // Skip unknown types
311        };
312
313        Ok(item)
314    }
315
316    /// Parse VERTEX_SE2 line
317    pub fn parse_vertex_se2(parts: &[&str], line_num: usize) -> Result<VertexSE2, IoError> {
318        if parts.len() < 5 {
319            return Err(IoError::MissingFields { line: line_num });
320        }
321
322        let id = parts[1]
323            .parse::<usize>()
324            .map_err(|_| IoError::InvalidNumber {
325                line: line_num,
326                value: parts[1].to_string(),
327            })?;
328
329        let x = parts[2]
330            .parse::<f64>()
331            .map_err(|_| IoError::InvalidNumber {
332                line: line_num,
333                value: parts[2].to_string(),
334            })?;
335
336        let y = parts[3]
337            .parse::<f64>()
338            .map_err(|_| IoError::InvalidNumber {
339                line: line_num,
340                value: parts[3].to_string(),
341            })?;
342
343        let theta = parts[4]
344            .parse::<f64>()
345            .map_err(|_| IoError::InvalidNumber {
346                line: line_num,
347                value: parts[4].to_string(),
348            })?;
349
350        Ok(VertexSE2::new(id, x, y, theta))
351    }
352
353    /// Parse VERTEX_SE3:QUAT line
354    pub fn parse_vertex_se3(parts: &[&str], line_num: usize) -> Result<VertexSE3, IoError> {
355        if parts.len() < 9 {
356            return Err(IoError::MissingFields { line: line_num });
357        }
358
359        let id = parts[1]
360            .parse::<usize>()
361            .map_err(|_| IoError::InvalidNumber {
362                line: line_num,
363                value: parts[1].to_string(),
364            })?;
365
366        let x = parts[2]
367            .parse::<f64>()
368            .map_err(|_| IoError::InvalidNumber {
369                line: line_num,
370                value: parts[2].to_string(),
371            })?;
372
373        let y = parts[3]
374            .parse::<f64>()
375            .map_err(|_| IoError::InvalidNumber {
376                line: line_num,
377                value: parts[3].to_string(),
378            })?;
379
380        let z = parts[4]
381            .parse::<f64>()
382            .map_err(|_| IoError::InvalidNumber {
383                line: line_num,
384                value: parts[4].to_string(),
385            })?;
386
387        let qx = parts[5]
388            .parse::<f64>()
389            .map_err(|_| IoError::InvalidNumber {
390                line: line_num,
391                value: parts[5].to_string(),
392            })?;
393
394        let qy = parts[6]
395            .parse::<f64>()
396            .map_err(|_| IoError::InvalidNumber {
397                line: line_num,
398                value: parts[6].to_string(),
399            })?;
400
401        let qz = parts[7]
402            .parse::<f64>()
403            .map_err(|_| IoError::InvalidNumber {
404                line: line_num,
405                value: parts[7].to_string(),
406            })?;
407
408        let qw = parts[8]
409            .parse::<f64>()
410            .map_err(|_| IoError::InvalidNumber {
411                line: line_num,
412                value: parts[8].to_string(),
413            })?;
414
415        let translation = nalgebra::Vector3::new(x, y, z);
416        let quaternion = nalgebra::Quaternion::new(qw, qx, qy, qz);
417
418        // Validate quaternion normalization
419        let quat_norm = (qw * qw + qx * qx + qy * qy + qz * qz).sqrt();
420        if (quat_norm - 1.0).abs() > 0.01 {
421            return Err(IoError::InvalidQuaternion {
422                line: line_num,
423                norm: quat_norm,
424            });
425        }
426
427        // Always normalize for numerical safety
428        let quaternion = quaternion.normalize();
429
430        Ok(VertexSE3::from_translation_quaternion(
431            id,
432            translation,
433            quaternion,
434        ))
435    }
436
437    /// Parse EDGE_SE2 line
438    fn parse_edge_se2(parts: &[&str], line_num: usize) -> Result<EdgeSE2, IoError> {
439        if parts.len() < 12 {
440            return Err(IoError::MissingFields { line: line_num });
441        }
442
443        let from = parts[1]
444            .parse::<usize>()
445            .map_err(|_| IoError::InvalidNumber {
446                line: line_num,
447                value: parts[1].to_string(),
448            })?;
449
450        let to = parts[2]
451            .parse::<usize>()
452            .map_err(|_| IoError::InvalidNumber {
453                line: line_num,
454                value: parts[2].to_string(),
455            })?;
456
457        // Parse measurement (dx, dy, dtheta)
458        let dx = parts[3]
459            .parse::<f64>()
460            .map_err(|_| IoError::InvalidNumber {
461                line: line_num,
462                value: parts[3].to_string(),
463            })?;
464        let dy = parts[4]
465            .parse::<f64>()
466            .map_err(|_| IoError::InvalidNumber {
467                line: line_num,
468                value: parts[4].to_string(),
469            })?;
470        let dtheta = parts[5]
471            .parse::<f64>()
472            .map_err(|_| IoError::InvalidNumber {
473                line: line_num,
474                value: parts[5].to_string(),
475            })?;
476
477        // Parse information matrix (upper triangular: i11, i12, i13, i22, i23, i33)
478        let info_values: Result<Vec<f64>, _> =
479            parts[6..12].iter().map(|s| s.parse::<f64>()).collect();
480
481        let info_values = info_values.map_err(|_| IoError::Parse {
482            line: line_num,
483            message: "Invalid information matrix values".to_string(),
484        })?;
485
486        let information = nalgebra::Matrix3::new(
487            info_values[0],
488            info_values[1],
489            info_values[2],
490            info_values[1],
491            info_values[3],
492            info_values[4],
493            info_values[2],
494            info_values[4],
495            info_values[5],
496        );
497
498        Ok(EdgeSE2::new(from, to, dx, dy, dtheta, information))
499    }
500
501    /// Parse EDGE_SE3:QUAT line (placeholder implementation)
502    fn parse_edge_se3(parts: &[&str], line_num: usize) -> Result<EdgeSE3, IoError> {
503        // EDGE_SE3:QUAT from_id to_id tx ty tz qx qy qz qw [information matrix values]
504        if parts.len() < 10 {
505            return Err(IoError::MissingFields { line: line_num });
506        }
507
508        // Parse vertex IDs
509        let from = parts[1]
510            .parse::<usize>()
511            .map_err(|_| IoError::InvalidNumber {
512                line: line_num,
513                value: parts[1].to_string(),
514            })?;
515
516        let to = parts[2]
517            .parse::<usize>()
518            .map_err(|_| IoError::InvalidNumber {
519                line: line_num,
520                value: parts[2].to_string(),
521            })?;
522
523        // Parse translation (tx, ty, tz)
524        let tx = parts[3]
525            .parse::<f64>()
526            .map_err(|_| IoError::InvalidNumber {
527                line: line_num,
528                value: parts[3].to_string(),
529            })?;
530
531        let ty = parts[4]
532            .parse::<f64>()
533            .map_err(|_| IoError::InvalidNumber {
534                line: line_num,
535                value: parts[4].to_string(),
536            })?;
537
538        let tz = parts[5]
539            .parse::<f64>()
540            .map_err(|_| IoError::InvalidNumber {
541                line: line_num,
542                value: parts[5].to_string(),
543            })?;
544
545        let translation = nalgebra::Vector3::new(tx, ty, tz);
546
547        // Parse rotation quaternion (qx, qy, qz, qw)
548        let qx = parts[6]
549            .parse::<f64>()
550            .map_err(|_| IoError::InvalidNumber {
551                line: line_num,
552                value: parts[6].to_string(),
553            })?;
554
555        let qy = parts[7]
556            .parse::<f64>()
557            .map_err(|_| IoError::InvalidNumber {
558                line: line_num,
559                value: parts[7].to_string(),
560            })?;
561
562        let qz = parts[8]
563            .parse::<f64>()
564            .map_err(|_| IoError::InvalidNumber {
565                line: line_num,
566                value: parts[8].to_string(),
567            })?;
568
569        let qw = parts[9]
570            .parse::<f64>()
571            .map_err(|_| IoError::InvalidNumber {
572                line: line_num,
573                value: parts[9].to_string(),
574            })?;
575
576        let rotation =
577            nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(qw, qx, qy, qz));
578
579        // Parse information matrix (upper triangular: i11, i12, i13, i14, i15, i16, i22, i23, i24, i25, i26, i33, i34, i35, i36, i44, i45, i46, i55, i56, i66)
580        let info_values: Result<Vec<f64>, _> =
581            parts[10..31].iter().map(|s| s.parse::<f64>()).collect();
582
583        let info_values = info_values.map_err(|_| IoError::Parse {
584            line: line_num,
585            message: "Invalid information matrix values".to_string(),
586        })?;
587
588        let information = nalgebra::Matrix6::new(
589            info_values[0],
590            info_values[1],
591            info_values[2],
592            info_values[3],
593            info_values[4],
594            info_values[5],
595            info_values[1],
596            info_values[6],
597            info_values[7],
598            info_values[8],
599            info_values[9],
600            info_values[10],
601            info_values[2],
602            info_values[7],
603            info_values[11],
604            info_values[12],
605            info_values[13],
606            info_values[14],
607            info_values[3],
608            info_values[8],
609            info_values[12],
610            info_values[15],
611            info_values[16],
612            info_values[17],
613            info_values[4],
614            info_values[9],
615            info_values[13],
616            info_values[16],
617            info_values[18],
618            info_values[19],
619            info_values[5],
620            info_values[10],
621            info_values[14],
622            info_values[17],
623            info_values[19],
624            info_values[20],
625        );
626
627        Ok(EdgeSE3::new(from, to, translation, rotation, information))
628    }
629}
630
631/// Enum for parsed items (used in parallel processing)
632enum ParsedItem {
633    VertexSE2(VertexSE2),
634    VertexSE3(VertexSE3),
635    EdgeSE2(EdgeSE2),
636    EdgeSE3(Box<EdgeSE3>),
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    type TestResult = Result<(), Box<dyn std::error::Error>>;
644
645    #[test]
646    fn test_parse_vertex_se2() -> TestResult {
647        let parts = vec!["VERTEX_SE2", "0", "1.0", "2.0", "0.5"];
648        let vertex = G2oLoader::parse_vertex_se2(&parts, 1)?;
649
650        assert_eq!(vertex.id(), 0);
651        assert_eq!(vertex.x(), 1.0);
652        assert_eq!(vertex.y(), 2.0);
653        assert_eq!(vertex.theta(), 0.5);
654
655        Ok(())
656    }
657
658    #[test]
659    fn test_parse_vertex_se3() -> TestResult {
660        let parts = vec![
661            "VERTEX_SE3:QUAT",
662            "1",
663            "1.0",
664            "2.0",
665            "3.0",
666            "0.0",
667            "0.0",
668            "0.0",
669            "1.0",
670        ];
671        let vertex = G2oLoader::parse_vertex_se3(&parts, 1)?;
672
673        assert_eq!(vertex.id(), 1);
674        assert_eq!(vertex.translation(), nalgebra::Vector3::new(1.0, 2.0, 3.0));
675        assert!(vertex.rotation().quaternion().w > 0.99); // Should be identity quaternion
676
677        Ok(())
678    }
679
680    #[test]
681    fn test_error_handling() {
682        // Test invalid number
683        let parts = vec!["VERTEX_SE2", "invalid", "1.0", "2.0", "0.5"];
684        let result = G2oLoader::parse_vertex_se2(&parts, 1);
685        assert!(matches!(result, Err(IoError::InvalidNumber { .. })));
686
687        // Test missing fields
688        let parts = vec!["VERTEX_SE2", "0"];
689        let result = G2oLoader::parse_vertex_se2(&parts, 1);
690        assert!(matches!(result, Err(IoError::MissingFields { .. })));
691    }
692}