apex_solver/io/
g2o.rs

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