-
Notifications
You must be signed in to change notification settings - Fork 13
Add Nonlinear FG interface #34
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
Conversation
|
@marcrasi I blatantly copied the @dellaert Since the |
|
BTW, for Pose2SLAM demo the solution converges applying CGLS two times, which is amazing! |
|
Very cool. Have a full day of meetings but hope to get to this before our 1:1. Amazing that the code works but convergence is not amazing if you start close to the optimum :-) |
marcrasi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read part of this but now I have to run to a meeting. I'll read more later. Exciting!
| /// ================ | ||
| /// `Input`: the input values as key-value pairs | ||
| /// | ||
| public protocol NonlinearFactor: Differentiable & Factor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the NonlinearFactor does not need to be Differentiable, because you never need to take derivatives with respect to the factor itself. You only take derivatives of the factor's error function with respect to the input values.
Removing Differentiable is useful because then you can have an Array<NonlinearFactor> without needing to create the AnyNonlinearFactor type.
Maybe NonlinearFactor should be renamed Factor and then LinearFactor should refine it? This would make sense to me because Factor is the most general thing that can behave in any way (nonlinearly or linearly), and LinearFactor is a special case that is guaranteed to be linear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree on the first one, but we will still need the type-erased wrapper if we have associated types in NonlinearFactor, right?
@dellaert What's your stance on this (pt. 2)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we will need the type-erased wrapper if there are associated types in NonlinearFactor. But currently there aren't any associated types (other than the TangentVector that comes from Differentiable), and I can't think of any associated types that you will need in the future.
| /// The most general factor protocol. | ||
| public protocol Factor { | ||
| var keys: Array<Int> { get set } | ||
| var keys: Array<Int> { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any place where you need to get the keys from a Factor? (Of course, the implementations of the factors need to use their keys, but do any users of factors need to get the keys?) If not, you could simplify the Factor API by completely removing the keys requirement.
And if you can remove keys, then you can also simplify the protocol hierarchy by completely removing Factor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we still have this in some of the algorithms for iterating through the keys to lookup the Values, but that may change if we have a better encapsulated API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need it for direct solvers, to decide on the ordering.
| return Vector3(error.rot.theta, error.t.x, error.t.y) | ||
| } | ||
|
|
||
| public func linearize(_ values: Values) -> JacobianFactor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's very likely that we can write a default generic linearize function that automatically does this for all NonlinearFactors, so that we don't have to repeat this every time we define a factor.
I have to run to a meeting soon so I don't have time to figure it out now, but I'll follow up later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in #49!
| /// | ||
| public struct BetweenFactor: NonlinearFactor { | ||
| @noDerivative | ||
| public var keys: Array<Int> = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest storing these as public let key1, key2: Int instead of as an array, to make it clear that this isn't really a variable-length thing.
If you do need keys: Array<Int> to satisfy the Factor requirement, you can make that a computed property:
public var keys: Array<Int> { [key1, key2] }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is me simply being lazy :)
| difference | ||
| ) | ||
|
|
||
| return Vector3(error.rot.theta, error.t.x, error.t.y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should actually return something using local coordinates (between(values[key1].baseAs(Pose2.self), values[key2].baseAs(Pose2.self).localCoordinates(around: difference)), right?
If so, you could add a TODO and a github issue for this, because this seems to work for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, this is on my TODO :)
Will change shortly...
| /// The class that holds Key-Value pairs. | ||
| public struct Values: Differentiable & KeyPathIterable { | ||
| public typealias ScalarType = Double | ||
| var _values: [AnyDifferentiable] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The standard Swift style is to make fields private (or fileprivate so that you can use it in other extensions in the same file) instead of using _.
|
|
||
| optimizer.optimize(gfg: gfg, initial: &dx) | ||
|
|
||
| for i in 0..<5 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see two possible improvements to the Values API.
- You could make a subscript that does casting, which would allow you to mutate variables without storing them in an intermediate
var. Something likeval[i, as: Pose2.self].move(along: ...). Values.TangentVectorcould beVectorValues, which would allow you to replace this whole loop with a single lineval.move(along: dx).
Let me know if you want me to help write code for either of these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree on both!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there might be some additional work required to enable (2) after #37 is merged -- it might not be totally easy to use Manifold with Values because Values contains some type erased stuff and type erased stuff sometimes needs some extra work to make it play nicely with everything else.
I'll investigate how to get (2) working.
Your suggestion of fixing this in a separate PR so that we don't block this PR sounds good to me!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in #49!
|
Will review now... |
dellaert
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice progress - talk soon
|
|
||
| import TensorFlow | ||
|
|
||
| public func pinv(_ m: Tensor<Double>) -> Tensor<Double> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document - even when name is obvious. Say what it will compute in different cases.
| /// The most general factor protocol. | ||
| public protocol Factor { | ||
| var keys: Array<Int> { get set } | ||
| var keys: Array<Int> { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need it for direct solvers, to decide on the ordering.
Tests/SwiftFusionTests/Inference/NonlinearFactorGraphTests.swift
Outdated
Show resolved
Hide resolved
|
|
||
| optimizer.optimize(gfg: gfg, initial: &dx) | ||
|
|
||
| for i in 0..<5 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree on both!
| } | ||
| } | ||
|
|
||
| let dumpjson = { (p: Pose2) -> String in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are we printing stuff in a test?
| public struct NonlinearFactorGraph: FactorGraph { | ||
| public typealias KeysType = Array<Int> | ||
|
|
||
| public typealias FactorsType = Array<AnyNonlinearFactor> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You said during our Friday meeting that you still haven't been able to eliminate AnyNonlinearFactor and replace this with Array<NonlinearFactor>.
I figured out why. The associatedtype FactorsType : Collection where FactorsType.Element: Factor in protocol FactorGraph doesn't work when you use Array<NonlinearFactor>, and the reason it doesn't work is that existentials can't conform to protocols. (This is a limitation of the Swift type system that probably won't go away any time soon.)
The easiest way to solve this would be to delete the protocol FactorGraph. Nothing actually relies on abstracting over different FactorGraph implementations yet, so this doesn't break anything.
I think this is worth it because deleting AnyNonlinearFactor makes everything a lot simpler and nicer.
It's also probably possible to redesign the FactorGraph protocol so that it doesn't have a constraint that breaks Array<NonlinearFactor>. I'd suggest writing the FactorGraph protocol later when we have a use case for that, because the use case will make it clearer what protocol FactorGraph needs to have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcrasi Oh that's a great find! I'll remove the protocol for now :)
Thanks for the explanation!
Tests/SwiftFusionTests/Inference/NonlinearFactorGraphTests.swift
Outdated
Show resolved
Hide resolved
I think you should be able to generalize BetweenFactor without introducing a new All you need is a protocol with the methods that protocol LieGroup {
static func * (Self, Self) -> Self
func inverse() -> Self
func localCoordinates(Self) -> LocalCoordinates
}
struct BetweenFactor<T: LieGroup> {
var difference: T
init(_ key1: Int, _ key2: Int, _ difference: T) { ... }
func error(_ value: Value) -> T.LocalCoordinates {
let v1 = values[key1].baseAs(T.self)
let v2 = values[key2].baseAs(T.self)
return difference.localCoordinates(v2.inverse() * v1)
}
}
fg += BetweenFactor(0, 1, Pose2(...))
fg += BetweenFactor(2, 3, Rot2(...)) |
|
Sounds good to me! |
|
Closed as we have #44 |
This adds a nonlinear factor graph interface for SwiftFusion.
Currently only implemented BetweenFactor for Pose2.
TODO:
AnyComposablefor generalizingBetweenFactortoRot2, etc.