commit
b1e91671fd
|
@ -2166,6 +2166,34 @@ virtual class NoiseModelFactor: gtsam::NonlinearFactor {
|
||||||
Vector whitenedError(const gtsam::Values& x) const;
|
Vector whitenedError(const gtsam::Values& x) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#include <gtsam/nonlinear/CustomFactor.h>
|
||||||
|
virtual class CustomFactor: gtsam::NoiseModelFactor {
|
||||||
|
/*
|
||||||
|
* Note CustomFactor will not be wrapped for MATLAB, as there is no supporting machinery there.
|
||||||
|
* This is achieved by adding `gtsam::CustomFactor` to the ignore list in `matlab/CMakeLists.txt`.
|
||||||
|
*/
|
||||||
|
CustomFactor();
|
||||||
|
/*
|
||||||
|
* Example:
|
||||||
|
* ```
|
||||||
|
* def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
* <calculated error>
|
||||||
|
* if not H is None:
|
||||||
|
* <calculate the Jacobian>
|
||||||
|
* H[0] = J1 # 2-d numpy array for a Jacobian block
|
||||||
|
* H[1] = J2
|
||||||
|
* ...
|
||||||
|
* return error # 1-d numpy array
|
||||||
|
*
|
||||||
|
* cf = CustomFactor(noise_model, keys, error_func)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
CustomFactor(const gtsam::SharedNoiseModel& noiseModel, const gtsam::KeyVector& keys,
|
||||||
|
const gtsam::CustomErrorFunction& errorFunction);
|
||||||
|
|
||||||
|
void print(string s = "", gtsam::KeyFormatter keyFormatter = gtsam::DefaultKeyFormatter);
|
||||||
|
};
|
||||||
|
|
||||||
#include <gtsam/nonlinear/Values.h>
|
#include <gtsam/nonlinear/Values.h>
|
||||||
class Values {
|
class Values {
|
||||||
Values();
|
Values();
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
|
||||||
|
* Atlanta, Georgia 30332-0415
|
||||||
|
* All Rights Reserved
|
||||||
|
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||||
|
|
||||||
|
* See LICENSE for the license information
|
||||||
|
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file CustomFactor.cpp
|
||||||
|
* @brief Class to enable arbitrary factors with runtime swappable error function.
|
||||||
|
* @author Fan Jiang
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <gtsam/nonlinear/CustomFactor.h>
|
||||||
|
|
||||||
|
namespace gtsam {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calculates the unwhitened error by invoking the callback functor (i.e. from Python).
|
||||||
|
*/
|
||||||
|
Vector CustomFactor::unwhitenedError(const Values& x, boost::optional<std::vector<Matrix>&> H) const {
|
||||||
|
if(this->active(x)) {
|
||||||
|
|
||||||
|
if(H) {
|
||||||
|
/*
|
||||||
|
* In this case, we pass the raw pointer to the `std::vector<Matrix>` object directly to pybind.
|
||||||
|
* As the type `std::vector<Matrix>` has been marked as opaque in `preamble.h`, any changes in
|
||||||
|
* Python will be immediately reflected on the C++ side.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```
|
||||||
|
* def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
* <calculated error>
|
||||||
|
* if not H is None:
|
||||||
|
* <calculate the Jacobian>
|
||||||
|
* H[0] = J1
|
||||||
|
* H[1] = J2
|
||||||
|
* ...
|
||||||
|
* return error
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
return this->error_function_(*this, x, H.get_ptr());
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* In this case, we pass the a `nullptr` to pybind, and it will translate to `None` in Python.
|
||||||
|
* Users can check for `None` in their callback to determine if the Jacobian is requested.
|
||||||
|
*/
|
||||||
|
return this->error_function_(*this, x, nullptr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Vector::Zero(this->dim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomFactor::print(const std::string &s, const KeyFormatter &keyFormatter) const {
|
||||||
|
std::cout << s << "CustomFactor on ";
|
||||||
|
auto keys_ = this->keys();
|
||||||
|
bool f = false;
|
||||||
|
for (const Key &k: keys_) {
|
||||||
|
if (f)
|
||||||
|
std::cout << ", ";
|
||||||
|
std::cout << keyFormatter(k);
|
||||||
|
f = true;
|
||||||
|
}
|
||||||
|
std::cout << "\n";
|
||||||
|
if (this->noiseModel_)
|
||||||
|
this->noiseModel_->print(" noise model: ");
|
||||||
|
else
|
||||||
|
std::cout << "no noise model" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
|
||||||
|
* Atlanta, Georgia 30332-0415
|
||||||
|
* All Rights Reserved
|
||||||
|
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||||
|
|
||||||
|
* See LICENSE for the license information
|
||||||
|
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file CustomFactor.h
|
||||||
|
* @brief Class to enable arbitrary factors with runtime swappable error function.
|
||||||
|
* @author Fan Jiang
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtsam/nonlinear/NonlinearFactor.h>
|
||||||
|
|
||||||
|
using namespace gtsam;
|
||||||
|
|
||||||
|
namespace gtsam {
|
||||||
|
|
||||||
|
using JacobianVector = std::vector<Matrix>;
|
||||||
|
|
||||||
|
class CustomFactor;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE
|
||||||
|
* ==========
|
||||||
|
* pybind11 will invoke a copy if this is `JacobianVector &`, and modifications in Python will not be reflected.
|
||||||
|
*
|
||||||
|
* This is safe because this is passing a const pointer, and pybind11 will maintain the `std::vector` memory layout.
|
||||||
|
* Thus the pointer will never be invalidated.
|
||||||
|
*/
|
||||||
|
using CustomErrorFunction = std::function<Vector(const CustomFactor &, const Values &, const JacobianVector *)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Custom factor that takes a std::function as the error
|
||||||
|
* @addtogroup nonlinear
|
||||||
|
* \nosubgrouping
|
||||||
|
*
|
||||||
|
* This factor is mainly for creating a custom factor in Python.
|
||||||
|
*/
|
||||||
|
class CustomFactor: public NoiseModelFactor {
|
||||||
|
protected:
|
||||||
|
CustomErrorFunction error_function_;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
using Base = NoiseModelFactor;
|
||||||
|
using This = CustomFactor;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor for I/O
|
||||||
|
*/
|
||||||
|
CustomFactor() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param noiseModel shared pointer to noise model
|
||||||
|
* @param keys keys of the variables
|
||||||
|
* @param errorFunction the error functional
|
||||||
|
*/
|
||||||
|
CustomFactor(const SharedNoiseModel &noiseModel, const KeyVector &keys, const CustomErrorFunction &errorFunction) :
|
||||||
|
Base(noiseModel, keys) {
|
||||||
|
this->error_function_ = errorFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
~CustomFactor() override = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the errorFunction closure, which is a std::function object
|
||||||
|
* One can check if a derivative is needed in the errorFunction by checking the length of Jacobian array
|
||||||
|
*/
|
||||||
|
Vector unwhitenedError(const Values &x, boost::optional<std::vector<Matrix> &> H = boost::none) const override;
|
||||||
|
|
||||||
|
/** print */
|
||||||
|
void print(const std::string &s,
|
||||||
|
const KeyFormatter &keyFormatter = DefaultKeyFormatter) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark not sendable
|
||||||
|
*/
|
||||||
|
bool sendable() const override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
/** Serialization function */
|
||||||
|
friend class boost::serialization::access;
|
||||||
|
template<class ARCHIVE>
|
||||||
|
void serialize(ARCHIVE &ar, const unsigned int /*version*/) {
|
||||||
|
ar & boost::serialization::make_nvp("CustomFactor",
|
||||||
|
boost::serialization::base_object<Base>(*this));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
|
@ -95,7 +95,7 @@ public:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a factor should be used based on a set of values.
|
* Checks whether a factor should be used based on a set of values.
|
||||||
* This is primarily used to implment inequality constraints that
|
* This is primarily used to implement inequality constraints that
|
||||||
* require a variable active set. For all others, the default implementation
|
* require a variable active set. For all others, the default implementation
|
||||||
* returning true solves this problem.
|
* returning true solves this problem.
|
||||||
*
|
*
|
||||||
|
@ -134,6 +134,14 @@ public:
|
||||||
*/
|
*/
|
||||||
virtual shared_ptr rekey(const KeyVector& new_keys) const;
|
virtual shared_ptr rekey(const KeyVector& new_keys) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the factor be evaluated in the same thread as the caller
|
||||||
|
* This is to enable factors that has shared states (like the Python GIL lock)
|
||||||
|
*/
|
||||||
|
virtual bool sendable() const {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}; // \class NonlinearFactor
|
}; // \class NonlinearFactor
|
||||||
|
|
||||||
/// traits
|
/// traits
|
||||||
|
|
|
@ -325,7 +325,7 @@ public:
|
||||||
// Operator that linearizes a given range of the factors
|
// Operator that linearizes a given range of the factors
|
||||||
void operator()(const tbb::blocked_range<size_t>& blocked_range) const {
|
void operator()(const tbb::blocked_range<size_t>& blocked_range) const {
|
||||||
for (size_t i = blocked_range.begin(); i != blocked_range.end(); ++i) {
|
for (size_t i = blocked_range.begin(); i != blocked_range.end(); ++i) {
|
||||||
if (nonlinearGraph_[i])
|
if (nonlinearGraph_[i] && nonlinearGraph_[i]->sendable())
|
||||||
result_[i] = nonlinearGraph_[i]->linearize(linearizationPoint_);
|
result_[i] = nonlinearGraph_[i]->linearize(linearizationPoint_);
|
||||||
else
|
else
|
||||||
result_[i] = GaussianFactor::shared_ptr();
|
result_[i] = GaussianFactor::shared_ptr();
|
||||||
|
@ -348,9 +348,19 @@ GaussianFactorGraph::shared_ptr NonlinearFactorGraph::linearize(const Values& li
|
||||||
|
|
||||||
linearFG->resize(size());
|
linearFG->resize(size());
|
||||||
TbbOpenMPMixedScope threadLimiter; // Limits OpenMP threads since we're mixing TBB and OpenMP
|
TbbOpenMPMixedScope threadLimiter; // Limits OpenMP threads since we're mixing TBB and OpenMP
|
||||||
|
|
||||||
|
// First linearize all sendable factors
|
||||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, size()),
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, size()),
|
||||||
_LinearizeOneFactor(*this, linearizationPoint, *linearFG));
|
_LinearizeOneFactor(*this, linearizationPoint, *linearFG));
|
||||||
|
|
||||||
|
// Linearize all non-sendable factors
|
||||||
|
for(int i = 0; i < size(); i++) {
|
||||||
|
auto& factor = (*this)[i];
|
||||||
|
if(factor && !(factor->sendable())) {
|
||||||
|
(*linearFG)[i] = factor->linearize(linearizationPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
linearFG->reserve(size());
|
linearFG->reserve(size());
|
||||||
|
|
|
@ -61,7 +61,8 @@ endif()
|
||||||
# ignoring the non-concrete types (type aliases)
|
# ignoring the non-concrete types (type aliases)
|
||||||
set(ignore
|
set(ignore
|
||||||
gtsam::Point2
|
gtsam::Point2
|
||||||
gtsam::Point3)
|
gtsam::Point3
|
||||||
|
gtsam::CustomFactor)
|
||||||
|
|
||||||
# Wrap
|
# Wrap
|
||||||
matlab_wrap(${GTSAM_SOURCE_DIR}/gtsam/gtsam.i "${GTSAM_ADDITIONAL_LIBRARIES}"
|
matlab_wrap(${GTSAM_SOURCE_DIR}/gtsam/gtsam.i "${GTSAM_ADDITIONAL_LIBRARIES}"
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
# GTSAM Python-based factors
|
||||||
|
|
||||||
|
One now can build factors purely in Python using the `CustomFactor` factor.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
In order to use a Python-based factor, one needs to have a Python function with the following signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import gtsam
|
||||||
|
import numpy as np
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
def error_func(this: gtsam.CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`this` is a reference to the `CustomFactor` object. This is required because one can reuse the same
|
||||||
|
`error_func` for multiple factors. `v` is a reference to the current set of values, and `H` is a list of
|
||||||
|
**references** to the list of required Jacobians (see the corresponding C++ documentation).
|
||||||
|
|
||||||
|
If `H` is `None`, it means the current factor evaluation does not need Jacobians. For example, the `error`
|
||||||
|
method on a factor does not need Jacobians, so we don't evaluate them to save CPU. If `H` is not `None`,
|
||||||
|
each entry of `H` can be assigned a `numpy` array, as the Jacobian for the corresponding variable.
|
||||||
|
|
||||||
|
After defining `error_func`, one can create a `CustomFactor` just like any other factor in GTSAM:
|
||||||
|
|
||||||
|
```python
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
# constructor(<noise model>, <list of keys>, <error callback>)
|
||||||
|
cf = gtsam.CustomFactor(noise_model, [X(0), X(1)], error_func)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following is a simple `BetweenFactor` implemented in Python.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import gtsam
|
||||||
|
import numpy as np
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
expected = Pose2(2, 2, np.pi / 2)
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
"""
|
||||||
|
Error function that mimics a BetweenFactor
|
||||||
|
:param this: reference to the current CustomFactor being evaluated
|
||||||
|
:param v: Values object
|
||||||
|
:param H: list of references to the Jacobian arrays
|
||||||
|
:return: the non-linear error
|
||||||
|
"""
|
||||||
|
key0 = this.keys()[0]
|
||||||
|
key1 = this.keys()[1]
|
||||||
|
gT1, gT2 = v.atPose2(key0), v.atPose2(key1)
|
||||||
|
error = expected.localCoordinates(gT1.between(gT2))
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
result = gT1.between(gT2)
|
||||||
|
H[0] = -result.inverse().AdjointMap()
|
||||||
|
H[1] = np.eye(3)
|
||||||
|
return error
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = gtsam.CustomFactor(noise_model, gtsam.KeyVector([0, 1]), error_func)
|
||||||
|
```
|
||||||
|
|
||||||
|
In general, the Python-based factor works just like their C++ counterparts.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
Because of the `pybind11`-based translation, the performance of `CustomFactor` is not guaranteed.
|
||||||
|
Also, because `pybind11` needs to lock the Python GIL lock for evaluation of each factor, parallel
|
||||||
|
evaluation of `CustomFactor` is not possible.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
`CustomFactor` is a `NonlinearFactor` that has a `std::function` as its callback.
|
||||||
|
This callback can be translated to a Python function call, thanks to `pybind11`'s functional support.
|
||||||
|
|
||||||
|
The constructor of `CustomFactor` is
|
||||||
|
```c++
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param noiseModel shared pointer to noise model
|
||||||
|
* @param keys keys of the variables
|
||||||
|
* @param errorFunction the error functional
|
||||||
|
*/
|
||||||
|
CustomFactor(const SharedNoiseModel& noiseModel, const KeyVector& keys, const CustomErrorFunction& errorFunction) :
|
||||||
|
Base(noiseModel, keys) {
|
||||||
|
this->error_function_ = errorFunction;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
At construction time, `pybind11` will pass the handle to the Python callback function as a `std::function` object.
|
||||||
|
|
||||||
|
Something worth special mention is this:
|
||||||
|
```c++
|
||||||
|
/*
|
||||||
|
* NOTE
|
||||||
|
* ==========
|
||||||
|
* pybind11 will invoke a copy if this is `JacobianVector &`, and modifications in Python will not be reflected.
|
||||||
|
*
|
||||||
|
* This is safe because this is passing a const pointer, and pybind11 will maintain the `std::vector` memory layout.
|
||||||
|
* Thus the pointer will never be invalidated.
|
||||||
|
*/
|
||||||
|
using CustomErrorFunction = std::function<Vector(const CustomFactor&, const Values&, const JacobianVector*)>;
|
||||||
|
```
|
||||||
|
|
||||||
|
which is not documented in `pybind11` docs. One needs to be aware of this if they wanted to implement similar
|
||||||
|
"mutable" arguments going across the Python-C++ boundary.
|
|
@ -0,0 +1,179 @@
|
||||||
|
"""
|
||||||
|
GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||||
|
Atlanta, Georgia 30332-0415
|
||||||
|
All Rights Reserved
|
||||||
|
|
||||||
|
See LICENSE for the license information
|
||||||
|
|
||||||
|
CustomFactor demo that simulates a 1-D sensor fusion task.
|
||||||
|
Author: Fan Jiang, Frank Dellaert
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_car():
|
||||||
|
# Simulate a car for one second
|
||||||
|
x0 = 0
|
||||||
|
dt = 0.25 # 4 Hz, typical GPS
|
||||||
|
v = 144 * 1000 / 3600 # 144 km/hour = 90mph, pretty fast
|
||||||
|
x = [x0 + v * dt * i for i in range(5)]
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
x = simulate_car()
|
||||||
|
print(f"Simulated car trajectory: {x}")
|
||||||
|
|
||||||
|
# %%
|
||||||
|
add_noise = True # set this to False to run with "perfect" measurements
|
||||||
|
|
||||||
|
# GPS measurements
|
||||||
|
sigma_gps = 3.0 # assume GPS is +/- 3m
|
||||||
|
g = [x[k] + (np.random.normal(scale=sigma_gps) if add_noise else 0)
|
||||||
|
for k in range(5)]
|
||||||
|
|
||||||
|
# Odometry measurements
|
||||||
|
sigma_odo = 0.1 # assume Odometry is 10cm accurate at 4Hz
|
||||||
|
o = [x[k + 1] - x[k] + (np.random.normal(scale=sigma_odo) if add_noise else 0)
|
||||||
|
for k in range(4)]
|
||||||
|
|
||||||
|
# Landmark measurements:
|
||||||
|
sigma_lm = 1 # assume landmark measurement is accurate up to 1m
|
||||||
|
|
||||||
|
# Assume first landmark is at x=5, we measure it at time k=0
|
||||||
|
lm_0 = 5.0
|
||||||
|
z_0 = x[0] - lm_0 + (np.random.normal(scale=sigma_lm) if add_noise else 0)
|
||||||
|
|
||||||
|
# Assume other landmark is at x=28, we measure it at time k=3
|
||||||
|
lm_3 = 28.0
|
||||||
|
z_3 = x[3] - lm_3 + (np.random.normal(scale=sigma_lm) if add_noise else 0)
|
||||||
|
|
||||||
|
unknown = [gtsam.symbol('x', k) for k in range(5)]
|
||||||
|
|
||||||
|
print("unknowns = ", list(map(gtsam.DefaultKeyFormatter, unknown)))
|
||||||
|
|
||||||
|
# We now can use nonlinear factor graphs
|
||||||
|
factor_graph = gtsam.NonlinearFactorGraph()
|
||||||
|
|
||||||
|
# Add factors for GPS measurements
|
||||||
|
I = np.eye(1)
|
||||||
|
gps_model = gtsam.noiseModel.Isotropic.Sigma(1, sigma_gps)
|
||||||
|
|
||||||
|
|
||||||
|
def error_gps(measurement: np.ndarray, this: gtsam.CustomFactor, values, jacobians: Optional[List[np.ndarray]]):
|
||||||
|
"""GPS Factor error function
|
||||||
|
:param measurement: GPS measurement, to be filled with `partial`
|
||||||
|
:param this: gtsam.CustomFactor handle
|
||||||
|
:param values: gtsam.Values
|
||||||
|
:param jacobians: Optional list of Jacobians
|
||||||
|
:return: the unwhitened error
|
||||||
|
"""
|
||||||
|
key = this.keys()[0]
|
||||||
|
estimate = values.atVector(key)
|
||||||
|
error = estimate - measurement
|
||||||
|
if jacobians is not None:
|
||||||
|
jacobians[0] = I
|
||||||
|
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
# Add the GPS factors
|
||||||
|
for k in range(5):
|
||||||
|
gf = gtsam.CustomFactor(gps_model, [unknown[k]], partial(error_gps, np.array([g[k]])))
|
||||||
|
factor_graph.add(gf)
|
||||||
|
|
||||||
|
# New Values container
|
||||||
|
v = gtsam.Values()
|
||||||
|
|
||||||
|
# Add initial estimates to the Values container
|
||||||
|
for i in range(5):
|
||||||
|
v.insert(unknown[i], np.array([0.0]))
|
||||||
|
|
||||||
|
# Initialize optimizer
|
||||||
|
params = gtsam.GaussNewtonParams()
|
||||||
|
optimizer = gtsam.GaussNewtonOptimizer(factor_graph, v, params)
|
||||||
|
|
||||||
|
# Optimize the factor graph
|
||||||
|
result = optimizer.optimize()
|
||||||
|
|
||||||
|
# calculate the error from ground truth
|
||||||
|
error = np.array([(result.atVector(unknown[k]) - x[k])[0] for k in range(5)])
|
||||||
|
|
||||||
|
print("Result with only GPS")
|
||||||
|
print(result, np.round(error, 2), f"\nJ(X)={0.5 * np.sum(np.square(error))}")
|
||||||
|
|
||||||
|
# Adding odometry will improve things a lot
|
||||||
|
odo_model = gtsam.noiseModel.Isotropic.Sigma(1, sigma_odo)
|
||||||
|
|
||||||
|
|
||||||
|
def error_odom(measurement: np.ndarray, this: gtsam.CustomFactor, values, jacobians: Optional[List[np.ndarray]]):
|
||||||
|
"""Odometry Factor error function
|
||||||
|
:param measurement: Odometry measurement, to be filled with `partial`
|
||||||
|
:param this: gtsam.CustomFactor handle
|
||||||
|
:param values: gtsam.Values
|
||||||
|
:param jacobians: Optional list of Jacobians
|
||||||
|
:return: the unwhitened error
|
||||||
|
"""
|
||||||
|
key1 = this.keys()[0]
|
||||||
|
key2 = this.keys()[1]
|
||||||
|
pos1, pos2 = values.atVector(key1), values.atVector(key2)
|
||||||
|
error = measurement - (pos1 - pos2)
|
||||||
|
if jacobians is not None:
|
||||||
|
jacobians[0] = I
|
||||||
|
jacobians[1] = -I
|
||||||
|
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
for k in range(4):
|
||||||
|
odof = gtsam.CustomFactor(odo_model, [unknown[k], unknown[k + 1]], partial(error_odom, np.array([o[k]])))
|
||||||
|
factor_graph.add(odof)
|
||||||
|
|
||||||
|
params = gtsam.GaussNewtonParams()
|
||||||
|
optimizer = gtsam.GaussNewtonOptimizer(factor_graph, v, params)
|
||||||
|
|
||||||
|
result = optimizer.optimize()
|
||||||
|
|
||||||
|
error = np.array([(result.atVector(unknown[k]) - x[k])[0] for k in range(5)])
|
||||||
|
|
||||||
|
print("Result with GPS+Odometry")
|
||||||
|
print(result, np.round(error, 2), f"\nJ(X)={0.5 * np.sum(np.square(error))}")
|
||||||
|
|
||||||
|
# This is great, but GPS noise is still apparent, so now we add the two landmarks
|
||||||
|
lm_model = gtsam.noiseModel.Isotropic.Sigma(1, sigma_lm)
|
||||||
|
|
||||||
|
|
||||||
|
def error_lm(measurement: np.ndarray, this: gtsam.CustomFactor, values, jacobians: Optional[List[np.ndarray]]):
|
||||||
|
"""Landmark Factor error function
|
||||||
|
:param measurement: Landmark measurement, to be filled with `partial`
|
||||||
|
:param this: gtsam.CustomFactor handle
|
||||||
|
:param values: gtsam.Values
|
||||||
|
:param jacobians: Optional list of Jacobians
|
||||||
|
:return: the unwhitened error
|
||||||
|
"""
|
||||||
|
key = this.keys()[0]
|
||||||
|
pos = values.atVector(key)
|
||||||
|
error = pos - measurement
|
||||||
|
if jacobians is not None:
|
||||||
|
jacobians[0] = I
|
||||||
|
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
factor_graph.add(gtsam.CustomFactor(lm_model, [unknown[0]], partial(error_lm, np.array([lm_0 + z_0]))))
|
||||||
|
factor_graph.add(gtsam.CustomFactor(lm_model, [unknown[3]], partial(error_lm, np.array([lm_3 + z_3]))))
|
||||||
|
|
||||||
|
params = gtsam.GaussNewtonParams()
|
||||||
|
optimizer = gtsam.GaussNewtonOptimizer(factor_graph, v, params)
|
||||||
|
|
||||||
|
result = optimizer.optimize()
|
||||||
|
|
||||||
|
error = np.array([(result.atVector(unknown[k]) - x[k])[0] for k in range(5)])
|
||||||
|
|
||||||
|
print("Result with GPS+Odometry+Landmark")
|
||||||
|
print(result, np.round(error, 2), f"\nJ(X)={0.5 * np.sum(np.square(error))}")
|
|
@ -1,14 +1,30 @@
|
||||||
// Please refer to: https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
|
/* Please refer to: https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
|
||||||
// These are required to save one copy operation on Python calls
|
* These are required to save one copy operation on Python calls.
|
||||||
|
*
|
||||||
|
* NOTES
|
||||||
|
* =================
|
||||||
|
*
|
||||||
|
* `PYBIND11_MAKE_OPAQUE` will mark the type as "opaque" for the pybind11 automatic STL binding,
|
||||||
|
* such that the raw objects can be accessed in Python. Without this they will be automatically
|
||||||
|
* converted to a Python object, and all mutations on Python side will not be reflected on C++.
|
||||||
|
*/
|
||||||
#ifdef GTSAM_ALLOCATOR_TBB
|
#ifdef GTSAM_ALLOCATOR_TBB
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Key, tbb::tbb_allocator<gtsam::Key>>);
|
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Key, tbb::tbb_allocator<gtsam::Key>>);
|
||||||
#else
|
#else
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Key>);
|
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Key>);
|
||||||
#endif
|
#endif
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Point2, Eigen::aligned_allocator<gtsam::Point2> >);
|
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Point2, Eigen::aligned_allocator<gtsam::Point2> >);
|
||||||
|
PYBIND11_MAKE_OPAQUE(gtsam::Point3Pairs);
|
||||||
|
PYBIND11_MAKE_OPAQUE(gtsam::Pose3Pairs);
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Pose3>);
|
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Pose3>);
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<boost::shared_ptr<gtsam::BetweenFactor<gtsam::Pose3> > >);
|
PYBIND11_MAKE_OPAQUE(std::vector<boost::shared_ptr<gtsam::BetweenFactor<gtsam::Pose3> > >);
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<boost::shared_ptr<gtsam::BetweenFactor<gtsam::Pose2> > >);
|
PYBIND11_MAKE_OPAQUE(std::vector<boost::shared_ptr<gtsam::BetweenFactor<gtsam::Pose2> > >);
|
||||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::IndexPair>);
|
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::IndexPair>);
|
||||||
PYBIND11_MAKE_OPAQUE(gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3Bundler> >);
|
PYBIND11_MAKE_OPAQUE(gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3Bundler> >);
|
||||||
PYBIND11_MAKE_OPAQUE(gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3_S2> >);
|
PYBIND11_MAKE_OPAQUE(gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3_S2> >);
|
||||||
|
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Matrix>); // JacobianVector
|
||||||
|
|
||||||
|
// TODO(fan): This is to fix the Argument-dependent lookup (ADL) of std::pair. We should find a way to NOT do this.
|
||||||
|
namespace std {
|
||||||
|
using gtsam::operator<<;
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
// Please refer to: https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
|
/* Please refer to: https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
|
||||||
// These are required to save one copy operation on Python calls
|
* These are required to save one copy operation on Python calls.
|
||||||
|
*
|
||||||
|
* NOTES
|
||||||
|
* =================
|
||||||
|
*
|
||||||
|
* `PYBIND11_MAKE_OPAQUE` will mark the type as "opaque" for the pybind11 automatic STL binding,
|
||||||
|
* such that the raw objects can be accessed in Python. Without this they will be automatically
|
||||||
|
* converted to a Python object, and all mutations on Python side will not be reflected on C++.
|
||||||
|
*
|
||||||
|
* `py::bind_vector` and similar machinery gives the std container a Python-like interface, but
|
||||||
|
* without the `<pybind11/stl.h>` copying mechanism. Combined with `PYBIND11_MAKE_OPAQUE` this
|
||||||
|
* allows the types to be modified with Python, and saves one copy operation.
|
||||||
|
*/
|
||||||
#ifdef GTSAM_ALLOCATOR_TBB
|
#ifdef GTSAM_ALLOCATOR_TBB
|
||||||
py::bind_vector<std::vector<gtsam::Key, tbb::tbb_allocator<gtsam::Key> > >(m_, "KeyVector");
|
py::bind_vector<std::vector<gtsam::Key, tbb::tbb_allocator<gtsam::Key> > >(m_, "KeyVector");
|
||||||
|
py::implicitly_convertible<py::list, std::vector<gtsam::Key, tbb::tbb_allocator<gtsam::Key> > >();
|
||||||
#else
|
#else
|
||||||
py::bind_vector<std::vector<gtsam::Key> >(m_, "KeyVector");
|
py::bind_vector<std::vector<gtsam::Key> >(m_, "KeyVector");
|
||||||
|
py::implicitly_convertible<py::list, std::vector<gtsam::Key> >();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
py::bind_vector<std::vector<gtsam::Point2, Eigen::aligned_allocator<gtsam::Point2> > >(m_, "Point2Vector");
|
py::bind_vector<std::vector<gtsam::Point2, Eigen::aligned_allocator<gtsam::Point2> > >(m_, "Point2Vector");
|
||||||
py::bind_vector<std::vector<gtsam::Point3Pair> >(m_, "Point3Pairs");
|
py::bind_vector<std::vector<gtsam::Point3Pair> >(m_, "Point3Pairs");
|
||||||
py::bind_vector<std::vector<gtsam::Pose3Pair> >(m_, "Pose3Pairs");
|
py::bind_vector<std::vector<gtsam::Pose3Pair> >(m_, "Pose3Pairs");
|
||||||
|
@ -17,3 +32,4 @@ py::bind_vector<gtsam::IndexPairVector>(m_, "IndexPairVector");
|
||||||
py::bind_map<gtsam::KeyPairDoubleMap>(m_, "KeyPairDoubleMap");
|
py::bind_map<gtsam::KeyPairDoubleMap>(m_, "KeyPairDoubleMap");
|
||||||
py::bind_vector<gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3_S2> > >(m_, "CameraSetCal3_S2");
|
py::bind_vector<gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3_S2> > >(m_, "CameraSetCal3_S2");
|
||||||
py::bind_vector<gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3Bundler> > >(m_, "CameraSetCal3Bundler");
|
py::bind_vector<gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3Bundler> > >(m_, "CameraSetCal3Bundler");
|
||||||
|
py::bind_vector<std::vector<gtsam::Matrix> >(m_, "JacobianVector");
|
||||||
|
|
|
@ -0,0 +1,207 @@
|
||||||
|
"""
|
||||||
|
GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||||
|
Atlanta, Georgia 30332-0415
|
||||||
|
All Rights Reserved
|
||||||
|
|
||||||
|
See LICENSE for the license information
|
||||||
|
|
||||||
|
CustomFactor unit tests.
|
||||||
|
Author: Fan Jiang
|
||||||
|
"""
|
||||||
|
from typing import List
|
||||||
|
import unittest
|
||||||
|
from gtsam import Values, Pose2, CustomFactor
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomFactor(GtsamTestCase):
|
||||||
|
def test_new(self):
|
||||||
|
"""Test the creation of a new CustomFactor"""
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
"""Minimal error function stub"""
|
||||||
|
return np.array([1, 0, 0])
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = CustomFactor(noise_model, gtsam.KeyVector([0]), error_func)
|
||||||
|
|
||||||
|
def test_new_keylist(self):
|
||||||
|
"""Test the creation of a new CustomFactor"""
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
"""Minimal error function stub"""
|
||||||
|
return np.array([1, 0, 0])
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = CustomFactor(noise_model, [0], error_func)
|
||||||
|
|
||||||
|
def test_call(self):
|
||||||
|
"""Test if calling the factor works (only error)"""
|
||||||
|
expected_pose = Pose2(1, 1, 0)
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]) -> np.ndarray:
|
||||||
|
"""Minimal error function with no Jacobian"""
|
||||||
|
key0 = this.keys()[0]
|
||||||
|
error = -v.atPose2(key0).localCoordinates(expected_pose)
|
||||||
|
return error
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = CustomFactor(noise_model, [0], error_func)
|
||||||
|
v = Values()
|
||||||
|
v.insert(0, Pose2(1, 0, 0))
|
||||||
|
e = cf.error(v)
|
||||||
|
|
||||||
|
self.assertEqual(e, 0.5)
|
||||||
|
|
||||||
|
def test_jacobian(self):
|
||||||
|
"""Tests if the factor result matches the GTSAM Pose2 unit test"""
|
||||||
|
|
||||||
|
gT1 = Pose2(1, 2, np.pi / 2)
|
||||||
|
gT2 = Pose2(-1, 4, np.pi)
|
||||||
|
|
||||||
|
expected = Pose2(2, 2, np.pi / 2)
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
"""
|
||||||
|
the custom error function. One can freely use variables captured
|
||||||
|
from the outside scope. Or the variables can be acquired by indexing `v`.
|
||||||
|
Jacobian is passed by modifying the H array of numpy matrices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key0 = this.keys()[0]
|
||||||
|
key1 = this.keys()[1]
|
||||||
|
gT1, gT2 = v.atPose2(key0), v.atPose2(key1)
|
||||||
|
error = expected.localCoordinates(gT1.between(gT2))
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
result = gT1.between(gT2)
|
||||||
|
H[0] = -result.inverse().AdjointMap()
|
||||||
|
H[1] = np.eye(3)
|
||||||
|
return error
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = CustomFactor(noise_model, gtsam.KeyVector([0, 1]), error_func)
|
||||||
|
v = Values()
|
||||||
|
v.insert(0, gT1)
|
||||||
|
v.insert(1, gT2)
|
||||||
|
|
||||||
|
bf = gtsam.BetweenFactorPose2(0, 1, expected, noise_model)
|
||||||
|
|
||||||
|
gf = cf.linearize(v)
|
||||||
|
gf_b = bf.linearize(v)
|
||||||
|
|
||||||
|
J_cf, b_cf = gf.jacobian()
|
||||||
|
J_bf, b_bf = gf_b.jacobian()
|
||||||
|
np.testing.assert_allclose(J_cf, J_bf)
|
||||||
|
np.testing.assert_allclose(b_cf, b_bf)
|
||||||
|
|
||||||
|
def test_printing(self):
|
||||||
|
"""Tests if the factor result matches the GTSAM Pose2 unit test"""
|
||||||
|
gT1 = Pose2(1, 2, np.pi / 2)
|
||||||
|
gT2 = Pose2(-1, 4, np.pi)
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, _: List[np.ndarray]):
|
||||||
|
"""Minimal error function stub"""
|
||||||
|
return np.array([1, 0, 0])
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
from gtsam.symbol_shorthand import X
|
||||||
|
cf = CustomFactor(noise_model, [X(0), X(1)], error_func)
|
||||||
|
|
||||||
|
cf_string = """CustomFactor on x0, x1
|
||||||
|
noise model: unit (3)
|
||||||
|
"""
|
||||||
|
self.assertEqual(cf_string, repr(cf))
|
||||||
|
|
||||||
|
def test_no_jacobian(self):
|
||||||
|
"""Tests that we will not calculate the Jacobian if not requested"""
|
||||||
|
|
||||||
|
gT1 = Pose2(1, 2, np.pi / 2)
|
||||||
|
gT2 = Pose2(-1, 4, np.pi)
|
||||||
|
|
||||||
|
expected = Pose2(2, 2, np.pi / 2)
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
"""
|
||||||
|
Error function that mimics a BetweenFactor
|
||||||
|
:param this: reference to the current CustomFactor being evaluated
|
||||||
|
:param v: Values object
|
||||||
|
:param H: list of references to the Jacobian arrays
|
||||||
|
:return: the non-linear error
|
||||||
|
"""
|
||||||
|
key0 = this.keys()[0]
|
||||||
|
key1 = this.keys()[1]
|
||||||
|
gT1, gT2 = v.atPose2(key0), v.atPose2(key1)
|
||||||
|
error = expected.localCoordinates(gT1.between(gT2))
|
||||||
|
|
||||||
|
self.assertTrue(H is None) # Should be true if we only request the error
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
result = gT1.between(gT2)
|
||||||
|
H[0] = -result.inverse().AdjointMap()
|
||||||
|
H[1] = np.eye(3)
|
||||||
|
return error
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = CustomFactor(noise_model, gtsam.KeyVector([0, 1]), error_func)
|
||||||
|
v = Values()
|
||||||
|
v.insert(0, gT1)
|
||||||
|
v.insert(1, gT2)
|
||||||
|
|
||||||
|
bf = gtsam.BetweenFactorPose2(0, 1, expected, noise_model)
|
||||||
|
|
||||||
|
e_cf = cf.error(v)
|
||||||
|
e_bf = bf.error(v)
|
||||||
|
np.testing.assert_allclose(e_cf, e_bf)
|
||||||
|
|
||||||
|
def test_optimization(self):
|
||||||
|
"""Tests if a factor graph with a CustomFactor can be properly optimized"""
|
||||||
|
gT1 = Pose2(1, 2, np.pi / 2)
|
||||||
|
gT2 = Pose2(-1, 4, np.pi)
|
||||||
|
|
||||||
|
expected = Pose2(2, 2, np.pi / 2)
|
||||||
|
|
||||||
|
def error_func(this: CustomFactor, v: gtsam.Values, H: List[np.ndarray]):
|
||||||
|
"""
|
||||||
|
Error function that mimics a BetweenFactor
|
||||||
|
:param this: reference to the current CustomFactor being evaluated
|
||||||
|
:param v: Values object
|
||||||
|
:param H: list of references to the Jacobian arrays
|
||||||
|
:return: the non-linear error
|
||||||
|
"""
|
||||||
|
key0 = this.keys()[0]
|
||||||
|
key1 = this.keys()[1]
|
||||||
|
gT1, gT2 = v.atPose2(key0), v.atPose2(key1)
|
||||||
|
error = expected.localCoordinates(gT1.between(gT2))
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
result = gT1.between(gT2)
|
||||||
|
H[0] = -result.inverse().AdjointMap()
|
||||||
|
H[1] = np.eye(3)
|
||||||
|
return error
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
cf = CustomFactor(noise_model, [0, 1], error_func)
|
||||||
|
|
||||||
|
fg = gtsam.NonlinearFactorGraph()
|
||||||
|
fg.add(cf)
|
||||||
|
fg.add(gtsam.PriorFactorPose2(0, gT1, noise_model))
|
||||||
|
|
||||||
|
v = Values()
|
||||||
|
v.insert(0, Pose2(0, 0, 0))
|
||||||
|
v.insert(1, Pose2(0, 0, 0))
|
||||||
|
|
||||||
|
params = gtsam.LevenbergMarquardtParams()
|
||||||
|
optimizer = gtsam.LevenbergMarquardtOptimizer(fg, v, params)
|
||||||
|
result = optimizer.optimize()
|
||||||
|
|
||||||
|
self.gtsamAssertEquals(result.atPose2(0), gT1, tol=1e-5)
|
||||||
|
self.gtsamAssertEquals(result.atPose2(1), gT2, tol=1e-5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue