Skip to content

Commit cae8b79

Browse files
Add basic tests for g2opy
1 parent 309933f commit cae8b79

File tree

8 files changed

+928
-0
lines changed

8 files changed

+928
-0
lines changed

pytest.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[pytest]
2+
testpaths = python/tests
3+
python_files = test_*.py
4+
python_classes = Test*
5+
python_functions = test_*
6+
addopts = -v --tb=short

python/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Pytest test suite for g2o Python bindings (g2opy)."""

python/tests/conftest.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Pytest fixtures for g2o Python bindings tests."""
2+
3+
import sys
4+
import tempfile
5+
from pathlib import Path
6+
7+
# Ensure build lib is in path BEFORE any imports - this is critical
8+
build_lib_path = Path(__file__).resolve().parent.parent.parent / "build" / "lib"
9+
build_lib_str = str(build_lib_path)
10+
if build_lib_str not in sys.path:
11+
sys.path.insert(0, build_lib_str)
12+
13+
# This must come after path setup
14+
import g2opy as g2o
15+
import numpy as np
16+
import pytest
17+
18+
19+
def pytest_runtest_setup(item):
20+
"""Inject g2o into test module globals before each test runs."""
21+
if item.module:
22+
item.module.g2o = g2o
23+
24+
25+
@pytest.fixture
26+
def g2o_module():
27+
"""Provide access to g2o module."""
28+
return g2o
29+
30+
31+
@pytest.fixture
32+
def temp_g2o_file():
33+
"""Create a temporary file for saving .g2o graphs."""
34+
with tempfile.NamedTemporaryFile(suffix=".g2o", delete=False) as f:
35+
yield f.name
36+
Path(f.name).unlink(missing_ok=True)
37+
38+
39+
@pytest.fixture
40+
def basic_se2_optimizer():
41+
"""Create a basic SE2 optimizer ready for optimization."""
42+
optimizer = g2o.SparseOptimizer()
43+
solver = g2o.BlockSolverSE2(g2o.LinearSolverEigenSE2())
44+
optimizer.set_algorithm(g2o.OptimizationAlgorithmLevenberg(solver))
45+
return optimizer
46+
47+
48+
@pytest.fixture
49+
def basic_se3_optimizer():
50+
"""Create a basic SE3 optimizer ready for optimization."""
51+
optimizer = g2o.SparseOptimizer()
52+
solver = g2o.BlockSolverSE3(g2o.LinearSolverEigenSE3())
53+
optimizer.set_algorithm(g2o.OptimizationAlgorithmLevenberg(solver))
54+
return optimizer
55+
56+
57+
@pytest.fixture
58+
def simple_se2_graph(basic_se2_optimizer):
59+
"""Create a simple 2D SLAM graph with 3 poses and 2 edges."""
60+
optimizer = basic_se2_optimizer
61+
62+
# Add 3 vertices
63+
poses = [
64+
g2o.SE2(0, 0, 0),
65+
g2o.SE2(1, 0, 0),
66+
g2o.SE2(1, 1, np.pi / 2),
67+
]
68+
69+
for i, pose in enumerate(poses):
70+
v = g2o.VertexSE2()
71+
v.set_id(i)
72+
v.set_estimate(pose)
73+
if i == 0:
74+
v.set_fixed(True) # Fix first vertex
75+
optimizer.add_vertex(v)
76+
77+
# Add 2 edges connecting consecutive poses
78+
measurements = [
79+
g2o.SE2(1, 0, 0),
80+
g2o.SE2(0, 1, np.pi / 2),
81+
]
82+
83+
for i, measurement in enumerate(measurements):
84+
edge = g2o.EdgeSE2()
85+
edge.set_vertex(0, optimizer.vertex(i))
86+
edge.set_vertex(1, optimizer.vertex(i + 1))
87+
edge.set_measurement(measurement)
88+
edge.set_information(np.eye(3))
89+
optimizer.add_edge(edge)
90+
91+
return optimizer
92+
93+
94+
@pytest.fixture
95+
def simple_se3_graph(basic_se3_optimizer):
96+
"""Create a simple 3D SLAM graph with 2 poses and 1 edge."""
97+
optimizer = basic_se3_optimizer
98+
99+
# Add 2 SE3 vertices
100+
poses = [
101+
g2o.Isometry3d(np.eye(3), [0, 0, 0]),
102+
g2o.Isometry3d(np.eye(3), [1, 0, 0]),
103+
]
104+
105+
for i, pose in enumerate(poses):
106+
v = g2o.VertexSE3()
107+
v.set_id(i)
108+
v.set_estimate(pose)
109+
if i == 0:
110+
v.set_fixed(True)
111+
optimizer.add_vertex(v)
112+
113+
# Add edge
114+
edge = g2o.EdgeSE3()
115+
edge.set_vertex(0, optimizer.vertex(0))
116+
edge.set_vertex(1, optimizer.vertex(1))
117+
edge.set_measurement(g2o.Isometry3d(np.eye(3), [1, 0, 0]))
118+
edge.set_information(np.eye(6))
119+
optimizer.add_edge(edge)
120+
121+
return optimizer
122+
123+
124+
@pytest.fixture
125+
def vector_vertex():
126+
"""Create a VectorXVertex for testing."""
127+
vertex = g2o.VectorXVertex()
128+
vertex.set_dimension(3)
129+
vertex.set_estimate(np.array([1.0, 2.0, 3.0]))
130+
return vertex
131+
132+
133+
@pytest.fixture
134+
def vector_edge(vector_vertex):
135+
"""Create a VariableVectorXEdge for testing."""
136+
edge = g2o.VariableVectorXEdge()
137+
edge.set_dimension(3)
138+
edge.resize(1)
139+
edge.set_vertex(0, vector_vertex)
140+
edge.set_measurement(np.array([1.5, 2.5, 3.5]))
141+
edge.set_information(np.eye(3))
142+
return edge

python/tests/test_core.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Tests for g2o core functionality (optimizer, solvers, algorithms)."""
2+
3+
# g2o will be available after conftest.py sets sys.path
4+
import g2opy as g2o
5+
import numpy as np
6+
import pytest
7+
8+
9+
class TestSparseOptimizer:
10+
"""Test SparseOptimizer basic functionality."""
11+
12+
def test_create_optimizer(self):
13+
"""Test creating a SparseOptimizer."""
14+
optimizer = g2o.SparseOptimizer()
15+
assert optimizer is not None
16+
assert len(optimizer.vertices()) == 0
17+
assert len(optimizer.edges()) == 0
18+
19+
def test_add_vertex(self, basic_se2_optimizer):
20+
"""Test adding vertices to optimizer."""
21+
v = g2o.VertexSE2()
22+
v.set_id(0)
23+
v.set_estimate(g2o.SE2(0, 0, 0))
24+
25+
basic_se2_optimizer.add_vertex(v)
26+
assert len(basic_se2_optimizer.vertices()) == 1
27+
assert basic_se2_optimizer.vertex(0) is not None
28+
29+
def test_add_edge(self, simple_se2_graph):
30+
"""Test adding edges to optimizer."""
31+
assert len(simple_se2_graph.edges()) == 2
32+
33+
def test_vertex_retrieval(self, simple_se2_graph):
34+
"""Test retrieving vertices."""
35+
for i in range(3):
36+
v = simple_se2_graph.vertex(i)
37+
assert v is not None
38+
assert v.id() == i
39+
40+
def test_chi2_compute(self, simple_se2_graph):
41+
"""Test computing chi2 error."""
42+
initial_chi2 = simple_se2_graph.chi2()
43+
assert initial_chi2 >= 0
44+
45+
def test_initialize_optimization(self, simple_se2_graph):
46+
"""Test optimization initialization."""
47+
simple_se2_graph.initialize_optimization()
48+
# Should not raise an exception
49+
50+
def test_set_verbose(self, basic_se2_optimizer):
51+
"""Test setting verbose output."""
52+
basic_se2_optimizer.set_verbose(True)
53+
basic_se2_optimizer.set_verbose(False)
54+
# Should not raise an exception
55+
56+
57+
class TestBlockSolvers:
58+
"""Test BlockSolver configurations."""
59+
60+
def test_block_solver_se2(self):
61+
"""Test BlockSolverSE2."""
62+
solver = g2o.BlockSolverSE2(g2o.LinearSolverEigenSE2())
63+
assert solver is not None
64+
65+
def test_block_solver_se3(self):
66+
"""Test BlockSolverSE3."""
67+
solver = g2o.BlockSolverSE3(g2o.LinearSolverEigenSE3())
68+
assert solver is not None
69+
70+
def test_block_solver_variable(self):
71+
"""Test variable dimension BlockSolverX."""
72+
solver = g2o.BlockSolverX(g2o.LinearSolverEigenX())
73+
assert solver is not None
74+
75+
76+
class TestLinearSolvers:
77+
"""Test various linear solver implementations."""
78+
79+
def test_linear_solver_eigen_se2(self):
80+
"""Test LinearSolverEigenSE2."""
81+
solver = g2o.LinearSolverEigenSE2()
82+
assert solver is not None
83+
84+
def test_linear_solver_eigen_se3(self):
85+
"""Test LinearSolverEigenSE3."""
86+
solver = g2o.LinearSolverEigenSE3()
87+
assert solver is not None
88+
89+
def test_linear_solver_eigen_x(self):
90+
"""Test LinearSolverEigenX (variable dimension)."""
91+
solver = g2o.LinearSolverEigenX()
92+
assert solver is not None
93+
94+
95+
class TestOptimizationAlgorithms:
96+
"""Test optimization algorithm implementations."""
97+
98+
def test_levenberg_marquardt(self, basic_se2_optimizer):
99+
"""Test Levenberg-Marquardt algorithm."""
100+
assert basic_se2_optimizer.algorithm() is not None
101+
102+
def test_gauss_newton_creation(self):
103+
"""Test creating Gauss-Newton algorithm."""
104+
solver = g2o.BlockSolverSE2(g2o.LinearSolverEigenSE2())
105+
algo = g2o.OptimizationAlgorithmGaussNewton(solver)
106+
assert algo is not None
107+
108+
def test_dogleg_creation(self):
109+
"""Test creating Dogleg algorithm."""
110+
solver = g2o.BlockSolverSE2(g2o.LinearSolverEigenSE2())
111+
algo = g2o.OptimizationAlgorithmDogleg(solver)
112+
assert algo is not None
113+
114+
115+
class TestErrorComputation:
116+
"""Test error and chi2 computation."""
117+
118+
def test_compute_active_errors(self, simple_se2_graph):
119+
"""Test computing active errors."""
120+
simple_se2_graph.compute_active_errors()
121+
chi2 = simple_se2_graph.chi2()
122+
assert chi2 >= 0
123+
124+
def test_chi2_before_optimization(self, simple_se2_graph):
125+
"""Test chi2 computation before optimization."""
126+
simple_se2_graph.initialize_optimization()
127+
simple_se2_graph.compute_active_errors()
128+
initial_chi2 = simple_se2_graph.chi2()
129+
130+
simple_se2_graph.optimize(1)
131+
simple_se2_graph.compute_active_errors()
132+
final_chi2 = simple_se2_graph.chi2()
133+
134+
# Chi2 should improve or stay the same
135+
assert final_chi2 <= initial_chi2 + 1e-6

0 commit comments

Comments
 (0)