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

[WIP] Add LM Pose3Example and G2O 3D Reader #85

Merged
merged 9 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions Examples/Pose3SLAMG2O/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2020 The SwiftFusion Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Loads a g2o file into a factor graph and then runs inference on the factor graph.
///
/// See https://lucacarlone.mit.edu/datasets/ for g2o specification and example datasets.
///
/// Usage: Pose3SLAMG2O [path to .g2o file]
///
/// Missing features:
/// - Does not take g2o information matrix into account.
/// - Does not use a proper general purpose solver.
/// - Has not been compared against other implementations, so it could be wrong.

import Foundation
import SwiftFusion
import TensorFlow

struct FileHandlerOutputStream: TextOutputStream {
private let fileHandle: FileHandle
let encoding: String.Encoding

init(_ fileHandle: FileHandle, encoding: String.Encoding = .utf8) {
self.fileHandle = fileHandle
self.encoding = encoding
}

mutating func write(_ string: String) {
if let data = string.data(using: encoding) {
fileHandle.write(data)
}
}
}

func main() {
// Parse commandline.
guard CommandLine.arguments.count == 3 else {
print("Usage: Pose3SLAMG2O [path to .g2o file] [path to output csv file]")
return
}
let g2oURL = URL(fileURLWithPath: CommandLine.arguments[1])
let fileManager = FileManager.default
let filePath = URL(fileURLWithPath: CommandLine.arguments[2])

print("Storing result at \(filePath.path)")

if fileManager.fileExists(atPath: filePath.path) {
do {
try fileManager.removeItem(atPath: filePath.path)
} catch let error {
print("error occurred, here are the details:\n \(error)")
}
}

// Load .g2o file.
let problem = try! G2OReader.G2ONewFactorGraph(g2oFile3D: g2oURL)

var val = problem.initialGuess
var graph = problem.graph

graph.store(NewPriorFactor3(TypedID(0), Pose3(Rot3.fromTangent(Vector3.zero), Vector3.zero)))

var optimizer = LM()
optimizer.verbosity = .TRYLAMBDA

do {
try optimizer.optimize(graph: graph, initial: &val)
} catch let error {
print("The solver gave up, message: \(error.localizedDescription)")
}

fileManager.createFile(atPath: filePath.path, contents: nil)
let fileHandle = try! FileHandle(forUpdating: URL(fileURLWithPath: filePath.path))
var output = FileHandlerOutputStream(fileHandle)

for i in problem.initialGuessId {
let t = val[i].t
output.write("\(t.x), \(t.y), \(t.z)\n")
}
}

main()
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ let package = Package(
name: "Pose2SLAMG2O",
dependencies: ["SwiftFusion"],
path: "Examples/Pose2SLAMG2O"),
.target(
name: "Pose3SLAMG2O",
dependencies: ["SwiftFusion"],
path: "Examples/Pose3SLAMG2O"),
.testTarget(
name: "SwiftFusionTests",
dependencies: ["SwiftFusion"]),
Expand Down
44 changes: 36 additions & 8 deletions Sources/Benchmarks/Pose3SLAM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ import Benchmark
import SwiftFusion

let pose3SLAM = BenchmarkSuite(name: "Pose3SLAM") { suite in

var gridDataset =
var gridDataset_old =
try! G2OReader.G2ONonlinearFactorGraph(g2oFile3D: try! cachedDataset("pose3example.txt"))
// check(gridDataset.graph.error(gridDataset.initialGuess), near: 12.99, accuracy: 1e-2)

// check(gridDataset_old.graph.error(gridDataset_old.initialGuess), near: 12.99, accuracy: 1e-2)
// Uses `NonlinearFactorGraph` on the Intel dataset.
// The solvers are configured to run for a constant number of steps.
// The nonlinear solver is 5 iterations of Gauss-Newton.
Expand All @@ -31,11 +30,11 @@ let pose3SLAM = BenchmarkSuite(name: "Pose3SLAM") { suite in
"NonlinearFactorGraph, Pose3Example, 50 Gauss-Newton steps, 200 CGLS steps",
settings: Iterations(1)
) {
var val = gridDataset.initialGuess
gridDataset.graph += PriorFactor(0, Pose3())
var val = gridDataset_old.initialGuess
gridDataset_old.graph += PriorFactor(0, Pose3())
for _ in 0..<40 {
print("error = \(gridDataset.graph.error(val))")
let gfg = gridDataset.graph.linearize(val)
print("error = \(gridDataset_old.graph.error(val))")
let gfg = gridDataset_old.graph.linearize(val)
let optimizer = CGLS(precision: 0, max_iteration: 200)
var dx = VectorValues()
for i in 0..<val.count {
Expand All @@ -49,4 +48,33 @@ let pose3SLAM = BenchmarkSuite(name: "Pose3SLAM") { suite in
print(val[i, as: Pose3.self].t)
}
}

var gridDataset = try! G2OReader.G2ONewFactorGraph(g2oFile3D: try! cachedDataset("pose3example.txt"))
// try! G2OReader.G2ONewFactorGraph(g2oFile3D: try! cachedDataset("sphere2500.g2o"))
// check(gridDataset.graph.error(gridDataset.initialGuess), near: 12.99, accuracy: 1e-2)

// Uses `NonlinearFactorGraph` on the Intel dataset.
// The solvers are configured to run for a constant number of steps.
// The nonlinear solver is 5 iterations of Gauss-Newton.
// The linear solver is 100 iterations of CGLS.
ProfFan marked this conversation as resolved.
Show resolved Hide resolved
suite.benchmark(
"NewFactorGraph, sphere2500, 30 LM steps, 200 CGLS steps",
settings: Iterations(1)
) {
var val = gridDataset.initialGuess
var graph = gridDataset.graph

graph.store(NewPriorFactor3(TypedID(0), Pose3(Rot3.fromTangent(Vector3.zero), Vector3.zero)))

var optimizer = LM()
optimizer.verbosity = .SUMMARY
optimizer.max_iteration = 30
optimizer.max_inner_iteration = 200

do {
try optimizer.optimize(graph: graph, initial: &val)
} catch let error {
print("The solver gave up, message: \(error.localizedDescription)")
}
}
}
6 changes: 5 additions & 1 deletion Sources/SwiftFusion/Datasets/DatasetCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ fileprivate let datasets = [
"sphere_bignoise_vertex3.g2o":
"https://github.com/HeYijia/GraphSLAM_tutorials_code/raw/master/g2o_test/data/sphere_bignoise_vertex3.g2o",
"pose3example.txt":
"https://github.com/borglab/gtsam/raw/master/examples/Data/pose3example.txt"
"https://github.com/borglab/gtsam/raw/master/examples/Data/pose3example.txt",
"parking-garage.g2o":
"https://github.com/david-m-rosen/SE-Sync/raw/master/data/parking-garage.g2o",
"sphere2500.g2o":
"https://github.com/david-m-rosen/SE-Sync/raw/master/data/sphere2500.g2o"
]

/// Returns a dataset cached on the local system.
Expand Down
21 changes: 19 additions & 2 deletions Sources/SwiftFusion/Datasets/G2OReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,42 @@ public enum G2OReader {
}

/// A G2O problem expressed as a `NewFactorGraph`.
public struct G2ONewFactorGraph {
public struct G2ONewFactorGraph<Pose: LieGroup> {
/// The initial guess.
public var initialGuess = VariableAssignments()

public var initialGuessId = Array<TypedID<Pose, Int>>()
ProfFan marked this conversation as resolved.
Show resolved Hide resolved

/// The factor graph representing the measurements.
public var graph = NewFactorGraph()

/// Creates a problem from the given 2D file.
public init(g2oFile2D: URL) throws {
public init(g2oFile2D: URL) throws where Pose == Pose2 {
try G2OReader.read2D(file: g2oFile2D) { entry in
switch entry {
case .initialGuess(index: let id, pose: let guess):
let typedID = initialGuess.store(guess)
assert(typedID.perTypeID == id)
initialGuessId.append(typedID)
case .measurement(frameIndex: let id1, measuredIndex: let id2, pose: let difference):
graph.store(NewBetweenFactor2(TypedID(id1), TypedID(id2), difference))
}
}
}

/// Creates a problem from the given 2D file.
ProfFan marked this conversation as resolved.
Show resolved Hide resolved
public init(g2oFile3D: URL) throws where Pose == Pose3 {
try G2OReader.read3D(file: g2oFile3D) { entry in
switch entry {
case .initialGuess(index: let id, pose: let guess):
let typedID = initialGuess.store(guess)
assert(typedID.perTypeID == id)
initialGuessId.append(typedID)
case .measurement(frameIndex: let id1, measuredIndex: let id2, pose: let difference):
graph.store(NewBetweenFactor3(TypedID(id1), TypedID(id2), difference))
}
}
}
}

/// An entry in a G2O file.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFusion/Inference/NewFactorGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public struct NewFactorGraph {
}

/// Returns the error vectors, at `x`, of all the linearizable factors.
func errorVectors(at x: VariableAssignments) -> AllVectors {
public func errorVectors(at x: VariableAssignments) -> AllVectors {
return AllVectors(storage: storage.compactMapValues { factors in
guard let linearizableFactors = factors.cast(to: AnyLinearizableFactorStorage.self) else {
return nil
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFusion/Inference/NewGaussianFactorGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public struct NewGaussianFactorGraph {
}

/// Returns the error vectors, at `x`, of all the factors.
func errorVectors(at x: AllVectors) -> AllVectors {
public func errorVectors(at x: AllVectors) -> AllVectors {
return AllVectors(storage: storage.mapValues { factors in
AnyArrayBuffer(factors.errorVectors(at: x))
})
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFusion/Inference/NewJacobianFactor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public struct NewJacobianFactor<
}

public func errorVector(at x: Variables) -> ErrorVector {
return errorVector_linearComponent(x) + error
return error - errorVector_linearComponent(x)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm pretty confused by the signs.

I see that we need to calculate b - Ax, but I'm not sure if errorVector should be the method that does this.

Changing errorVector to b - Ax changes the meaning of linearization (the comment on NewFactorGraph.linearized implies that errorVector should be Ax + b).

Also, changing errorVector to b - Ax makes it so that errorVector_linearComponent isn't actually the linear component of errorVector and more.

Maybe we should make a new method called residual that calculates b - Ax?

Or we could negate the implementations of errorVector_linearComponent and errorVector_linearComponent_adjoint, and leave errorVector(at: x) = errorVector_linearComponent(x) + error. Then we'd also have to update the comment on NewFactorGraph.linearized to say:

  /// The linear approximation satisfies the approximate equality:
  ///   `self.linearized(at: x).errorVectors(at: dx)` ≈ `self.errorVectors(at: x.moved(along: -1 * dx))`
  /// where the equality is exact when `dx == x.linearizedZero`.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea I feel a bit confused too :( IMHO, in CGLS the objective is to reach the solution where Ax=b, thus by moving Ax to the right we have 0 = b - Ax(the "error"). In GN we approximate the L2 error function (which should be >=0) f(x) by its linearization at x0, where f(dx+x_0)=Ax+b (Jacobian A, bias b), and we arrived at the same equation as in CGLS.

I think error here is confusing as it is actually the "error of error", as what we linearized to the JacobianFactor IS the error. However, I am not sure if changing this is actually a good idea, as if we want linear factors to be nonlinear compatible we need the protocol conformances.

Copy link
Member

Choose a reason for hiding this comment

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

nonlinear error: |h(x)-z|
linear error |Ax-b|

}

public func errorVector_linearComponent(_ x: Variables) -> ErrorVector {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftFusion/Optimizers/CGLS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class CGLS {
var gamma = s.norm // γ(0) = ||s(0)||^2

while step < max_iteration {
// print("[CGLS ] residual = \(r.norm), true = \(gfg.residual(x).norm)")
let q = gfg * p // q(k) = A * p(k)
let alpha: Double = gamma / q.norm // α(k) = γ(k)/||q(k)||^2
x = x + (alpha * p) // x(k+1) = x(k) + α(k) * p(k)
Expand Down Expand Up @@ -93,6 +94,7 @@ public struct GenericCGLS {
var gamma = s.squaredNorm // γ(0) = ||s(0)||^2

while step < max_iteration {
// print("[CGLS ] residual = \(r.squaredNorm), true = \(gfg.errorVectors(at: x).squaredNorm)")
let q = gfg.errorVectors_linearComponent(at: p) // q(k) = A * p(k)
let alpha: Double = gamma / q.squaredNorm // α(k) = γ(k)/||q(k)||^2
x = x + (alpha * p) // x(k+1) = x(k) + α(k) * p(k)
Expand Down
Loading