Skip to content
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

Functorized Factor #329

Merged
merged 7 commits into from
Jul 13, 2020
Merged

Functorized Factor #329

merged 7 commits into from
Jul 13, 2020

Conversation

varunagrawal
Copy link
Collaborator

@varunagrawal varunagrawal commented Jun 1, 2020

This PR adds a new type of factor which allows for more dynamic computation.

Consider the case that the input type to a factor's evaluateError method is different from the measurement, and the factor requires some intermediate processing of the input to match the type (both syntactically and semantically) of the measurement. The FunctorizedFactor accepts the functor argument type as a template argument and operates on the input, allowing for considerable reuse.

E.g. if we wish to compute the error from a simple linear matrix operation Ax = b, we could define a factor like:

Key key = Symbol('X', 0);
auto model = noiseModel::Isotropic::Sigma(9, 1);

/// Functor that takes a matrix and multiplies every element by m
class MultiplyFunctor {
  double m_; ///< simple multiplier

public:
  MultiplyFunctor(double m) : m_(m) {}

  Matrix operator()(const Matrix &X,
                    OptionalJacobian<-1, -1> H = boost::none) const {
    if (H)
      *H = m_ * Matrix::Identity(X.rows() * X.cols(), X.rows() * X.cols());
    return m_ * X;
  }
};

Matrix X = Matrix::Identity(3, 3), measurement = Matrix::Identity(3, 3);
double multiplier = 1.0;

// `multiplier` will be passed in to the functor constructor
auto factor = MakeFunctorizedFactor<Matrix>(key, measurement, model, multiplier);

// This factor now evaluates `m*A` and computes the error to `b` -> `mA - b`
Matrix error = factor.evaluateError(X);

Thus the factor is completely determined by the functor passed in, thus providing a great amount of simplicity.

The factor also supports std::function and C++ lambda types.


This change is Reviewable

@varunagrawal varunagrawal added enhancement Improvement to GTSAM feature New proposed feature design Design choices labels Jun 1, 2020
@varunagrawal varunagrawal self-assigned this Jun 1, 2020
@@ -619,6 +619,7 @@ void Isotropic::WhitenInPlace(Eigen::Block<Matrix> H) const {
// Unit
/* ************************************************************************* */
void Unit::print(const std::string& name) const {
//TODO(Varun): Do we need that space at the end?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove from this PR

/**
* Factor which evaluates functor and uses the result to compute
* error on provided measurement.
* The provided FUNCTOR should provide two definitions: `argument_type` which
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type aliases

* corresponds to the type of input it accepts and `return_type` which indicates
* the type of the return value. This factor uses those type values to construct
* the functor.
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the example from the description after you fix it (constructor has no key, jacobian is ignored although it would be simple in your example)

/// @{
GTSAM_EXPORT friend std::ostream &
operator<<(std::ostream &os, const FunctorizedFactor<FUNCTOR> &f) {
os << " noise model sigmas: " << f.noiseModel_->sigmas().transpose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream operator is not part of Testable. Move it somewhere else? Or, in fact remove it. Adding streaming is a big design exercise, large refactor we might not want to tackle now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a note, there are factors that have the stream operator as a part of Testable.

const {
const FunctorizedFactor<FUNCTOR> *e =
dynamic_cast<const FunctorizedFactor<FUNCTOR>*>(&other);
const bool base = Base::equals(*e, tol);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fail if e == nullptr

}
};

// TODO(Varun): Include or kill?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if factors have it, include. But do they?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do. Included.

@dellaert
Copy link
Member

dellaert commented Jun 1, 2020

Awesome. Some comments...

*/
template <typename FUNCTOR>
class GTSAM_EXPORT FunctorizedFactor
: public NoiseModelFactor1<typename FUNCTOR::argument_type> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to allow this to accept more variables? It seems like it should be reasonably common to have

|h(x1, x2, ...) - z|

where we define a functor for h(x1, x2, ...).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup it takes any number of arguments. That's why you have the template <typename... Args> on the constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to clarify, I meant rather than NoiseModelFactor1, also be able to have NoiseModelFactor2, 3, etc

So right now from the constructor templating it's able to do something like

h(x, theta1, theta2, ...)

where theta are fixed parameters, but I was wondering about multiple variables

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. Yeah that's phase 2. Now that we have this factor working, it should be straightforward to extend it to multiple keys.

@varunagrawal varunagrawal requested review from dellaert and gchenfc June 1, 2020 23:53
@varunagrawal varunagrawal mentioned this pull request Jun 18, 2020
26 tasks
Copy link
Member

@dellaert dellaert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this is just not ready yet - since this introduces a new core factor we need to think harder.

/**
* Factor which evaluates functor and uses the result to compute
* error on provided measurement.
* The provided FUNCTOR should provide two type aliases: `argument_type` which
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I’m wondering whether this is just any std::function? And whether we could use it with a lambda? https://en.cppreference.com/w/cpp/utility/functional/function

I propose you add two unit tests that exercise those cases and we can see if and how it leads to a change in your requirements. It could also be much simpler to just say that the Functor has to satisfy the standard function concept

In fact, reading more, we probably have to:

https://stackoverflow.com/questions/35907454/why-stdfunctionargument-type-has-been-deprecated

“Now that we have decltype that can be used to deduce a callable's return type, variadic templates and perfect forwarding to forward arbitrary number of arguments to functions, etc. the pre-C++11 mechanisms are way too cumbersome to use.”

https://en.cppreference.com/w/cpp/language/decltype

The following post seems to hint that we might not want to know the argument types though: https://stackoverflow.com/questions/6512019/can-we-get-the-type-of-a-lambda-argument

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually thought about this at first and had some initial difficulty which I don't remember right now. Then again, my understanding of functionals in C++ has improved quite a bit since then so I'll tackle this today.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've not had a lot of success with this without over-complicating the code. Something to consider would be just removing the need for argument_type and return_type from the FUNCTOR and making it match the traits of std::function so that they can be used interchangeably.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. And it should also do lambdas. Did you make the two unit tests I proposed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did, but I've been having trouble getting the code to compile.

* double multiplier = 2.0;
* FunctorizedFactor<MultiplyFunctor> factor(keyX, measurement, model, multiplier);
*/
template <typename FUNCTOR>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To address my other comment, perhaps we should instead template on T. And then a Functor/lambda/std::function passed needs to take (T, jacobian). Not necessarily dynamic jacobian, btw...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that we won't know what the return type is since we don't have anything to call decltype on. We need the return type to define the measurement type.

* @param model: Noise model
* @param args: Variable number of arguments used to instantiate functor
*/
template <typename... Args>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would only be one constructor. The other takes a lambda or function object...

By adding the helper function MakeFunctorizedFactor, we now only need to provide the argument type in the template parameter list. This considerably simplifies the factor declaration, while removing the need for argument type and return type in the functor definition.

Also added tests for std::function and lambda functions.
@varunagrawal
Copy link
Collaborator Author

So lots of good stuff thanks to my discussion with @dellaert. Declaring the FunctorizedFactor is crazy simple now (with lots of nice function template deduction happening behind the scenes), and we have eliminated the need for functors to define argument_type and return_type in their definitions.

@varunagrawal varunagrawal requested a review from dellaert July 7, 2020 01:59
@varunagrawal varunagrawal added this to the GTSAM 4.1 milestone Jul 12, 2020
@varunagrawal
Copy link
Collaborator Author

Merging since this PR doesn't introduce any breaking changes

@varunagrawal varunagrawal merged commit 05ad893 into develop Jul 13, 2020
@varunagrawal varunagrawal deleted the feature/functorized-factor branch July 13, 2020 02:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design choices enhancement Improvement to GTSAM feature New proposed feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants