Skip to content

Commit 6b3a158

Browse files
authored
Merge branch 'borglab:develop' into develop
2 parents 2afb78d + 48ffd93 commit 6b3a158

File tree

82 files changed

+17337
-1152
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+17337
-1152
lines changed

doc/examples.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Examples
2+
3+
This section contains python examples in interactive Python notebooks (`*.ipynb`). Python notebooks with an <img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/> button near the top can be opened in your browser, where you can run the files yourself and make edits to play with and understand GTSAM.

doc/expressions.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Expressions
2+
## Motivation
3+
GTSAM is an optimization library for objective functions expressed as a factor graph over a set of unknown variables. In the continuous case, the variables are typically vectors or elements on a manifold (such as the 3D rotation manifold). The factors compute vector-valued errors that need to be minimized, and are typically only connected to a handful of unknowns.
4+
5+
In the continuous case, the main optimization methods we have implemented are variants of Gauss-Newton non-linear optimization or conjugate gradient methods. Let us assume there are m factors over n unknowns. For either optimization method, we need to evaluate the sparse Jacobian matrix of the entire factor graph, which is a sparse block-matrix of m block-rows and n-block columns.
6+
7+
The sparse Jacobian is built up factor by factor, corresponding to the block-rows. A typical non-linear least-square term is $|h(x)-z|^2$ where $h(x)$ is a measurement function, which we need to be able to linearize as
8+
$$
9+
h(x) \approx h(x_0+dx)+H(x_0)dx
10+
$$
11+
Note the above is for vector unknowns, for Lie groups and manifold variables, see [doc/math.pdf](https://github.com/borglab/gtsam/blob/develop/doc/math.pdf) for details.
12+
13+
## Expressions
14+
In many cases one can use GTSAM 4 Expressions to implement factors. Expressions are objects of type Expression<T>, and there are three main expression flavors:
15+
16+
- constants, e.g., `Expression<Point2> kExpr(Point2(3,4))`
17+
- unknowns, e.g., `Expression<Point3> pExpr(123)` where 123 is a key.
18+
- functions, e.g., `Expression<double> sumExpr(h, kexpr, pExpr)`
19+
20+
The latter case is an example of wrapping a binary measurement function `h`. To be able to wrap `h`, it needs to be able to compute its local derivatives, i.e., it has to have the signature
21+
```c++
22+
double h(const Point2& a, const Point3& b,
23+
OptionalJacobian<1, 2> Ha, OptionalJacobian<1, 3> Hb)
24+
```
25+
In this case the output type 'T' is 'double', the two arguments have type Point2 and Point3 respectively, and the two remaining arguments provide a way to compute the function Jacobians, if needed. The templated type `OptionalJacobian<M,N>` behaves very much like `std::optional<Eigen::Matrix<double,M,N>`. If an actual matrix is passed in, the function is expected to treat it as an output argument in which to write the Jacobian for the result wrp. the corresponding input argument. *The matrix to write in will be allocated before the call.*
26+
27+
Expression constructors exist for both methods and functions with different arities. Note that an expression is templated with the output type T, not with the argument types. However, the constructor will infer the argument types from inspecting the signature of the function f, and will in this example expect two additional arguments of type Expression<Point2> and Expression<Point3>, respectively.
28+
29+
As an example, here is the constructor declaration for wrapping unary functions:
30+
```c++
31+
template<typename A>
32+
Expression(typename UnaryFunction<A>::type function,
33+
const Expression<A>& expression);
34+
```
35+
where (in this case) the function type is defined by
36+
```c++
37+
template<class A1>
38+
struct UnaryFunction {
39+
typedef boost::function<
40+
T(const A1&, typename MakeOptionalJacobian<T, A1>::type)> type;
41+
};
42+
```
43+
## Some measurement function examples
44+
An example of a simple unary function is `gtsam::norm3` in [Point3.cpp](https://github.com/borglab/gtsam/blob/develop/gtsam/geometry/Point3.cpp#L41):
45+
```c++
46+
double norm3(const Point3 & p, OptionalJacobian<1, 3> H = {}) {
47+
double r = sqrt(p.x() * p.x() + p.y() * p.y() + p.z() * p.z());
48+
if (H) *H << p.x() / r, p.y() / r, p.z() / r;
49+
return r;
50+
}
51+
```
52+
The key new concept here is OptionalJacobian, which acts like a std::optional: if it evaluates to true, you should write the Jacobian of the function in it. It acts as a fixed-size Eigen matrix.
53+
54+
As we said above, expressions also support binary functions, ternary functions, and methods. An example of a binary function is 'Point3::cross':
55+
56+
```c++
57+
Point3 cross(const Point3 &p, const Point3 & q,
58+
OptionalJacobian<3, 3> H1 = {}, OptionalJacobian<3, 3> H2 = {}) {
59+
if (H1) *H1 << skewSymmetric(-q.x(), -q.y(), -q.z());
60+
if (H2) *H2 << skewSymmetric(p.x(), p.y(), p.z());
61+
return Point3(p.y() * q.z() - p.z() * q.y(), p.z() * q.x() - p.x() * q.z(), p.x() * q.y() - p.y() * q.x());
62+
}
63+
```
64+
Example of using cross:
65+
```c++
66+
using namespace gtsam;
67+
Matrix3 H1, H2;
68+
Point3 p(1,2,3), q(4,5,6), r = cross(p,q,H1,H2);
69+
```
70+
## Using Expressions for Inference
71+
The way expressions are used is by creating unknown Expressions for the unknown variables we are optimizing for:
72+
```c++
73+
Expression<Point3> x(‘x’,1);
74+
auto h = Expression<Point3>(& norm3, x);
75+
```
76+
For convenient creation of factors with expressions, we provide a new factor graph type `ExpressionFactorGraph`, which is just a `NonlinearFactorGraph` with an extra method addExpressionFactor(h, z, n) that takes a measurement expression h, an actual measurement z, and a measurement noise model R. With this, we can add a GTSAM nonlinear factor $|h(x)-z|^2$ to a `NonlinearFactorGraph` by
77+
```c++
78+
graph.addExpressionFactor(h, z, R)
79+
```
80+
In the above, the unknown in the example can be retrieved by the `gtsam::Symbol(‘x’,1)`, which evaluates to a uint64 identifier.
81+
82+
## Composing Expressions
83+
The key coolness behind expressions, however, is that you can compose them into expression trees, as long as the leaves know how to do their own derivatives:
84+
```c++
85+
Expression<Point3> x1(‘x’1), x2(‘x’,2);
86+
auto h = Expression<Point3>(& cross, x1, x2);
87+
auto g = Expression<Point3>(& norm3, h);
88+
```
89+
Because we typedef Point3_ to Expression<Point3>, we can write this very concisely as
90+
```c++
91+
auto g = Point3_(& norm3, Point3_(& cross, x1(‘x’1), x2(‘x’,2)));
92+
```
93+
## PoseSLAM Example
94+
Using expressions, it is simple to quickly create a factor graph corresponding to a PoseSLAM problem, where our only measurements are relative poses between a series of unknown 2D or 3D poses. The following code snippet from [Pose2SLAMExampleExpressions.cpp](https://github.com/borglab/gtsam/blob/develop/examples/Pose2SLAMExampleExpressions.cpp) is used to create a simple Pose2 example (where the robot is moving on a plane):
95+
```c++
96+
1 ExpressionFactorGraph graph;
97+
2 Expression<Pose2> x1(1), x2(2), x3(3), x4(4), x5(5);
98+
3 graph.addExpressionFactor(x1, Pose2(0, 0, 0), priorNoise);
99+
4 graph.addExpressionFactor(between(x1,x2), Pose2(2, 0, 0 ), model);
100+
5 graph.addExpressionFactor(between(x2,x3), Pose2(2, 0, M_PI_2), model);
101+
6 graph.addExpressionFactor(between(x3,x4), Pose2(2, 0, M_PI_2), model);
102+
7 graph.addExpressionFactor(between(x4,x5), Pose2(2, 0, M_PI_2), model);
103+
8 graph.addExpressionFactor(between(x5,x2), Pose2(2, 0, M_PI_2), model);
104+
```
105+
This is what is going on:
106+
- In line 1, we create an empty factor graph.
107+
- In line 2 we create the 5 unknown poses, of type `Expression<Pose2>`, with keys 1 to 5. These are what we will optimize over.
108+
- Line 3 then creates a simple factor that gives a prior on `x1` (the first argument), namely that it is at the origin `Pose2(0, 0, 0)` (the second argument), with a particular probability density given by `priorNoise` (the third argument).
109+
- Lines 4-7 adds factors for the odometry constraints, i.e., the movement between successive poses of the robot. The function `between(t1,t2)` is implemented in [nonlinear/expressions.h](https://github.com/borglab/gtsam/blob/develop/gtsam/nonlinear/expressions.h) and is equivalent to calling the constructor Expression<T>(traits<T>::Between, t1, t2).
110+
- Finally, line 8 creates a loop closure constraint between poses x2 and x5.
111+
112+
Another good example of its use is in
113+
[SFMExampleExpressions.cpp](https://github.com/borglab/gtsam/blob/develop/examples/SFMExampleExpressions.cpp).

doc/generating/gpt_generate.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""
2+
GTSAM Copyright 2010-2025, Georgia Tech Research Corporation,
3+
Atlanta, Georgia 30332-0415
4+
All Rights Reserved
5+
6+
See LICENSE for the license information
7+
8+
Author: Porter Zach
9+
10+
This script generates interactive Python notebooks (.ipynb) that document GTSAM
11+
header files. Since inserting the text of the file directly into the prompt
12+
might be too many tokens, it retrieves the header file content from the GTSAM
13+
GitHub repository. It then sends it to OpenAI's API for processing, and saves
14+
the generated documentation as a Jupyter notebook.
15+
16+
Functions:
17+
is_url_valid(url: str) -> bool:
18+
Verifies that the supplied URL does not return a 404.
19+
20+
save_ipynb(text: str, file_path: str) -> str:
21+
Saves the provided text to a single Markdown cell in a new .ipynb file.
22+
23+
generate_ipynb(file_path: str, openai_client):
24+
Generates an interactive Python notebook for the given GTSAM header file
25+
by sending a request to OpenAI's API and saving the response.
26+
27+
Usage:
28+
Run the script with paths to GTSAM header files as arguments. For example:
29+
python gpt_generate.py gtsam/geometry/Pose3.h
30+
"""
31+
32+
import os
33+
import time
34+
import requests
35+
import argparse
36+
import nbformat as nbf
37+
from openai import OpenAI
38+
39+
_output_folder = "output"
40+
_gtsam_gh_base = "https://raw.githubusercontent.com/borglab/gtsam/refs/heads/develop/"
41+
_asst_id = "asst_na7wYBtXyGU0x5t2RdcnpxzP"
42+
_request_text = "Document the file found at {}."
43+
44+
45+
def is_url_valid(url):
46+
"""Verify that the supplied URL does not return a 404."""
47+
try:
48+
response = requests.head(url, allow_redirects=True)
49+
return response.status_code != 404
50+
except requests.RequestException:
51+
return False
52+
53+
54+
def save_ipynb(text: str, file_path: str):
55+
"""Save text to a single Markdown cell in a new .ipynb file."""
56+
script_dir = os.path.dirname(os.path.abspath(__file__))
57+
output_dir = os.path.join(script_dir, _output_folder)
58+
os.makedirs(output_dir, exist_ok=True)
59+
output_file = os.path.splitext(os.path.basename(file_path))[0] + ".ipynb"
60+
output_full_path = os.path.join(output_dir, output_file)
61+
62+
nb = nbf.v4.new_notebook()
63+
new_cell = nbf.v4.new_markdown_cell(text)
64+
nb['cells'].append(new_cell)
65+
66+
with open(output_full_path, 'w', encoding='utf-8') as file:
67+
nbf.write(nb, file)
68+
69+
return output_file
70+
71+
72+
def generate_ipynb(file_path: str, openai_client):
73+
"""Generate an interactive Python notebook for the given GTSAM header file.
74+
75+
Args:
76+
file_path (str): The fully-qualified path from the root of the gtsam
77+
repository to the header file that will be documented.
78+
openai_client (openai.OpenAI): The OpenAI client to use.
79+
"""
80+
# Create the URL to get the header file from.
81+
url = _gtsam_gh_base + file_path
82+
83+
if not is_url_valid(url):
84+
print(f"{url} was not found on the server, or an error occurred.")
85+
return
86+
87+
print(f"Sending request to OpenAI to document {url}.")
88+
89+
# Create a new thread and send the request
90+
thread = openai_client.beta.threads.create()
91+
openai_client.beta.threads.messages.create(
92+
thread_id=thread.id, role="user", content=_request_text.format(url))
93+
94+
run = openai_client.beta.threads.runs.create(thread_id=thread.id,
95+
assistant_id=_asst_id)
96+
97+
print("Waiting for the assistant to process the request...")
98+
99+
# Wait for request to be processed
100+
while True:
101+
run_status = openai_client.beta.threads.runs.retrieve(
102+
thread_id=thread.id, run_id=run.id)
103+
if run_status.status == "completed":
104+
break
105+
time.sleep(2)
106+
107+
print("Request processed. Retrieving response...")
108+
109+
# Fetch messages
110+
messages = openai_client.beta.threads.messages.list(thread_id=thread.id)
111+
# Retrieve response text and strip ```markdown ... ```
112+
text = messages.data[0].content[0].text.value.strip('`').strip('markdown')
113+
114+
# Write output to file
115+
output_filename = save_ipynb(text, file_path)
116+
117+
print(f"Response retrieved. Find output in {output_filename}.")
118+
119+
120+
if __name__ == "__main__":
121+
parser = argparse.ArgumentParser(
122+
prog="gpt_generate",
123+
description=
124+
"Generates .ipynb documentation files given paths to GTSAM header files."
125+
)
126+
parser.add_argument(
127+
"file_paths",
128+
nargs='+',
129+
help=
130+
"The paths to the header files from the root gtsam directory, e.g. 'gtsam/geometry/Pose3.h'."
131+
)
132+
args = parser.parse_args()
133+
134+
# Retrieves API key from environment variable OPENAI_API_KEY
135+
client = OpenAI()
136+
137+
for file_path in args.file_paths:
138+
generate_ipynb(file_path, client)
File renamed without changes.

gtsam_unstable/examples/FixedLagSmootherExample.cpp renamed to examples/FixedLagSmootherExample.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
* - We have measurements between each pose from multiple odometry sensors
2323
*/
2424

25-
// This example demonstrates the use of the Fixed-Lag Smoothers in GTSAM unstable
25+
// This example demonstrates the use of the Fixed-Lag Smoothers in GTSAM
2626
#include <gtsam/nonlinear/BatchFixedLagSmoother.h>
27-
#include <gtsam_unstable/nonlinear/IncrementalFixedLagSmoother.h>
27+
#include <gtsam/nonlinear/IncrementalFixedLagSmoother.h>
2828

2929
// In GTSAM, measurement functions are represented as 'factors'. Several common factors
3030
// have been provided with the library for solving robotics/SLAM/Bundle Adjustment problems.

examples/Hybrid_City10000.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,10 @@ class Experiment {
109109
std::cout << "Smoother update: " << newFactors_.size() << std::endl;
110110
gttic_(SmootherUpdate);
111111
clock_t beforeUpdate = clock();
112-
auto linearized = newFactors_.linearize(initial_);
113-
smoother_.update(*linearized, initial_);
112+
smoother_.update(newFactors_, initial_, maxNrHypotheses);
113+
clock_t afterUpdate = clock();
114114
allFactors_.push_back(newFactors_);
115115
newFactors_.resize(0);
116-
clock_t afterUpdate = clock();
117116
return afterUpdate - beforeUpdate;
118117
}
119118

@@ -262,10 +261,20 @@ class Experiment {
262261
std::string timeFileName = "Hybrid_City10000_time.txt";
263262
outfileTime.open(timeFileName);
264263
for (auto accTime : timeList) {
265-
outfileTime << accTime << std::endl;
264+
outfileTime << accTime / CLOCKS_PER_SEC << std::endl;
266265
}
267266
outfileTime.close();
268267
std::cout << "Output " << timeFileName << " file." << std::endl;
268+
269+
std::ofstream timingFile;
270+
std::string timingFileName = "Hybrid_City10000_timing.txt";
271+
timingFile.open(timingFileName);
272+
for (size_t i = 0; i < smootherUpdateTimes.size(); i++) {
273+
auto p = smootherUpdateTimes.at(i);
274+
timingFile << p.first << ", " << p.second / CLOCKS_PER_SEC << std::endl;
275+
}
276+
timingFile.close();
277+
std::cout << "Wrote timing information to " << timingFileName << std::endl;
269278
}
270279
};
271280

examples/ISAM2_City10000.cpp

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ class Experiment {
7474
// Initialize local variables
7575
size_t index = 0;
7676

77+
std::vector<std::pair<size_t, double>> smootherUpdateTimes;
78+
7779
std::list<double> timeList;
7880

7981
// Set up initial prior
@@ -82,10 +84,15 @@ class Experiment {
8284
graph_.addPrior<Pose2>(X(0), priorPose, kPriorNoiseModel);
8385

8486
// Initial update
87+
clock_t beforeUpdate = clock();
8588
isam2_.update(graph_, initial_);
89+
results = isam2_.calculateBestEstimate();
90+
clock_t afterUpdate = clock();
91+
smootherUpdateTimes.push_back(
92+
std::make_pair(index, afterUpdate - beforeUpdate));
8693
graph_.resize(0);
8794
initial_.clear();
88-
results = isam2_.calculateBestEstimate();
95+
index += 1;
8996

9097
// Start main loop
9198
size_t keyS = 0;
@@ -127,10 +134,15 @@ class Experiment {
127134
index++;
128135
}
129136

137+
clock_t beforeUpdate = clock();
130138
isam2_.update(graph_, initial_);
139+
results = isam2_.calculateBestEstimate();
140+
clock_t afterUpdate = clock();
141+
smootherUpdateTimes.push_back(
142+
std::make_pair(index, afterUpdate - beforeUpdate));
131143
graph_.resize(0);
132144
initial_.clear();
133-
results = isam2_.calculateBestEstimate();
145+
index += 1;
134146

135147
// Print loop index and time taken in processor clock ticks
136148
if (index % 50 == 0 && keyS != keyT - 1) {
@@ -175,6 +187,16 @@ class Experiment {
175187
outfileTime.close();
176188
std::cout << "Written cumulative time to: " << timeFileName << " file."
177189
<< std::endl;
190+
191+
std::ofstream timingFile;
192+
std::string timingFileName = "ISAM2_City10000_timing.txt";
193+
timingFile.open(timingFileName);
194+
for (size_t i = 0; i < smootherUpdateTimes.size(); i++) {
195+
auto p = smootherUpdateTimes.at(i);
196+
timingFile << p.first << ", " << p.second / CLOCKS_PER_SEC << std::endl;
197+
}
198+
timingFile.close();
199+
std::cout << "Wrote timing information to " << timingFileName << std::endl;
178200
}
179201
};
180202

gtsam_unstable/examples/IncrementalFixedLagSmootherExample.cpp renamed to examples/IncrementalFixedLagSmootherExample.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
#include <gtsam/nonlinear/Values.h>
6060
#include <gtsam/nonlinear/NonlinearFactorGraph.h>
6161
#include <gtsam/slam/BetweenFactor.h>
62-
#include <gtsam_unstable/nonlinear/IncrementalFixedLagSmoother.h>
62+
#include <gtsam/nonlinear/IncrementalFixedLagSmoother.h>
6363
#include <gtsam/inference/Symbol.h>
6464
#include <gtsam/slam/dataset.h> // for writeG2o
6565

gtsam/3rdparty/metis/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cmake_minimum_required(VERSION 3.0)
1+
cmake_minimum_required(VERSION 3.5)
22
project(METIS)
33

44
# Add flags for currect directory and below

gtsam/discrete/DecisionTreeFactor.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,9 @@ namespace gtsam {
547547
/* ************************************************************************ */
548548
DiscreteFactor::shared_ptr DecisionTreeFactor::restrict(
549549
const DiscreteValues& assignment) const {
550-
throw std::runtime_error("DecisionTreeFactor::restrict not implemented");
550+
ADT restricted_tree = ADT::restrict(assignment);
551+
return std::make_shared<DecisionTreeFactor>(this->discreteKeys(),
552+
restricted_tree);
551553
}
552554

553555
/* ************************************************************************ */

0 commit comments

Comments
 (0)