-
Notifications
You must be signed in to change notification settings - Fork 837
Custom Factors in Python #767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
866d6b1
3638b37
66e397c
4708d7a
3c327ff
5d1fd83
615a932
a8ed71a
7de3714
0e44261
880d5b5
22ddab7
56faf3c
93ebc9d
1ebf675
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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::str 6D40 ing &s, | ||
const KeyFormatter &keyFormatter = DefaultKeyFormatter) const override; | ||
|
||
/** | ||
* Mark not sendable | ||
*/ | ||
bool sendable() const override { | ||
return false; | ||
} | ||
|
||
ProfFan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)); | ||
} | ||
}; | ||
|
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,7 +95,7 @@ class GTSAM_EXPORT NonlinearFactor: public Factor { | |
|
||
/** | ||
* 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 @@ class GTSAM_EXPORT NonlinearFactor: public Factor { | |
*/ | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t understand the name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I shamelessly borrowed this concept from Rust (here). Basically it indicates whether an object can be shared with another thread. Maybe |
||
return true; | ||
} | ||
|
||
}; // \class NonlinearFactor | ||
|
||
/// traits | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either it’s a betweenfactor or it’s not. Pose to compare with still seems to be zero? I thought we had discussed a better, more representative example, but I don’t recall what. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not zero now, if you look at the last unit test. |
||
|
||
```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. |
Uh oh!
There was an error while loading. Please reload this page.