commit
b1e91671fd
|
@ -2166,6 +2166,34 @@ virtual class NoiseModelFactor: gtsam::NonlinearFactor {
|
|||
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>
|
||||
class 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.
|
||||
* 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
|
||||
* returning true solves this problem.
|
||||
*
|
||||
|
@ -134,6 +134,14 @@ public:
|
|||
*/
|
||||
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
|
||||
|
||||
/// traits
|
||||
|
|
|
@ -325,7 +325,7 @@ public:
|
|||
// Operator that linearizes a given range of the factors
|
||||
void operator()(const tbb::blocked_range<size_t>& blocked_range) const {
|
||||
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_);
|
||||
else
|
||||
result_[i] = GaussianFactor::shared_ptr();
|
||||
|
@ -348,9 +348,19 @@ GaussianFactorGraph::shared_ptr NonlinearFactorGraph::linearize(const Values& li
|
|||
|
||||
linearFG->resize(size());
|
||||
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()),
|
||||
_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
|
||||
|
||||
linearFG->reserve(size());
|
||||
|
|
|
@ -61,7 +61,8 @@ endif()
|
|||
# ignoring the non-concrete types (type aliases)
|
||||
set(ignore
|
||||
gtsam::Point2
|
||||
gtsam::Point3)
|
||||
gtsam::Point3
|
||||
gtsam::CustomFactor)
|
||||
|
||||
# Wrap
|
||||
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
|
||||
// These are required to save one copy operation on Python calls
|
||||
/* Please refer to: https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
|
||||
* 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
|
||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Key, tbb::tbb_allocator<gtsam::Key>>);
|
||||
#else
|
||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::Key>);
|
||||
#endif
|
||||
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<boost::shared_ptr<gtsam::BetweenFactor<gtsam::Pose3> > >);
|
||||
PYBIND11_MAKE_OPAQUE(std::vector<boost::shared_ptr<gtsam::BetweenFactor<gtsam::Pose2> > >);
|
||||
PYBIND11_MAKE_OPAQUE(std::vector<gtsam::IndexPair>);
|
||||
PYBIND11_MAKE_OPAQUE(gtsam::CameraSet<gtsam::PinholeCamera<gtsam::Cal3Bundler> >);
|
||||
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
|
||||
// These are required to save one copy operation on Python calls
|
||||
/* Please refer to: https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
|
||||
* 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
|
||||
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
|
||||
py::bind_vector<std::vector<gtsam::Key> >(m_, "KeyVector");
|
||||
py::implicitly_convertible<py::list, std::vector<gtsam::Key> >();
|
||||
#endif
|
||||
|
||||
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::Pose3Pair> >(m_, "Pose3Pairs");
|
||||
|
@ -17,3 +32,4 @@ py::bind_vector<gtsam::IndexPairVector>(m_, "IndexPairVector");
|
|||
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::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