From c659336cf8744fa435d6cbd830894cb0895dd847 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 30 Dec 2021 12:12:54 -0500 Subject: [PATCH 01/91] Removed shared_ptr from Bayes nets and factor graphs --- gtsam/linear/SubgraphBuilder.cpp | 21 ++++----- gtsam/linear/SubgraphBuilder.h | 9 ++-- gtsam/linear/SubgraphPreconditioner.cpp | 50 ++++++++++---------- gtsam/linear/SubgraphPreconditioner.h | 25 +++++----- gtsam/linear/SubgraphSolver.cpp | 18 ++++---- gtsam/linear/SubgraphSolver.h | 11 ++--- tests/smallExample.h | 15 +++--- tests/testSubgraphPreconditioner.cpp | 61 ++++++++++++------------- tests/testSubgraphSolver.cpp | 14 +++--- 9 files changed, 106 insertions(+), 118 deletions(-) diff --git a/gtsam/linear/SubgraphBuilder.cpp b/gtsam/linear/SubgraphBuilder.cpp index 1919d38bec..18e19cd20d 100644 --- a/gtsam/linear/SubgraphBuilder.cpp +++ b/gtsam/linear/SubgraphBuilder.cpp @@ -446,30 +446,29 @@ SubgraphBuilder::Weights SubgraphBuilder::weights( } /*****************************************************************************/ -GaussianFactorGraph::shared_ptr buildFactorSubgraph( - const GaussianFactorGraph &gfg, const Subgraph &subgraph, - const bool clone) { - auto subgraphFactors = boost::make_shared(); - subgraphFactors->reserve(subgraph.size()); +GaussianFactorGraph buildFactorSubgraph(const GaussianFactorGraph &gfg, + const Subgraph &subgraph, + const bool clone) { + GaussianFactorGraph subgraphFactors; + subgraphFactors.reserve(subgraph.size()); for (const auto &e : subgraph) { const auto factor = gfg[e.index]; - subgraphFactors->push_back(clone ? factor->clone() : factor); + subgraphFactors.push_back(clone ? factor->clone() : factor); } return subgraphFactors; } /**************************************************************************************************/ -std::pair // -splitFactorGraph(const GaussianFactorGraph &factorGraph, - const Subgraph &subgraph) { +std::pair splitFactorGraph( + const GaussianFactorGraph &factorGraph, const Subgraph &subgraph) { // Get the subgraph by calling cheaper method auto subgraphFactors = buildFactorSubgraph(factorGraph, subgraph, false); // Now, copy all factors then set subGraph factors to zero - auto remaining = boost::make_shared(factorGraph); + GaussianFactorGraph remaining = factorGraph; for (const auto &e : subgraph) { - remaining->remove(e.index); + remaining.remove(e.index); } return std::make_pair(subgraphFactors, remaining); diff --git a/gtsam/linear/SubgraphBuilder.h b/gtsam/linear/SubgraphBuilder.h index 84a477a5e0..a900c7531a 100644 --- a/gtsam/linear/SubgraphBuilder.h +++ b/gtsam/linear/SubgraphBuilder.h @@ -172,12 +172,13 @@ class GTSAM_EXPORT SubgraphBuilder { }; /** Select the factors in a factor graph according to the subgraph. */ -boost::shared_ptr buildFactorSubgraph( - const GaussianFactorGraph &gfg, const Subgraph &subgraph, const bool clone); +GaussianFactorGraph buildFactorSubgraph(const GaussianFactorGraph &gfg, + const Subgraph &subgraph, + const bool clone); /** Split the graph into a subgraph and the remaining edges. * Note that the remaining factorgraph has null factors. */ -std::pair, boost::shared_ptr > -splitFactorGraph(const GaussianFactorGraph &factorGraph, const Subgraph &subgraph); +std::pair splitFactorGraph( + const GaussianFactorGraph &factorGraph, const Subgraph &subgraph); } // namespace gtsam diff --git a/gtsam/linear/SubgraphPreconditioner.cpp b/gtsam/linear/SubgraphPreconditioner.cpp index 215200818d..3eb3f45766 100644 --- a/gtsam/linear/SubgraphPreconditioner.cpp +++ b/gtsam/linear/SubgraphPreconditioner.cpp @@ -77,16 +77,16 @@ static void setSubvector(const Vector &src, const KeyInfo &keyInfo, /* ************************************************************************* */ // Convert any non-Jacobian factors to Jacobians (e.g. Hessian -> Jacobian with // Cholesky) -static GaussianFactorGraph::shared_ptr convertToJacobianFactors( +static GaussianFactorGraph convertToJacobianFactors( const GaussianFactorGraph &gfg) { - auto result = boost::make_shared(); + GaussianFactorGraph result; for (const auto &factor : gfg) if (factor) { auto jf = boost::dynamic_pointer_cast(factor); if (!jf) { jf = boost::make_shared(*factor); } - result->push_back(jf); + result.push_back(jf); } return result; } @@ -96,42 +96,42 @@ SubgraphPreconditioner::SubgraphPreconditioner(const SubgraphPreconditionerParam parameters_(p) {} /* ************************************************************************* */ -SubgraphPreconditioner::SubgraphPreconditioner(const sharedFG& Ab2, - const sharedBayesNet& Rc1, const sharedValues& xbar, const SubgraphPreconditionerParameters &p) : - Ab2_(convertToJacobianFactors(*Ab2)), Rc1_(Rc1), xbar_(xbar), - b2bar_(new Errors(-Ab2_->gaussianErrors(*xbar))), parameters_(p) { +SubgraphPreconditioner::SubgraphPreconditioner(const GaussianFactorGraph& Ab2, + const GaussianBayesNet& Rc1, const VectorValues& xbar, const SubgraphPreconditionerParameters &p) : + Ab2_(convertToJacobianFactors(Ab2)), Rc1_(Rc1), xbar_(xbar), + b2bar_(-Ab2_.gaussianErrors(xbar)), parameters_(p) { } /* ************************************************************************* */ // x = xbar + inv(R1)*y VectorValues SubgraphPreconditioner::x(const VectorValues& y) const { - return *xbar_ + Rc1_->backSubstitute(y); + return xbar_ + Rc1_.backSubstitute(y); } /* ************************************************************************* */ double SubgraphPreconditioner::error(const VectorValues& y) const { Errors e(y); VectorValues x = this->x(y); - Errors e2 = Ab2()->gaussianErrors(x); + Errors e2 = Ab2_.gaussianErrors(x); return 0.5 * (dot(e, e) + dot(e2,e2)); } /* ************************************************************************* */ // gradient is y + inv(R1')*A2'*(A2*inv(R1)*y-b2bar), VectorValues SubgraphPreconditioner::gradient(const VectorValues &y) const { - VectorValues x = Rc1()->backSubstitute(y); /* inv(R1)*y */ - Errors e = (*Ab2() * x - *b2bar()); /* (A2*inv(R1)*y-b2bar) */ + VectorValues x = Rc1_.backSubstitute(y); /* inv(R1)*y */ + Errors e = Ab2_ * x - b2bar_; /* (A2*inv(R1)*y-b2bar) */ VectorValues v = VectorValues::Zero(x); - Ab2()->transposeMultiplyAdd(1.0, e, v); /* A2'*(A2*inv(R1)*y-b2bar) */ - return y + Rc1()->backSubstituteTranspose(v); + Ab2_.transposeMultiplyAdd(1.0, e, v); /* A2'*(A2*inv(R1)*y-b2bar) */ + return y + Rc1_.backSubstituteTranspose(v); } /* ************************************************************************* */ // Apply operator A, A*y = [I;A2*inv(R1)]*y = [y; A2*inv(R1)*y] -Errors SubgraphPreconditioner::operator*(const VectorValues& y) const { +Errors SubgraphPreconditioner::operator*(const VectorValues &y) const { Errors e(y); - VectorValues x = Rc1()->backSubstitute(y); /* x=inv(R1)*y */ - Errors e2 = *Ab2() * x; /* A2*x */ + VectorValues x = Rc1_.backSubstitute(y); /* x=inv(R1)*y */ + Errors e2 = Ab2_ * x; /* A2*x */ e.splice(e.end(), e2); return e; } @@ -147,8 +147,8 @@ void SubgraphPreconditioner::multiplyInPlace(const VectorValues& y, Errors& e) c } // Add A2 contribution - VectorValues x = Rc1()->backSubstitute(y); // x=inv(R1)*y - Ab2()->multiplyInPlace(x, ei); // use iterator version + VectorValues x = Rc1_.backSubstitute(y); // x=inv(R1)*y + Ab2_.multiplyInPlace(x, ei); // use iterator version } /* ************************************************************************* */ @@ -190,14 +190,14 @@ void SubgraphPreconditioner::transposeMultiplyAdd2 (double alpha, while (it != end) e2.push_back(*(it++)); VectorValues x = VectorValues::Zero(y); // x = 0 - Ab2_->transposeMultiplyAdd(1.0,e2,x); // x += A2'*e2 - y += alpha * Rc1_->backSubstituteTranspose(x); // y += alpha*inv(R1')*x + Ab2_.transposeMultiplyAdd(1.0,e2,x); // x += A2'*e2 + y += alpha * Rc1_.backSubstituteTranspose(x); // y += alpha*inv(R1')*x } /* ************************************************************************* */ void SubgraphPreconditioner::print(const std::string& s) const { cout << s << endl; - Ab2_->print(); + Ab2_.print(); } /*****************************************************************************/ @@ -205,7 +205,7 @@ void SubgraphPreconditioner::solve(const Vector &y, Vector &x) const { assert(x.size() == y.size()); /* back substitute */ - for (const auto &cg : boost::adaptors::reverse(*Rc1_)) { + for (const auto &cg : boost::adaptors::reverse(Rc1_)) { /* collect a subvector of x that consists of the parents of cg (S) */ const KeyVector parentKeys(cg->beginParents(), cg->endParents()); const KeyVector frontalKeys(cg->beginFrontals(), cg->endFrontals()); @@ -228,7 +228,7 @@ void SubgraphPreconditioner::transposeSolve(const Vector &y, Vector &x) const { std::copy(y.data(), y.data() + y.rows(), x.data()); /* in place back substitute */ - for (const auto &cg : *Rc1_) { + for (const auto &cg : Rc1_) { const KeyVector frontalKeys(cg->beginFrontals(), cg->endFrontals()); const Vector rhsFrontal = getSubvector(x, keyInfo_, frontalKeys); const Vector solFrontal = @@ -261,10 +261,10 @@ void SubgraphPreconditioner::build(const GaussianFactorGraph &gfg, const KeyInfo keyInfo_ = keyInfo; /* build factor subgraph */ - GaussianFactorGraph::shared_ptr gfg_subgraph = buildFactorSubgraph(gfg, subgraph, true); + auto gfg_subgraph = buildFactorSubgraph(gfg, subgraph, true); /* factorize and cache BayesNet */ - Rc1_ = gfg_subgraph->eliminateSequential(); + Rc1_ = gfg_subgraph.eliminateSequential(); } /*****************************************************************************/ diff --git a/gtsam/linear/SubgraphPreconditioner.h b/gtsam/linear/SubgraphPreconditioner.h index 681c12e401..81c8968b16 100644 --- a/gtsam/linear/SubgraphPreconditioner.h +++ b/gtsam/linear/SubgraphPreconditioner.h @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include #include @@ -53,16 +55,12 @@ namespace gtsam { public: typedef boost::shared_ptr shared_ptr; - typedef boost::shared_ptr sharedBayesNet; - typedef boost::shared_ptr sharedFG; - typedef boost::shared_ptr sharedValues; - typedef boost::shared_ptr sharedErrors; private: - sharedFG Ab2_; - sharedBayesNet Rc1_; - sharedValues xbar_; ///< A1 \ b1 - sharedErrors b2bar_; ///< A2*xbar - b2 + GaussianFactorGraph Ab2_; + GaussianBayesNet Rc1_; + VectorValues xbar_; ///< A1 \ b1 + Errors b2bar_; ///< A2*xbar - b2 KeyInfo keyInfo_; SubgraphPreconditionerParameters parameters_; @@ -77,7 +75,7 @@ namespace gtsam { * @param Rc1: the Bayes Net R1*x=c1 * @param xbar: the solution to R1*x=c1 */ - SubgraphPreconditioner(const sharedFG& Ab2, const sharedBayesNet& Rc1, const sharedValues& xbar, + SubgraphPreconditioner(const GaussianFactorGraph& Ab2, const GaussianBayesNet& Rc1, const VectorValues& xbar, const SubgraphPreconditionerParameters &p = SubgraphPreconditionerParameters()); ~SubgraphPreconditioner() override {} @@ -86,13 +84,13 @@ namespace gtsam { void print(const std::string& s = "SubgraphPreconditioner") const; /** Access Ab2 */ - const sharedFG& Ab2() const { return Ab2_; } + const GaussianFactorGraph& Ab2() const { return Ab2_; } /** Access Rc1 */ - const sharedBayesNet& Rc1() const { return Rc1_; } + const GaussianBayesNet& Rc1() const { return Rc1_; } /** Access b2bar */ - const sharedErrors b2bar() const { return b2bar_; } + const Errors b2bar() const { return b2bar_; } /** * Add zero-mean i.i.d. Gaussian prior terms to each variable @@ -104,8 +102,7 @@ namespace gtsam { /* A zero VectorValues with the structure of xbar */ VectorValues zero() const { - assert(xbar_); - return VectorValues::Zero(*xbar_); + return VectorValues::Zero(xbar_); } /** diff --git a/gtsam/linear/SubgraphSolver.cpp b/gtsam/linear/SubgraphSolver.cpp index f49f9a135f..9de630dc22 100644 --- a/gtsam/linear/SubgraphSolver.cpp +++ b/gtsam/linear/SubgraphSolver.cpp @@ -34,24 +34,24 @@ namespace gtsam { SubgraphSolver::SubgraphSolver(const GaussianFactorGraph &Ab, const Parameters ¶meters, const Ordering& ordering) : parameters_(parameters) { - GaussianFactorGraph::shared_ptr Ab1,Ab2; + GaussianFactorGraph Ab1, Ab2; std::tie(Ab1, Ab2) = splitGraph(Ab); if (parameters_.verbosity()) - cout << "Split A into (A1) " << Ab1->size() << " and (A2) " << Ab2->size() + cout << "Split A into (A1) " << Ab1.size() << " and (A2) " << Ab2.size() << " factors" << endl; - auto Rc1 = Ab1->eliminateSequential(ordering, EliminateQR); - auto xbar = boost::make_shared(Rc1->optimize()); + auto Rc1 = Ab1.eliminateSequential(ordering, EliminateQR); + auto xbar = Rc1.optimize(); pc_ = boost::make_shared(Ab2, Rc1, xbar); } /**************************************************************************************************/ // Taking eliminated tree [R1|c] and constraint graph [A2|b2] -SubgraphSolver::SubgraphSolver(const GaussianBayesNet::shared_ptr &Rc1, - const GaussianFactorGraph::shared_ptr &Ab2, +SubgraphSolver::SubgraphSolver(const GaussianBayesNet &Rc1, + const GaussianFactorGraph &Ab2, const Parameters ¶meters) : parameters_(parameters) { - auto xbar = boost::make_shared(Rc1->optimize()); + auto xbar = Rc1.optimize(); pc_ = boost::make_shared(Ab2, Rc1, xbar); } @@ -59,7 +59,7 @@ SubgraphSolver::SubgraphSolver(const GaussianBayesNet::shared_ptr &Rc1, // Taking subgraphs [A1|b1] and [A2|b2] // delegate up SubgraphSolver::SubgraphSolver(const GaussianFactorGraph &Ab1, - const GaussianFactorGraph::shared_ptr &Ab2, + const GaussianFactorGraph &Ab2, const Parameters ¶meters, const Ordering &ordering) : SubgraphSolver(Ab1.eliminateSequential(ordering, EliminateQR), Ab2, @@ -78,7 +78,7 @@ VectorValues SubgraphSolver::optimize(const GaussianFactorGraph &gfg, return VectorValues(); } /**************************************************************************************************/ -pair // +pair // SubgraphSolver::splitGraph(const GaussianFactorGraph &factorGraph) { /* identify the subgraph structure */ diff --git a/gtsam/linear/SubgraphSolver.h b/gtsam/linear/SubgraphSolver.h index a417383215..0598b33212 100644 --- a/gtsam/linear/SubgraphSolver.h +++ b/gtsam/linear/SubgraphSolver.h @@ -99,15 +99,13 @@ class GTSAM_EXPORT SubgraphSolver : public IterativeSolver { * eliminate Ab1. We take Ab1 as a const reference, as it will be transformed * into Rc1, but take Ab2 as a shared pointer as we need to keep it around. */ - SubgraphSolver(const GaussianFactorGraph &Ab1, - const boost::shared_ptr &Ab2, + SubgraphSolver(const GaussianFactorGraph &Ab1, const GaussianFactorGraph &Ab2, const Parameters ¶meters, const Ordering &ordering); /** * The same as above, but we assume A1 was solved by caller. * We take two shared pointers as we keep both around. */ - SubgraphSolver(const boost::shared_ptr &Rc1, - const boost::shared_ptr &Ab2, + SubgraphSolver(const GaussianBayesNet &Rc1, const GaussianFactorGraph &Ab2, const Parameters ¶meters); /// Destructor @@ -131,9 +129,8 @@ class GTSAM_EXPORT SubgraphSolver : public IterativeSolver { /// @{ /// Split graph using Kruskal algorithm, treating binary factors as edges. - std::pair < boost::shared_ptr, - boost::shared_ptr > splitGraph( - const GaussianFactorGraph &gfg); + std::pair splitGraph( + const GaussianFactorGraph &gfg); /// @} }; diff --git a/tests/smallExample.h b/tests/smallExample.h index 944899e701..ca9a8580fc 100644 --- a/tests/smallExample.h +++ b/tests/smallExample.h @@ -679,26 +679,25 @@ inline Ordering planarOrdering(size_t N) { } /* ************************************************************************* */ -inline std::pair splitOffPlanarTree(size_t N, - const GaussianFactorGraph& original) { - auto T = boost::make_shared(), C= boost::make_shared(); +inline std::pair splitOffPlanarTree( + size_t N, const GaussianFactorGraph& original) { + GaussianFactorGraph T, C; // Add the x11 constraint to the tree - T->push_back(original[0]); + T.push_back(original[0]); // Add all horizontal constraints to the tree size_t i = 1; for (size_t x = 1; x < N; x++) - for (size_t y = 1; y <= N; y++, i++) - T->push_back(original[i]); + for (size_t y = 1; y <= N; y++, i++) T.push_back(original[i]); // Add first vertical column of constraints to T, others to C for (size_t x = 1; x <= N; x++) for (size_t y = 1; y < N; y++, i++) if (x == 1) - T->push_back(original[i]); + T.push_back(original[i]); else - C->push_back(original[i]); + C.push_back(original[i]); return std::make_pair(T, C); } diff --git a/tests/testSubgraphPreconditioner.cpp b/tests/testSubgraphPreconditioner.cpp index 84ccc131ac..534ef2f97e 100644 --- a/tests/testSubgraphPreconditioner.cpp +++ b/tests/testSubgraphPreconditioner.cpp @@ -77,8 +77,8 @@ TEST(SubgraphPreconditioner, planarGraph) { DOUBLES_EQUAL(0, error(A, xtrue), 1e-9); // check zero error for xtrue // Check that xtrue is optimal - GaussianBayesNet::shared_ptr R1 = A.eliminateSequential(); - VectorValues actual = R1->optimize(); + GaussianBayesNet R1 = A.eliminateSequential(); + VectorValues actual = R1.optimize(); EXPECT(assert_equal(xtrue, actual)); } @@ -90,14 +90,14 @@ TEST(SubgraphPreconditioner, splitOffPlanarTree) { boost::tie(A, xtrue) = planarGraph(3); // Get the spanning tree and constraints, and check their sizes - GaussianFactorGraph::shared_ptr T, C; + GaussianFactorGraph T, C; boost::tie(T, C) = splitOffPlanarTree(3, A); - LONGS_EQUAL(9, T->size()); - LONGS_EQUAL(4, C->size()); + LONGS_EQUAL(9, T.size()); + LONGS_EQUAL(4, C.size()); // Check that the tree can be solved to give the ground xtrue - GaussianBayesNet::shared_ptr R1 = T->eliminateSequential(); - VectorValues xbar = R1->optimize(); + GaussianBayesNet R1 = T.eliminateSequential(); + VectorValues xbar = R1.optimize(); EXPECT(assert_equal(xtrue, xbar)); } @@ -110,31 +110,29 @@ TEST(SubgraphPreconditioner, system) { boost::tie(Ab, xtrue) = planarGraph(N); // A*x-b // Get the spanning tree and remaining graph - GaussianFactorGraph::shared_ptr Ab1, Ab2; // A1*x-b1 and A2*x-b2 + GaussianFactorGraph Ab1, Ab2; // A1*x-b1 and A2*x-b2 boost::tie(Ab1, Ab2) = splitOffPlanarTree(N, Ab); // Eliminate the spanning tree to build a prior const Ordering ord = planarOrdering(N); - auto Rc1 = Ab1->eliminateSequential(ord); // R1*x-c1 - VectorValues xbar = Rc1->optimize(); // xbar = inv(R1)*c1 + auto Rc1 = Ab1.eliminateSequential(ord); // R1*x-c1 + VectorValues xbar = Rc1.optimize(); // xbar = inv(R1)*c1 // Create Subgraph-preconditioned system - VectorValues::shared_ptr xbarShared( - new VectorValues(xbar)); // TODO: horrible - const SubgraphPreconditioner system(Ab2, Rc1, xbarShared); + const SubgraphPreconditioner system(Ab2, Rc1, xbar); // Get corresponding matrices for tests. Add dummy factors to Ab2 to make // sure it works with the ordering. - Ordering ordering = Rc1->ordering(); // not ord in general! - Ab2->add(key(1, 1), Z_2x2, Z_2x1); - Ab2->add(key(1, 2), Z_2x2, Z_2x1); - Ab2->add(key(1, 3), Z_2x2, Z_2x1); + Ordering ordering = Rc1.ordering(); // not ord in general! + Ab2.add(key(1, 1), Z_2x2, Z_2x1); + Ab2.add(key(1, 2), Z_2x2, Z_2x1); + Ab2.add(key(1, 3), Z_2x2, Z_2x1); Matrix A, A1, A2; Vector b, b1, b2; std::tie(A, b) = Ab.jacobian(ordering); - std::tie(A1, b1) = Ab1->jacobian(ordering); - std::tie(A2, b2) = Ab2->jacobian(ordering); - Matrix R1 = Rc1->matrix(ordering).first; + std::tie(A1, b1) = Ab1.jacobian(ordering); + std::tie(A2, b2) = Ab2.jacobian(ordering); + Matrix R1 = Rc1.matrix(ordering).first; Matrix Abar(13 * 2, 9 * 2); Abar.topRows(9 * 2) = Matrix::Identity(9 * 2, 9 * 2); Abar.bottomRows(8) = A2.topRows(8) * R1.inverse(); @@ -151,7 +149,7 @@ TEST(SubgraphPreconditioner, system) { y1[key(3, 3)] = Vector2(1.0, -1.0); // Check backSubstituteTranspose works with R1 - VectorValues actual = Rc1->backSubstituteTranspose(y1); + VectorValues actual = Rc1.backSubstituteTranspose(y1); Vector expected = R1.transpose().inverse() * vec(y1); EXPECT(assert_equal(expected, vec(actual))); @@ -230,7 +228,7 @@ TEST(SubgraphSolver, Solves) { system.build(Ab, keyInfo, lambda); // Create a perturbed (non-zero) RHS - const auto xbar = system.Rc1()->optimize(); // merely for use in zero below + const auto xbar = system.Rc1().optimize(); // merely for use in zero below auto values_y = VectorValues::Zero(xbar); auto it = values_y.begin(); it->second.setConstant(100); @@ -238,13 +236,13 @@ TEST(SubgraphSolver, Solves) { it->second.setConstant(-100); // Solve the VectorValues way - auto values_x = system.Rc1()->backSubstitute(values_y); + auto values_x = system.Rc1().backSubstitute(values_y); // Solve the matrix way, this really just checks BN::backSubstitute // This only works with Rc1 ordering, not with keyInfo ! // TODO(frank): why does this not work with an arbitrary ordering? - const auto ord = system.Rc1()->ordering(); - const Matrix R1 = system.Rc1()->matrix(ord).first; + const auto ord = system.Rc1().ordering(); + const Matrix R1 = system.Rc1().matrix(ord).first; auto ord_y = values_y.vector(ord); auto vector_x = R1.inverse() * ord_y; EXPECT(assert_equal(vector_x, values_x.vector(ord))); @@ -261,7 +259,7 @@ TEST(SubgraphSolver, Solves) { // Test that transposeSolve does implement x = R^{-T} y // We do this by asserting it gives same answer as backSubstituteTranspose - auto values_x2 = system.Rc1()->backSubstituteTranspose(values_y); + auto values_x2 = system.Rc1().backSubstituteTranspose(values_y); Vector solveT_x = Vector::Zero(N); system.transposeSolve(vector_y, solveT_x); EXPECT(assert_equal(values_x2.vector(ordering), solveT_x)); @@ -277,18 +275,15 @@ TEST(SubgraphPreconditioner, conjugateGradients) { boost::tie(Ab, xtrue) = planarGraph(N); // A*x-b // Get the spanning tree - GaussianFactorGraph::shared_ptr Ab1, Ab2; // A1*x-b1 and A2*x-b2 + GaussianFactorGraph Ab1, Ab2; // A1*x-b1 and A2*x-b2 boost::tie(Ab1, Ab2) = splitOffPlanarTree(N, Ab); // Eliminate the spanning tree to build a prior - SubgraphPreconditioner::sharedBayesNet Rc1 = - Ab1->eliminateSequential(); // R1*x-c1 - VectorValues xbar = Rc1->optimize(); // xbar = inv(R1)*c1 + GaussianBayesNet Rc1 = Ab1.eliminateSequential(); // R1*x-c1 + VectorValues xbar = Rc1.optimize(); // xbar = inv(R1)*c1 // Create Subgraph-preconditioned system - VectorValues::shared_ptr xbarShared( - new VectorValues(xbar)); // TODO: horrible - SubgraphPreconditioner system(Ab2, Rc1, xbarShared); + SubgraphPreconditioner system(Ab2, Rc1, xbar); // Create zero config y0 and perturbed config y1 VectorValues y0 = VectorValues::Zero(xbar); diff --git a/tests/testSubgraphSolver.cpp b/tests/testSubgraphSolver.cpp index cca13c822c..336e01b9b9 100644 --- a/tests/testSubgraphSolver.cpp +++ b/tests/testSubgraphSolver.cpp @@ -68,10 +68,10 @@ TEST( SubgraphSolver, splitFactorGraph ) auto subgraph = builder(Ab); EXPECT_LONGS_EQUAL(9, subgraph.size()); - GaussianFactorGraph::shared_ptr Ab1, Ab2; + GaussianFactorGraph Ab1, Ab2; std::tie(Ab1, Ab2) = splitFactorGraph(Ab, subgraph); - EXPECT_LONGS_EQUAL(9, Ab1->size()); - EXPECT_LONGS_EQUAL(13, Ab2->size()); + EXPECT_LONGS_EQUAL(9, Ab1.size()); + EXPECT_LONGS_EQUAL(13, Ab2.size()); } /* ************************************************************************* */ @@ -99,12 +99,12 @@ TEST( SubgraphSolver, constructor2 ) std::tie(Ab, xtrue) = example::planarGraph(N); // A*x-b // Get the spanning tree - GaussianFactorGraph::shared_ptr Ab1, Ab2; // A1*x-b1 and A2*x-b2 + GaussianFactorGraph Ab1, Ab2; // A1*x-b1 and A2*x-b2 std::tie(Ab1, Ab2) = example::splitOffPlanarTree(N, Ab); // The second constructor takes two factor graphs, so the caller can specify // the preconditioner (Ab1) and the constraints that are left out (Ab2) - SubgraphSolver solver(*Ab1, Ab2, kParameters, kOrdering); + SubgraphSolver solver(Ab1, Ab2, kParameters, kOrdering); VectorValues optimized = solver.optimize(); DOUBLES_EQUAL(0.0, error(Ab, optimized), 1e-5); } @@ -119,11 +119,11 @@ TEST( SubgraphSolver, constructor3 ) std::tie(Ab, xtrue) = example::planarGraph(N); // A*x-b // Get the spanning tree and corresponding kOrdering - GaussianFactorGraph::shared_ptr Ab1, Ab2; // A1*x-b1 and A2*x-b2 + GaussianFactorGraph Ab1, Ab2; // A1*x-b1 and A2*x-b2 std::tie(Ab1, Ab2) = example::splitOffPlanarTree(N, Ab); // The caller solves |A1*x-b1|^2 == |R1*x-c1|^2, where R1 is square UT - auto Rc1 = Ab1->eliminateSequential(); + auto Rc1 = Ab1.eliminateSequential(); // The third constructor allows the caller to pass an already solved preconditioner Rc1_ // as a Bayes net, in addition to the "loop closing constraints" Ab2, as before From 910f879a0b7065e307d615da96589c79a71efefa Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 30 Dec 2021 12:18:49 -0500 Subject: [PATCH 02/91] Fix compilation issues --- gtsam/linear/SubgraphPreconditioner.cpp | 2 +- gtsam/linear/SubgraphSolver.cpp | 4 ++-- tests/testSubgraphPreconditioner.cpp | 8 ++++---- tests/testSubgraphSolver.cpp | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gtsam/linear/SubgraphPreconditioner.cpp b/gtsam/linear/SubgraphPreconditioner.cpp index 3eb3f45766..6689cdbed4 100644 --- a/gtsam/linear/SubgraphPreconditioner.cpp +++ b/gtsam/linear/SubgraphPreconditioner.cpp @@ -264,7 +264,7 @@ void SubgraphPreconditioner::build(const GaussianFactorGraph &gfg, const KeyInfo auto gfg_subgraph = buildFactorSubgraph(gfg, subgraph, true); /* factorize and cache BayesNet */ - Rc1_ = gfg_subgraph.eliminateSequential(); + Rc1_ = *gfg_subgraph.eliminateSequential(); } /*****************************************************************************/ diff --git a/gtsam/linear/SubgraphSolver.cpp b/gtsam/linear/SubgraphSolver.cpp index 9de630dc22..0156c717e3 100644 --- a/gtsam/linear/SubgraphSolver.cpp +++ b/gtsam/linear/SubgraphSolver.cpp @@ -40,7 +40,7 @@ SubgraphSolver::SubgraphSolver(const GaussianFactorGraph &Ab, cout << "Split A into (A1) " << Ab1.size() << " and (A2) " << Ab2.size() << " factors" << endl; - auto Rc1 = Ab1.eliminateSequential(ordering, EliminateQR); + auto Rc1 = *Ab1.eliminateSequential(ordering, EliminateQR); auto xbar = Rc1.optimize(); pc_ = boost::make_shared(Ab2, Rc1, xbar); } @@ -62,7 +62,7 @@ SubgraphSolver::SubgraphSolver(const GaussianFactorGraph &Ab1, const GaussianFactorGraph &Ab2, const Parameters ¶meters, const Ordering &ordering) - : SubgraphSolver(Ab1.eliminateSequential(ordering, EliminateQR), Ab2, + : SubgraphSolver(*Ab1.eliminateSequential(ordering, EliminateQR), Ab2, parameters) {} /**************************************************************************************************/ diff --git a/tests/testSubgraphPreconditioner.cpp b/tests/testSubgraphPreconditioner.cpp index 534ef2f97e..eeba38b68e 100644 --- a/tests/testSubgraphPreconditioner.cpp +++ b/tests/testSubgraphPreconditioner.cpp @@ -77,7 +77,7 @@ TEST(SubgraphPreconditioner, planarGraph) { DOUBLES_EQUAL(0, error(A, xtrue), 1e-9); // check zero error for xtrue // Check that xtrue is optimal - GaussianBayesNet R1 = A.eliminateSequential(); + GaussianBayesNet R1 = *A.eliminateSequential(); VectorValues actual = R1.optimize(); EXPECT(assert_equal(xtrue, actual)); } @@ -96,7 +96,7 @@ TEST(SubgraphPreconditioner, splitOffPlanarTree) { LONGS_EQUAL(4, C.size()); // Check that the tree can be solved to give the ground xtrue - GaussianBayesNet R1 = T.eliminateSequential(); + GaussianBayesNet R1 = *T.eliminateSequential(); VectorValues xbar = R1.optimize(); EXPECT(assert_equal(xtrue, xbar)); } @@ -115,7 +115,7 @@ TEST(SubgraphPreconditioner, system) { // Eliminate the spanning tree to build a prior const Ordering ord = planarOrdering(N); - auto Rc1 = Ab1.eliminateSequential(ord); // R1*x-c1 + auto Rc1 = *Ab1.eliminateSequential(ord); // R1*x-c1 VectorValues xbar = Rc1.optimize(); // xbar = inv(R1)*c1 // Create Subgraph-preconditioned system @@ -279,7 +279,7 @@ TEST(SubgraphPreconditioner, conjugateGradients) { boost::tie(Ab1, Ab2) = splitOffPlanarTree(N, Ab); // Eliminate the spanning tree to build a prior - GaussianBayesNet Rc1 = Ab1.eliminateSequential(); // R1*x-c1 + GaussianBayesNet Rc1 = *Ab1.eliminateSequential(); // R1*x-c1 VectorValues xbar = Rc1.optimize(); // xbar = inv(R1)*c1 // Create Subgraph-preconditioned system diff --git a/tests/testSubgraphSolver.cpp b/tests/testSubgraphSolver.cpp index 336e01b9b9..5d8d88775b 100644 --- a/tests/testSubgraphSolver.cpp +++ b/tests/testSubgraphSolver.cpp @@ -123,7 +123,7 @@ TEST( SubgraphSolver, constructor3 ) std::tie(Ab1, Ab2) = example::splitOffPlanarTree(N, Ab); // The caller solves |A1*x-b1|^2 == |R1*x-c1|^2, where R1 is square UT - auto Rc1 = Ab1.eliminateSequential(); + auto Rc1 = *Ab1.eliminateSequential(); // The third constructor allows the caller to pass an already solved preconditioner Rc1_ // as a Bayes net, in addition to the "loop closing constraints" Ab2, as before From fceb65a908e6ef8bd674313cda2667b30694ba55 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 30 Dec 2021 12:32:35 -0500 Subject: [PATCH 03/91] Fix wrap of subgraph --- gtsam/linear/linear.i | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtsam/linear/linear.i b/gtsam/linear/linear.i index 7b1ce550f0..deebe0bfc6 100644 --- a/gtsam/linear/linear.i +++ b/gtsam/linear/linear.i @@ -625,7 +625,7 @@ virtual class SubgraphSolverParameters : gtsam::ConjugateGradientParameters { virtual class SubgraphSolver { SubgraphSolver(const gtsam::GaussianFactorGraph &A, const gtsam::SubgraphSolverParameters ¶meters, const gtsam::Ordering& ordering); - SubgraphSolver(const gtsam::GaussianFactorGraph &Ab1, const gtsam::GaussianFactorGraph* Ab2, const gtsam::SubgraphSolverParameters ¶meters, const gtsam::Ordering& ordering); + SubgraphSolver(const gtsam::GaussianFactorGraph &Ab1, const gtsam::GaussianFactorGraph& Ab2, const gtsam::SubgraphSolverParameters ¶meters, const gtsam::Ordering& ordering); gtsam::VectorValues optimize() const; }; From 3ef6974235a62356e80b4be0267933e6c8d56d37 Mon Sep 17 00:00:00 2001 From: lcarlone Date: Sat, 8 Jan 2022 20:25:00 -0500 Subject: [PATCH 04/91] added robustness to triangulation --- gtsam/geometry/tests/testTriangulation.cpp | 88 ++++++++++++++++++++++ gtsam/geometry/triangulation.h | 59 ++++++++++----- 2 files changed, 128 insertions(+), 19 deletions(-) diff --git a/gtsam/geometry/tests/testTriangulation.cpp b/gtsam/geometry/tests/testTriangulation.cpp index 5fdb911d02..3a09f49bc7 100644 --- a/gtsam/geometry/tests/testTriangulation.cpp +++ b/gtsam/geometry/tests/testTriangulation.cpp @@ -182,6 +182,94 @@ TEST(triangulation, fourPoses) { #endif } +//****************************************************************************** +TEST(triangulation, threePoses_robustNoiseModel) { + + Pose3 pose3 = pose1 * Pose3(Rot3::Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -.1)); + PinholeCamera camera3(pose3, *sharedCal); + Point2 z3 = camera3.project(landmark); + + vector poses; + Point2Vector measurements; + poses += pose1, pose2, pose3; + measurements += z1, z2, z3; + + // noise free, so should give exactly the landmark + boost::optional actual = + triangulatePoint3(poses, sharedCal, measurements); + EXPECT(assert_equal(landmark, *actual, 1e-2)); + + // Add outlier + measurements.at(0) += Point2(100, 120); // very large pixel noise! + + // now estimate does not match landmark + boost::optional actual2 = // + triangulatePoint3(poses, sharedCal, measurements); + // DLT is surprisingly robust, but still off (actual error is around 0.26m): + EXPECT( (landmark - *actual2).norm() >= 0.2); + EXPECT( (landmark - *actual2).norm() <= 0.5); + + // Again with nonlinear optimization + boost::optional actual3 = + triangulatePoint3(poses, sharedCal, measurements, 1e-9, true); + // result from nonlinear (but non-robust optimization) is close to DLT and still off + EXPECT(assert_equal(*actual2, *actual3, 0.1)); + + // Again with nonlinear optimization, this time with robust loss + auto model = noiseModel::Robust::Create( + noiseModel::mEstimator::Huber::Create(1.345), noiseModel::Unit::Create(2)); + boost::optional actual4 = triangulatePoint3( + poses, sharedCal, measurements, 1e-9, true, model); + // using the Huber loss we now have a quite small error!! nice! + EXPECT(assert_equal(landmark, *actual4, 0.05)); +} + +//****************************************************************************** +TEST(triangulation, fourPoses_robustNoiseModel) { + + Pose3 pose3 = pose1 * Pose3(Rot3::Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -.1)); + PinholeCamera camera3(pose3, *sharedCal); + Point2 z3 = camera3.project(landmark); + + vector poses; + Point2Vector measurements; + poses += pose1, pose1, pose2, pose3; // 2 measurements from pose 1 + measurements += z1, z1, z2, z3; + + // noise free, so should give exactly the landmark + boost::optional actual = + triangulatePoint3(poses, sharedCal, measurements); + EXPECT(assert_equal(landmark, *actual, 1e-2)); + + // Add outlier + measurements.at(0) += Point2(100, 120); // very large pixel noise! + // add noise on other measurements: + measurements.at(1) += Point2(0.1, 0.2); // small noise + measurements.at(2) += Point2(0.2, 0.2); + measurements.at(3) += Point2(0.3, 0.1); + + // now estimate does not match landmark + boost::optional actual2 = // + triangulatePoint3(poses, sharedCal, measurements); + // DLT is surprisingly robust, but still off (actual error is around 0.17m): + EXPECT( (landmark - *actual2).norm() >= 0.1); + EXPECT( (landmark - *actual2).norm() <= 0.5); + + // Again with nonlinear optimization + boost::optional actual3 = + triangulatePoint3(poses, sharedCal, measurements, 1e-9, true); + // result from nonlinear (but non-robust optimization) is close to DLT and still off + EXPECT(assert_equal(*actual2, *actual3, 0.1)); + + // Again with nonlinear optimization, this time with robust loss + auto model = noiseModel::Robust::Create( + noiseModel::mEstimator::Huber::Create(1.345), noiseModel::Unit::Create(2)); + boost::optional actual4 = triangulatePoint3( + poses, sharedCal, measurements, 1e-9, true, model); + // using the Huber loss we now have a quite small error!! nice! + EXPECT(assert_equal(landmark, *actual4, 0.05)); +} + //****************************************************************************** TEST(triangulation, fourPoses_distinct_Ks) { Cal3_S2 K1(1500, 1200, 0, 640, 480); diff --git a/gtsam/geometry/triangulation.h b/gtsam/geometry/triangulation.h index aaa8d1a269..5651ae8d86 100644 --- a/gtsam/geometry/triangulation.h +++ b/gtsam/geometry/triangulation.h @@ -14,6 +14,7 @@ * @brief Functions for triangulation * @date July 31, 2013 * @author Chris Beall + * @author Luca Carlone */ #pragma once @@ -105,18 +106,18 @@ template std::pair triangulationGraph( const std::vector& poses, boost::shared_ptr sharedCal, const Point2Vector& measurements, Key landmarkKey, - const Point3& initialEstimate) { + const Point3& initialEstimate, + const SharedNoiseModel& model = nullptr) { Values values; values.insert(landmarkKey, initialEstimate); // Initial landmark value NonlinearFactorGraph graph; static SharedNoiseModel unit2(noiseModel::Unit::Create(2)); - static SharedNoiseModel prior_model(noiseModel::Isotropic::Sigma(6, 1e-6)); for (size_t i = 0; i < measurements.size(); i++) { const Pose3& pose_i = poses[i]; typedef PinholePose Camera; Camera camera_i(pose_i, sharedCal); graph.emplace_shared > // - (camera_i, measurements[i], unit2, landmarkKey); + (camera_i, measurements[i], model? model : unit2, landmarkKey); } return std::make_pair(graph, values); } @@ -134,7 +135,8 @@ template std::pair triangulationGraph( const CameraSet& cameras, const typename CAMERA::MeasurementVector& measurements, Key landmarkKey, - const Point3& initialEstimate) { + const Point3& initialEstimate, + const SharedNoiseModel& model = nullptr) { Values values; values.insert(landmarkKey, initialEstimate); // Initial landmark value NonlinearFactorGraph graph; @@ -143,7 +145,7 @@ std::pair triangulationGraph( for (size_t i = 0; i < measurements.size(); i++) { const CAMERA& camera_i = cameras[i]; graph.emplace_shared > // - (camera_i, measurements[i], unit, landmarkKey); + (camera_i, measurements[i], model? model : unit, landmarkKey); } return std::make_pair(graph, values); } @@ -169,13 +171,14 @@ GTSAM_EXPORT Point3 optimize(const NonlinearFactorGraph& graph, template Point3 triangulateNonlinear(const std::vector& poses, boost::shared_ptr sharedCal, - const Point2Vector& measurements, const Point3& initialEstimate) { + const Point2Vector& measurements, const Point3& initialEstimate, + const SharedNoiseModel& model = nullptr) { // Create a factor graph and initial values Values values; NonlinearFactorGraph graph; boost::tie(graph, values) = triangulationGraph // - (poses, sharedCal, measurements, Symbol('p', 0), initialEstimate); + (poses, sharedCal, measurements, Symbol('p', 0), initialEstimate, model); return optimize(graph, values, Symbol('p', 0)); } @@ -190,13 +193,14 @@ Point3 triangulateNonlinear(const std::vector& poses, template Point3 triangulateNonlinear( const CameraSet& cameras, - const typename CAMERA::MeasurementVector& measurements, const Point3& initialEstimate) { + const typename CAMERA::MeasurementVector& measurements, const Point3& initialEstimate, + const SharedNoiseModel& model = nullptr) { // Create a factor graph and initial values Values values; NonlinearFactorGraph graph; boost::tie(graph, values) = triangulationGraph // - (cameras, measurements, Symbol('p', 0), initialEstimate); + (cameras, measurements, Symbol('p', 0), initialEstimate, model); return optimize(graph, values, Symbol('p', 0)); } @@ -239,7 +243,8 @@ template Point3 triangulatePoint3(const std::vector& poses, boost::shared_ptr sharedCal, const Point2Vector& measurements, double rank_tol = 1e-9, - bool optimize = false) { + bool optimize = false, + const SharedNoiseModel& model = nullptr) { assert(poses.size() == measurements.size()); if (poses.size() < 2) @@ -254,7 +259,7 @@ Point3 triangulatePoint3(const std::vector& poses, // Then refine using non-linear optimization if (optimize) point = triangulateNonlinear // - (poses, sharedCal, measurements, point); + (poses, sharedCal, measurements, point, model); #ifdef GTSAM_THROW_CHEIRALITY_EXCEPTION // verify that the triangulated point lies in front of all cameras @@ -284,7 +289,8 @@ template Point3 triangulatePoint3( const CameraSet& cameras, const typename CAMERA::MeasurementVector& measurements, double rank_tol = 1e-9, - bool optimize = false) { + bool optimize = false, + const SharedNoiseModel& model = nullptr) { size_t m = cameras.size(); assert(measurements.size() == m); @@ -298,7 +304,7 @@ Point3 triangulatePoint3( // The n refine using non-linear optimization if (optimize) - point = triangulateNonlinear(cameras, measurements, point); + point = triangulateNonlinear(cameras, measurements, point, model); #ifdef GTSAM_THROW_CHEIRALITY_EXCEPTION // verify that the triangulated point lies in front of all cameras @@ -317,9 +323,10 @@ template Point3 triangulatePoint3( const CameraSet >& cameras, const Point2Vector& measurements, double rank_tol = 1e-9, - bool optimize = false) { + bool optimize = false, + const SharedNoiseModel& model = nullptr) { return triangulatePoint3 > // - (cameras, measurements, rank_tol, optimize); + (cameras, measurements, rank_tol, optimize, model); } struct GTSAM_EXPORT TriangulationParameters { @@ -341,20 +348,29 @@ struct GTSAM_EXPORT TriangulationParameters { */ double dynamicOutlierRejectionThreshold; + SharedNoiseModel noiseModel; ///< used in the nonlinear triangulation + + bool invDepthLinearTriangulation; ///< if set to true, we use an inverse depth alternative to DL + /** * Constructor * @param rankTol tolerance used to check if point triangulation is degenerate * @param enableEPI if true refine triangulation with embedded LM iterations * @param landmarkDistanceThreshold flag as degenerate if point further than this * @param dynamicOutlierRejectionThreshold or if average error larger than this + * @param invDepthLinearTriangulation use inverse depth approach to linear triangulation * */ TriangulationParameters(const double _rankTolerance = 1.0, const bool _enableEPI = false, double _landmarkDistanceThreshold = -1, - double _dynamicOutlierRejectionThreshold = -1) : + double _dynamicOutlierRejectionThreshold = -1, + const SharedNoiseModel& _noiseModel = nullptr, + const bool _invDepthLinearTriangulation = false) : rankTolerance(_rankTolerance), enableEPI(_enableEPI), // landmarkDistanceThreshold(_landmarkDistanceThreshold), // - dynamicOutlierRejectionThreshold(_dynamicOutlierRejectionThreshold) { + dynamicOutlierRejectionThreshold(_dynamicOutlierRejectionThreshold), + noiseModel(_noiseModel), + invDepthLinearTriangulation(_invDepthLinearTriangulation){ } // stream to output @@ -366,6 +382,9 @@ struct GTSAM_EXPORT TriangulationParameters { << std::endl; os << "dynamicOutlierRejectionThreshold = " << p.dynamicOutlierRejectionThreshold << std::endl; + os << "noise model" << std::endl; + os << "invDepthLinearTriangulation = " << p.invDepthLinearTriangulation + << std::endl; return os; } @@ -379,6 +398,7 @@ struct GTSAM_EXPORT TriangulationParameters { ar & BOOST_SERIALIZATION_NVP(enableEPI); ar & BOOST_SERIALIZATION_NVP(landmarkDistanceThreshold); ar & BOOST_SERIALIZATION_NVP(dynamicOutlierRejectionThreshold); + ar & BOOST_SERIALIZATION_NVP(invDepthLinearTriangulation); } }; @@ -468,8 +488,9 @@ TriangulationResult triangulateSafe(const CameraSet& cameras, else // We triangulate the 3D position of the landmark try { - Point3 point = triangulatePoint3(cameras, measured, - params.rankTolerance, params.enableEPI); + Point3 point = + triangulatePoint3(cameras, measured, params.rankTolerance, + params.enableEPI, params.noiseModel); // Check landmark distance and re-projection errors to avoid outliers size_t i = 0; From c407609b8f8d2d6f7c110bc757325826f83e75e1 Mon Sep 17 00:00:00 2001 From: lcarlone Date: Sat, 8 Jan 2022 20:27:27 -0500 Subject: [PATCH 05/91] moved invDepth-related variables --- gtsam/geometry/triangulation.h | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/gtsam/geometry/triangulation.h b/gtsam/geometry/triangulation.h index 5651ae8d86..af01d3a364 100644 --- a/gtsam/geometry/triangulation.h +++ b/gtsam/geometry/triangulation.h @@ -350,27 +350,23 @@ struct GTSAM_EXPORT TriangulationParameters { SharedNoiseModel noiseModel; ///< used in the nonlinear triangulation - bool invDepthLinearTriangulation; ///< if set to true, we use an inverse depth alternative to DL - /** * Constructor * @param rankTol tolerance used to check if point triangulation is degenerate * @param enableEPI if true refine triangulation with embedded LM iterations * @param landmarkDistanceThreshold flag as degenerate if point further than this * @param dynamicOutlierRejectionThreshold or if average error larger than this - * @param invDepthLinearTriangulation use inverse depth approach to linear triangulation + * @param noiseModel noise model to use during nonlinear triangulation * */ TriangulationParameters(const double _rankTolerance = 1.0, const bool _enableEPI = false, double _landmarkDistanceThreshold = -1, double _dynamicOutlierRejectionThreshold = -1, - const SharedNoiseModel& _noiseModel = nullptr, - const bool _invDepthLinearTriangulation = false) : + const SharedNoiseModel& _noiseModel = nullptr) : rankTolerance(_rankTolerance), enableEPI(_enableEPI), // landmarkDistanceThreshold(_landmarkDistanceThreshold), // dynamicOutlierRejectionThreshold(_dynamicOutlierRejectionThreshold), - noiseModel(_noiseModel), - invDepthLinearTriangulation(_invDepthLinearTriangulation){ + noiseModel(_noiseModel){ } // stream to output @@ -383,8 +379,6 @@ struct GTSAM_EXPORT TriangulationParameters { os << "dynamicOutlierRejectionThreshold = " << p.dynamicOutlierRejectionThreshold << std::endl; os << "noise model" << std::endl; - os << "invDepthLinearTriangulation = " << p.invDepthLinearTriangulation - << std::endl; return os; } @@ -398,7 +392,6 @@ struct GTSAM_EXPORT TriangulationParameters { ar & BOOST_SERIALIZATION_NVP(enableEPI); ar & BOOST_SERIALIZATION_NVP(landmarkDistanceThreshold); ar & BOOST_SERIALIZATION_NVP(dynamicOutlierRejectionThreshold); - ar & BOOST_SERIALIZATION_NVP(invDepthLinearTriangulation); } }; From b60ca0c10724ea3c85ca04575295d38c1947418c Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 09:57:59 -0700 Subject: [PATCH 06/91] Update test_Triangulation.py --- python/gtsam/tests/test_Triangulation.py | 64 +++++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/python/gtsam/tests/test_Triangulation.py b/python/gtsam/tests/test_Triangulation.py index 399cf019d0..ec50692c3f 100644 --- a/python/gtsam/tests/test_Triangulation.py +++ b/python/gtsam/tests/test_Triangulation.py @@ -9,6 +9,7 @@ Author: Frank Dellaert & Fan Jiang (Python) """ import unittest +from typing import Union import numpy as np @@ -20,14 +21,16 @@ from gtsam.utils.test_case import GtsamTestCase -class TestVisualISAMExample(GtsamTestCase): +UPRIGHT = Rot3.Ypr(-np.pi / 2, 0., -np.pi / 2) + + +class TestTriangulationExample(GtsamTestCase): """ Tests for triangulation with shared and individual calibrations """ def setUp(self): """ Set up two camera poses """ # Looking along X-axis, 1 meter above ground plane (x-y) - upright = Rot3.Ypr(-np.pi / 2, 0., -np.pi / 2) - pose1 = Pose3(upright, Point3(0, 0, 1)) + pose1 = Pose3(UPRIGHT, Point3(0, 0, 1)) # create second camera 1 meter to the right of first camera pose2 = pose1.compose(Pose3(Rot3(), Point3(1, 0, 0))) @@ -39,7 +42,7 @@ def setUp(self): # landmark ~5 meters infront of camera self.landmark = Point3(5, 0.5, 1.2) - def generate_measurements(self, calibration, camera_model, cal_params, camera_set=None): + def generate_measurements(self, calibration: Union[Cal3Bundler, Cal3_S2], camera_model, cal_params, camera_set=None): """ Generate vector of measurements for given calibration and camera model. @@ -48,6 +51,7 @@ def generate_measurements(self, calibration, camera_model, cal_params, camera_se camera_model: Camera model e.g. PinholeCameraCal3_S2 cal_params: Iterable of camera parameters for `calibration` e.g. [K1, K2] camera_set: Cameraset object (for individual calibrations) + Returns: list of measurements and list/CameraSet object for cameras """ @@ -66,7 +70,7 @@ def generate_measurements(self, calibration, camera_model, cal_params, camera_se return measurements, cameras - def test_TriangulationExample(self): + def test_TriangulationExample(self) -> None: """ Tests triangulation with shared Cal3_S2 calibration""" # Some common constants sharedCal = (1500, 1200, 0, 640, 480) @@ -95,7 +99,7 @@ def test_TriangulationExample(self): self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-2) - def test_distinct_Ks(self): + def test_distinct_Ks(self) -> None: """ Tests triangulation with individual Cal3_S2 calibrations """ # two camera parameters K1 = (1500, 1200, 0, 640, 480) @@ -112,7 +116,7 @@ def test_distinct_Ks(self): optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) - def test_distinct_Ks_Bundler(self): + def test_distinct_Ks_Bundler(self) -> None: """ Tests triangulation with individual Cal3Bundler calibrations""" # two camera parameters K1 = (1500, 0, 0, 640, 480) @@ -128,7 +132,53 @@ def test_distinct_Ks_Bundler(self): rank_tol=1e-9, optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) + + def test_triangulation_robust_three_poses(self) -> None: + """Ensure triangulation with a robust model works.""" + sharedCal = Cal3_S2(1500, 1200, 0, 640, 480) + # landmark ~5 meters infront of camera + landmark = Point3(5, 0.5, 1.2) + + pose1 = Pose3(UPRIGHT, Point3(0, 0, 1)) + pose2 = pose1 * Pose3(Rot3(), Point3(1, 0, 0)) + pose3 = pose1 * Pose3(Rot3.Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -.1)) + + camera1 = PinholeCameraCal3_S2(pose1, sharedCal) + camera2 = PinholeCameraCal3_S2(pose2, sharedCal) + camera3 = PinholeCameraCal3_S2(pose3, sharedCal) + + z1: Point2 = camera1.project(landmark) + z2: Point2 = camera2.project(landmark) + z3: Point2 = camera3.project(landmark) + + poses = [pose1, pose2, pose3] + measurements = Point2Vector([z1, z2, z3]) + + # noise free, so should give exactly the landmark + actual = gtsam.triangulatePoint3(poses, sharedCal, measurements) + self.assert_equal(landmark, actual, 1e-2) + + # Add outlier + measurements.at(0) += Point2(100, 120) # very large pixel noise! + + # now estimate does not match landmark + actual2 = gtsam.triangulatePoint3(poses, sharedCal, measurements) + # DLT is surprisingly robust, but still off (actual error is around 0.26m) + self.assertTrue( (landmark - actual2).norm() >= 0.2) + self.assertTrue( (landmark - actual2).norm() <= 0.5) + + # Again with nonlinear optimization + actual3 = gtsam.triangulatePoint3(poses, sharedCal, measurements, 1e-9, true) + # result from nonlinear (but non-robust optimization) is close to DLT and still off + self.assertEqual(actual2, actual3, 0.1) + + # Again with nonlinear optimization, this time with robust loss + model = noiseModel.Robust.Create(noiseModel.mEstimator.Huber.Create(1.345), noiseModel.Unit.Create(2)) + actual4 = gtsam.triangulatePoint3(poses, sharedCal, measurements, 1e-9, true, model) + # using the Huber loss we now have a quite small error!! nice! + self.assertEqual(landmark, actual4, 0.05) + if __name__ == "__main__": unittest.main() From d66b1d7a849faff591e0f39b34af80aec85db715 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 13:01:23 -0500 Subject: [PATCH 07/91] fix syntax errors --- python/gtsam/tests/test_Triangulation.py | 138 ++++++++++++----------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/python/gtsam/tests/test_Triangulation.py b/python/gtsam/tests/test_Triangulation.py index ec50692c3f..1a304bdc88 100644 --- a/python/gtsam/tests/test_Triangulation.py +++ b/python/gtsam/tests/test_Triangulation.py @@ -6,29 +6,39 @@ See LICENSE for the license information Test Triangulation -Author: Frank Dellaert & Fan Jiang (Python) +Authors: Frank Dellaert & Fan Jiang (Python) & Sushmita Warrier & John Lambert """ import unittest -from typing import Union +from typing import Optional, Union import numpy as np import gtsam -from gtsam import (Cal3_S2, Cal3Bundler, CameraSetCal3_S2, - CameraSetCal3Bundler, PinholeCameraCal3_S2, - PinholeCameraCal3Bundler, Point2Vector, Point3, Pose3, - Pose3Vector, Rot3) +from gtsam import ( + Cal3_S2, + Cal3Bundler, + CameraSetCal3_S2, + CameraSetCal3Bundler, + PinholeCameraCal3_S2, + PinholeCameraCal3Bundler, + Point2, + Point2Vector, + Point3, + Pose3, + Pose3Vector, + Rot3, +) from gtsam.utils.test_case import GtsamTestCase -UPRIGHT = Rot3.Ypr(-np.pi / 2, 0., -np.pi / 2) +UPRIGHT = Rot3.Ypr(-np.pi / 2, 0.0, -np.pi / 2) class TestTriangulationExample(GtsamTestCase): - """ Tests for triangulation with shared and individual calibrations """ + """Tests for triangulation with shared and individual calibrations""" def setUp(self): - """ Set up two camera poses """ + """Set up two camera poses""" # Looking along X-axis, 1 meter above ground plane (x-y) pose1 = Pose3(UPRIGHT, Point3(0, 0, 1)) @@ -42,16 +52,22 @@ def setUp(self): # landmark ~5 meters infront of camera self.landmark = Point3(5, 0.5, 1.2) - def generate_measurements(self, calibration: Union[Cal3Bundler, Cal3_S2], camera_model, cal_params, camera_set=None): + def generate_measurements( + self, + calibration: Union[Cal3Bundler, Cal3_S2], + camera_model, + cal_params, + camera_set: Optional[Union[CameraSetCal3Bundler, CameraSetCal3_S2]] = None, + ): """ Generate vector of measurements for given calibration and camera model. - Args: + Args: calibration: Camera calibration e.g. Cal3_S2 camera_model: Camera model e.g. PinholeCameraCal3_S2 cal_params: Iterable of camera parameters for `calibration` e.g. [K1, K2] camera_set: Cameraset object (for individual calibrations) - + Returns: list of measurements and list/CameraSet object for cameras """ @@ -71,19 +87,15 @@ def generate_measurements(self, calibration: Union[Cal3Bundler, Cal3_S2], camera return measurements, cameras def test_TriangulationExample(self) -> None: - """ Tests triangulation with shared Cal3_S2 calibration""" + """Tests triangulation with shared Cal3_S2 calibration""" # Some common constants sharedCal = (1500, 1200, 0, 640, 480) - measurements, _ = self.generate_measurements(Cal3_S2, - PinholeCameraCal3_S2, - (sharedCal, sharedCal)) + measurements, _ = self.generate_measurements(Cal3_S2, PinholeCameraCal3_S2, (sharedCal, sharedCal)) - triangulated_landmark = gtsam.triangulatePoint3(self.poses, - Cal3_S2(sharedCal), - measurements, - rank_tol=1e-9, - optimize=True) + triangulated_landmark = gtsam.triangulatePoint3( + self.poses, Cal3_S2(sharedCal), measurements, rank_tol=1e-9, optimize=True + ) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) # Add some noise and try again: result should be ~ (4.995, 0.499167, 1.19814) @@ -91,59 +103,49 @@ def test_TriangulationExample(self) -> None: measurements_noisy.append(measurements[0] - np.array([0.1, 0.5])) measurements_noisy.append(measurements[1] - np.array([-0.2, 0.3])) - triangulated_landmark = gtsam.triangulatePoint3(self.poses, - Cal3_S2(sharedCal), - measurements_noisy, - rank_tol=1e-9, - optimize=True) + triangulated_landmark = gtsam.triangulatePoint3( + self.poses, Cal3_S2(sharedCal), measurements_noisy, rank_tol=1e-9, optimize=True + ) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-2) def test_distinct_Ks(self) -> None: - """ Tests triangulation with individual Cal3_S2 calibrations """ + """Tests triangulation with individual Cal3_S2 calibrations""" # two camera parameters K1 = (1500, 1200, 0, 640, 480) K2 = (1600, 1300, 0, 650, 440) - measurements, cameras = self.generate_measurements(Cal3_S2, - PinholeCameraCal3_S2, - (K1, K2), - camera_set=CameraSetCal3_S2) + measurements, cameras = self.generate_measurements( + Cal3_S2, PinholeCameraCal3_S2, (K1, K2), camera_set=CameraSetCal3_S2 + ) - triangulated_landmark = gtsam.triangulatePoint3(cameras, - measurements, - rank_tol=1e-9, - optimize=True) + triangulated_landmark = gtsam.triangulatePoint3(cameras, measurements, rank_tol=1e-9, optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) def test_distinct_Ks_Bundler(self) -> None: - """ Tests triangulation with individual Cal3Bundler calibrations""" + """Tests triangulation with individual Cal3Bundler calibrations""" # two camera parameters K1 = (1500, 0, 0, 640, 480) K2 = (1600, 0, 0, 650, 440) - measurements, cameras = self.generate_measurements(Cal3Bundler, - PinholeCameraCal3Bundler, - (K1, K2), - camera_set=CameraSetCal3Bundler) + measurements, cameras = self.generate_measurements( + Cal3Bundler, PinholeCameraCal3Bundler, (K1, K2), camera_set=CameraSetCal3Bundler + ) - triangulated_landmark = gtsam.triangulatePoint3(cameras, - measurements, - rank_tol=1e-9, - optimize=True) + triangulated_landmark = gtsam.triangulatePoint3(cameras, measurements, rank_tol=1e-9, optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) - + def test_triangulation_robust_three_poses(self) -> None: """Ensure triangulation with a robust model works.""" sharedCal = Cal3_S2(1500, 1200, 0, 640, 480) # landmark ~5 meters infront of camera landmark = Point3(5, 0.5, 1.2) - + pose1 = Pose3(UPRIGHT, Point3(0, 0, 1)) pose2 = pose1 * Pose3(Rot3(), Point3(1, 0, 0)) - pose3 = pose1 * Pose3(Rot3.Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -.1)) - + pose3 = pose1 * Pose3(Rot3.Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -0.1)) + camera1 = PinholeCameraCal3_S2(pose1, sharedCal) camera2 = PinholeCameraCal3_S2(pose2, sharedCal) camera3 = PinholeCameraCal3_S2(pose3, sharedCal) @@ -151,34 +153,36 @@ def test_triangulation_robust_three_poses(self) -> None: z1: Point2 = camera1.project(landmark) z2: Point2 = camera2.project(landmark) z3: Point2 = camera3.project(landmark) - - poses = [pose1, pose2, pose3] + + poses = gtsam.Pose3Vector([pose1, pose2, pose3]) measurements = Point2Vector([z1, z2, z3]) - + # noise free, so should give exactly the landmark - actual = gtsam.triangulatePoint3(poses, sharedCal, measurements) - self.assert_equal(landmark, actual, 1e-2) - + actual = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=False) + self.assertTrue(np.allclose(landmark, actual, atol=1e-2)) + # Add outlier - measurements.at(0) += Point2(100, 120) # very large pixel noise! - + measurements[0] += Point2(100, 120) # very large pixel noise! + # now estimate does not match landmark - actual2 = gtsam.triangulatePoint3(poses, sharedCal, measurements) + actual2 = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=False) # DLT is surprisingly robust, but still off (actual error is around 0.26m) - self.assertTrue( (landmark - actual2).norm() >= 0.2) - self.assertTrue( (landmark - actual2).norm() <= 0.5) - + self.assertTrue(np.linalg.norm(landmark - actual2) >= 0.2) + self.assertTrue(np.linalg.norm(landmark - actual2) <= 0.5) + # Again with nonlinear optimization - actual3 = gtsam.triangulatePoint3(poses, sharedCal, measurements, 1e-9, true) + actual3 = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=True) # result from nonlinear (but non-robust optimization) is close to DLT and still off - self.assertEqual(actual2, actual3, 0.1) - + self.assertTrue(np.allclose(actual2, actual3, atol=0.1)) + # Again with nonlinear optimization, this time with robust loss - model = noiseModel.Robust.Create(noiseModel.mEstimator.Huber.Create(1.345), noiseModel.Unit.Create(2)) - actual4 = gtsam.triangulatePoint3(poses, sharedCal, measurements, 1e-9, true, model) + model = gtsam.noiseModel.Robust.Create( + gtsam.noiseModel.mEstimator.Huber.Create(1.345), gtsam.noiseModel.Unit.Create(2) + ) + actual4 = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=True, model=model) # using the Huber loss we now have a quite small error!! nice! - self.assertEqual(landmark, actual4, 0.05) - + self.assertTrue(np.allclose(landmark, actual4, atol=0.05)) + if __name__ == "__main__": unittest.main() From f009a14151be468772378011d938fc76bb8efa84 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 13:24:08 -0500 Subject: [PATCH 08/91] add missing type hint --- python/gtsam/tests/test_Triangulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/gtsam/tests/test_Triangulation.py b/python/gtsam/tests/test_Triangulation.py index 1a304bdc88..9eed71ae6e 100644 --- a/python/gtsam/tests/test_Triangulation.py +++ b/python/gtsam/tests/test_Triangulation.py @@ -55,7 +55,7 @@ def setUp(self): def generate_measurements( self, calibration: Union[Cal3Bundler, Cal3_S2], - camera_model, + camera_model: Union[PinholeCameraCal3Bundler, PinholeCameraCal3_S2], cal_params, camera_set: Optional[Union[CameraSetCal3Bundler, CameraSetCal3_S2]] = None, ): From e2993eafe61c08bd2d1ed758ed1e3004b1882b5c Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 13:41:54 -0500 Subject: [PATCH 09/91] yapf pep8 reformat --- python/gtsam/tests/test_Triangulation.py | 87 +++++++++++++++++------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/python/gtsam/tests/test_Triangulation.py b/python/gtsam/tests/test_Triangulation.py index 9eed71ae6e..c3b9870d89 100644 --- a/python/gtsam/tests/test_Triangulation.py +++ b/python/gtsam/tests/test_Triangulation.py @@ -9,7 +9,7 @@ Authors: Frank Dellaert & Fan Jiang (Python) & Sushmita Warrier & John Lambert """ import unittest -from typing import Optional, Union +from typing import Iterable, Optional, Union import numpy as np @@ -30,7 +30,6 @@ ) from gtsam.utils.test_case import GtsamTestCase - UPRIGHT = Rot3.Ypr(-np.pi / 2, 0.0, -np.pi / 2) @@ -56,9 +55,11 @@ def generate_measurements( self, calibration: Union[Cal3Bundler, Cal3_S2], camera_model: Union[PinholeCameraCal3Bundler, PinholeCameraCal3_S2], - cal_params, - camera_set: Optional[Union[CameraSetCal3Bundler, CameraSetCal3_S2]] = None, - ): + cal_params: Iterable[Iterable[Union[int, float]]], + camera_set: Optional[Union[CameraSetCal3Bundler, + CameraSetCal3_S2]] = None, + ) -> Tuple[Point2Vector, Union[CameraSetCal3Bundler, CameraSetCal3_S2, + List[Cal3Bundler], List[Cal3_S2]]]: """ Generate vector of measurements for given calibration and camera model. @@ -91,11 +92,16 @@ def test_TriangulationExample(self) -> None: # Some common constants sharedCal = (1500, 1200, 0, 640, 480) - measurements, _ = self.generate_measurements(Cal3_S2, PinholeCameraCal3_S2, (sharedCal, sharedCal)) + measurements, _ = self.generate_measurements( + calibration=Cal3_S2, + camera_model=PinholeCameraCal3_S2, + cal_params=(sharedCal, sharedCal)) - triangulated_landmark = gtsam.triangulatePoint3( - self.poses, Cal3_S2(sharedCal), measurements, rank_tol=1e-9, optimize=True - ) + triangulated_landmark = gtsam.triangulatePoint3(self.poses, + Cal3_S2(sharedCal), + measurements, + rank_tol=1e-9, + optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) # Add some noise and try again: result should be ~ (4.995, 0.499167, 1.19814) @@ -103,9 +109,11 @@ def test_TriangulationExample(self) -> None: measurements_noisy.append(measurements[0] - np.array([0.1, 0.5])) measurements_noisy.append(measurements[1] - np.array([-0.2, 0.3])) - triangulated_landmark = gtsam.triangulatePoint3( - self.poses, Cal3_S2(sharedCal), measurements_noisy, rank_tol=1e-9, optimize=True - ) + triangulated_landmark = gtsam.triangulatePoint3(self.poses, + Cal3_S2(sharedCal), + measurements_noisy, + rank_tol=1e-9, + optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-2) @@ -116,10 +124,15 @@ def test_distinct_Ks(self) -> None: K2 = (1600, 1300, 0, 650, 440) measurements, cameras = self.generate_measurements( - Cal3_S2, PinholeCameraCal3_S2, (K1, K2), camera_set=CameraSetCal3_S2 - ) - - triangulated_landmark = gtsam.triangulatePoint3(cameras, measurements, rank_tol=1e-9, optimize=True) + calibration=Cal3_S2, + camera_model=PinholeCameraCal3_S2, + cal_params=(K1, K2), + camera_set=CameraSetCal3_S2) + + triangulated_landmark = gtsam.triangulatePoint3(cameras, + measurements, + rank_tol=1e-9, + optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) def test_distinct_Ks_Bundler(self) -> None: @@ -129,10 +142,15 @@ def test_distinct_Ks_Bundler(self) -> None: K2 = (1600, 0, 0, 650, 440) measurements, cameras = self.generate_measurements( - Cal3Bundler, PinholeCameraCal3Bundler, (K1, K2), camera_set=CameraSetCal3Bundler - ) - - triangulated_landmark = gtsam.triangulatePoint3(cameras, measurements, rank_tol=1e-9, optimize=True) + calibration=Cal3Bundler, + camera_model=PinholeCameraCal3Bundler, + cal_params=(K1, K2), + camera_set=CameraSetCal3Bundler) + + triangulated_landmark = gtsam.triangulatePoint3(cameras, + measurements, + rank_tol=1e-9, + optimize=True) self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9) def test_triangulation_robust_three_poses(self) -> None: @@ -158,28 +176,45 @@ def test_triangulation_robust_three_poses(self) -> None: measurements = Point2Vector([z1, z2, z3]) # noise free, so should give exactly the landmark - actual = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=False) + actual = gtsam.triangulatePoint3(poses, + sharedCal, + measurements, + rank_tol=1e-9, + optimize=False) self.assertTrue(np.allclose(landmark, actual, atol=1e-2)) # Add outlier measurements[0] += Point2(100, 120) # very large pixel noise! # now estimate does not match landmark - actual2 = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=False) + actual2 = gtsam.triangulatePoint3(poses, + sharedCal, + measurements, + rank_tol=1e-9, + optimize=False) # DLT is surprisingly robust, but still off (actual error is around 0.26m) self.assertTrue(np.linalg.norm(landmark - actual2) >= 0.2) self.assertTrue(np.linalg.norm(landmark - actual2) <= 0.5) # Again with nonlinear optimization - actual3 = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=True) + actual3 = gtsam.triangulatePoint3(poses, + sharedCal, + measurements, + rank_tol=1e-9, + optimize=True) # result from nonlinear (but non-robust optimization) is close to DLT and still off self.assertTrue(np.allclose(actual2, actual3, atol=0.1)) # Again with nonlinear optimization, this time with robust loss model = gtsam.noiseModel.Robust.Create( - gtsam.noiseModel.mEstimator.Huber.Create(1.345), gtsam.noiseModel.Unit.Create(2) - ) - actual4 = gtsam.triangulatePoint3(poses, sharedCal, measurements, rank_tol=1e-9, optimize=True, model=model) + gtsam.noiseModel.mEstimator.Huber.Create(1.345), + gtsam.noiseModel.Unit.Create(2)) + actual4 = gtsam.triangulatePoint3(poses, + sharedCal, + measurements, + rank_tol=1e-9, + optimize=True, + model=model) # using the Huber loss we now have a quite small error!! nice! self.assertTrue(np.allclose(landmark, actual4, atol=0.05)) From 1614ce094f3d2db252785e3983289f4ee13e835f Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 14:55:29 -0500 Subject: [PATCH 10/91] wrap new noise model arg for gtsam.triangulatePoint3 --- gtsam/geometry/geometry.i | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/gtsam/geometry/geometry.i b/gtsam/geometry/geometry.i index 0def842651..1e42966f84 100644 --- a/gtsam/geometry/geometry.i +++ b/gtsam/geometry/geometry.i @@ -923,27 +923,34 @@ class StereoCamera { gtsam::Point3 triangulatePoint3(const gtsam::Pose3Vector& poses, gtsam::Cal3_S2* sharedCal, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulatePoint3(const gtsam::Pose3Vector& poses, gtsam::Cal3DS2* sharedCal, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulatePoint3(const gtsam::Pose3Vector& poses, gtsam::Cal3Bundler* sharedCal, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulatePoint3(const gtsam::CameraSetCal3_S2& cameras, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulatePoint3(const gtsam::CameraSetCal3Bundler& cameras, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulatePoint3(const gtsam::CameraSetCal3Fisheye& cameras, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulatePoint3(const gtsam::CameraSetCal3Unified& cameras, const gtsam::Point2Vector& measurements, - double rank_tol, bool optimize); + double rank_tol, bool optimize, + const gtsam::SharedNoiseModel& model = nullptr); gtsam::Point3 triangulateNonlinear(const gtsam::Pose3Vector& poses, gtsam::Cal3_S2* sharedCal, const gtsam::Point2Vector& measurements, From 0f1ff48db5a551b5dda15b6f1138739f977b0d05 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 16:49:12 -0500 Subject: [PATCH 11/91] add missing type hint import --- python/gtsam/tests/test_Triangulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/gtsam/tests/test_Triangulation.py b/python/gtsam/tests/test_Triangulation.py index c3b9870d89..46977b57e7 100644 --- a/python/gtsam/tests/test_Triangulation.py +++ b/python/gtsam/tests/test_Triangulation.py @@ -9,7 +9,7 @@ Authors: Frank Dellaert & Fan Jiang (Python) & Sushmita Warrier & John Lambert """ import unittest -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Tuple, Union import numpy as np From 0ff9110f3c967ebccd1a067db5d2a0954dd88437 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 12 Jan 2022 15:39:09 -0700 Subject: [PATCH 12/91] add missing type hint annotation import --- python/gtsam/tests/test_Triangulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/gtsam/tests/test_Triangulation.py b/python/gtsam/tests/test_Triangulation.py index 46977b57e7..0a258a0afc 100644 --- a/python/gtsam/tests/test_Triangulation.py +++ b/python/gtsam/tests/test_Triangulation.py @@ -9,7 +9,7 @@ Authors: Frank Dellaert & Fan Jiang (Python) & Sushmita Warrier & John Lambert """ import unittest -from typing import Iterable, Optional, Tuple, Union +from typing import Iterable, List, Optional, Tuple, Union import numpy as np From 2e8dcdd41067de904e441f27db9ff559ca2eff6e Mon Sep 17 00:00:00 2001 From: Calvin Date: Thu, 13 Jan 2022 18:11:55 -0600 Subject: [PATCH 13/91] Added a convenience function for plotting 2D points. --- python/gtsam/utils/plot.py | 62 +++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/python/gtsam/utils/plot.py b/python/gtsam/utils/plot.py index 7ea3930774..32f07179b7 100644 --- a/python/gtsam/utils/plot.py +++ b/python/gtsam/utils/plot.py @@ -10,7 +10,7 @@ from mpl_toolkits.mplot3d import Axes3D # pylint: disable=unused-import import gtsam -from gtsam import Marginals, Point3, Pose2, Pose3, Values +from gtsam import Marginals, Point2, Point3, Pose2, Pose3, Values def set_axes_equal(fignum: int) -> None: @@ -108,6 +108,66 @@ def plot_covariance_ellipse_3d(axes, axes.plot_surface(x, y, z, alpha=alpha, cmap='hot') +def plot_point2_on_axes(axes, + point: Point2, + linespec: str, + P: Optional[np.ndarray] = None) -> None: + """ + Plot a 2D point on given axis `axes` with given `linespec`. + + Args: + axes (matplotlib.axes.Axes): Matplotlib axes. + point: The point to be plotted. + linespec: String representing formatting options for Matplotlib. + P: Marginal covariance matrix to plot the uncertainty of the estimation. + """ + axes.plot([point[0]], [point[1]], linespec, marker='.', markersize=10) + if P is not None: + w, v = np.linalg.eig(P) + + # k = 2.296 + k = 5.0 + + angle = np.arctan2(v[1, 0], v[0, 0]) + e1 = patches.Ellipse(point, + np.sqrt(w[0] * k), + np.sqrt(w[1] * k), + np.rad2deg(angle), + fill=False) + axes.add_patch(e1) + + +def plot_point2( + fignum: int, + point: Point2, + linespec: str, + P: np.ndarray = None, + axis_labels: Iterable[str] = ("X axis", "Y axis"), +) -> plt.Figure: + """ + Plot a 2D point on given figure with given `linespec`. + + Args: + fignum: Integer representing the figure number to use for plotting. + point: The point to be plotted. + linespec: String representing formatting options for Matplotlib. + P: Marginal covariance matrix to plot the uncertainty of the estimation. + axis_labels: List of axis labels to set. + + Returns: + fig: The matplotlib figure. + + """ + fig = plt.figure(fignum) + axes = fig.gca() + plot_point2_on_axes(axes, point, linespec, P) + + axes.set_xlabel(axis_labels[0]) + axes.set_ylabel(axis_labels[1]) + + return fig + + def plot_pose2_on_axes(axes, pose: Pose2, axis_length: float = 0.1, From 10e1bd2f6167bcf667090564bf287a1ee492f6e0 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Mon, 17 Jan 2022 22:59:17 -0500 Subject: [PATCH 14/91] sample variants --- gtsam/discrete/DiscreteBayesNet.cpp | 126 ++++++++++---------- gtsam/discrete/DiscreteBayesNet.h | 38 +++++- gtsam/discrete/discrete.i | 2 + python/gtsam/tests/test_DiscreteBayesNet.py | 43 +++++-- 4 files changed, 137 insertions(+), 72 deletions(-) diff --git a/gtsam/discrete/DiscreteBayesNet.cpp b/gtsam/discrete/DiscreteBayesNet.cpp index c0dfd747c3..7294c8b296 100644 --- a/gtsam/discrete/DiscreteBayesNet.cpp +++ b/gtsam/discrete/DiscreteBayesNet.cpp @@ -25,65 +25,71 @@ namespace gtsam { - // Instantiate base class - template class FactorGraph; - - /* ************************************************************************* */ - bool DiscreteBayesNet::equals(const This& bn, double tol) const - { - return Base::equals(bn, tol); - } - - /* ************************************************************************* */ - double DiscreteBayesNet::evaluate(const DiscreteValues & values) const { - // evaluate all conditionals and multiply - double result = 1.0; - for(const DiscreteConditional::shared_ptr& conditional: *this) - result *= (*conditional)(values); - return result; - } - - /* ************************************************************************* */ - DiscreteValues DiscreteBayesNet::optimize() const { - // solve each node in turn in topological sort order (parents first) - DiscreteValues result; - for (auto conditional: boost::adaptors::reverse(*this)) - conditional->solveInPlace(&result); - return result; - } - - /* ************************************************************************* */ - DiscreteValues DiscreteBayesNet::sample() const { - // sample each node in turn in topological sort order (parents first) - DiscreteValues result; - for (auto conditional: boost::adaptors::reverse(*this)) - conditional->sampleInPlace(&result); - return result; - } - - /* *********************************************************************** */ - std::string DiscreteBayesNet::markdown( - const KeyFormatter& keyFormatter, - const DiscreteFactor::Names& names) const { - using std::endl; - std::stringstream ss; - ss << "`DiscreteBayesNet` of size " << size() << endl << endl; - for (const DiscreteConditional::shared_ptr& conditional : *this) - ss << conditional->markdown(keyFormatter, names) << endl; - return ss.str(); - } - - /* *********************************************************************** */ - std::string DiscreteBayesNet::html( - const KeyFormatter& keyFormatter, - const DiscreteFactor::Names& names) const { - using std::endl; - std::stringstream ss; - ss << "

DiscreteBayesNet of size " << size() << "

"; - for (const DiscreteConditional::shared_ptr& conditional : *this) - ss << conditional->html(keyFormatter, names) << endl; - return ss.str(); - } +// Instantiate base class +template class FactorGraph; /* ************************************************************************* */ -} // namespace +bool DiscreteBayesNet::equals(const This& bn, double tol) const { + return Base::equals(bn, tol); +} + +/* ************************************************************************* */ +double DiscreteBayesNet::evaluate(const DiscreteValues& values) const { + // evaluate all conditionals and multiply + double result = 1.0; + for (const DiscreteConditional::shared_ptr& conditional : *this) + result *= (*conditional)(values); + return result; +} + +/* ************************************************************************* */ +DiscreteValues DiscreteBayesNet::optimize() const { + DiscreteValues result; + return optimize(result); +} + +DiscreteValues DiscreteBayesNet::optimize(DiscreteValues result) const { + // solve each node in turn in topological sort order (parents first) + for (auto conditional : boost::adaptors::reverse(*this)) + conditional->solveInPlace(&result); + return result; +} + +/* ************************************************************************* */ +DiscreteValues DiscreteBayesNet::sample() const { + DiscreteValues result; + return sample(result); +} + +DiscreteValues DiscreteBayesNet::sample(DiscreteValues result) const { + // sample each node in turn in topological sort order (parents first) + for (auto conditional : boost::adaptors::reverse(*this)) + conditional->sampleInPlace(&result); + return result; +} + +/* *********************************************************************** */ +std::string DiscreteBayesNet::markdown( + const KeyFormatter& keyFormatter, + const DiscreteFactor::Names& names) const { + using std::endl; + std::stringstream ss; + ss << "`DiscreteBayesNet` of size " << size() << endl << endl; + for (const DiscreteConditional::shared_ptr& conditional : *this) + ss << conditional->markdown(keyFormatter, names) << endl; + return ss.str(); +} + +/* *********************************************************************** */ +std::string DiscreteBayesNet::html(const KeyFormatter& keyFormatter, + const DiscreteFactor::Names& names) const { + using std::endl; + std::stringstream ss; + ss << "

DiscreteBayesNet of size " << size() << "

"; + for (const DiscreteConditional::shared_ptr& conditional : *this) + ss << conditional->html(keyFormatter, names) << endl; + return ss.str(); +} + +/* ************************************************************************* */ +} // namespace gtsam diff --git a/gtsam/discrete/DiscreteBayesNet.h b/gtsam/discrete/DiscreteBayesNet.h index db20e7223a..bd5536135a 100644 --- a/gtsam/discrete/DiscreteBayesNet.h +++ b/gtsam/discrete/DiscreteBayesNet.h @@ -99,13 +99,47 @@ namespace gtsam { } /** - * Solve the DiscreteBayesNet by back-substitution + * @brief solve by back-substitution. + * + * Assumes the Bayes net is reverse topologically sorted, i.e. last + * conditional will be optimized first. If the Bayes net resulted from + * eliminating a factor graph, this is true for the elimination ordering. + * + * @return a sampled value for all variables. */ DiscreteValues optimize() const; - /** Do ancestral sampling */ + /** + * @brief solve by back-substitution, given certain variables. + * + * Assumes the Bayes net is reverse topologically sorted *and* that the + * Bayes net does not contain any conditionals for the given values. + * + * @return given values extended with optimized value for other variables. + */ + DiscreteValues optimize(DiscreteValues given) const; + + /** + * @brief do ancestral sampling + * + * Assumes the Bayes net is reverse topologically sorted, i.e. last + * conditional will be sampled first. If the Bayes net resulted from + * eliminating a factor graph, this is true for the elimination ordering. + * + * @return a sampled value for all variables. + */ DiscreteValues sample() const; + /** + * @brief do ancestral sampling, given certain variables. + * + * Assumes the Bayes net is reverse topologically sorted *and* that the + * Bayes net does not contain any conditionals for the given values. + * + * @return given values extended with sampled value for all other variables. + */ + DiscreteValues sample(DiscreteValues given) const; + ///@} /// @name Wrapper support /// @{ diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 7ce4bd9021..e4af27eb19 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -165,7 +165,9 @@ class DiscreteBayesNet { gtsam::DefaultKeyFormatter) const; double operator()(const gtsam::DiscreteValues& values) const; gtsam::DiscreteValues optimize() const; + gtsam::DiscreteValues optimize(gtsam::DiscreteValues given) const; gtsam::DiscreteValues sample() const; + gtsam::DiscreteValues sample(gtsam::DiscreteValues given) const; string markdown(const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; string markdown(const gtsam::KeyFormatter& keyFormatter, diff --git a/python/gtsam/tests/test_DiscreteBayesNet.py b/python/gtsam/tests/test_DiscreteBayesNet.py index 36f0d153d9..6abd660cfc 100644 --- a/python/gtsam/tests/test_DiscreteBayesNet.py +++ b/python/gtsam/tests/test_DiscreteBayesNet.py @@ -17,6 +17,17 @@ DiscreteKeys, DiscreteDistribution, DiscreteValues, Ordering) from gtsam.utils.test_case import GtsamTestCase +# Some keys: +Asia = (0, 2) +Smoking = (4, 2) +Tuberculosis = (3, 2) +LungCancer = (6, 2) + +Bronchitis = (7, 2) +Either = (5, 2) +XRay = (2, 2) +Dyspnea = (1, 2) + class TestDiscreteBayesNet(GtsamTestCase): """Tests for Discrete Bayes Nets.""" @@ -43,16 +54,6 @@ def test_constructor(self): def test_Asia(self): """Test full Asia example.""" - Asia = (0, 2) - Smoking = (4, 2) - Tuberculosis = (3, 2) - LungCancer = (6, 2) - - Bronchitis = (7, 2) - Either = (5, 2) - XRay = (2, 2) - Dyspnea = (1, 2) - asia = DiscreteBayesNet() asia.add(Asia, "99/1") asia.add(Smoking, "50/50") @@ -107,6 +108,28 @@ def test_Asia(self): actualSample = chordal2.sample() self.assertEqual(len(actualSample), 8) + def test_fragment(self): + """Test sampling and optimizing for Asia fragment.""" + + # Create a reverse-topologically sorted fragment: + fragment = DiscreteBayesNet() + fragment.add(Either, [Tuberculosis, LungCancer], "F T T T") + fragment.add(Tuberculosis, [Asia], "99/1 95/5") + fragment.add(LungCancer, [Smoking], "99/1 90/10") + + # Create assignment with missing values: + given = DiscreteValues() + for key in [Asia, Smoking]: + given[key[0]] = 0 + + # Now optimize fragment: + actual = fragment.optimize(given) + self.assertEqual(len(actual), 5) + + # Now sample from fragment: + actual = fragment.sample(given) + self.assertEqual(len(actual), 5) + if __name__ == "__main__": unittest.main() From 2430917e03b42500446611b5c64a641d124c6991 Mon Sep 17 00:00:00 2001 From: Calvin Date: Tue, 18 Jan 2022 12:57:48 -0600 Subject: [PATCH 15/91] Removed a spurious commented line and included a comment about what the K value signifies. --- python/gtsam/utils/plot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/gtsam/utils/plot.py b/python/gtsam/utils/plot.py index 32f07179b7..a632b852a2 100644 --- a/python/gtsam/utils/plot.py +++ b/python/gtsam/utils/plot.py @@ -125,7 +125,10 @@ def plot_point2_on_axes(axes, if P is not None: w, v = np.linalg.eig(P) - # k = 2.296 + # "Sigma" value for drawing the uncertainty ellipse. 5 sigma corresponds + # to a 99.9999% confidence, i.e. assuming the estimation has been + # computed properly, there is a 99.999% chance that the true position + # of the point will lie within the uncertainty ellipse. k = 5.0 angle = np.arctan2(v[1, 0], v[0, 0]) From 7557bd990a2844803186f98c6ea461122a682d76 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 18 Jan 2022 17:33:46 -0500 Subject: [PATCH 16/91] Some reformatting/docs/using --- gtsam/discrete/DiscreteFactorGraph.h | 43 +++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 08c3d893d9..1da840eb8e 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -64,33 +64,35 @@ template<> struct EliminationTraits * A Discrete Factor Graph is a factor graph where all factors are Discrete, i.e. * Factor == DiscreteFactor */ -class GTSAM_EXPORT DiscreteFactorGraph: public FactorGraph, -public EliminateableFactorGraph { -public: +class GTSAM_EXPORT DiscreteFactorGraph + : public FactorGraph, + public EliminateableFactorGraph { + public: + using This = DiscreteFactorGraph; ///< this class + using Base = FactorGraph; ///< base factor graph type + using BaseEliminateable = + EliminateableFactorGraph; ///< for elimination + using shared_ptr = boost::shared_ptr; ///< shared_ptr to This - typedef DiscreteFactorGraph This; ///< Typedef to this class - typedef FactorGraph Base; ///< Typedef to base factor graph type - typedef EliminateableFactorGraph BaseEliminateable; ///< Typedef to base elimination class - typedef boost::shared_ptr shared_ptr; ///< shared_ptr to this class + using Values = DiscreteValues; ///< backwards compatibility - using Values = DiscreteValues; ///< backwards compatibility - - /** A map from keys to values */ - typedef KeyVector Indices; + using Indices = KeyVector; ///> map from keys to values /** Default constructor */ DiscreteFactorGraph() {} /** Construct from iterator over factors */ - template - DiscreteFactorGraph(ITERATOR firstFactor, ITERATOR lastFactor) : Base(firstFactor, lastFactor) {} + template + DiscreteFactorGraph(ITERATOR firstFactor, ITERATOR lastFactor) + : Base(firstFactor, lastFactor) {} /** Construct from container of factors (shared_ptr or plain objects) */ - template + template explicit DiscreteFactorGraph(const CONTAINER& factors) : Base(factors) {} - /** Implicit copy/downcast constructor to override explicit template container constructor */ - template + /** Implicit copy/downcast constructor to override explicit template container + * constructor */ + template DiscreteFactorGraph(const FactorGraph& graph) : Base(graph) {} /// Destructor @@ -108,7 +110,7 @@ public EliminateableFactorGraph { void add(Args&&... args) { emplace_shared(std::forward(args)...); } - + /** Return the set of variables involved in the factors (set union) */ KeySet keys() const; @@ -163,9 +165,10 @@ public EliminateableFactorGraph { const DiscreteFactor::Names& names = {}) const; /// @} -}; // \ DiscreteFactorGraph +}; // \ DiscreteFactorGraph /// traits -template<> struct traits : public Testable {}; +template <> +struct traits : public Testable {}; -} // \ namespace gtsam +} // namespace gtsam From 1702c20a14b2719bde9fba89c2f3ca34c583e2eb Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 18 Jan 2022 17:33:56 -0500 Subject: [PATCH 17/91] Wrap push_back methods --- gtsam/discrete/discrete.i | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index e4af27eb19..539c15997b 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -230,11 +230,16 @@ class DiscreteFactorGraph { DiscreteFactorGraph(); DiscreteFactorGraph(const gtsam::DiscreteBayesNet& bayesNet); - void add(const gtsam::DiscreteKey& j, string table); + // Building the graph + void push_back(const gtsam::DiscreteFactor* factor); + void push_back(const gtsam::DiscreteConditional* conditional); + void push_back(const gtsam::DiscreteFactorGraph& graph); + void push_back(const gtsam::DiscreteBayesNet& bayesNet); + void push_back(const gtsam::DiscreteBayesTree& bayesTree); + void add(const gtsam::DiscreteKey& j, string spec); void add(const gtsam::DiscreteKey& j, const std::vector& spec); - - void add(const gtsam::DiscreteKeys& keys, string table); - void add(const std::vector& keys, string table); + void add(const gtsam::DiscreteKeys& keys, string spec); + void add(const std::vector& keys, string spec); bool empty() const; size_t size() const; From 2413fcb91f8d5227b18e9c1b4b7b32c9ccadf111 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 18 Jan 2022 20:10:18 -0500 Subject: [PATCH 18/91] Change default to not confuse people --- gtsam/inference/DotWriter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtsam/inference/DotWriter.h b/gtsam/inference/DotWriter.h index bd36da496c..a606d67df8 100644 --- a/gtsam/inference/DotWriter.h +++ b/gtsam/inference/DotWriter.h @@ -38,7 +38,7 @@ struct GTSAM_EXPORT DotWriter { explicit DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, bool plotFactorPoints = true, - bool connectKeysToFactor = true, bool binaryEdges = true) + bool connectKeysToFactor = true, bool binaryEdges = false) : figureWidthInches(figureWidthInches), figureHeightInches(figureHeightInches), plotFactorPoints(plotFactorPoints), From 4a10ea89a560b63963e4679a7672f967259c4520 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 18 Jan 2022 20:10:49 -0500 Subject: [PATCH 19/91] New, more powerful choose, yields a Conditional now --- gtsam/discrete/DiscreteConditional.cpp | 70 +++++++++---------- gtsam/discrete/DiscreteConditional.h | 17 ++++- gtsam/discrete/discrete.i | 3 +- .../tests/testDiscreteConditional.cpp | 28 ++++++++ 4 files changed, 75 insertions(+), 43 deletions(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index e8aa4511d8..77728051c7 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -149,61 +149,58 @@ void DiscreteConditional::print(const string& s, /* ******************************************************************************** */ bool DiscreteConditional::equals(const DiscreteFactor& other, - double tol) const { - if (!dynamic_cast(&other)) + double tol) const { + if (!dynamic_cast(&other)) { return false; - else { - const DecisionTreeFactor& f( - static_cast(other)); + } else { + const DecisionTreeFactor& f(static_cast(other)); return DecisionTreeFactor::equals(f, tol); } } -/* ******************************************************************************** */ +/* ************************************************************************** */ static DiscreteConditional::ADT Choose(const DiscreteConditional& conditional, - const DiscreteValues& parentsValues) { + const DiscreteValues& given, + bool forceComplete = true) { // Get the big decision tree with all the levels, and then go down the // branches based on the value of the parent variables. DiscreteConditional::ADT adt(conditional); size_t value; for (Key j : conditional.parents()) { try { - value = parentsValues.at(j); + value = given.at(j); adt = adt.choose(j, value); // ADT keeps getting smaller. } catch (std::out_of_range&) { - parentsValues.print("parentsValues: "); - throw runtime_error("DiscreteConditional::choose: parent value missing"); - }; + if (forceComplete) { + given.print("parentsValues: "); + throw runtime_error( + "DiscreteConditional::Choose: parent value missing"); + } + } } return adt; } -/* ******************************************************************************** */ -DecisionTreeFactor::shared_ptr DiscreteConditional::choose( - const DiscreteValues& parentsValues) const { - // Get the big decision tree with all the levels, and then go down the - // branches based on the value of the parent variables. - ADT adt(*this); - size_t value; - for (Key j : parents()) { - try { - value = parentsValues.at(j); - adt = adt.choose(j, value); // ADT keeps getting smaller. - } catch (exception&) { - parentsValues.print("parentsValues: "); - throw runtime_error("DiscreteConditional::choose: parent value missing"); - }; - } +/* ************************************************************************** */ +DiscreteConditional::shared_ptr DiscreteConditional::choose( + const DiscreteValues& given) const { + ADT adt = Choose(*this, given, false); // P(F|S=given) - // Convert ADT to factor. - DiscreteKeys discreteKeys; + // Collect all keys not in given. + DiscreteKeys dKeys; for (Key j : frontals()) { - discreteKeys.emplace_back(j, this->cardinality(j)); + dKeys.emplace_back(j, this->cardinality(j)); } - return boost::make_shared(discreteKeys, adt); + for (size_t i = nrFrontals(); i < size(); i++) { + Key j = keys_[i]; + if (given.count(j) == 0) { + dKeys.emplace_back(j, this->cardinality(j)); + } + } + return boost::make_shared(nrFrontals(), dKeys, adt); } -/* ******************************************************************************** */ +/* ************************************************************************** */ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( const DiscreteValues& frontalValues) const { // Get the big decision tree with all the levels, and then go down the @@ -217,7 +214,7 @@ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( } catch (exception&) { frontalValues.print("frontalValues: "); throw runtime_error("DiscreteConditional::choose: frontal value missing"); - }; + } } // Convert ADT to factor. @@ -242,7 +239,6 @@ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( /* ************************************************************************** */ void DiscreteConditional::solveInPlace(DiscreteValues* values) const { - // TODO(Abhijit): is this really the fastest way? He thinks it is. ADT pFS = Choose(*this, *values); // P(F|S=parentsValues) // Initialize @@ -276,11 +272,9 @@ void DiscreteConditional::sampleInPlace(DiscreteValues* values) const { (*values)[j] = sampled; // store result in partial solution } -/* ******************************************************************************** */ +/* ************************************************************************** */ size_t DiscreteConditional::solve(const DiscreteValues& parentsValues) const { - - // TODO: is this really the fastest way? I think it is. - ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) + ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) // Then, find the max over all remaining // TODO, only works for one key now, seems horribly slow this way diff --git a/gtsam/discrete/DiscreteConditional.h b/gtsam/discrete/DiscreteConditional.h index c3c8a66def..5908cc782e 100644 --- a/gtsam/discrete/DiscreteConditional.h +++ b/gtsam/discrete/DiscreteConditional.h @@ -157,9 +157,20 @@ class GTSAM_EXPORT DiscreteConditional return ADT::operator()(values); } - /** Restrict to given parent values, returns DecisionTreeFactor */ - DecisionTreeFactor::shared_ptr choose( - const DiscreteValues& parentsValues) const; + /** + * @brief restrict to given *parent* values. + * + * Note: does not need be complete set. Examples: + * + * P(C|D,E) + . -> P(C|D,E) + * P(C|D,E) + E -> P(C|D) + * P(C|D,E) + D -> P(C|E) + * P(C|D,E) + D,E -> P(C) + * P(C|D,E) + C -> error! + * + * @return a shared_ptr to a new DiscreteConditional + */ + shared_ptr choose(const DiscreteValues& given) const; /** Convert to a likelihood factor by providing value before bar. */ DecisionTreeFactor::shared_ptr likelihood( diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 539c15997b..56255e570e 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -107,8 +107,7 @@ virtual class DiscreteConditional : gtsam::DecisionTreeFactor { void printSignature( string s = "Discrete Conditional: ", const gtsam::KeyFormatter& formatter = gtsam::DefaultKeyFormatter) const; - gtsam::DecisionTreeFactor* choose( - const gtsam::DiscreteValues& parentsValues) const; + gtsam::DecisionTreeFactor* choose(const gtsam::DiscreteValues& given) const; gtsam::DecisionTreeFactor* likelihood( const gtsam::DiscreteValues& frontalValues) const; gtsam::DecisionTreeFactor* likelihood(size_t value) const; diff --git a/gtsam/discrete/tests/testDiscreteConditional.cpp b/gtsam/discrete/tests/testDiscreteConditional.cpp index 1256595170..c2d941eaa7 100644 --- a/gtsam/discrete/tests/testDiscreteConditional.cpp +++ b/gtsam/discrete/tests/testDiscreteConditional.cpp @@ -221,6 +221,34 @@ TEST(DiscreteConditional, likelihood) { EXPECT(assert_equal(expected1, *actual1, 1e-9)); } +/* ************************************************************************* */ +// Check choose on P(C|D,E) +TEST(DiscreteConditional, choose) { + DiscreteKey C(2, 2), D(4, 2), E(3, 2); + DiscreteConditional C_given_DE((C | D, E) = "4/1 1/1 1/1 1/4"); + + // Case 1: no given values: no-op + DiscreteValues given; + auto actual1 = C_given_DE.choose(given); + EXPECT(assert_equal(C_given_DE, *actual1, 1e-9)); + + // Case 2: 1 given value + given[D.first] = 1; + auto actual2 = C_given_DE.choose(given); + EXPECT_LONGS_EQUAL(1, actual2->nrFrontals()); + EXPECT_LONGS_EQUAL(1, actual2->nrParents()); + DiscreteConditional expected2(C | E = "1/1 1/4"); + EXPECT(assert_equal(expected2, *actual2, 1e-9)); + + // Case 2: 2 given values + given[E.first] = 0; + auto actual3 = C_given_DE.choose(given); + EXPECT_LONGS_EQUAL(1, actual3->nrFrontals()); + EXPECT_LONGS_EQUAL(0, actual3->nrParents()); + DiscreteConditional expected3(C % "1/1"); + EXPECT(assert_equal(expected3, *actual3, 1e-9)); +} + /* ************************************************************************* */ // Check markdown representation looks as expected, no parents. TEST(DiscreteConditional, markdown_prior) { From 75dff3272b1b684a6511859b54996ed4ae83b7d0 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 19 Jan 2022 12:32:22 -0500 Subject: [PATCH 20/91] Fix unit tests after default changed --- .../tests/testDiscreteFactorGraph.cpp | 16 +++++++++---- tests/testNonlinearFactorGraph.cpp | 24 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index ef9efbe026..c591881f81 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -376,8 +376,12 @@ TEST(DiscreteFactorGraph, Dot) { " var1[label=\"1\"];\n" " var2[label=\"2\"];\n" "\n" - " var0--var1;\n" - " var0--var2;\n" + " factor0[label=\"\", shape=point];\n" + " var0--factor0;\n" + " var1--factor0;\n" + " factor1[label=\"\", shape=point];\n" + " var0--factor1;\n" + " var2--factor1;\n" "}\n"; EXPECT(actual == expected); } @@ -401,8 +405,12 @@ TEST(DiscreteFactorGraph, DotWithNames) { " var1[label=\"A\"];\n" " var2[label=\"B\"];\n" "\n" - " var0--var1;\n" - " var0--var2;\n" + " factor0[label=\"\", shape=point];\n" + " var0--factor0;\n" + " var1--factor0;\n" + " factor1[label=\"\", shape=point];\n" + " var0--factor1;\n" + " var2--factor1;\n" "}\n"; EXPECT(actual == expected); } diff --git a/tests/testNonlinearFactorGraph.cpp b/tests/testNonlinearFactorGraph.cpp index 8a360e4542..e1a88d6169 100644 --- a/tests/testNonlinearFactorGraph.cpp +++ b/tests/testNonlinearFactorGraph.cpp @@ -341,9 +341,15 @@ TEST(NonlinearFactorGraph, dot) { "\n" " factor0[label=\"\", shape=point];\n" " var8646911284551352321--factor0;\n" - " var8646911284551352321--var8646911284551352322;\n" - " var8646911284551352321--var7782220156096217089;\n" - " var8646911284551352322--var7782220156096217089;\n" + " factor1[label=\"\", shape=point];\n" + " var8646911284551352321--factor1;\n" + " var8646911284551352322--factor1;\n" + " factor2[label=\"\", shape=point];\n" + " var8646911284551352321--factor2;\n" + " var7782220156096217089--factor2;\n" + " factor3[label=\"\", shape=point];\n" + " var8646911284551352322--factor3;\n" + " var7782220156096217089--factor3;\n" "}\n"; const NonlinearFactorGraph fg = createNonlinearFactorGraph(); @@ -363,9 +369,15 @@ TEST(NonlinearFactorGraph, dot_extra) { "\n" " factor0[label=\"\", shape=point];\n" " var8646911284551352321--factor0;\n" - " var8646911284551352321--var8646911284551352322;\n" - " var8646911284551352321--var7782220156096217089;\n" - " var8646911284551352322--var7782220156096217089;\n" + " factor1[label=\"\", shape=point];\n" + " var8646911284551352321--factor1;\n" + " var8646911284551352322--factor1;\n" + " factor2[label=\"\", shape=point];\n" + " var8646911284551352321--factor2;\n" + " var7782220156096217089--factor2;\n" + " factor3[label=\"\", shape=point];\n" + " var8646911284551352322--factor3;\n" + " var7782220156096217089--factor3;\n" "}\n"; const NonlinearFactorGraph fg = createNonlinearFactorGraph(); From e9d7843c3e528dfa77b1a55670407477899b1fe7 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 19 Jan 2022 15:14:22 -0500 Subject: [PATCH 21/91] Add formatter --- gtsam/discrete/DiscreteConditional.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index 77728051c7..eb31d2e1ea 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -143,7 +143,7 @@ void DiscreteConditional::print(const string& s, } } cout << "):\n"; - ADT::print(""); + ADT::print("", formatter); cout << endl; } From a1f5ae0a898b780a3cd95884f8300b4d84344017 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 19 Jan 2022 15:31:56 -0500 Subject: [PATCH 22/91] Wrap partial eliminate --- gtsam/discrete/discrete.i | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 56255e570e..e2310f4344 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -262,8 +262,12 @@ class DiscreteFactorGraph { gtsam::DiscreteBayesNet eliminateSequential(); gtsam::DiscreteBayesNet eliminateSequential(const gtsam::Ordering& ordering); + std::pair + eliminatePartialSequential(const gtsam::Ordering& ordering); gtsam::DiscreteBayesTree eliminateMultifrontal(); gtsam::DiscreteBayesTree eliminateMultifrontal(const gtsam::Ordering& ordering); + std::pair + eliminatePartialMultifrontal(const gtsam::Ordering& ordering); string markdown(const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; From 640a3b82efe457682b79ece379300ceb93d77097 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 19 Jan 2022 17:24:12 -0500 Subject: [PATCH 23/91] Use key formatter for dot --- .../tests/testDiscreteFactorGraph.cpp | 14 +++---- gtsam/inference/DotWriter.cpp | 23 ++++++----- gtsam/inference/DotWriter.h | 7 +--- gtsam/inference/FactorGraph-inst.h | 2 +- gtsam/nonlinear/NonlinearFactorGraph.cpp | 7 +++- tests/testNonlinearFactorGraph.cpp | 40 +++++++++---------- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index c591881f81..579244c57f 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -401,16 +401,16 @@ TEST(DiscreteFactorGraph, DotWithNames) { "graph {\n" " size=\"5,5\";\n" "\n" - " var0[label=\"C\"];\n" - " var1[label=\"A\"];\n" - " var2[label=\"B\"];\n" + " varC[label=\"C\"];\n" + " varA[label=\"A\"];\n" + " varB[label=\"B\"];\n" "\n" " factor0[label=\"\", shape=point];\n" - " var0--factor0;\n" - " var1--factor0;\n" + " varC--factor0;\n" + " varA--factor0;\n" " factor1[label=\"\", shape=point];\n" - " var0--factor1;\n" - " var2--factor1;\n" + " varC--factor1;\n" + " varB--factor1;\n" "}\n"; EXPECT(actual == expected); } diff --git a/gtsam/inference/DotWriter.cpp b/gtsam/inference/DotWriter.cpp index fb3ea05054..18130c35d7 100644 --- a/gtsam/inference/DotWriter.cpp +++ b/gtsam/inference/DotWriter.cpp @@ -35,7 +35,8 @@ void DotWriter::DrawVariable(Key key, const KeyFormatter& keyFormatter, const boost::optional& position, ostream* os) { // Label the node with the label from the KeyFormatter - *os << " var" << key << "[label=\"" << keyFormatter(key) << "\""; + *os << " var" << keyFormatter(key) << "[label=\"" << keyFormatter(key) + << "\""; if (position) { *os << ", pos=\"" << position->x() << "," << position->y() << "!\""; } @@ -51,22 +52,26 @@ void DotWriter::DrawFactor(size_t i, const boost::optional& position, *os << "];\n"; } -void DotWriter::ConnectVariables(Key key1, Key key2, ostream* os) { - *os << " var" << key1 << "--" - << "var" << key2 << ";\n"; +static void ConnectVariables(Key key1, Key key2, + const KeyFormatter& keyFormatter, + ostream* os) { + *os << " var" << keyFormatter(key1) << "--" + << "var" << keyFormatter(key2) << ";\n"; } -void DotWriter::ConnectVariableFactor(Key key, size_t i, ostream* os) { - *os << " var" << key << "--" +static void ConnectVariableFactor(Key key, const KeyFormatter& keyFormatter, + size_t i, ostream* os) { + *os << " var" << keyFormatter(key) << "--" << "factor" << i << ";\n"; } void DotWriter::processFactor(size_t i, const KeyVector& keys, + const KeyFormatter& keyFormatter, const boost::optional& position, ostream* os) const { if (plotFactorPoints) { if (binaryEdges && keys.size() == 2) { - ConnectVariables(keys[0], keys[1], os); + ConnectVariables(keys[0], keys[1], keyFormatter, os); } else { // Create dot for the factor. DrawFactor(i, position, os); @@ -74,7 +79,7 @@ void DotWriter::processFactor(size_t i, const KeyVector& keys, // Make factor-variable connections if (connectKeysToFactor) { for (Key key : keys) { - ConnectVariableFactor(key, i, os); + ConnectVariableFactor(key, keyFormatter, i, os); } } } @@ -83,7 +88,7 @@ void DotWriter::processFactor(size_t i, const KeyVector& keys, for (Key key1 : keys) { for (Key key2 : keys) { if (key2 > key1) { - ConnectVariables(key1, key2, os); + ConnectVariables(key1, key2, keyFormatter, os); } } } diff --git a/gtsam/inference/DotWriter.h b/gtsam/inference/DotWriter.h index a606d67df8..93c229c2b1 100644 --- a/gtsam/inference/DotWriter.h +++ b/gtsam/inference/DotWriter.h @@ -57,14 +57,9 @@ struct GTSAM_EXPORT DotWriter { static void DrawFactor(size_t i, const boost::optional& position, std::ostream* os); - /// Connect two variables. - static void ConnectVariables(Key key1, Key key2, std::ostream* os); - - /// Connect variable and factor. - static void ConnectVariableFactor(Key key, size_t i, std::ostream* os); - /// Draw a single factor, specified by its index i and its variable keys. void processFactor(size_t i, const KeyVector& keys, + const KeyFormatter& keyFormatter, const boost::optional& position, std::ostream* os) const; }; diff --git a/gtsam/inference/FactorGraph-inst.h b/gtsam/inference/FactorGraph-inst.h index 058075f2d5..3ea17fc7ff 100644 --- a/gtsam/inference/FactorGraph-inst.h +++ b/gtsam/inference/FactorGraph-inst.h @@ -144,7 +144,7 @@ void FactorGraph::dot(std::ostream& os, const auto& factor = at(i); if (factor) { const KeyVector& factorKeys = factor->keys(); - writer.processFactor(i, factorKeys, boost::none, &os); + writer.processFactor(i, factorKeys, keyFormatter, boost::none, &os); } } diff --git a/gtsam/nonlinear/NonlinearFactorGraph.cpp b/gtsam/nonlinear/NonlinearFactorGraph.cpp index 89236ea878..da8935d5fc 100644 --- a/gtsam/nonlinear/NonlinearFactorGraph.cpp +++ b/gtsam/nonlinear/NonlinearFactorGraph.cpp @@ -33,8 +33,10 @@ # include #endif +#include #include #include +#include using namespace std; @@ -127,7 +129,7 @@ void NonlinearFactorGraph::dot(std::ostream& os, const Values& values, // Create factors and variable connections size_t i = 0; for (const KeyVector& factorKeys : structure) { - writer.processFactor(i++, factorKeys, boost::none, &os); + writer.processFactor(i++, factorKeys, keyFormatter, boost::none, &os); } } else { // Create factors and variable connections @@ -135,7 +137,8 @@ void NonlinearFactorGraph::dot(std::ostream& os, const Values& values, const NonlinearFactor::shared_ptr& factor = at(i); if (factor) { const KeyVector& factorKeys = factor->keys(); - writer.processFactor(i, factorKeys, writer.factorPos(min, i), &os); + writer.processFactor(i, factorKeys, keyFormatter, + writer.factorPos(min, i), &os); } } } diff --git a/tests/testNonlinearFactorGraph.cpp b/tests/testNonlinearFactorGraph.cpp index e1a88d6169..05a6e7f45e 100644 --- a/tests/testNonlinearFactorGraph.cpp +++ b/tests/testNonlinearFactorGraph.cpp @@ -335,21 +335,21 @@ TEST(NonlinearFactorGraph, dot) { "graph {\n" " size=\"5,5\";\n" "\n" - " var7782220156096217089[label=\"l1\"];\n" - " var8646911284551352321[label=\"x1\"];\n" - " var8646911284551352322[label=\"x2\"];\n" + " varl1[label=\"l1\"];\n" + " varx1[label=\"x1\"];\n" + " varx2[label=\"x2\"];\n" "\n" " factor0[label=\"\", shape=point];\n" - " var8646911284551352321--factor0;\n" + " varx1--factor0;\n" " factor1[label=\"\", shape=point];\n" - " var8646911284551352321--factor1;\n" - " var8646911284551352322--factor1;\n" + " varx1--factor1;\n" + " varx2--factor1;\n" " factor2[label=\"\", shape=point];\n" - " var8646911284551352321--factor2;\n" - " var7782220156096217089--factor2;\n" + " varx1--factor2;\n" + " varl1--factor2;\n" " factor3[label=\"\", shape=point];\n" - " var8646911284551352322--factor3;\n" - " var7782220156096217089--factor3;\n" + " varx2--factor3;\n" + " varl1--factor3;\n" "}\n"; const NonlinearFactorGraph fg = createNonlinearFactorGraph(); @@ -363,21 +363,21 @@ TEST(NonlinearFactorGraph, dot_extra) { "graph {\n" " size=\"5,5\";\n" "\n" - " var7782220156096217089[label=\"l1\", pos=\"0,0!\"];\n" - " var8646911284551352321[label=\"x1\", pos=\"1,0!\"];\n" - " var8646911284551352322[label=\"x2\", pos=\"1,1.5!\"];\n" + " varl1[label=\"l1\", pos=\"0,0!\"];\n" + " varx1[label=\"x1\", pos=\"1,0!\"];\n" + " varx2[label=\"x2\", pos=\"1,1.5!\"];\n" "\n" " factor0[label=\"\", shape=point];\n" - " var8646911284551352321--factor0;\n" + " varx1--factor0;\n" " factor1[label=\"\", shape=point];\n" - " var8646911284551352321--factor1;\n" - " var8646911284551352322--factor1;\n" + " varx1--factor1;\n" + " varx2--factor1;\n" " factor2[label=\"\", shape=point];\n" - " var8646911284551352321--factor2;\n" - " var7782220156096217089--factor2;\n" + " varx1--factor2;\n" + " varl1--factor2;\n" " factor3[label=\"\", shape=point];\n" - " var8646911284551352322--factor3;\n" - " var7782220156096217089--factor3;\n" + " varx2--factor3;\n" + " varl1--factor3;\n" "}\n"; const NonlinearFactorGraph fg = createNonlinearFactorGraph(); From 7e956d2bb7829eb9a3e367395938875d543518c2 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 10:10:47 -0500 Subject: [PATCH 24/91] Fix docs --- gtsam/discrete/DiscreteConditional.cpp | 6 +++--- gtsam/discrete/DiscreteConditional.h | 2 +- gtsam/inference/Conditional.h | 7 ++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index eb31d2e1ea..8c0f91807a 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -248,17 +248,17 @@ void DiscreteConditional::solveInPlace(DiscreteValues* values) const { // Get all Possible Configurations const auto allPosbValues = frontalAssignments(); - // Find the MPE + // Find the maximum for (const auto& frontalVals : allPosbValues) { double pValueS = pFS(frontalVals); // P(F=value|S=parentsValues) - // Update MPE solution if better + // Update maximum solution if better if (pValueS > maxP) { maxP = pValueS; mpe = frontalVals; } } - // set values (inPlace) to mpe + // set values (inPlace) to maximum for (Key j : frontals()) { (*values)[j] = mpe[j]; } diff --git a/gtsam/discrete/DiscreteConditional.h b/gtsam/discrete/DiscreteConditional.h index 5908cc782e..de9d949714 100644 --- a/gtsam/discrete/DiscreteConditional.h +++ b/gtsam/discrete/DiscreteConditional.h @@ -182,7 +182,7 @@ class GTSAM_EXPORT DiscreteConditional /** * solve a conditional * @param parentsValues Known values of the parents - * @return MPE value of the child (1 frontal variable). + * @return maximum value for the (single) frontal variable. */ size_t solve(const DiscreteValues& parentsValues) const; diff --git a/gtsam/inference/Conditional.h b/gtsam/inference/Conditional.h index 295122879e..7594da78d0 100644 --- a/gtsam/inference/Conditional.h +++ b/gtsam/inference/Conditional.h @@ -25,15 +25,12 @@ namespace gtsam { /** - * TODO: Update comments. The following comments are out of date!!! - * - * Base class for conditional densities, templated on KEY type. This class - * provides storage for the keys involved in a conditional, and iterators and + * Base class for conditional densities. This class iterators and * access to the frontal and separator keys. * * Derived classes *must* redefine the Factor and shared_ptr typedefs to refer * to the associated factor type and shared_ptr type of the derived class. See - * IndexConditional and GaussianConditional for examples. + * SymbolicConditional and GaussianConditional for examples. * \nosubgrouping */ template From 0076db7e20c77b8ce8f9c04aab2dc36bd8dd2f93 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 10:11:32 -0500 Subject: [PATCH 25/91] cleanup --- gtsam/discrete/DiscreteKey.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/gtsam/discrete/DiscreteKey.cpp b/gtsam/discrete/DiscreteKey.cpp index 5ddad22b04..121d611038 100644 --- a/gtsam/discrete/DiscreteKey.cpp +++ b/gtsam/discrete/DiscreteKey.cpp @@ -33,16 +33,13 @@ namespace gtsam { KeyVector DiscreteKeys::indices() const { KeyVector js; - for(const DiscreteKey& key: *this) - js.push_back(key.first); + for (const DiscreteKey& key : *this) js.push_back(key.first); return js; } - map DiscreteKeys::cardinalities() const { - map cs; - cs.insert(begin(),end()); -// for(const DiscreteKey& key: *this) -// cs.insert(key); + map DiscreteKeys::cardinalities() const { + map cs; + cs.insert(begin(), end()); return cs; } From 6e4f50dfacc46bf5f73570bff163d51274ece64d Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 10:12:07 -0500 Subject: [PATCH 26/91] Better print and new `max` variant --- gtsam/discrete/DecisionTreeFactor.cpp | 9 +++++++-- gtsam/discrete/DecisionTreeFactor.h | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/gtsam/discrete/DecisionTreeFactor.cpp b/gtsam/discrete/DecisionTreeFactor.cpp index ad4cbad434..9de750f2eb 100644 --- a/gtsam/discrete/DecisionTreeFactor.cpp +++ b/gtsam/discrete/DecisionTreeFactor.cpp @@ -22,6 +22,7 @@ #include #include +#include #include using namespace std; @@ -65,9 +66,13 @@ namespace gtsam { /* ************************************************************************* */ void DecisionTreeFactor::print(const string& s, - const KeyFormatter& formatter) const { + const KeyFormatter& formatter) const { cout << s; - ADT::print("Potentials:",formatter); + cout << " f["; + for (auto&& key : keys()) + cout << boost::format(" (%1%,%2%),") % formatter(key) % cardinality(key); + cout << " ]" << endl; + ADT::print("Potentials:", formatter); } /* ************************************************************************* */ diff --git a/gtsam/discrete/DecisionTreeFactor.h b/gtsam/discrete/DecisionTreeFactor.h index 8beeb4c4a0..251575739d 100644 --- a/gtsam/discrete/DecisionTreeFactor.h +++ b/gtsam/discrete/DecisionTreeFactor.h @@ -127,11 +127,16 @@ namespace gtsam { return combine(keys, ADT::Ring::add); } - /// Create new factor by maximizing over all values with the same separator values + /// Create new factor by maximizing over all values with the same separator. shared_ptr max(size_t nrFrontals) const { return combine(nrFrontals, ADT::Ring::max); } + /// Create new factor by maximizing over all values with the same separator. + shared_ptr max(const Ordering& keys) const { + return combine(keys, ADT::Ring::max); + } + /// @} /// @name Advanced Interface /// @{ From ec39197cc3fb8b3a547d2b015f52537770eb80eb Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 10:12:31 -0500 Subject: [PATCH 27/91] `optimize` now computes MPE --- gtsam/discrete/DiscreteFactorGraph.cpp | 81 ++++++-- gtsam/discrete/DiscreteFactorGraph.h | 37 ++-- .../tests/testDiscreteFactorGraph.cpp | 183 +++++++++--------- 3 files changed, 189 insertions(+), 112 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.cpp b/gtsam/discrete/DiscreteFactorGraph.cpp index c1248c60b9..d8e9aa244f 100644 --- a/gtsam/discrete/DiscreteFactorGraph.cpp +++ b/gtsam/discrete/DiscreteFactorGraph.cpp @@ -95,22 +95,74 @@ namespace gtsam { // } // } - /* ************************************************************************* */ - DiscreteValues DiscreteFactorGraph::optimize() const - { + /* ************************************************************************ */ + /** + * @brief Lookup table for max-product + * + * This inherits from a DiscreteConditional but is not normalized to 1 + * + */ + class Lookup : public DiscreteConditional { + public: + Lookup(size_t nFrontals, const DiscreteKeys& keys, const ADT& potentials) + : DiscreteConditional(nFrontals, keys, potentials) {} + }; + + // Alternate eliminate function for MPE + std::pair // + EliminateForMPE(const DiscreteFactorGraph& factors, + const Ordering& frontalKeys) { + // PRODUCT: multiply all factors + gttic(product); + DecisionTreeFactor product; + for (auto&& factor : factors) product = (*factor) * product; + gttoc(product); + + // max out frontals, this is the factor on the separator + gttic(max); + DecisionTreeFactor::shared_ptr max = product.max(frontalKeys); + gttoc(max); + + // Ordering keys for the conditional so that frontalKeys are really in front + DiscreteKeys orderedKeys; + for (auto&& key : frontalKeys) + orderedKeys.emplace_back(key, product.cardinality(key)); + for (auto&& key : max->keys()) + orderedKeys.emplace_back(key, product.cardinality(key)); + + // Make lookup with product + gttic(lookup); + size_t nrFrontals = frontalKeys.size(); + auto lookup = boost::make_shared(nrFrontals, orderedKeys, product); + gttoc(lookup); + + return std::make_pair( + boost::dynamic_pointer_cast(lookup), max); + } + + /* ************************************************************************ */ + DiscreteBayesNet::shared_ptr DiscreteFactorGraph::maxProduct( + OptionalOrderingType orderingType) const { + gttic(DiscreteFactorGraph_maxProduct); + return BaseEliminateable::eliminateSequential(orderingType, + EliminateForMPE); + } + + /* ************************************************************************ */ + DiscreteValues DiscreteFactorGraph::optimize( + OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_optimize); - return BaseEliminateable::eliminateSequential()->optimize(); + return maxProduct()->optimize(); } - /* ************************************************************************* */ + /* ************************************************************************ */ std::pair // - EliminateDiscrete(const DiscreteFactorGraph& factors, const Ordering& frontalKeys) { - + EliminateDiscrete(const DiscreteFactorGraph& factors, + const Ordering& frontalKeys) { // PRODUCT: multiply all factors gttic(product); DecisionTreeFactor product; - for(const DiscreteFactor::shared_ptr& factor: factors) - product = (*factor) * product; + for (auto&& factor : factors) product = (*factor) * product; gttoc(product); // sum out frontals, this is the factor on the separator @@ -120,15 +172,18 @@ namespace gtsam { // Ordering keys for the conditional so that frontalKeys are really in front Ordering orderedKeys; - orderedKeys.insert(orderedKeys.end(), frontalKeys.begin(), frontalKeys.end()); - orderedKeys.insert(orderedKeys.end(), sum->keys().begin(), sum->keys().end()); + orderedKeys.insert(orderedKeys.end(), frontalKeys.begin(), + frontalKeys.end()); + orderedKeys.insert(orderedKeys.end(), sum->keys().begin(), + sum->keys().end()); // now divide product/sum to get conditional gttic(divide); - DiscreteConditional::shared_ptr cond(new DiscreteConditional(product, *sum, orderedKeys)); + auto conditional = + boost::make_shared(product, *sum, orderedKeys); gttoc(divide); - return std::make_pair(cond, sum); + return std::make_pair(conditional, sum); } /* ************************************************************************ */ diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 1da840eb8e..b4e98c876c 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -128,18 +128,31 @@ class GTSAM_EXPORT DiscreteFactorGraph const std::string& s = "DiscreteFactorGraph", const KeyFormatter& formatter = DefaultKeyFormatter) const override; - /** Solve the factor graph by performing variable elimination in COLAMD order using - * the dense elimination function specified in \c function, - * followed by back-substitution resulting from elimination. Is equivalent - * to calling graph.eliminateSequential()->optimize(). */ - DiscreteValues optimize() const; - - -// /** Permute the variables in the factors */ -// GTSAM_EXPORT void permuteWithInverse(const Permutation& inversePermutation); -// -// /** Apply a reduction, which is a remapping of variable indices. */ -// GTSAM_EXPORT void reduceWithInverse(const internal::Reduction& inverseReduction); + /** + * @brief Implement the max-product algorithm + * + * @param orderingType : one of COLAMD, METIS, NATURAL, CUSTOM + * @return DiscreteBayesNet::shared_ptr DAG with lookup tables + */ + boost::shared_ptr maxProduct( + OptionalOrderingType orderingType = boost::none) const; + + /** + * @brief Find the maximum probable explanation (MPE) by doing max-product. + * + * @param orderingType + * @return DiscreteValues : MPE + */ + DiscreteValues optimize( + OptionalOrderingType orderingType = boost::none) const; + + // /** Permute the variables in the factors */ + // GTSAM_EXPORT void permuteWithInverse(const Permutation& + // inversePermutation); + // + // /** Apply a reduction, which is a remapping of variable indices. */ + // GTSAM_EXPORT void reduceWithInverse(const internal::Reduction& + // inverseReduction); /// @name Wrapper support /// @{ diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index 579244c57f..14432d08cb 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -30,8 +30,8 @@ using namespace std; using namespace gtsam; /* ************************************************************************* */ -TEST_UNSAFE( DiscreteFactorGraph, debugScheduler) { - DiscreteKey PC(0,4), ME(1, 4), AI(2, 4), A(3, 3); +TEST_UNSAFE(DiscreteFactorGraph, debugScheduler) { + DiscreteKey PC(0, 4), ME(1, 4), AI(2, 4), A(3, 3); DiscreteFactorGraph graph; graph.add(AI, "1 0 0 1"); @@ -47,25 +47,18 @@ TEST_UNSAFE( DiscreteFactorGraph, debugScheduler) { graph.add(PC & ME, "0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0"); graph.add(PC & AI, "0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0"); -// graph.print("Graph: "); - DecisionTreeFactor product = graph.product(); - DecisionTreeFactor::shared_ptr sum = product.sum(1); -// sum->print("Debug SUM: "); - DiscreteConditional::shared_ptr cond(new DiscreteConditional(product, *sum)); - -// cond->print("marginal:"); + // Check MPE. + auto actualMPE = graph.optimize(); + DiscreteValues mpe; + insert(mpe)(0, 2)(1, 1)(2, 0)(3, 0); + EXPECT(assert_equal(mpe, actualMPE)); -// pair result = EliminateDiscrete(graph, 1); -// result.first->print("BayesNet: "); -// result.second->print("New factor: "); -// + // Check Bayes Net Ordering ordering; - ordering += Key(0),Key(1),Key(2),Key(3); - DiscreteEliminationTree eliminationTree(graph, ordering); -// eliminationTree.print("Elimination tree: "); - eliminationTree.eliminate(EliminateDiscrete); -// solver.optimize(); -// DiscreteBayesNet::shared_ptr bayesNet = solver.eliminate(); + ordering += Key(0), Key(1), Key(2), Key(3); + auto chordal = graph.eliminateSequential(ordering); + // happens to be the same, but not in general! + EXPECT(assert_equal(mpe, chordal->optimize())); } /* ************************************************************************* */ @@ -115,10 +108,9 @@ TEST_UNSAFE( DiscreteFactorGraph, DiscreteFactorGraphEvaluationTest) { } /* ************************************************************************* */ -TEST( DiscreteFactorGraph, test) -{ +TEST(DiscreteFactorGraph, test) { // Declare keys and ordering - DiscreteKey C(0,2), B(1,2), A(2,2); + DiscreteKey C(0, 2), B(1, 2), A(2, 2); // A simple factor graph (A)-fAC-(C)-fBC-(B) // with smoothness priors @@ -127,7 +119,6 @@ TEST( DiscreteFactorGraph, test) graph.add(C & B, "3 1 1 3"); // Test EliminateDiscrete - // FIXME: apparently Eliminate returns a conditional rather than a net Ordering frontalKeys; frontalKeys += Key(0); DiscreteConditional::shared_ptr conditional; @@ -138,7 +129,7 @@ TEST( DiscreteFactorGraph, test) CHECK(conditional); DiscreteBayesNet expected; Signature signature((C | B, A) = "9/1 1/1 1/1 1/9"); - // cout << signature << endl; + DiscreteConditional expectedConditional(signature); EXPECT(assert_equal(expectedConditional, *conditional)); expected.add(signature); @@ -151,7 +142,6 @@ TEST( DiscreteFactorGraph, test) // add conditionals to complete expected Bayes net expected.add(B | A = "5/3 3/5"); expected.add(A % "1/1"); - // GTSAM_PRINT(expected); // Test elimination tree Ordering ordering; @@ -162,42 +152,82 @@ TEST( DiscreteFactorGraph, test) boost::tie(actual, remainingGraph) = etree.eliminate(&EliminateDiscrete); EXPECT(assert_equal(expected, *actual)); -// // Test solver -// DiscreteBayesNet::shared_ptr actual2 = solver.eliminate(); -// EXPECT(assert_equal(expected, *actual2)); + DiscreteValues mpe; + insert(mpe)(0, 0)(1, 0)(2, 0); + EXPECT_DOUBLES_EQUAL(9, graph(mpe), 1e-5); // regression + + // Check Bayes Net + auto chordal = graph.eliminateSequential(); + auto notOptimal = chordal->optimize(); + // happens to be the same but not in general! + EXPECT(assert_equal(mpe, notOptimal)); - // Test optimization - DiscreteValues expectedValues; - insert(expectedValues)(0, 0)(1, 0)(2, 0); - auto actualValues = graph.optimize(); - EXPECT(assert_equal(expectedValues, actualValues)); + // Test eliminateSequential + DiscreteBayesNet::shared_ptr actual2 = graph.eliminateSequential(ordering); + EXPECT(assert_equal(expected, *actual2)); + auto notOptimal2 = actual2->optimize(); + // happens to be the same but not in general! + EXPECT(assert_equal(mpe, notOptimal2)); + + // Test mpe + auto actualMPE = graph.optimize(); + EXPECT(assert_equal(mpe, actualMPE)); } /* ************************************************************************* */ -TEST( DiscreteFactorGraph, testMPE) -{ +TEST_UNSAFE(DiscreteFactorGraph, testMPE) { // Declare a bunch of keys - DiscreteKey C(0,2), A(1,2), B(2,2); + DiscreteKey C(0, 2), A(1, 2), B(2, 2); // Create Factor graph DiscreteFactorGraph graph; graph.add(C & A, "0.2 0.8 0.3 0.7"); graph.add(C & B, "0.1 0.9 0.4 0.6"); - // graph.product().print(); - // DiscreteSequentialSolver(graph).eliminate()->print(); + // Check MPE. + auto actualMPE = graph.optimize(); + DiscreteValues mpe; + insert(mpe)(0, 0)(1, 1)(2, 1); + EXPECT(assert_equal(mpe, actualMPE)); + + // Check Bayes Net + auto chordal = graph.eliminateSequential(); + auto notOptimal = chordal->optimize(); + // happens to be the same but not in general + EXPECT(assert_equal(mpe, notOptimal)); +} + +/* ************************************************************************* */ +TEST(DiscreteFactorGraph, marginalIsNotMPE) { + // Declare 2 keys + DiscreteKey A(0, 2), B(1, 2); + + // Create Bayes net such that marginal on A is bigger for 0 than 1, but the + // MPE does not have A=0. + DiscreteBayesNet bayesNet; + bayesNet.add(B | A = "1/1 1/2"); + bayesNet.add(A % "10/9"); + + // The expected MPE is A=1, B=1 + DiscreteValues mpe; + insert(mpe)(0, 1)(1, 1); + + // Which we verify using max-product: + DiscreteFactorGraph graph(bayesNet); auto actualMPE = graph.optimize(); + EXPECT(assert_equal(mpe, actualMPE)); + EXPECT_DOUBLES_EQUAL(0.315789, graph(mpe), 1e-5); // regression - DiscreteValues expectedMPE; - insert(expectedMPE)(0, 0)(1, 1)(2, 1); - EXPECT(assert_equal(expectedMPE, actualMPE)); + // Optimize on BayesNet maximizes marginal, then the conditional marginals: + auto notOptimal = bayesNet.optimize(); + EXPECT(graph(notOptimal) < graph(mpe)); + EXPECT_DOUBLES_EQUAL(0.263158, graph(notOptimal), 1e-5); // regression } /* ************************************************************************* */ -TEST( DiscreteFactorGraph, testMPE_Darwiche09book_p244) -{ +TEST(DiscreteFactorGraph, testMPE_Darwiche09book_p244) { // The factor graph in Darwiche09book, page 244 - DiscreteKey A(4,2), C(3,2), S(2,2), T1(0,2), T2(1,2); + DiscreteKey A(4, 2), C(3, 2), S(2, 2), T1(0, 2), T2(1, 2); // Create Factor graph DiscreteFactorGraph graph; @@ -206,53 +236,32 @@ TEST( DiscreteFactorGraph, testMPE_Darwiche09book_p244) graph.add(C & T1, "0.80 0.20 0.20 0.80"); graph.add(S & C & T2, "0.80 0.20 0.20 0.80 0.95 0.05 0.05 0.95"); graph.add(T1 & T2 & A, "1 0 0 1 0 1 1 0"); - graph.add(A, "1 0");// evidence, A = yes (first choice in Darwiche) - //graph.product().print("Darwiche-product"); - // graph.product().potentials().dot("Darwiche-product"); - // DiscreteSequentialSolver(graph).eliminate()->print(); - - DiscreteValues expectedMPE; - insert(expectedMPE)(4, 0)(2, 0)(3, 1)(0, 1)(1, 1); - - // Use the solver machinery. - DiscreteBayesNet::shared_ptr chordal = graph.eliminateSequential(); - auto actualMPE = chordal->optimize(); - EXPECT(assert_equal(expectedMPE, actualMPE)); -// DiscreteConditional::shared_ptr root = chordal->back(); -// EXPECT_DOUBLES_EQUAL(0.4, (*root)(*actualMPE), 1e-9); - - // Let us create the Bayes tree here, just for fun, because we don't use it now -// typedef JunctionTreeOrdered JT; -// GenericMultifrontalSolver solver(graph); -// BayesTreeOrdered::shared_ptr bayesTree = solver.eliminate(&EliminateDiscrete); -//// bayesTree->print("Bayes Tree"); -// EXPECT_LONGS_EQUAL(2,bayesTree->size()); + graph.add(A, "1 0"); // evidence, A = yes (first choice in Darwiche) + + DiscreteValues mpe; + insert(mpe)(4, 0)(2, 1)(3, 1)(0, 1)(1, 1); + EXPECT_DOUBLES_EQUAL(0.33858, graph(mpe), 1e-5); // regression + // You can check visually by printing product: + // graph.product().print("Darwiche-product"); + // Check MPE. + auto actualMPE = graph.optimize(); + EXPECT(assert_equal(mpe, actualMPE)); + + // Check Bayes Net Ordering ordering; - ordering += Key(0),Key(1),Key(2),Key(3),Key(4); - DiscreteBayesTree::shared_ptr bayesTree = graph.eliminateMultifrontal(ordering); + ordering += Key(0), Key(1), Key(2), Key(3), Key(4); + auto chordal = graph.eliminateSequential(ordering); + auto notOptimal = chordal->optimize(); // not MPE ! + EXPECT(graph(notOptimal) < graph(mpe)); + + // Let us create the Bayes tree here, just for fun, because we don't use it + DiscreteBayesTree::shared_ptr bayesTree = + graph.eliminateMultifrontal(ordering); // bayesTree->print("Bayes Tree"); - EXPECT_LONGS_EQUAL(2,bayesTree->size()); - -#ifdef OLD -// Create the elimination tree manually -VariableIndexOrdered structure(graph); -typedef EliminationTreeOrdered ETree; -ETree::shared_ptr eTree = ETree::Create(graph, structure); -//eTree->print(">>>>>>>>>>> Elimination Tree <<<<<<<<<<<<<<<<<"); - -// eliminate normally and check solution -DiscreteBayesNet::shared_ptr bayesNet = eTree->eliminate(&EliminateDiscrete); -// bayesNet->print(">>>>>>>>>>>>>> Bayes Net <<<<<<<<<<<<<<<<<<"); -auto actualMPE = optimize(*bayesNet); -EXPECT(assert_equal(expectedMPE, actualMPE)); - -// Approximate and check solution -// DiscreteBayesNet::shared_ptr approximateNet = eTree->approximate(); -// approximateNet->print(">>>>>>>>>>>>>> Approximate Net <<<<<<<<<<<<<<<<<<"); -// EXPECT(assert_equal(expectedMPE, *actualMPE)); -#endif + EXPECT_LONGS_EQUAL(2, bayesTree->size()); } + #ifdef OLD /* ************************************************************************* */ From 34a3b022d948a93b8e741a2366e2d20316e1b52e Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 13:08:16 -0500 Subject: [PATCH 28/91] New lookup classes --- gtsam/discrete/DiscreteLookupDAG.cpp | 153 +++++++++++++++++++++++++++ gtsam/discrete/DiscreteLookupDAG.h | 138 ++++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 gtsam/discrete/DiscreteLookupDAG.cpp create mode 100644 gtsam/discrete/DiscreteLookupDAG.h diff --git a/gtsam/discrete/DiscreteLookupDAG.cpp b/gtsam/discrete/DiscreteLookupDAG.cpp new file mode 100644 index 0000000000..37e45de80e --- /dev/null +++ b/gtsam/discrete/DiscreteLookupDAG.cpp @@ -0,0 +1,153 @@ +/* ---------------------------------------------------------------------------- + + * 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 DiscreteLookupTable.cpp + * @date Feb 14, 2011 + * @author Duy-Nguyen Ta + * @author Frank Dellaert + */ + +#include +#include + +#include +#include + +using std::pair; +using std::vector; + +namespace gtsam { + +// Instantiate base class +template class GTSAM_EXPORT + Conditional; + +/* ************************************************************************** */ +// TODO(dellaert): copy/paste from DiscreteConditional.cpp :-( +void DiscreteLookupTable::print(const std::string& s, + const KeyFormatter& formatter) const { + using std::cout; + using std::endl; + + cout << s << " g( "; + for (const_iterator it = beginFrontals(); it != endFrontals(); ++it) { + cout << formatter(*it) << " "; + } + if (nrParents()) { + cout << "; "; + for (const_iterator it = beginParents(); it != endParents(); ++it) { + cout << formatter(*it) << " "; + } + } + cout << "):\n"; + ADT::print("", formatter); + cout << endl; +} + +/* ************************************************************************* */ +// TODO(dellaert): copy/paste from DiscreteConditional.cpp :-( +vector DiscreteLookupTable::frontalAssignments() const { + vector> pairs; + for (Key key : frontals()) pairs.emplace_back(key, cardinalities_.at(key)); + vector> rpairs(pairs.rbegin(), pairs.rend()); + return DiscreteValues::CartesianProduct(rpairs); +} + +/* ************************************************************************** */ +// TODO(dellaert): copy/paste from DiscreteConditional.cpp :-( +static DiscreteLookupTable::ADT Choose(const DiscreteLookupTable& conditional, + const DiscreteValues& given, + bool forceComplete = true) { + // Get the big decision tree with all the levels, and then go down the + // branches based on the value of the parent variables. + DiscreteLookupTable::ADT adt(conditional); + size_t value; + for (Key j : conditional.parents()) { + try { + value = given.at(j); + adt = adt.choose(j, value); // ADT keeps getting smaller. + } catch (std::out_of_range&) { + if (forceComplete) { + given.print("parentsValues: "); + throw std::runtime_error( + "DiscreteLookupTable::Choose: parent value missing"); + } + } + } + return adt; +} + +/* ************************************************************************** */ +void DiscreteLookupTable::argmaxInPlace(DiscreteValues* values) const { + ADT pFS = Choose(*this, *values); // P(F|S=parentsValues) + + // Initialize + DiscreteValues mpe; + double maxP = 0; + + // Get all Possible Configurations + const auto allPosbValues = frontalAssignments(); + + // Find the maximum + for (const auto& frontalVals : allPosbValues) { + double pValueS = pFS(frontalVals); // P(F=value|S=parentsValues) + // Update maximum solution if better + if (pValueS > maxP) { + maxP = pValueS; + mpe = frontalVals; + } + } + + // set values (inPlace) to maximum + for (Key j : frontals()) { + (*values)[j] = mpe[j]; + } +} + +/* ************************************************************************** */ +size_t DiscreteLookupTable::argmax(const DiscreteValues& parentsValues) const { + ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) + + // Then, find the max over all remaining + // TODO(Duy): only works for one key now, seems horribly slow this way + size_t mpe = 0; + DiscreteValues frontals; + double maxP = 0; + assert(nrFrontals() == 1); + Key j = (firstFrontalKey()); + for (size_t value = 0; value < cardinality(j); value++) { + frontals[j] = value; + double pValueS = pFS(frontals); // P(F=value|S=parentsValues) + // Update MPE solution if better + if (pValueS > maxP) { + maxP = pValueS; + mpe = value; + } + } + return mpe; +} + +/* ************************************************************************** */ +DiscreteValues DiscreteLookupDAG::argmax() const { + DiscreteValues result; + return argmax(result); +} + +DiscreteValues DiscreteLookupDAG::argmax(DiscreteValues result) const { + // Argmax each node in turn in topological sort order (parents first). + for (auto lookupTable : boost::adaptors::reverse(*this)) + lookupTable->argmaxInPlace(&result); + return result; +} +/* ************************************************************************** */ + +} // namespace gtsam diff --git a/gtsam/discrete/DiscreteLookupDAG.h b/gtsam/discrete/DiscreteLookupDAG.h new file mode 100644 index 0000000000..a69b0b1eea --- /dev/null +++ b/gtsam/discrete/DiscreteLookupDAG.h @@ -0,0 +1,138 @@ +/* ---------------------------------------------------------------------------- + + * 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 DiscreteLookupDAG.h + * @date JAnuary, 2022 + * @author Frank dellaert + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace gtsam { + +/** + * @brief DiscreteLookupTable table for max-product + */ +class DiscreteLookupTable + : public DecisionTreeFactor, + public Conditional { + public: + using This = DiscreteLookupTable; + using shared_ptr = boost::shared_ptr; + using BaseConditional = Conditional; + + /** + * @brief Construct a new Discrete Lookup Table object + * + * @param nFrontals number of frontal variables + * @param keys a orted list of gtsam::Keys + * @param potentials the algebraic decision tree with lookup values + */ + DiscreteLookupTable(size_t nFrontals, const DiscreteKeys& keys, + const ADT& potentials) + : DecisionTreeFactor(keys, potentials), BaseConditional(nFrontals) {} + + /// GTSAM-style print + void print( + const std::string& s = "Discrete Lookup Table: ", + const KeyFormatter& formatter = DefaultKeyFormatter) const override; + + /** + * @brief return assignment for single frontal variable that maximizes value. + * @param parentsValues Known assignments for the parents. + * @return maximizing assignment for the frontal variable. + */ + size_t argmax(const DiscreteValues& parentsValues) const; + + /** + * @brief Calculate assignment for frontal variables that maximizes value. + * @param (in/out) parentsValues Known assignments for the parents. + */ + void argmaxInPlace(DiscreteValues* parentsValues) const; + + /// Return all assignments for frontal variables. + std::vector frontalAssignments() const; +}; + +/** A DAG made from lookup tables, as defined above. */ +class GTSAM_EXPORT DiscreteLookupDAG : public BayesNet { + public: + using Base = BayesNet; + using This = DiscreteLookupDAG; + using shared_ptr = boost::shared_ptr; + + /// @name Standard Constructors + /// @{ + + /// Construct empty DAG. + DiscreteLookupDAG() {} + + /// Destructor + virtual ~DiscreteLookupDAG() {} + + /// @} + + /// @name Testable + /// @{ + + /** Check equality */ + bool equals(const This& bn, double tol = 1e-9) const; + + /// @} + + /// @name Standard Interface + /// @{ + + /** + * @brief argmax by back-substitution. + * + * Assumes the DAG is reverse topologically sorted, i.e. last + * conditional will be optimized first. If the DAG resulted from + * eliminating a factor graph, this is true for the elimination ordering. + * + * @return optimal assignment for all variables. + */ + DiscreteValues argmax() const; + + /** + * @brief argmax by back-substitution, given certain variables. + * + * Assumes the DAG is reverse topologically sorted *and* that the + * DAG does not contain any conditionals for the given variables. + * + * @return given assignment extended w. optimal assignment for all variables. + */ + DiscreteValues argmax(DiscreteValues given) const; + /// @} + + private: + /** Serialization function */ + friend class boost::serialization::access; + template + void serialize(ARCHIVE& ar, const unsigned int /*version*/) { + ar& BOOST_SERIALIZATION_BASE_OBJECT_NVP(Base); + } +}; + +// traits +template <> +struct traits : public Testable {}; + +} // namespace gtsam From fcdb5b43c1d67160b814b7a877a8eda8b1bc3f48 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 13:09:04 -0500 Subject: [PATCH 29/91] Deprecated solve --- gtsam/discrete/DiscreteDistribution.cpp | 17 +++++++++++++++++ gtsam/discrete/DiscreteDistribution.h | 12 +++++++++--- .../discrete/tests/testDiscreteDistribution.cpp | 6 ++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/gtsam/discrete/DiscreteDistribution.cpp b/gtsam/discrete/DiscreteDistribution.cpp index 7397714709..5f6fba6a28 100644 --- a/gtsam/discrete/DiscreteDistribution.cpp +++ b/gtsam/discrete/DiscreteDistribution.cpp @@ -49,4 +49,21 @@ std::vector DiscreteDistribution::pmf() const { return array; } +/* ************************************************************************** */ +size_t DiscreteDistribution::argmax() const { + size_t maxValue = 0; + double maxP = 0; + assert(nrFrontals() == 1); + Key j = firstFrontalKey(); + for (size_t value = 0; value < cardinality(j); value++) { + double pValueS = (*this)(value); + // Update MPE solution if better + if (pValueS > maxP) { + maxP = pValueS; + maxValue = value; + } + } + return maxValue; +} + } // namespace gtsam diff --git a/gtsam/discrete/DiscreteDistribution.h b/gtsam/discrete/DiscreteDistribution.h index fae6e355bd..8dcc75733f 100644 --- a/gtsam/discrete/DiscreteDistribution.h +++ b/gtsam/discrete/DiscreteDistribution.h @@ -91,10 +91,10 @@ class GTSAM_EXPORT DiscreteDistribution : public DiscreteConditional { std::vector pmf() const; /** - * solve a conditional - * @return MPE value of the child (1 frontal variable). + * @brief Return assignment that maximizes distribution. + * @return Optimal assignment (1 frontal variable). */ - size_t solve() const { return Base::solve({}); } + size_t argmax() const; /** * sample @@ -103,6 +103,12 @@ class GTSAM_EXPORT DiscreteDistribution : public DiscreteConditional { size_t sample() const { return Base::sample(); } /// @} +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 + /// @name Deprecated functionality + /// @{ + size_t GTSAM_DEPRECATED solve() const { return Base::solve({}); } + /// @} +#endif }; // DiscreteDistribution diff --git a/gtsam/discrete/tests/testDiscreteDistribution.cpp b/gtsam/discrete/tests/testDiscreteDistribution.cpp index 5c0c42e737..5e59aaa65b 100644 --- a/gtsam/discrete/tests/testDiscreteDistribution.cpp +++ b/gtsam/discrete/tests/testDiscreteDistribution.cpp @@ -74,6 +74,12 @@ TEST(DiscreteDistribution, sample) { prior.sample(); } +/* ************************************************************************* */ +TEST(DiscreteDistribution, argmax) { + DiscreteDistribution prior(X % "2/3"); + EXPECT_LONGS_EQUAL(prior.argmax(), 1); +} + /* ************************************************************************* */ int main() { TestResult tr; From 5add858c24c15df4336f6654ab0d867b6757c1e7 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 13:18:28 -0500 Subject: [PATCH 30/91] Now doing MPE with DAG class --- gtsam/discrete/DiscreteFactorGraph.cpp | 43 +++++++----- gtsam/discrete/DiscreteFactorGraph.h | 9 +-- .../tests/testDiscreteFactorGraph.cpp | 68 ++++++++----------- 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.cpp b/gtsam/discrete/DiscreteFactorGraph.cpp index d8e9aa244f..a166fdce93 100644 --- a/gtsam/discrete/DiscreteFactorGraph.cpp +++ b/gtsam/discrete/DiscreteFactorGraph.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -96,18 +97,6 @@ namespace gtsam { // } /* ************************************************************************ */ - /** - * @brief Lookup table for max-product - * - * This inherits from a DiscreteConditional but is not normalized to 1 - * - */ - class Lookup : public DiscreteConditional { - public: - Lookup(size_t nFrontals, const DiscreteKeys& keys, const ADT& potentials) - : DiscreteConditional(nFrontals, keys, potentials) {} - }; - // Alternate eliminate function for MPE std::pair // EliminateForMPE(const DiscreteFactorGraph& factors, @@ -133,7 +122,8 @@ namespace gtsam { // Make lookup with product gttic(lookup); size_t nrFrontals = frontalKeys.size(); - auto lookup = boost::make_shared(nrFrontals, orderedKeys, product); + auto lookup = boost::make_shared(nrFrontals, + orderedKeys, product); gttoc(lookup); return std::make_pair( @@ -141,18 +131,37 @@ namespace gtsam { } /* ************************************************************************ */ - DiscreteBayesNet::shared_ptr DiscreteFactorGraph::maxProduct( + DiscreteLookupDAG DiscreteFactorGraph::maxProduct( OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_maxProduct); - return BaseEliminateable::eliminateSequential(orderingType, - EliminateForMPE); + + // The solution below is a bitclunky: the elimination machinery does not + // allow for differently *typed* versions of elimination, so we eliminate + // into a Bayes Net using the special eliminate function above, and then + // create the DiscreteLookupDAG after the fact, in linear time. + auto bayesNet = + BaseEliminateable::eliminateSequential(orderingType, EliminateForMPE); + + // Copy to the DAG + DiscreteLookupDAG dag; + for (auto&& conditional : *bayesNet) { + if (auto lookupTable = + boost::dynamic_pointer_cast(conditional)) { + dag.push_back(lookupTable); + } else { + throw std::runtime_error( + "DiscreteFactorGraph::maxProduct: Expected look up table."); + } + } + return dag; } /* ************************************************************************ */ DiscreteValues DiscreteFactorGraph::optimize( OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_optimize); - return maxProduct()->optimize(); + DiscreteLookupDAG dag = maxProduct(); + return dag.argmax(); } /* ************************************************************************ */ diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index b4e98c876c..7c658f5484 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -18,10 +18,11 @@ #pragma once -#include +#include +#include #include +#include #include -#include #include #include @@ -132,9 +133,9 @@ class GTSAM_EXPORT DiscreteFactorGraph * @brief Implement the max-product algorithm * * @param orderingType : one of COLAMD, METIS, NATURAL, CUSTOM - * @return DiscreteBayesNet::shared_ptr DAG with lookup tables + * @return DiscreteLookupDAG::shared_ptr DAG with lookup tables */ - boost::shared_ptr maxProduct( + DiscreteLookupDAG maxProduct( OptionalOrderingType orderingType = boost::none) const; /** diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index 14432d08cb..e63cc26b8f 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -52,13 +52,6 @@ TEST_UNSAFE(DiscreteFactorGraph, debugScheduler) { DiscreteValues mpe; insert(mpe)(0, 2)(1, 1)(2, 0)(3, 0); EXPECT(assert_equal(mpe, actualMPE)); - - // Check Bayes Net - Ordering ordering; - ordering += Key(0), Key(1), Key(2), Key(3); - auto chordal = graph.eliminateSequential(ordering); - // happens to be the same, but not in general! - EXPECT(assert_equal(mpe, chordal->optimize())); } /* ************************************************************************* */ @@ -125,57 +118,46 @@ TEST(DiscreteFactorGraph, test) { DecisionTreeFactor::shared_ptr newFactor; boost::tie(conditional, newFactor) = EliminateDiscrete(graph, frontalKeys); - // Check Bayes net + // Check Conditional CHECK(conditional); - DiscreteBayesNet expected; Signature signature((C | B, A) = "9/1 1/1 1/1 1/9"); - DiscreteConditional expectedConditional(signature); EXPECT(assert_equal(expectedConditional, *conditional)); - expected.add(signature); // Check Factor CHECK(newFactor); DecisionTreeFactor expectedFactor(B & A, "10 6 6 10"); EXPECT(assert_equal(expectedFactor, *newFactor)); - // add conditionals to complete expected Bayes net - expected.add(B | A = "5/3 3/5"); - expected.add(A % "1/1"); - - // Test elimination tree + // Test using elimination tree Ordering ordering; ordering += Key(0), Key(1), Key(2); DiscreteEliminationTree etree(graph, ordering); DiscreteBayesNet::shared_ptr actual; DiscreteFactorGraph::shared_ptr remainingGraph; boost::tie(actual, remainingGraph) = etree.eliminate(&EliminateDiscrete); - EXPECT(assert_equal(expected, *actual)); - - DiscreteValues mpe; - insert(mpe)(0, 0)(1, 0)(2, 0); - EXPECT_DOUBLES_EQUAL(9, graph(mpe), 1e-5); // regression - // Check Bayes Net - auto chordal = graph.eliminateSequential(); - auto notOptimal = chordal->optimize(); - // happens to be the same but not in general! - EXPECT(assert_equal(mpe, notOptimal)); + // Check Bayes net + DiscreteBayesNet expectedBayesNet; + expectedBayesNet.add(signature); + expectedBayesNet.add(B | A = "5/3 3/5"); + expectedBayesNet.add(A % "1/1"); + EXPECT(assert_equal(expectedBayesNet, *actual)); // Test eliminateSequential DiscreteBayesNet::shared_ptr actual2 = graph.eliminateSequential(ordering); - EXPECT(assert_equal(expected, *actual2)); - auto notOptimal2 = actual2->optimize(); - // happens to be the same but not in general! - EXPECT(assert_equal(mpe, notOptimal2)); + EXPECT(assert_equal(expectedBayesNet, *actual2)); // Test mpe + DiscreteValues mpe; + insert(mpe)(0, 0)(1, 0)(2, 0); auto actualMPE = graph.optimize(); EXPECT(assert_equal(mpe, actualMPE)); + EXPECT_DOUBLES_EQUAL(9, graph(mpe), 1e-5); // regression } /* ************************************************************************* */ -TEST_UNSAFE(DiscreteFactorGraph, testMPE) { +TEST_UNSAFE(DiscreteFactorGraph, testMaxProduct) { // Declare a bunch of keys DiscreteKey C(0, 2), A(1, 2), B(2, 2); @@ -184,17 +166,20 @@ TEST_UNSAFE(DiscreteFactorGraph, testMPE) { graph.add(C & A, "0.2 0.8 0.3 0.7"); graph.add(C & B, "0.1 0.9 0.4 0.6"); - // Check MPE. - auto actualMPE = graph.optimize(); + // Created expected MPE DiscreteValues mpe; insert(mpe)(0, 0)(1, 1)(2, 1); - EXPECT(assert_equal(mpe, actualMPE)); - // Check Bayes Net - auto chordal = graph.eliminateSequential(); - auto notOptimal = chordal->optimize(); - // happens to be the same but not in general - EXPECT(assert_equal(mpe, notOptimal)); + // Do max-product with different orderings + for (Ordering::OrderingType orderingType : + {Ordering::COLAMD, Ordering::METIS, Ordering::NATURAL, + Ordering::CUSTOM}) { + DiscreteLookupDAG dag = graph.maxProduct(orderingType); + auto actualMPE = dag.argmax(); + EXPECT(assert_equal(mpe, actualMPE)); + auto actualMPE2 = graph.optimize(); // all in one + EXPECT(assert_equal(mpe, actualMPE2)); + } } /* ************************************************************************* */ @@ -218,10 +203,12 @@ TEST(DiscreteFactorGraph, marginalIsNotMPE) { EXPECT(assert_equal(mpe, actualMPE)); EXPECT_DOUBLES_EQUAL(0.315789, graph(mpe), 1e-5); // regression +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 // Optimize on BayesNet maximizes marginal, then the conditional marginals: auto notOptimal = bayesNet.optimize(); EXPECT(graph(notOptimal) < graph(mpe)); EXPECT_DOUBLES_EQUAL(0.263158, graph(notOptimal), 1e-5); // regression +#endif } /* ************************************************************************* */ @@ -252,8 +239,11 @@ TEST(DiscreteFactorGraph, testMPE_Darwiche09book_p244) { Ordering ordering; ordering += Key(0), Key(1), Key(2), Key(3), Key(4); auto chordal = graph.eliminateSequential(ordering); + EXPECT_LONGS_EQUAL(2, chordal->size()); +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 auto notOptimal = chordal->optimize(); // not MPE ! EXPECT(graph(notOptimal) < graph(mpe)); +#endif // Let us create the Bayes tree here, just for fun, because we don't use it DiscreteBayesTree::shared_ptr bayesTree = From 756430074478dd8c91b40a1519961752dca02633 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 13:18:46 -0500 Subject: [PATCH 31/91] deprecated solve --- gtsam/discrete/DiscreteConditional.cpp | 20 +++++++++++--------- gtsam/discrete/DiscreteConditional.h | 18 ++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index 8c0f91807a..db0ef1048e 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -238,6 +238,7 @@ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( } /* ************************************************************************** */ +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 void DiscreteConditional::solveInPlace(DiscreteValues* values) const { ADT pFS = Choose(*this, *values); // P(F|S=parentsValues) @@ -264,14 +265,6 @@ void DiscreteConditional::solveInPlace(DiscreteValues* values) const { } } -/* ******************************************************************************** */ -void DiscreteConditional::sampleInPlace(DiscreteValues* values) const { - assert(nrFrontals() == 1); - Key j = (firstFrontalKey()); - size_t sampled = sample(*values); // Sample variable given parents - (*values)[j] = sampled; // store result in partial solution -} - /* ************************************************************************** */ size_t DiscreteConditional::solve(const DiscreteValues& parentsValues) const { ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) @@ -294,8 +287,17 @@ size_t DiscreteConditional::solve(const DiscreteValues& parentsValues) const { } return mpe; } +#endif -/* ******************************************************************************** */ +/* ************************************************************************** */ +void DiscreteConditional::sampleInPlace(DiscreteValues* values) const { + assert(nrFrontals() == 1); + Key j = (firstFrontalKey()); + size_t sampled = sample(*values); // Sample variable given parents + (*values)[j] = sampled; // store result in partial solution +} + +/* ************************************************************************** */ size_t DiscreteConditional::sample(const DiscreteValues& parentsValues) const { static mt19937 rng(2); // random number generator diff --git a/gtsam/discrete/DiscreteConditional.h b/gtsam/discrete/DiscreteConditional.h index de9d949714..ef0a4c9072 100644 --- a/gtsam/discrete/DiscreteConditional.h +++ b/gtsam/discrete/DiscreteConditional.h @@ -179,13 +179,6 @@ class GTSAM_EXPORT DiscreteConditional /** Single variable version of likelihood. */ DecisionTreeFactor::shared_ptr likelihood(size_t parent_value) const; - /** - * solve a conditional - * @param parentsValues Known values of the parents - * @return maximum value for the (single) frontal variable. - */ - size_t solve(const DiscreteValues& parentsValues) const; - /** * sample * @param parentsValues Known values of the parents @@ -203,9 +196,6 @@ class GTSAM_EXPORT DiscreteConditional /// @name Advanced Interface /// @{ - /// solve a conditional, in place - void solveInPlace(DiscreteValues* parentsValues) const; - /// sample in place, stores result in partial solution void sampleInPlace(DiscreteValues* parentsValues) const; @@ -228,6 +218,14 @@ class GTSAM_EXPORT DiscreteConditional const Names& names = {}) const override; /// @} + +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 + /// @name Deprecated functionality + /// @{ + size_t GTSAM_DEPRECATED solve(const DiscreteValues& parentsValues) const; + void GTSAM_DEPRECATED solveInPlace(DiscreteValues* parentsValues) const; + /// @} +#endif }; // DiscreteConditional From e22f8f04bc7352adbcce3d59be4cdfcec3e9f602 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 13:18:54 -0500 Subject: [PATCH 32/91] deprecated optimize --- gtsam/discrete/DiscreteBayesNet.cpp | 7 ++++ gtsam/discrete/DiscreteBayesNet.h | 36 +++++++------------ gtsam/discrete/tests/testDiscreteBayesNet.cpp | 15 +------- 3 files changed, 20 insertions(+), 38 deletions(-) diff --git a/gtsam/discrete/DiscreteBayesNet.cpp b/gtsam/discrete/DiscreteBayesNet.cpp index 7294c8b296..ccc52585e6 100644 --- a/gtsam/discrete/DiscreteBayesNet.cpp +++ b/gtsam/discrete/DiscreteBayesNet.cpp @@ -43,6 +43,7 @@ double DiscreteBayesNet::evaluate(const DiscreteValues& values) const { } /* ************************************************************************* */ +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 DiscreteValues DiscreteBayesNet::optimize() const { DiscreteValues result; return optimize(result); @@ -50,10 +51,16 @@ DiscreteValues DiscreteBayesNet::optimize() const { DiscreteValues DiscreteBayesNet::optimize(DiscreteValues result) const { // solve each node in turn in topological sort order (parents first) +#ifdef _MSC_VER +#pragma message("DiscreteBayesNet::optimize (deprecated) does not compute MPE!") +#else +#warning "DiscreteBayesNet::optimize (deprecated) does not compute MPE!" +#endif for (auto conditional : boost::adaptors::reverse(*this)) conditional->solveInPlace(&result); return result; } +#endif /* ************************************************************************* */ DiscreteValues DiscreteBayesNet::sample() const { diff --git a/gtsam/discrete/DiscreteBayesNet.h b/gtsam/discrete/DiscreteBayesNet.h index bd5536135a..4916cad7c0 100644 --- a/gtsam/discrete/DiscreteBayesNet.h +++ b/gtsam/discrete/DiscreteBayesNet.h @@ -31,12 +31,12 @@ namespace gtsam { -/** A Bayes net made from linear-Discrete densities */ +/** A Bayes net made from discrete conditional distributions. */ class GTSAM_EXPORT DiscreteBayesNet: public BayesNet { public: - typedef FactorGraph Base; + typedef BayesNet Base; typedef DiscreteBayesNet This; typedef DiscreteConditional ConditionalType; typedef boost::shared_ptr shared_ptr; @@ -45,7 +45,7 @@ namespace gtsam { /// @name Standard Constructors /// @{ - /** Construct empty factor graph */ + /// Construct empty Bayes net. DiscreteBayesNet() {} /** Construct from iterator over conditionals */ @@ -98,27 +98,6 @@ namespace gtsam { return evaluate(values); } - /** - * @brief solve by back-substitution. - * - * Assumes the Bayes net is reverse topologically sorted, i.e. last - * conditional will be optimized first. If the Bayes net resulted from - * eliminating a factor graph, this is true for the elimination ordering. - * - * @return a sampled value for all variables. - */ - DiscreteValues optimize() const; - - /** - * @brief solve by back-substitution, given certain variables. - * - * Assumes the Bayes net is reverse topologically sorted *and* that the - * Bayes net does not contain any conditionals for the given values. - * - * @return given values extended with optimized value for other variables. - */ - DiscreteValues optimize(DiscreteValues given) const; - /** * @brief do ancestral sampling * @@ -152,7 +131,16 @@ namespace gtsam { std::string html(const KeyFormatter& keyFormatter = DefaultKeyFormatter, const DiscreteFactor::Names& names = {}) const; + ///@} + +#ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 + /// @name Deprecated functionality + /// @{ + + DiscreteValues GTSAM_DEPRECATED optimize() const; + DiscreteValues GTSAM_DEPRECATED optimize(DiscreteValues given) const; /// @} +#endif private: /** Serialization function */ diff --git a/gtsam/discrete/tests/testDiscreteBayesNet.cpp b/gtsam/discrete/tests/testDiscreteBayesNet.cpp index 0ba53c69ab..c35d4742c0 100644 --- a/gtsam/discrete/tests/testDiscreteBayesNet.cpp +++ b/gtsam/discrete/tests/testDiscreteBayesNet.cpp @@ -106,26 +106,13 @@ TEST(DiscreteBayesNet, Asia) { DiscreteConditional expected2(Bronchitis % "11/9"); EXPECT(assert_equal(expected2, *chordal->back())); - // solve - auto actualMPE = chordal->optimize(); - DiscreteValues expectedMPE; - insert(expectedMPE)(Asia.first, 0)(Dyspnea.first, 0)(XRay.first, 0)( - Tuberculosis.first, 0)(Smoking.first, 0)(Either.first, 0)( - LungCancer.first, 0)(Bronchitis.first, 0); - EXPECT(assert_equal(expectedMPE, actualMPE)); - // add evidence, we were in Asia and we have dyspnea fg.add(Asia, "0 1"); fg.add(Dyspnea, "0 1"); // solve again, now with evidence DiscreteBayesNet::shared_ptr chordal2 = fg.eliminateSequential(ordering); - auto actualMPE2 = chordal2->optimize(); - DiscreteValues expectedMPE2; - insert(expectedMPE2)(Asia.first, 1)(Dyspnea.first, 1)(XRay.first, 0)( - Tuberculosis.first, 0)(Smoking.first, 1)(Either.first, 0)( - LungCancer.first, 0)(Bronchitis.first, 1); - EXPECT(assert_equal(expectedMPE2, actualMPE2)); + EXPECT(assert_equal(expected2, *chordal->back())); // now sample from it DiscreteValues expectedSample; From 2f49612b8c1132fa97457439f57330e8eb914c70 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 14:06:50 -0500 Subject: [PATCH 33/91] MPE now works --- gtsam/discrete/DiscreteLookupDAG.cpp | 4 -- gtsam/discrete/DiscreteLookupDAG.h | 18 ++++-- .../tests/testDiscreteFactorGraph.cpp | 2 +- .../discrete/tests/testDiscreteLookupDAG.cpp | 58 +++++++++++++++++++ 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 gtsam/discrete/tests/testDiscreteLookupDAG.cpp diff --git a/gtsam/discrete/DiscreteLookupDAG.cpp b/gtsam/discrete/DiscreteLookupDAG.cpp index 37e45de80e..4fe3a53a47 100644 --- a/gtsam/discrete/DiscreteLookupDAG.cpp +++ b/gtsam/discrete/DiscreteLookupDAG.cpp @@ -27,10 +27,6 @@ using std::vector; namespace gtsam { -// Instantiate base class -template class GTSAM_EXPORT - Conditional; - /* ************************************************************************** */ // TODO(dellaert): copy/paste from DiscreteConditional.cpp :-( void DiscreteLookupTable::print(const std::string& s, diff --git a/gtsam/discrete/DiscreteLookupDAG.h b/gtsam/discrete/DiscreteLookupDAG.h index a69b0b1eea..31cb3dfbf8 100644 --- a/gtsam/discrete/DiscreteLookupDAG.h +++ b/gtsam/discrete/DiscreteLookupDAG.h @@ -22,17 +22,19 @@ #include #include - +#include +#include #include namespace gtsam { /** * @brief DiscreteLookupTable table for max-product + * + * Inherits from discrete conditional for convenience, but is not normalized. + * Is used in pax-product algorithm. */ -class DiscreteLookupTable - : public DecisionTreeFactor, - public Conditional { +class DiscreteLookupTable : public DiscreteConditional { public: using This = DiscreteLookupTable; using shared_ptr = boost::shared_ptr; @@ -47,7 +49,7 @@ class DiscreteLookupTable */ DiscreteLookupTable(size_t nFrontals, const DiscreteKeys& keys, const ADT& potentials) - : DecisionTreeFactor(keys, potentials), BaseConditional(nFrontals) {} + : DiscreteConditional(nFrontals, keys, potentials) {} /// GTSAM-style print void print( @@ -100,6 +102,12 @@ class GTSAM_EXPORT DiscreteLookupDAG : public BayesNet { /// @name Standard Interface /// @{ + /** Add a DiscreteLookupTable */ + template + void add(Args&&... args) { + emplace_shared(std::forward(args)...); + } + /** * @brief argmax by back-substitution. * diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index e63cc26b8f..f4819dab54 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -239,7 +239,7 @@ TEST(DiscreteFactorGraph, testMPE_Darwiche09book_p244) { Ordering ordering; ordering += Key(0), Key(1), Key(2), Key(3), Key(4); auto chordal = graph.eliminateSequential(ordering); - EXPECT_LONGS_EQUAL(2, chordal->size()); + EXPECT_LONGS_EQUAL(5, chordal->size()); #ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 auto notOptimal = chordal->optimize(); // not MPE ! EXPECT(graph(notOptimal) < graph(mpe)); diff --git a/gtsam/discrete/tests/testDiscreteLookupDAG.cpp b/gtsam/discrete/tests/testDiscreteLookupDAG.cpp new file mode 100644 index 0000000000..04b8597804 --- /dev/null +++ b/gtsam/discrete/tests/testDiscreteLookupDAG.cpp @@ -0,0 +1,58 @@ +/* ---------------------------------------------------------------------------- + + * 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 + + * -------------------------------------------------------------------------- */ + +/* + * testDiscreteLookupDAG.cpp + * + * @date January, 2022 + * @author Frank Dellaert + */ + +#include +#include +#include + +#include +#include + +using namespace gtsam; +using namespace boost::assign; + +/* ************************************************************************* */ +TEST(DiscreteLookupDAG, argmax) { + using ADT = AlgebraicDecisionTree; + + // Declare 2 keys + DiscreteKey A(0, 2), B(1, 2); + + // Create lookup table corresponding to "marginalIsNotMPE" in testDFG. + DiscreteLookupDAG dag; + + ADT adtB(DiscreteKeys{B, A}, std::vector{0.5, 1. / 3, 0.5, 2. / 3}); + dag.add(1, DiscreteKeys{B, A}, adtB); + + ADT adtA(A, 0.5 * 10 / 19, (2. / 3) * (9. / 19)); + dag.add(1, DiscreteKeys{A}, adtA); + + // The expected MPE is A=1, B=1 + DiscreteValues mpe; + insert(mpe)(0, 1)(1, 1); + + // check: + auto actualMPE = dag.argmax(); + EXPECT(assert_equal(mpe, actualMPE)); +} +/* ************************************************************************* */ +int main() { + TestResult tr; + return TestRegistry::runAllTests(tr); +} +/* ************************************************************************* */ From e713897235ce2dda6363f81675186c09143d361b Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 14:26:35 -0500 Subject: [PATCH 34/91] made internal protected choose to avoid copy/paste in Lookup --- gtsam/discrete/DiscreteConditional.cpp | 53 +++++++++++++------------- gtsam/discrete/DiscreteConditional.h | 19 +++++---- gtsam/discrete/DiscreteLookupDAG.cpp | 39 ++----------------- gtsam/discrete/DiscreteLookupDAG.h | 3 -- 4 files changed, 41 insertions(+), 73 deletions(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index db0ef1048e..164a45f407 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -16,26 +16,25 @@ * @author Frank Dellaert */ +#include +#include #include #include #include -#include -#include - -#include #include +#include #include +#include #include #include -#include #include -#include +#include using namespace std; +using std::pair; using std::stringstream; using std::vector; -using std::pair; namespace gtsam { // Instantiate base class @@ -147,7 +146,7 @@ void DiscreteConditional::print(const string& s, cout << endl; } -/* ******************************************************************************** */ +/* ************************************************************************** */ bool DiscreteConditional::equals(const DiscreteFactor& other, double tol) const { if (!dynamic_cast(&other)) { @@ -159,14 +158,13 @@ bool DiscreteConditional::equals(const DiscreteFactor& other, } /* ************************************************************************** */ -static DiscreteConditional::ADT Choose(const DiscreteConditional& conditional, - const DiscreteValues& given, - bool forceComplete = true) { +DiscreteConditional::ADT DiscreteConditional::choose( + const DiscreteValues& given, bool forceComplete) const { // Get the big decision tree with all the levels, and then go down the // branches based on the value of the parent variables. - DiscreteConditional::ADT adt(conditional); + DiscreteConditional::ADT adt(*this); size_t value; - for (Key j : conditional.parents()) { + for (Key j : parents()) { try { value = given.at(j); adt = adt.choose(j, value); // ADT keeps getting smaller. @@ -174,7 +172,7 @@ static DiscreteConditional::ADT Choose(const DiscreteConditional& conditional, if (forceComplete) { given.print("parentsValues: "); throw runtime_error( - "DiscreteConditional::Choose: parent value missing"); + "DiscreteConditional::choose: parent value missing"); } } } @@ -184,7 +182,7 @@ static DiscreteConditional::ADT Choose(const DiscreteConditional& conditional, /* ************************************************************************** */ DiscreteConditional::shared_ptr DiscreteConditional::choose( const DiscreteValues& given) const { - ADT adt = Choose(*this, given, false); // P(F|S=given) + ADT adt = choose(given, false); // P(F|S=given) // Collect all keys not in given. DiscreteKeys dKeys; @@ -225,7 +223,7 @@ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( return boost::make_shared(discreteKeys, adt); } -/* ******************************************************************************** */ +/* ****************************************************************************/ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( size_t parent_value) const { if (nrFrontals() != 1) @@ -240,7 +238,7 @@ DecisionTreeFactor::shared_ptr DiscreteConditional::likelihood( /* ************************************************************************** */ #ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 void DiscreteConditional::solveInPlace(DiscreteValues* values) const { - ADT pFS = Choose(*this, *values); // P(F|S=parentsValues) + ADT pFS = choose(*values, true); // P(F|S=parentsValues) // Initialize DiscreteValues mpe; @@ -267,25 +265,24 @@ void DiscreteConditional::solveInPlace(DiscreteValues* values) const { /* ************************************************************************** */ size_t DiscreteConditional::solve(const DiscreteValues& parentsValues) const { - ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) + ADT pFS = choose(parentsValues, true); // P(F|S=parentsValues) // Then, find the max over all remaining - // TODO, only works for one key now, seems horribly slow this way - size_t mpe = 0; - DiscreteValues frontals; + size_t max = 0; double maxP = 0; + DiscreteValues frontals; assert(nrFrontals() == 1); Key j = (firstFrontalKey()); for (size_t value = 0; value < cardinality(j); value++) { frontals[j] = value; double pValueS = pFS(frontals); // P(F=value|S=parentsValues) - // Update MPE solution if better + // Update solution if better if (pValueS > maxP) { maxP = pValueS; - mpe = value; + max = value; } } - return mpe; + return max; } #endif @@ -302,7 +299,7 @@ size_t DiscreteConditional::sample(const DiscreteValues& parentsValues) const { static mt19937 rng(2); // random number generator // Get the correct conditional density - ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) + ADT pFS = choose(parentsValues, true); // P(F|S=parentsValues) // TODO(Duy): only works for one key now, seems horribly slow this way if (nrFrontals() != 1) { @@ -325,7 +322,8 @@ size_t DiscreteConditional::sample(const DiscreteValues& parentsValues) const { return distribution(rng); } -/* ******************************************************************************** */ +/* ******************************************************************************** + */ size_t DiscreteConditional::sample(size_t parent_value) const { if (nrParents() != 1) throw std::invalid_argument( @@ -336,7 +334,8 @@ size_t DiscreteConditional::sample(size_t parent_value) const { return sample(values); } -/* ******************************************************************************** */ +/* ******************************************************************************** + */ size_t DiscreteConditional::sample() const { if (nrParents() != 0) throw std::invalid_argument( diff --git a/gtsam/discrete/DiscreteConditional.h b/gtsam/discrete/DiscreteConditional.h index ef0a4c9072..af05e932bb 100644 --- a/gtsam/discrete/DiscreteConditional.h +++ b/gtsam/discrete/DiscreteConditional.h @@ -93,14 +93,14 @@ class GTSAM_EXPORT DiscreteConditional DiscreteConditional(const DiscreteKey& key, const std::string& spec) : DiscreteConditional(Signature(key, {}, spec)) {} - /** + /** * @brief construct P(X|Y) = f(X,Y)/f(Y) from f(X,Y) and f(Y) - * Assumes but *does not check* that f(Y)=sum_X f(X,Y). + * Assumes but *does not check* that f(Y)=sum_X f(X,Y). */ DiscreteConditional(const DecisionTreeFactor& joint, const DecisionTreeFactor& marginal); - /** + /** * @brief construct P(X|Y) = f(X,Y)/f(Y) from f(X,Y) and f(Y) * Assumes but *does not check* that f(Y)=sum_X f(X,Y). * Makes sure the keys are ordered as given. Does not check orderedKeys. @@ -157,17 +157,17 @@ class GTSAM_EXPORT DiscreteConditional return ADT::operator()(values); } - /** + /** * @brief restrict to given *parent* values. - * + * * Note: does not need be complete set. Examples: - * + * * P(C|D,E) + . -> P(C|D,E) * P(C|D,E) + E -> P(C|D) * P(C|D,E) + D -> P(C|E) * P(C|D,E) + D,E -> P(C) * P(C|D,E) + C -> error! - * + * * @return a shared_ptr to a new DiscreteConditional */ shared_ptr choose(const DiscreteValues& given) const; @@ -226,6 +226,11 @@ class GTSAM_EXPORT DiscreteConditional void GTSAM_DEPRECATED solveInPlace(DiscreteValues* parentsValues) const; /// @} #endif + + protected: + /// Internal version of choose + DiscreteConditional::ADT choose(const DiscreteValues& given, + bool forceComplete) const; }; // DiscreteConditional diff --git a/gtsam/discrete/DiscreteLookupDAG.cpp b/gtsam/discrete/DiscreteLookupDAG.cpp index 4fe3a53a47..1edf508a1e 100644 --- a/gtsam/discrete/DiscreteLookupDAG.cpp +++ b/gtsam/discrete/DiscreteLookupDAG.cpp @@ -49,42 +49,9 @@ void DiscreteLookupTable::print(const std::string& s, cout << endl; } -/* ************************************************************************* */ -// TODO(dellaert): copy/paste from DiscreteConditional.cpp :-( -vector DiscreteLookupTable::frontalAssignments() const { - vector> pairs; - for (Key key : frontals()) pairs.emplace_back(key, cardinalities_.at(key)); - vector> rpairs(pairs.rbegin(), pairs.rend()); - return DiscreteValues::CartesianProduct(rpairs); -} - -/* ************************************************************************** */ -// TODO(dellaert): copy/paste from DiscreteConditional.cpp :-( -static DiscreteLookupTable::ADT Choose(const DiscreteLookupTable& conditional, - const DiscreteValues& given, - bool forceComplete = true) { - // Get the big decision tree with all the levels, and then go down the - // branches based on the value of the parent variables. - DiscreteLookupTable::ADT adt(conditional); - size_t value; - for (Key j : conditional.parents()) { - try { - value = given.at(j); - adt = adt.choose(j, value); // ADT keeps getting smaller. - } catch (std::out_of_range&) { - if (forceComplete) { - given.print("parentsValues: "); - throw std::runtime_error( - "DiscreteLookupTable::Choose: parent value missing"); - } - } - } - return adt; -} - /* ************************************************************************** */ void DiscreteLookupTable::argmaxInPlace(DiscreteValues* values) const { - ADT pFS = Choose(*this, *values); // P(F|S=parentsValues) + ADT pFS = choose(*values, true); // P(F|S=parentsValues) // Initialize DiscreteValues mpe; @@ -111,13 +78,13 @@ void DiscreteLookupTable::argmaxInPlace(DiscreteValues* values) const { /* ************************************************************************** */ size_t DiscreteLookupTable::argmax(const DiscreteValues& parentsValues) const { - ADT pFS = Choose(*this, parentsValues); // P(F|S=parentsValues) + ADT pFS = choose(parentsValues, true); // P(F|S=parentsValues) // Then, find the max over all remaining // TODO(Duy): only works for one key now, seems horribly slow this way size_t mpe = 0; - DiscreteValues frontals; double maxP = 0; + DiscreteValues frontals; assert(nrFrontals() == 1); Key j = (firstFrontalKey()); for (size_t value = 0; value < cardinality(j); value++) { diff --git a/gtsam/discrete/DiscreteLookupDAG.h b/gtsam/discrete/DiscreteLookupDAG.h index 31cb3dfbf8..1b3a38b406 100644 --- a/gtsam/discrete/DiscreteLookupDAG.h +++ b/gtsam/discrete/DiscreteLookupDAG.h @@ -68,9 +68,6 @@ class DiscreteLookupTable : public DiscreteConditional { * @param (in/out) parentsValues Known assignments for the parents. */ void argmaxInPlace(DiscreteValues* parentsValues) const; - - /// Return all assignments for frontal variables. - std::vector frontalAssignments() const; }; /** A DAG made from lookup tables, as defined above. */ From b17fcfb64f77ff2f867a2f5a214a6817aefca708 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 14:47:28 -0500 Subject: [PATCH 35/91] optimalAssignment -> optimize. Not deprecating as in unstable. --- gtsam_unstable/discrete/CSP.cpp | 12 ----------- gtsam_unstable/discrete/CSP.h | 6 ------ gtsam_unstable/discrete/Scheduler.cpp | 17 ---------------- gtsam_unstable/discrete/Scheduler.h | 3 --- .../discrete/examples/schedulingExample.cpp | 2 +- .../discrete/examples/schedulingQuals12.cpp | 2 +- .../discrete/examples/schedulingQuals13.cpp | 2 +- gtsam_unstable/discrete/tests/testCSP.cpp | 20 ++++++++----------- .../discrete/tests/testScheduler.cpp | 2 +- gtsam_unstable/discrete/tests/testSudoku.cpp | 8 ++++---- 10 files changed, 16 insertions(+), 58 deletions(-) diff --git a/gtsam_unstable/discrete/CSP.cpp b/gtsam_unstable/discrete/CSP.cpp index e204a67796..08143c469f 100644 --- a/gtsam_unstable/discrete/CSP.cpp +++ b/gtsam_unstable/discrete/CSP.cpp @@ -14,18 +14,6 @@ using namespace std; namespace gtsam { -/// Find the best total assignment - can be expensive -DiscreteValues CSP::optimalAssignment() const { - DiscreteBayesNet::shared_ptr chordal = this->eliminateSequential(); - return chordal->optimize(); -} - -/// Find the best total assignment - can be expensive -DiscreteValues CSP::optimalAssignment(const Ordering& ordering) const { - DiscreteBayesNet::shared_ptr chordal = this->eliminateSequential(ordering); - return chordal->optimize(); -} - bool CSP::runArcConsistency(const VariableIndex& index, Domains* domains) const { bool changed = false; diff --git a/gtsam_unstable/discrete/CSP.h b/gtsam_unstable/discrete/CSP.h index e7fb301156..40853bed66 100644 --- a/gtsam_unstable/discrete/CSP.h +++ b/gtsam_unstable/discrete/CSP.h @@ -43,12 +43,6 @@ class GTSAM_UNSTABLE_EXPORT CSP : public DiscreteFactorGraph { // return result; // } - /// Find the best total assignment - can be expensive. - DiscreteValues optimalAssignment() const; - - /// Find the best total assignment, with given ordering - can be expensive. - DiscreteValues optimalAssignment(const Ordering& ordering) const; - // /* // * Perform loopy belief propagation // * True belief propagation would check for each value in domain diff --git a/gtsam_unstable/discrete/Scheduler.cpp b/gtsam_unstable/discrete/Scheduler.cpp index f166405932..b86df6c290 100644 --- a/gtsam_unstable/discrete/Scheduler.cpp +++ b/gtsam_unstable/discrete/Scheduler.cpp @@ -255,23 +255,6 @@ DiscreteBayesNet::shared_ptr Scheduler::eliminate() const { return chordal; } -/** Find the best total assignment - can be expensive */ -DiscreteValues Scheduler::optimalAssignment() const { - DiscreteBayesNet::shared_ptr chordal = eliminate(); - - if (ISDEBUG("Scheduler::optimalAssignment")) { - DiscreteBayesNet::const_iterator it = chordal->end() - 1; - const Student& student = students_.front(); - cout << endl; - (*it)->print(student.name_); - } - - gttic(my_optimize); - DiscreteValues mpe = chordal->optimize(); - gttoc(my_optimize); - return mpe; -} - /** find the assignment of students to slots with most possible committees */ DiscreteValues Scheduler::bestSchedule() const { DiscreteValues best; diff --git a/gtsam_unstable/discrete/Scheduler.h b/gtsam_unstable/discrete/Scheduler.h index a97368bb25..8d269e81a6 100644 --- a/gtsam_unstable/discrete/Scheduler.h +++ b/gtsam_unstable/discrete/Scheduler.h @@ -147,9 +147,6 @@ class GTSAM_UNSTABLE_EXPORT Scheduler : public CSP { /** Eliminate, return a Bayes net */ DiscreteBayesNet::shared_ptr eliminate() const; - /** Find the best total assignment - can be expensive */ - DiscreteValues optimalAssignment() const; - /** find the assignment of students to slots with most possible committees */ DiscreteValues bestSchedule() const; diff --git a/gtsam_unstable/discrete/examples/schedulingExample.cpp b/gtsam_unstable/discrete/examples/schedulingExample.cpp index 2a9addf918..7ed00bcf61 100644 --- a/gtsam_unstable/discrete/examples/schedulingExample.cpp +++ b/gtsam_unstable/discrete/examples/schedulingExample.cpp @@ -122,7 +122,7 @@ void runLargeExample() { // SETDEBUG("timing-verbose", true); SETDEBUG("DiscreteConditional::DiscreteConditional", true); gttic(large); - auto MPE = scheduler.optimalAssignment(); + auto MPE = scheduler.optimize(); gttoc(large); tictoc_finishedIteration(); tictoc_print(); diff --git a/gtsam_unstable/discrete/examples/schedulingQuals12.cpp b/gtsam_unstable/discrete/examples/schedulingQuals12.cpp index 8260bfb068..e6a47f5f86 100644 --- a/gtsam_unstable/discrete/examples/schedulingQuals12.cpp +++ b/gtsam_unstable/discrete/examples/schedulingQuals12.cpp @@ -143,7 +143,7 @@ void runLargeExample() { } #else gttic(large); - auto MPE = scheduler.optimalAssignment(); + auto MPE = scheduler.optimize(); gttoc(large); tictoc_finishedIteration(); tictoc_print(); diff --git a/gtsam_unstable/discrete/examples/schedulingQuals13.cpp b/gtsam_unstable/discrete/examples/schedulingQuals13.cpp index cf3ce04535..82ea16a47a 100644 --- a/gtsam_unstable/discrete/examples/schedulingQuals13.cpp +++ b/gtsam_unstable/discrete/examples/schedulingQuals13.cpp @@ -167,7 +167,7 @@ void runLargeExample() { } #else gttic(large); - auto MPE = scheduler.optimalAssignment(); + auto MPE = scheduler.optimize(); gttoc(large); tictoc_finishedIteration(); tictoc_print(); diff --git a/gtsam_unstable/discrete/tests/testCSP.cpp b/gtsam_unstable/discrete/tests/testCSP.cpp index 88defd9860..fb386b2553 100644 --- a/gtsam_unstable/discrete/tests/testCSP.cpp +++ b/gtsam_unstable/discrete/tests/testCSP.cpp @@ -132,7 +132,7 @@ TEST(CSP, allInOne) { EXPECT(assert_equal(expectedProduct, product)); // Solve - auto mpe = csp.optimalAssignment(); + auto mpe = csp.optimize(); DiscreteValues expected; insert(expected)(ID.first, 1)(UT.first, 0)(AZ.first, 1); EXPECT(assert_equal(expected, mpe)); @@ -172,22 +172,18 @@ TEST(CSP, WesternUS) { csp.addAllDiff(WY, CO); csp.addAllDiff(CO, NM); + DiscreteValues mpe; + insert(mpe)(0, 2)(1, 3)(2, 2)(3, 1)(4, 1)(5, 3)(6, 3)(7, 2)(8, 0)(9, 1)(10, 0); + // Create ordering according to example in ND-CSP.lyx Ordering ordering; ordering += Key(0), Key(1), Key(2), Key(3), Key(4), Key(5), Key(6), Key(7), Key(8), Key(9), Key(10); + // Solve using that ordering: - auto mpe = csp.optimalAssignment(ordering); - // GTSAM_PRINT(mpe); - DiscreteValues expected; - insert(expected)(WA.first, 1)(CA.first, 1)(NV.first, 3)(OR.first, 0)( - MT.first, 1)(WY.first, 0)(NM.first, 3)(CO.first, 2)(ID.first, 2)( - UT.first, 1)(AZ.first, 0); + auto actualMPE = csp.optimize(ordering); - // TODO: Fix me! mpe result seems to be right. (See the printing) - // It has the same prob as the expected solution. - // Is mpe another solution, or the expected solution is unique??? - EXPECT(assert_equal(expected, mpe)); + EXPECT(assert_equal(mpe, actualMPE)); EXPECT_DOUBLES_EQUAL(1, csp(mpe), 1e-9); // Write out the dual graph for hmetis @@ -227,7 +223,7 @@ TEST(CSP, ArcConsistency) { EXPECT_DOUBLES_EQUAL(1, csp(valid), 1e-9); // Solve - auto mpe = csp.optimalAssignment(); + auto mpe = csp.optimize(); DiscreteValues expected; insert(expected)(ID.first, 1)(UT.first, 0)(AZ.first, 2); EXPECT(assert_equal(expected, mpe)); diff --git a/gtsam_unstable/discrete/tests/testScheduler.cpp b/gtsam_unstable/discrete/tests/testScheduler.cpp index 7822cbd38b..086057a466 100644 --- a/gtsam_unstable/discrete/tests/testScheduler.cpp +++ b/gtsam_unstable/discrete/tests/testScheduler.cpp @@ -122,7 +122,7 @@ TEST(schedulingExample, test) { // Do exact inference gttic(small); - auto MPE = s.optimalAssignment(); + auto MPE = s.optimize(); gttoc(small); // print MPE, commented out as unit tests don't print diff --git a/gtsam_unstable/discrete/tests/testSudoku.cpp b/gtsam_unstable/discrete/tests/testSudoku.cpp index 35f3ba8437..8b28581699 100644 --- a/gtsam_unstable/discrete/tests/testSudoku.cpp +++ b/gtsam_unstable/discrete/tests/testSudoku.cpp @@ -100,7 +100,7 @@ class Sudoku : public CSP { /// solve and print solution void printSolution() const { - auto MPE = optimalAssignment(); + auto MPE = optimize(); printAssignment(MPE); } @@ -126,7 +126,7 @@ TEST(Sudoku, small) { 0, 1, 0, 0); // optimize and check - auto solution = csp.optimalAssignment(); + auto solution = csp.optimize(); DiscreteValues expected; insert(expected)(csp.key(0, 0), 0)(csp.key(0, 1), 1)(csp.key(0, 2), 2)( csp.key(0, 3), 3)(csp.key(1, 0), 2)(csp.key(1, 1), 3)(csp.key(1, 2), 0)( @@ -148,7 +148,7 @@ TEST(Sudoku, small) { EXPECT_LONGS_EQUAL(16, new_csp.size()); // Check that solution - auto new_solution = new_csp.optimalAssignment(); + auto new_solution = new_csp.optimize(); // csp.printAssignment(new_solution); EXPECT(assert_equal(expected, new_solution)); } @@ -250,7 +250,7 @@ TEST(Sudoku, AJC_3star_Feb8_2012) { EXPECT_LONGS_EQUAL(81, new_csp.size()); // Check that solution - auto solution = new_csp.optimalAssignment(); + auto solution = new_csp.optimize(); // csp.printAssignment(solution); EXPECT_LONGS_EQUAL(6, solution.at(key99)); } From 2ac79af17fe202f61092ea8355c321d5000fff7b Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 14:47:46 -0500 Subject: [PATCH 36/91] Added optimize variants that take custom ordering --- gtsam/discrete/DiscreteFactorGraph.cpp | 39 ++++++++++++++------------ gtsam/discrete/DiscreteFactorGraph.h | 16 +++++++++++ gtsam/discrete/DiscreteLookupDAG.cpp | 17 +++++++++++ gtsam/discrete/DiscreteLookupDAG.h | 5 ++++ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.cpp b/gtsam/discrete/DiscreteFactorGraph.cpp index a166fdce93..7c03d21f9c 100644 --- a/gtsam/discrete/DiscreteFactorGraph.cpp +++ b/gtsam/discrete/DiscreteFactorGraph.cpp @@ -131,36 +131,39 @@ namespace gtsam { } /* ************************************************************************ */ + // The max-product solution below is a bit clunky: the elimination machinery + // does not allow for differently *typed* versions of elimination, so we + // eliminate into a Bayes Net using the special eliminate function above, and + // then create the DiscreteLookupDAG after the fact, in linear time. + DiscreteLookupDAG DiscreteFactorGraph::maxProduct( OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_maxProduct); - - // The solution below is a bitclunky: the elimination machinery does not - // allow for differently *typed* versions of elimination, so we eliminate - // into a Bayes Net using the special eliminate function above, and then - // create the DiscreteLookupDAG after the fact, in linear time. auto bayesNet = BaseEliminateable::eliminateSequential(orderingType, EliminateForMPE); + return DiscreteLookupDAG::FromBayesNet(*bayesNet); + } - // Copy to the DAG - DiscreteLookupDAG dag; - for (auto&& conditional : *bayesNet) { - if (auto lookupTable = - boost::dynamic_pointer_cast(conditional)) { - dag.push_back(lookupTable); - } else { - throw std::runtime_error( - "DiscreteFactorGraph::maxProduct: Expected look up table."); - } - } - return dag; + DiscreteLookupDAG DiscreteFactorGraph::maxProduct( + const Ordering& ordering) const { + gttic(DiscreteFactorGraph_maxProduct); + auto bayesNet = + BaseEliminateable::eliminateSequential(ordering, EliminateForMPE); + return DiscreteLookupDAG::FromBayesNet(*bayesNet); } /* ************************************************************************ */ DiscreteValues DiscreteFactorGraph::optimize( OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_optimize); - DiscreteLookupDAG dag = maxProduct(); + DiscreteLookupDAG dag = maxProduct(orderingType); + return dag.argmax(); + } + + DiscreteValues DiscreteFactorGraph::optimize( + const Ordering& ordering) const { + gttic(DiscreteFactorGraph_optimize); + DiscreteLookupDAG dag = maxProduct(ordering); return dag.argmax(); } diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 7c658f5484..59827f9a57 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -138,6 +138,14 @@ class GTSAM_EXPORT DiscreteFactorGraph DiscreteLookupDAG maxProduct( OptionalOrderingType orderingType = boost::none) const; + /** + * @brief Implement the max-product algorithm + * + * @param ordering + * @return DiscreteLookupDAG::shared_ptr `DAG with lookup tables + */ + DiscreteLookupDAG maxProduct(const Ordering& ordering) const; + /** * @brief Find the maximum probable explanation (MPE) by doing max-product. * @@ -147,6 +155,14 @@ class GTSAM_EXPORT DiscreteFactorGraph DiscreteValues optimize( OptionalOrderingType orderingType = boost::none) const; + /** + * @brief Find the maximum probable explanation (MPE) by doing max-product. + * + * @param ordering + * @return DiscreteValues : MPE + */ + DiscreteValues optimize(const Ordering& ordering) const; + // /** Permute the variables in the factors */ // GTSAM_EXPORT void permuteWithInverse(const Permutation& // inversePermutation); diff --git a/gtsam/discrete/DiscreteLookupDAG.cpp b/gtsam/discrete/DiscreteLookupDAG.cpp index 1edf508a1e..16620cc249 100644 --- a/gtsam/discrete/DiscreteLookupDAG.cpp +++ b/gtsam/discrete/DiscreteLookupDAG.cpp @@ -16,6 +16,7 @@ * @author Frank Dellaert */ +#include #include #include @@ -99,6 +100,22 @@ size_t DiscreteLookupTable::argmax(const DiscreteValues& parentsValues) const { return mpe; } +/* ************************************************************************** */ +DiscreteLookupDAG DiscreteLookupDAG::FromBayesNet( + const DiscreteBayesNet& bayesNet) { + DiscreteLookupDAG dag; + for (auto&& conditional : bayesNet) { + if (auto lookupTable = + boost::dynamic_pointer_cast(conditional)) { + dag.push_back(lookupTable); + } else { + throw std::runtime_error( + "DiscreteFactorGraph::maxProduct: Expected look up table."); + } + } + return dag; +} + /* ************************************************************************** */ DiscreteValues DiscreteLookupDAG::argmax() const { DiscreteValues result; diff --git a/gtsam/discrete/DiscreteLookupDAG.h b/gtsam/discrete/DiscreteLookupDAG.h index 1b3a38b406..f1eb24ec36 100644 --- a/gtsam/discrete/DiscreteLookupDAG.h +++ b/gtsam/discrete/DiscreteLookupDAG.h @@ -28,6 +28,8 @@ namespace gtsam { +class DiscreteBayesNet; + /** * @brief DiscreteLookupTable table for max-product * @@ -83,6 +85,9 @@ class GTSAM_EXPORT DiscreteLookupDAG : public BayesNet { /// Construct empty DAG. DiscreteLookupDAG() {} + // Create from BayesNet with LookupTables + static DiscreteLookupDAG FromBayesNet(const DiscreteBayesNet& bayesNet); + /// Destructor virtual ~DiscreteLookupDAG() {} From ad21632fd27331f21fde0cd416bb0b669ef5003d Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 17:35:33 -0500 Subject: [PATCH 37/91] fix typos --- gtsam/discrete/tests/testDiscreteDistribution.cpp | 2 +- python/gtsam/tests/test_DiscreteDistribution.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gtsam/discrete/tests/testDiscreteDistribution.cpp b/gtsam/discrete/tests/testDiscreteDistribution.cpp index 5e59aaa65b..d88b510f81 100644 --- a/gtsam/discrete/tests/testDiscreteDistribution.cpp +++ b/gtsam/discrete/tests/testDiscreteDistribution.cpp @@ -10,7 +10,7 @@ * -------------------------------------------------------------------------- */ /* - * @file testDiscretePrior.cpp + * @file testDiscreteDistribution.cpp * @brief unit tests for DiscreteDistribution * @author Frank dellaert * @date December 2021 diff --git a/python/gtsam/tests/test_DiscreteDistribution.py b/python/gtsam/tests/test_DiscreteDistribution.py index fa999fd6b5..3986bf2dfc 100644 --- a/python/gtsam/tests/test_DiscreteDistribution.py +++ b/python/gtsam/tests/test_DiscreteDistribution.py @@ -20,7 +20,7 @@ X = 0, 2 -class TestDiscretePrior(GtsamTestCase): +class TestDiscreteDistribution(GtsamTestCase): """Tests for Discrete Priors.""" def test_constructor(self): From 125708fbb754887962da1a5ab962fd330913e2cf Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 17:35:39 -0500 Subject: [PATCH 38/91] Fix wrapper --- gtsam/discrete/discrete.i | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index e2310f4344..97c267aba2 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -111,11 +111,9 @@ virtual class DiscreteConditional : gtsam::DecisionTreeFactor { gtsam::DecisionTreeFactor* likelihood( const gtsam::DiscreteValues& frontalValues) const; gtsam::DecisionTreeFactor* likelihood(size_t value) const; - size_t solve(const gtsam::DiscreteValues& parentsValues) const; size_t sample(const gtsam::DiscreteValues& parentsValues) const; size_t sample(size_t value) const; size_t sample() const; - void solveInPlace(gtsam::DiscreteValues @parentsValues) const; void sampleInPlace(gtsam::DiscreteValues @parentsValues) const; string markdown(const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; @@ -138,7 +136,7 @@ virtual class DiscreteDistribution : gtsam::DiscreteConditional { gtsam::DefaultKeyFormatter) const; double operator()(size_t value) const; std::vector pmf() const; - size_t solve() const; + size_t argmax() const; }; #include @@ -163,8 +161,6 @@ class DiscreteBayesNet { void saveGraph(string s, const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; double operator()(const gtsam::DiscreteValues& values) const; - gtsam::DiscreteValues optimize() const; - gtsam::DiscreteValues optimize(gtsam::DiscreteValues given) const; gtsam::DiscreteValues sample() const; gtsam::DiscreteValues sample(gtsam::DiscreteValues given) const; string markdown(const gtsam::KeyFormatter& keyFormatter = @@ -217,6 +213,21 @@ class DiscreteBayesTree { std::map> names) const; }; +#include +class DiscreteLookupDAG { + DiscreteLookupDAG(); + void push_back(const gtsam::DiscreteLookupTable* table); + bool empty() const; + size_t size() const; + gtsam::KeySet keys() const; + const gtsam::DiscreteLookupTable* at(size_t i) const; + void print(string s = "DiscreteLookupDAG\n", + const gtsam::KeyFormatter& keyFormatter = + gtsam::DefaultKeyFormatter) const; + gtsam::DiscreteValues argmax() const; + gtsam::DiscreteValues argmax(gtsam::DiscreteValues given) const; +}; + #include class DotWriter { DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, @@ -260,6 +271,9 @@ class DiscreteFactorGraph { double operator()(const gtsam::DiscreteValues& values) const; gtsam::DiscreteValues optimize() const; + gtsam::DiscreteLookupDAG maxProduct(); + gtsam::DiscreteLookupDAG maxProduct(const gtsam::Ordering& ordering); + gtsam::DiscreteBayesNet eliminateSequential(); gtsam::DiscreteBayesNet eliminateSequential(const gtsam::Ordering& ordering); std::pair From 03314ed781fccd71bc98985d556d7f334b7dac24 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 17:39:06 -0500 Subject: [PATCH 39/91] updates to fix various issues --- gtsam/discrete/DiscreteConditional.cpp | 6 ++---- gtsam/discrete/DiscreteFactorGraph.h | 8 -------- gtsam/discrete/DiscreteLookupDAG.cpp | 8 +------- gtsam/discrete/DiscreteLookupDAG.h | 26 +++++++++----------------- 4 files changed, 12 insertions(+), 36 deletions(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index 164a45f407..9a4897b727 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -322,8 +322,7 @@ size_t DiscreteConditional::sample(const DiscreteValues& parentsValues) const { return distribution(rng); } -/* ******************************************************************************** - */ +/* ************************************************************************** */ size_t DiscreteConditional::sample(size_t parent_value) const { if (nrParents() != 1) throw std::invalid_argument( @@ -334,8 +333,7 @@ size_t DiscreteConditional::sample(size_t parent_value) const { return sample(values); } -/* ******************************************************************************** - */ +/* ************************************************************************** */ size_t DiscreteConditional::sample() const { if (nrParents() != 0) throw std::invalid_argument( diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 59827f9a57..e0f0a104bd 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -163,14 +163,6 @@ class GTSAM_EXPORT DiscreteFactorGraph */ DiscreteValues optimize(const Ordering& ordering) const; - // /** Permute the variables in the factors */ - // GTSAM_EXPORT void permuteWithInverse(const Permutation& - // inversePermutation); - // - // /** Apply a reduction, which is a remapping of variable indices. */ - // GTSAM_EXPORT void reduceWithInverse(const internal::Reduction& - // inverseReduction); - /// @name Wrapper support /// @{ diff --git a/gtsam/discrete/DiscreteLookupDAG.cpp b/gtsam/discrete/DiscreteLookupDAG.cpp index 16620cc249..d96b38b0ec 100644 --- a/gtsam/discrete/DiscreteLookupDAG.cpp +++ b/gtsam/discrete/DiscreteLookupDAG.cpp @@ -10,7 +10,7 @@ * -------------------------------------------------------------------------- */ /** - * @file DiscreteLookupTable.cpp + * @file DiscreteLookupDAG.cpp * @date Feb 14, 2011 * @author Duy-Nguyen Ta * @author Frank Dellaert @@ -116,12 +116,6 @@ DiscreteLookupDAG DiscreteLookupDAG::FromBayesNet( return dag; } -/* ************************************************************************** */ -DiscreteValues DiscreteLookupDAG::argmax() const { - DiscreteValues result; - return argmax(result); -} - DiscreteValues DiscreteLookupDAG::argmax(DiscreteValues result) const { // Argmax each node in turn in topological sort order (parents first). for (auto lookupTable : boost::adaptors::reverse(*this)) diff --git a/gtsam/discrete/DiscreteLookupDAG.h b/gtsam/discrete/DiscreteLookupDAG.h index f1eb24ec36..8cb651f28a 100644 --- a/gtsam/discrete/DiscreteLookupDAG.h +++ b/gtsam/discrete/DiscreteLookupDAG.h @@ -11,7 +11,7 @@ /** * @file DiscreteLookupDAG.h - * @date JAnuary, 2022 + * @date January, 2022 * @author Frank dellaert */ @@ -34,7 +34,7 @@ class DiscreteBayesNet; * @brief DiscreteLookupTable table for max-product * * Inherits from discrete conditional for convenience, but is not normalized. - * Is used in pax-product algorithm. + * Is used in the max-product algorithm. */ class DiscreteLookupTable : public DiscreteConditional { public: @@ -85,7 +85,7 @@ class GTSAM_EXPORT DiscreteLookupDAG : public BayesNet { /// Construct empty DAG. DiscreteLookupDAG() {} - // Create from BayesNet with LookupTables + /// Create from BayesNet with LookupTables static DiscreteLookupDAG FromBayesNet(const DiscreteBayesNet& bayesNet); /// Destructor @@ -111,25 +111,17 @@ class GTSAM_EXPORT DiscreteLookupDAG : public BayesNet { } /** - * @brief argmax by back-substitution. + * @brief argmax by back-substitution, optionally given certain variables. * * Assumes the DAG is reverse topologically sorted, i.e. last - * conditional will be optimized first. If the DAG resulted from - * eliminating a factor graph, this is true for the elimination ordering. - * - * @return optimal assignment for all variables. - */ - DiscreteValues argmax() const; - - /** - * @brief argmax by back-substitution, given certain variables. - * - * Assumes the DAG is reverse topologically sorted *and* that the - * DAG does not contain any conditionals for the given variables. + * conditional will be optimized first *and* that the + * DAG does not contain any conditionals for the given variables. If the DAG + * resulted from eliminating a factor graph, this is true for the elimination + * ordering. * * @return given assignment extended w. optimal assignment for all variables. */ - DiscreteValues argmax(DiscreteValues given) const; + DiscreteValues argmax(DiscreteValues given = DiscreteValues()) const; /// @} private: From f9b14893c86fdde1dc870b9e2d20e50390b71633 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 18:10:47 -0500 Subject: [PATCH 40/91] moved argmax to conditional --- gtsam/discrete/DiscreteConditional.cpp | 20 ++++++++++++++++++++ gtsam/discrete/DiscreteConditional.h | 6 ++++++ gtsam/discrete/DiscreteDistribution.cpp | 17 ----------------- gtsam/discrete/DiscreteDistribution.h | 12 ------------ 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/gtsam/discrete/DiscreteConditional.cpp b/gtsam/discrete/DiscreteConditional.cpp index 9a4897b727..06b2856f8f 100644 --- a/gtsam/discrete/DiscreteConditional.cpp +++ b/gtsam/discrete/DiscreteConditional.cpp @@ -286,6 +286,26 @@ size_t DiscreteConditional::solve(const DiscreteValues& parentsValues) const { } #endif +/* ************************************************************************** */ +size_t DiscreteConditional::argmax() const { + size_t maxValue = 0; + double maxP = 0; + assert(nrFrontals() == 1); + assert(nrParents() == 0); + DiscreteValues frontals; + Key j = firstFrontalKey(); + for (size_t value = 0; value < cardinality(j); value++) { + frontals[j] = value; + double pValueS = (*this)(frontals); + // Update MPE solution if better + if (pValueS > maxP) { + maxP = pValueS; + maxValue = value; + } + } + return maxValue; +} + /* ************************************************************************** */ void DiscreteConditional::sampleInPlace(DiscreteValues* values) const { assert(nrFrontals() == 1); diff --git a/gtsam/discrete/DiscreteConditional.h b/gtsam/discrete/DiscreteConditional.h index af05e932bb..48d94a3837 100644 --- a/gtsam/discrete/DiscreteConditional.h +++ b/gtsam/discrete/DiscreteConditional.h @@ -192,6 +192,12 @@ class GTSAM_EXPORT DiscreteConditional /// Zero parent version. size_t sample() const; + /** + * @brief Return assignment that maximizes distribution. + * @return Optimal assignment (1 frontal variable). + */ + size_t argmax() const; + /// @} /// @name Advanced Interface /// @{ diff --git a/gtsam/discrete/DiscreteDistribution.cpp b/gtsam/discrete/DiscreteDistribution.cpp index 5f6fba6a28..7397714709 100644 --- a/gtsam/discrete/DiscreteDistribution.cpp +++ b/gtsam/discrete/DiscreteDistribution.cpp @@ -49,21 +49,4 @@ std::vector DiscreteDistribution::pmf() const { return array; } -/* ************************************************************************** */ -size_t DiscreteDistribution::argmax() const { - size_t maxValue = 0; - double maxP = 0; - assert(nrFrontals() == 1); - Key j = firstFrontalKey(); - for (size_t value = 0; value < cardinality(j); value++) { - double pValueS = (*this)(value); - // Update MPE solution if better - if (pValueS > maxP) { - maxP = pValueS; - maxValue = value; - } - } - return maxValue; -} - } // namespace gtsam diff --git a/gtsam/discrete/DiscreteDistribution.h b/gtsam/discrete/DiscreteDistribution.h index 8dcc75733f..c5147dbc19 100644 --- a/gtsam/discrete/DiscreteDistribution.h +++ b/gtsam/discrete/DiscreteDistribution.h @@ -90,18 +90,6 @@ class GTSAM_EXPORT DiscreteDistribution : public DiscreteConditional { /// Return entire probability mass function. std::vector pmf() const; - /** - * @brief Return assignment that maximizes distribution. - * @return Optimal assignment (1 frontal variable). - */ - size_t argmax() const; - - /** - * sample - * @return sample from conditional - */ - size_t sample() const { return Base::sample(); } - /// @} #ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 /// @name Deprecated functionality From e3c98b0fafee3352847f0aeffbc1a7490fdcd488 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 18:12:30 -0500 Subject: [PATCH 41/91] Fix python tests --- python/gtsam/tests/test_DiscreteBayesNet.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/python/gtsam/tests/test_DiscreteBayesNet.py b/python/gtsam/tests/test_DiscreteBayesNet.py index 6abd660cfc..3ae3b625cd 100644 --- a/python/gtsam/tests/test_DiscreteBayesNet.py +++ b/python/gtsam/tests/test_DiscreteBayesNet.py @@ -79,7 +79,7 @@ def test_Asia(self): self.gtsamAssertEquals(chordal.at(7), expected2) # solve - actualMPE = chordal.optimize() + actualMPE = fg.optimize() expectedMPE = DiscreteValues() for key in [Asia, Dyspnea, XRay, Tuberculosis, Smoking, Either, LungCancer, Bronchitis]: expectedMPE[key[0]] = 0 @@ -94,8 +94,7 @@ def test_Asia(self): fg.add(Dyspnea, "0 1") # solve again, now with evidence - chordal2 = fg.eliminateSequential(ordering) - actualMPE2 = chordal2.optimize() + actualMPE2 = fg.optimize() expectedMPE2 = DiscreteValues() for key in [XRay, Tuberculosis, Either, LungCancer]: expectedMPE2[key[0]] = 0 @@ -105,6 +104,7 @@ def test_Asia(self): list(expectedMPE2.items())) # now sample from it + chordal2 = fg.eliminateSequential(ordering) actualSample = chordal2.sample() self.assertEqual(len(actualSample), 8) @@ -122,10 +122,6 @@ def test_fragment(self): for key in [Asia, Smoking]: given[key[0]] = 0 - # Now optimize fragment: - actual = fragment.optimize(given) - self.assertEqual(len(actual), 5) - # Now sample from fragment: actual = fragment.sample(given) self.assertEqual(len(actual), 5) From 99a97da5f77b506e83daf5cb76b02fa16188e615 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Fri, 21 Jan 2022 18:12:38 -0500 Subject: [PATCH 42/91] Fix examples --- examples/DiscreteBayesNetExample.cpp | 9 ++++----- examples/DiscreteBayesNet_FG.cpp | 8 ++++---- examples/HMMExample.cpp | 8 ++++---- examples/UGM_chain.cpp | 5 ++--- examples/UGM_small.cpp | 5 ++--- gtsam_unstable/discrete/examples/schedulingExample.cpp | 8 ++++---- gtsam_unstable/discrete/examples/schedulingQuals12.cpp | 4 ++-- gtsam_unstable/discrete/examples/schedulingQuals13.cpp | 4 ++-- 8 files changed, 24 insertions(+), 27 deletions(-) diff --git a/examples/DiscreteBayesNetExample.cpp b/examples/DiscreteBayesNetExample.cpp index febc1e1288..dfd7beb63b 100644 --- a/examples/DiscreteBayesNetExample.cpp +++ b/examples/DiscreteBayesNetExample.cpp @@ -53,10 +53,9 @@ int main(int argc, char **argv) { // Create solver and eliminate Ordering ordering; ordering += Key(0), Key(1), Key(2), Key(3), Key(4), Key(5), Key(6), Key(7); - DiscreteBayesNet::shared_ptr chordal = fg.eliminateSequential(ordering); // solve - auto mpe = chordal->optimize(); + auto mpe = fg.optimize(); GTSAM_PRINT(mpe); // We can also build a Bayes tree (directed junction tree). @@ -69,14 +68,14 @@ int main(int argc, char **argv) { fg.add(Dyspnea, "0 1"); // solve again, now with evidence - DiscreteBayesNet::shared_ptr chordal2 = fg.eliminateSequential(ordering); - auto mpe2 = chordal2->optimize(); + auto mpe2 = fg.optimize(); GTSAM_PRINT(mpe2); // We can also sample from it + DiscreteBayesNet::shared_ptr chordal = fg.eliminateSequential(ordering); cout << "\n10 samples:" << endl; for (size_t i = 0; i < 10; i++) { - auto sample = chordal2->sample(); + auto sample = chordal->sample(); GTSAM_PRINT(sample); } return 0; diff --git a/examples/DiscreteBayesNet_FG.cpp b/examples/DiscreteBayesNet_FG.cpp index 69283a1be7..88904001a0 100644 --- a/examples/DiscreteBayesNet_FG.cpp +++ b/examples/DiscreteBayesNet_FG.cpp @@ -85,7 +85,7 @@ int main(int argc, char **argv) { } // "Most Probable Explanation", i.e., configuration with largest value - auto mpe = graph.eliminateSequential()->optimize(); + auto mpe = graph.optimize(); cout << "\nMost Probable Explanation (MPE):" << endl; print(mpe); @@ -96,8 +96,7 @@ int main(int argc, char **argv) { graph.add(Cloudy, "1 0"); // solve again, now with evidence - DiscreteBayesNet::shared_ptr chordal = graph.eliminateSequential(); - auto mpe_with_evidence = chordal->optimize(); + auto mpe_with_evidence = graph.optimize(); cout << "\nMPE given C=0:" << endl; print(mpe_with_evidence); @@ -110,7 +109,8 @@ int main(int argc, char **argv) { cout << "\nP(W=1|C=0):" << marginals.marginalProbabilities(WetGrass)[1] << endl; - // We can also sample from it + // We can also sample from the eliminated graph + DiscreteBayesNet::shared_ptr chordal = graph.eliminateSequential(); cout << "\n10 samples:" << endl; for (size_t i = 0; i < 10; i++) { auto sample = chordal->sample(); diff --git a/examples/HMMExample.cpp b/examples/HMMExample.cpp index b46baf4e09..3a76730016 100644 --- a/examples/HMMExample.cpp +++ b/examples/HMMExample.cpp @@ -59,16 +59,16 @@ int main(int argc, char **argv) { // Convert to factor graph DiscreteFactorGraph factorGraph(hmm); + // Do max-prodcut + auto mpe = factorGraph.optimize(); + GTSAM_PRINT(mpe); + // Create solver and eliminate // This will create a DAG ordered with arrow of time reversed DiscreteBayesNet::shared_ptr chordal = factorGraph.eliminateSequential(ordering); chordal->print("Eliminated"); - // solve - auto mpe = chordal->optimize(); - GTSAM_PRINT(mpe); - // We can also sample from it cout << "\n10 samples:" << endl; for (size_t k = 0; k < 10; k++) { diff --git a/examples/UGM_chain.cpp b/examples/UGM_chain.cpp index ababef0220..ad21af9fa7 100644 --- a/examples/UGM_chain.cpp +++ b/examples/UGM_chain.cpp @@ -68,9 +68,8 @@ int main(int argc, char** argv) { << graph.size() << " factors (Unary+Edge)."; // "Decoding", i.e., configuration with largest value - // We use sequential variable elimination - DiscreteBayesNet::shared_ptr chordal = graph.eliminateSequential(); - auto optimalDecoding = chordal->optimize(); + // Uses max-product. + auto optimalDecoding = graph.optimize(); optimalDecoding.print("\nMost Probable Explanation (optimalDecoding)\n"); // "Inference" Computing marginals for each node diff --git a/examples/UGM_small.cpp b/examples/UGM_small.cpp index 24bd0c0ba7..bc6a413178 100644 --- a/examples/UGM_small.cpp +++ b/examples/UGM_small.cpp @@ -61,9 +61,8 @@ int main(int argc, char** argv) { } // "Decoding", i.e., configuration with largest value (MPE) - // We use sequential variable elimination - DiscreteBayesNet::shared_ptr chordal = graph.eliminateSequential(); - auto optimalDecoding = chordal->optimize(); + // Uses max-product + auto optimalDecoding = graph.optimize(); GTSAM_PRINT(optimalDecoding); // "Inference" Computing marginals diff --git a/gtsam_unstable/discrete/examples/schedulingExample.cpp b/gtsam_unstable/discrete/examples/schedulingExample.cpp index 7ed00bcf61..487edc97a3 100644 --- a/gtsam_unstable/discrete/examples/schedulingExample.cpp +++ b/gtsam_unstable/discrete/examples/schedulingExample.cpp @@ -165,11 +165,11 @@ void solveStaged(size_t addMutex = 2) { root->print(""/*scheduler.studentName(s)*/); // solve root node only - DiscreteValues values; - size_t bestSlot = root->solve(values); + size_t bestSlot = root->argmax(); // get corresponding count DiscreteKey dkey = scheduler.studentKey(6 - s); + DiscreteValues values; values[dkey.first] = bestSlot; size_t count = (*root)(values); @@ -319,11 +319,11 @@ void accomodateStudent() { // GTSAM_PRINT(*chordal); // solve root node only - DiscreteValues values; - size_t bestSlot = root->solve(values); + size_t bestSlot = root->argmax(); // get corresponding count DiscreteKey dkey = scheduler.studentKey(0); + DiscreteValues values; values[dkey.first] = bestSlot; size_t count = (*root)(values); cout << boost::format("%s = %d (%d), count = %d") % scheduler.studentName(0) diff --git a/gtsam_unstable/discrete/examples/schedulingQuals12.cpp b/gtsam_unstable/discrete/examples/schedulingQuals12.cpp index e6a47f5f86..830d59ba73 100644 --- a/gtsam_unstable/discrete/examples/schedulingQuals12.cpp +++ b/gtsam_unstable/discrete/examples/schedulingQuals12.cpp @@ -190,11 +190,11 @@ void solveStaged(size_t addMutex = 2) { root->print(""/*scheduler.studentName(s)*/); // solve root node only - DiscreteValues values; - size_t bestSlot = root->solve(values); + size_t bestSlot = root->argmax(); // get corresponding count DiscreteKey dkey = scheduler.studentKey(NRSTUDENTS - 1 - s); + DiscreteValues values; values[dkey.first] = bestSlot; size_t count = (*root)(values); diff --git a/gtsam_unstable/discrete/examples/schedulingQuals13.cpp b/gtsam_unstable/discrete/examples/schedulingQuals13.cpp index 82ea16a47a..b24f9bf0a4 100644 --- a/gtsam_unstable/discrete/examples/schedulingQuals13.cpp +++ b/gtsam_unstable/discrete/examples/schedulingQuals13.cpp @@ -212,11 +212,11 @@ void solveStaged(size_t addMutex = 2) { root->print(""/*scheduler.studentName(s)*/); // solve root node only - DiscreteValues values; - size_t bestSlot = root->solve(values); + size_t bestSlot = root->argmax(); // get corresponding count DiscreteKey dkey = scheduler.studentKey(NRSTUDENTS - 1 - s); + DiscreteValues values; values[dkey.first] = bestSlot; double count = (*root)(values); From 06150d143c9a2e03a8ef5b4ba687103bef3d3a11 Mon Sep 17 00:00:00 2001 From: Varun Agrawal Date: Fri, 21 Jan 2022 00:23:42 -0500 Subject: [PATCH 43/91] fix for setup.py install deprecation --- .github/scripts/python.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/python.sh b/.github/scripts/python.sh index 6cc62d2b06..6f5643fc75 100644 --- a/.github/scripts/python.sh +++ b/.github/scripts/python.sh @@ -83,6 +83,6 @@ cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ make -j2 install cd $GITHUB_WORKSPACE/build/python -$PYTHON setup.py install --user --prefix= +pip install --user --install-option="--prefix=" . cd $GITHUB_WORKSPACE/python/gtsam/tests $PYTHON -m unittest discover -v From 635eda6741e9ac55c840a4998331bcb873b3923c Mon Sep 17 00:00:00 2001 From: Varun Agrawal Date: Sat, 22 Jan 2022 10:17:12 -0500 Subject: [PATCH 44/91] Update python install in CI to use pip --- .github/scripts/python.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/python.sh b/.github/scripts/python.sh index 6cc62d2b06..0855dbc21b 100644 --- a/.github/scripts/python.sh +++ b/.github/scripts/python.sh @@ -83,6 +83,6 @@ cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ make -j2 install cd $GITHUB_WORKSPACE/build/python -$PYTHON setup.py install --user --prefix= +$PYTHON -m pip install --user . cd $GITHUB_WORKSPACE/python/gtsam/tests $PYTHON -m unittest discover -v From 5566ff64ebe36222ebadd72e66561cbf0cc0ce18 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:04:25 -0500 Subject: [PATCH 45/91] cpp file for utilities --- gtsam/base/utilities.cpp | 13 +++++++++++++ gtsam/base/utilities.h | 12 ++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 gtsam/base/utilities.cpp diff --git a/gtsam/base/utilities.cpp b/gtsam/base/utilities.cpp new file mode 100644 index 0000000000..189156c910 --- /dev/null +++ b/gtsam/base/utilities.cpp @@ -0,0 +1,13 @@ +#include + +namespace gtsam { + +std::string RedirectCout::str() const { + return ssBuffer_.str(); +} + +RedirectCout::~RedirectCout() { + std::cout.rdbuf(coutBuffer_); +} + +} diff --git a/gtsam/base/utilities.h b/gtsam/base/utilities.h index 8eb5617a8e..d9b92b8aa3 100644 --- a/gtsam/base/utilities.h +++ b/gtsam/base/utilities.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + namespace gtsam { /** * For Python __str__(). @@ -12,14 +16,10 @@ struct RedirectCout { RedirectCout() : ssBuffer_(), coutBuffer_(std::cout.rdbuf(ssBuffer_.rdbuf())) {} /// return the string - std::string str() const { - return ssBuffer_.str(); - } + std::string str() const; /// destructor -- redirect stdout buffer to its original buffer - ~RedirectCout() { - std::cout.rdbuf(coutBuffer_); - } + ~RedirectCout(); private: std::stringstream ssBuffer_; From 26890652c437140d4a2ce928cf2768546080eb4d Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:04:40 -0500 Subject: [PATCH 46/91] Default constructor --- gtsam/discrete/DiscreteMarginals.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gtsam/discrete/DiscreteMarginals.h b/gtsam/discrete/DiscreteMarginals.h index 27352a2110..a2207a10b0 100644 --- a/gtsam/discrete/DiscreteMarginals.h +++ b/gtsam/discrete/DiscreteMarginals.h @@ -37,6 +37,8 @@ class GTSAM_EXPORT DiscreteMarginals { public: + DiscreteMarginals() {} + /** Construct a marginals class. * @param graph The factor graph defining the full joint density on all variables. */ From 4cba05a2f7e0110e4f1996bd7ef808c6c76de9a4 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:05:48 -0500 Subject: [PATCH 47/91] New constructor --- gtsam/discrete/DiscreteKey.cpp | 8 +++----- gtsam/discrete/DiscreteKey.h | 9 +++++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/gtsam/discrete/DiscreteKey.cpp b/gtsam/discrete/DiscreteKey.cpp index 5ddad22b04..0857f8c1eb 100644 --- a/gtsam/discrete/DiscreteKey.cpp +++ b/gtsam/discrete/DiscreteKey.cpp @@ -38,11 +38,9 @@ namespace gtsam { return js; } - map DiscreteKeys::cardinalities() const { - map cs; - cs.insert(begin(),end()); -// for(const DiscreteKey& key: *this) -// cs.insert(key); + map DiscreteKeys::cardinalities() const { + map cs; + cs.insert(begin(), end()); return cs; } diff --git a/gtsam/discrete/DiscreteKey.h b/gtsam/discrete/DiscreteKey.h index ae4dac38fc..ce0c56dbeb 100644 --- a/gtsam/discrete/DiscreteKey.h +++ b/gtsam/discrete/DiscreteKey.h @@ -28,8 +28,8 @@ namespace gtsam { /** - * Key type for discrete conditionals - * Includes name and cardinality + * Key type for discrete variables. + * Includes Key and cardinality. */ using DiscreteKey = std::pair; @@ -45,6 +45,11 @@ namespace gtsam { /// Construct from a key explicit DiscreteKeys(const DiscreteKey& key) { push_back(key); } + /// Construct from cardinalities. + explicit DiscreteKeys(std::map cardinalities) { + for (auto&& kv : cardinalities) emplace_back(kv); + } + /// Construct from a vector of keys DiscreteKeys(const std::vector& keys) : std::vector(keys) { From 785b39d3c04a97944457debc734a71efc254dfa8 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:06:06 -0500 Subject: [PATCH 48/91] discreteKeys method --- gtsam/discrete/DiscreteFactorGraph.cpp | 18 ++++++++++++++++-- gtsam/discrete/DiscreteFactorGraph.h | 3 +++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.cpp b/gtsam/discrete/DiscreteFactorGraph.cpp index c1248c60b9..cb7c165959 100644 --- a/gtsam/discrete/DiscreteFactorGraph.cpp +++ b/gtsam/discrete/DiscreteFactorGraph.cpp @@ -43,11 +43,25 @@ namespace gtsam { /* ************************************************************************* */ KeySet DiscreteFactorGraph::keys() const { KeySet keys; - for(const sharedFactor& factor: *this) - if (factor) keys.insert(factor->begin(), factor->end()); + for (const sharedFactor& factor : *this) { + if (factor) keys.insert(factor->begin(), factor->end()); + } return keys; } + /* ************************************************************************* */ + DiscreteKeys DiscreteFactorGraph::discreteKeys() const { + DiscreteKeys result; + for (auto&& factor : *this) { + if (auto p = boost::dynamic_pointer_cast(factor)) { + DiscreteKeys factor_keys = p->discreteKeys(); + result.insert(result.end(), factor_keys.begin(), factor_keys.end()); + } + } + + return result; + } + /* ************************************************************************* */ DecisionTreeFactor DiscreteFactorGraph::product() const { DecisionTreeFactor result; diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 1da840eb8e..baa0213d55 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -114,6 +114,9 @@ class GTSAM_EXPORT DiscreteFactorGraph /** Return the set of variables involved in the factors (set union) */ KeySet keys() const; + /// Return the DiscreteKeys in this factor graph. + DiscreteKeys discreteKeys() const; + /** return product of all factors as a single factor */ DecisionTreeFactor product() const; From b875914f93aa7e017ceaa8bac7a628299de239dc Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:06:33 -0500 Subject: [PATCH 49/91] expNormalize, from Kevin Doherty --- gtsam/discrete/DiscreteFactor.cpp | 47 +++++++++++++++++++++++++++++++ gtsam/discrete/DiscreteFactor.h | 20 +++++++++++++ 2 files changed, 67 insertions(+) diff --git a/gtsam/discrete/DiscreteFactor.cpp b/gtsam/discrete/DiscreteFactor.cpp index 0cf7f2a5e8..08309e2e17 100644 --- a/gtsam/discrete/DiscreteFactor.cpp +++ b/gtsam/discrete/DiscreteFactor.cpp @@ -17,12 +17,59 @@ * @author Frank Dellaert */ +#include #include +#include #include using namespace std; namespace gtsam { +/* ************************************************************************* */ +std::vector expNormalize(const std::vector& logProbs) { + double maxLogProb = -std::numeric_limits::infinity(); + for (size_t i = 0; i < logProbs.size(); i++) { + double logProb = logProbs[i]; + if ((logProb != std::numeric_limits::infinity()) && + logProb > maxLogProb) { + maxLogProb = logProb; + } + } + + // After computing the max = "Z" of the log probabilities L_i, we compute + // the log of the normalizing constant, log S, where S = sum_j exp(L_j - Z). + double total = 0.0; + for (size_t i = 0; i < logProbs.size(); i++) { + double probPrime = exp(logProbs[i] - maxLogProb); + total += probPrime; + } + double logTotal = log(total); + + // Now we compute the (normalized) probability (for each i): + // p_i = exp(L_i - Z - log S) + double checkNormalization = 0.0; + std::vector probs; + for (size_t i = 0; i < logProbs.size(); i++) { + double prob = exp(logProbs[i] - maxLogProb - logTotal); + probs.push_back(prob); + checkNormalization += prob; + } + + // Numerical tolerance for floating point comparisons + double tol = 1e-9; + + if (!gtsam::fpEqual(checkNormalization, 1.0, tol)) { + std::string errMsg = + std::string("expNormalize failed to normalize probabilities. ") + + std::string("Expected normalization constant = 1.0. Got value: ") + + std::to_string(checkNormalization) + + std::string( + "\n This could have resulted from numerical overflow/underflow."); + throw std::logic_error(errMsg); + } + return probs; +} + } // namespace gtsam diff --git a/gtsam/discrete/DiscreteFactor.h b/gtsam/discrete/DiscreteFactor.h index 8f39fbc23f..212ade8cfb 100644 --- a/gtsam/discrete/DiscreteFactor.h +++ b/gtsam/discrete/DiscreteFactor.h @@ -122,4 +122,24 @@ class GTSAM_EXPORT DiscreteFactor: public Factor { // traits template<> struct traits : public Testable {}; + +/** + * @brief Normalize a set of log probabilities. + * + * Normalizing a set of log probabilities in a numerically stable way is + * tricky. To avoid overflow/underflow issues, we compute the largest + * (finite) log probability and subtract it from each log probability before + * normalizing. This comes from the observation that if: + * p_i = exp(L_i) / ( sum_j exp(L_j) ), + * Then, + * p_i = exp(Z) exp(L_i - Z) / (exp(Z) sum_j exp(L_j - Z)), + * = exp(L_i - Z) / ( sum_j exp(L_j - Z) ) + * + * Setting Z = max_j L_j, we can avoid numerical issues that arise when all + * of the (unnormalized) log probabilities are either very large or very + * small. + */ +std::vector expNormalize(const std::vector &logProbs); + + }// namespace gtsam From eb4309d26431b6edf287e4ec32dbbb8ea219c3be Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:06:52 -0500 Subject: [PATCH 50/91] discreteKeys method --- gtsam/discrete/DecisionTreeFactor.cpp | 14 +++++++++++++- gtsam/discrete/DecisionTreeFactor.h | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/gtsam/discrete/DecisionTreeFactor.cpp b/gtsam/discrete/DecisionTreeFactor.cpp index ad4cbad434..7bd9e9b7fe 100644 --- a/gtsam/discrete/DecisionTreeFactor.cpp +++ b/gtsam/discrete/DecisionTreeFactor.cpp @@ -67,7 +67,7 @@ namespace gtsam { void DecisionTreeFactor::print(const string& s, const KeyFormatter& formatter) const { cout << s; - ADT::print("Potentials:",formatter); + ADT::print("", formatter); } /* ************************************************************************* */ @@ -163,6 +163,18 @@ namespace gtsam { return result; } + /* ************************************************************************* */ + DiscreteKeys DecisionTreeFactor::discreteKeys() const { + DiscreteKeys result; + for (auto&& key : keys()) { + DiscreteKey dkey(key, cardinality(key)); + if (std::find(result.begin(), result.end(), dkey) == result.end()) { + result.push_back(dkey); + } + } + return result; + } + /* ************************************************************************* */ static std::string valueFormatter(const double& v) { return (boost::format("%4.2g") % v).str(); diff --git a/gtsam/discrete/DecisionTreeFactor.h b/gtsam/discrete/DecisionTreeFactor.h index 8beeb4c4a0..0bfdf6b902 100644 --- a/gtsam/discrete/DecisionTreeFactor.h +++ b/gtsam/discrete/DecisionTreeFactor.h @@ -183,6 +183,9 @@ namespace gtsam { /// Enumerate all values into a map from values to double. std::vector> enumerate() const; + /// Return all the discrete keys associated with this factor. + DiscreteKeys discreteKeys() const; + /// @} /// @name Wrapper support /// @{ From deae4499a1094fefb43222dba1762f79c1ba1a0a Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:07:09 -0500 Subject: [PATCH 51/91] remove export, typo --- gtsam/discrete/DecisionTree-inl.h | 6 +++++- gtsam/discrete/DecisionTree.h | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/gtsam/discrete/DecisionTree-inl.h b/gtsam/discrete/DecisionTree-inl.h index ab14b2a726..84116ccd5f 100644 --- a/gtsam/discrete/DecisionTree-inl.h +++ b/gtsam/discrete/DecisionTree-inl.h @@ -604,7 +604,7 @@ namespace gtsam { using MXChoice = typename DecisionTree::Choice; auto choice = boost::dynamic_pointer_cast(f); if (!choice) throw std::invalid_argument( - "DecisionTree::Convert: Invalid NodePtr"); + "DecisionTree::convertFrom: Invalid NodePtr"); // get new label const M oldLabel = choice->label(); @@ -634,6 +634,8 @@ namespace gtsam { using Choice = typename DecisionTree::Choice; auto choice = boost::dynamic_pointer_cast(node); + if (!choice) + throw std::invalid_argument("DecisionTree::Visit: Invalid NodePtr"); for (auto&& branch : choice->branches()) (*this)(branch); // recurse! } }; @@ -663,6 +665,8 @@ namespace gtsam { using Choice = typename DecisionTree::Choice; auto choice = boost::dynamic_pointer_cast(node); + if (!choice) + throw std::invalid_argument("DecisionTree::VisitWith: Invalid NodePtr"); for (size_t i = 0; i < choice->nrChoices(); i++) { choices[choice->label()] = i; // Set assignment for label to i (*this)(choice->branches()[i]); // recurse! diff --git a/gtsam/discrete/DecisionTree.h b/gtsam/discrete/DecisionTree.h index 9692094e19..78f3a75b72 100644 --- a/gtsam/discrete/DecisionTree.h +++ b/gtsam/discrete/DecisionTree.h @@ -38,7 +38,7 @@ namespace gtsam { * Y = function range (any algebra), e.g., bool, int, double */ template - class GTSAM_EXPORT DecisionTree { + class DecisionTree { protected: /// Default method for comparison of two objects of type Y. @@ -340,4 +340,11 @@ namespace gtsam { return f.apply(g, op); } + /// unzip a DecisionTree if its leaves are `std::pair` + template + std::pair, DecisionTree > unzip(const DecisionTree > &input) { + return std::make_pair(DecisionTree(input, [](std::pair i) { return i.first; }), + DecisionTree(input, [](std::pair i) { return i.second; })); + } + } // namespace gtsam From 51352bf3f5fe3d35b2e6e7038bf0f00d12b709e5 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:07:26 -0500 Subject: [PATCH 52/91] more precise printing --- gtsam/discrete/AlgebraicDecisionTree.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtsam/discrete/AlgebraicDecisionTree.h b/gtsam/discrete/AlgebraicDecisionTree.h index d2e05927a3..566357a485 100644 --- a/gtsam/discrete/AlgebraicDecisionTree.h +++ b/gtsam/discrete/AlgebraicDecisionTree.h @@ -163,7 +163,7 @@ namespace gtsam { const typename Base::LabelFormatter& labelFormatter = &DefaultFormatter) const { auto valueFormatter = [](const double& v) { - return (boost::format("%4.2g") % v).str(); + return (boost::format("%4.4g") % v).str(); }; Base::print(s, labelFormatter, valueFormatter); } From 59d1a0601602f201f3a94be2e293c0565a0afeee Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:07:33 -0500 Subject: [PATCH 53/91] typo --- gtsam/discrete/tests/testDiscreteDistribution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtsam/discrete/tests/testDiscreteDistribution.cpp b/gtsam/discrete/tests/testDiscreteDistribution.cpp index 5c0c42e737..19bf7c3770 100644 --- a/gtsam/discrete/tests/testDiscreteDistribution.cpp +++ b/gtsam/discrete/tests/testDiscreteDistribution.cpp @@ -10,7 +10,7 @@ * -------------------------------------------------------------------------- */ /* - * @file testDiscretePrior.cpp + * @file testDiscreteDistribution.cpp * @brief unit tests for DiscreteDistribution * @author Frank dellaert * @date December 2021 From a281a6522bfccb3f08b06bba31d7de84b91ade6e Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:07:44 -0500 Subject: [PATCH 54/91] test new unzip method --- gtsam/discrete/tests/testDecisionTree.cpp | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/gtsam/discrete/tests/testDecisionTree.cpp b/gtsam/discrete/tests/testDecisionTree.cpp index 2e6ec59f72..1029417764 100644 --- a/gtsam/discrete/tests/testDecisionTree.cpp +++ b/gtsam/discrete/tests/testDecisionTree.cpp @@ -375,6 +375,31 @@ TEST(DecisionTree, labels) { EXPECT_LONGS_EQUAL(2, labels.size()); } +/* ******************************************************************************** */ +// Test retrieving all labels. +TEST(DecisionTree, unzip) { + using DTP = DecisionTree>; + using DT1 = DecisionTree; + using DT2 = DecisionTree; + + // Create small two-level tree + string A("A"), B("B"), C("C"); + DTP tree(B, + DTP(A, {0, "zero"}, {1, "one"}), + DTP(A, {2, "two"}, {1337, "l33t"}) + ); + + DT1 dt1; + DT2 dt2; + std::tie(dt1, dt2) = unzip(tree); + + DT1 tree1(B, DT1(A, 0, 1), DT1(A, 2, 1337)); + DT2 tree2(B, DT2(A, "zero", "one"), DT2(A, "two", "l33t")); + + EXPECT(tree1.equals(dt1)); + EXPECT(tree2.equals(dt2)); +} + /* ************************************************************************* */ int main() { TestResult tr; From f2518d11f0c32880094f627bd7754fb992341bc8 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:08:06 -0500 Subject: [PATCH 55/91] Change template type --- gtsam/inference/MetisIndex-inl.h | 4 ++-- gtsam/inference/MetisIndex.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gtsam/inference/MetisIndex-inl.h b/gtsam/inference/MetisIndex-inl.h index eb9670254f..6465233728 100644 --- a/gtsam/inference/MetisIndex-inl.h +++ b/gtsam/inference/MetisIndex-inl.h @@ -23,8 +23,8 @@ namespace gtsam { /* ************************************************************************* */ -template -void MetisIndex::augment(const FactorGraph& factors) { +template +void MetisIndex::augment(const FACTORGRAPH& factors) { std::map > iAdjMap; // Stores a set of keys that are adjacent to key x, with adjMap.first std::map >::iterator iAdjMapIt; std::set keySet; diff --git a/gtsam/inference/MetisIndex.h b/gtsam/inference/MetisIndex.h index 7ec435caa1..7431bff4c1 100644 --- a/gtsam/inference/MetisIndex.h +++ b/gtsam/inference/MetisIndex.h @@ -62,8 +62,8 @@ class GTSAM_EXPORT MetisIndex { nKeys_(0) { } - template - MetisIndex(const FG& factorGraph) : + template + MetisIndex(const FACTORGRAPH& factorGraph) : nKeys_(0) { augment(factorGraph); } @@ -78,8 +78,8 @@ class GTSAM_EXPORT MetisIndex { * Augment the variable index with new factors. This can be used when * solving problems incrementally. */ - template - void augment(const FactorGraph& factors); + template + void augment(const FACTORGRAPH& factors); const std::vector& xadj() const { return xadj_; From 823239090d1fbfb7fedc362217dcb916ce786edd Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:08:17 -0500 Subject: [PATCH 56/91] exact equality --- gtsam/inference/FactorGraph.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gtsam/inference/FactorGraph.h b/gtsam/inference/FactorGraph.h index 9c0f10f9a5..afea63da84 100644 --- a/gtsam/inference/FactorGraph.h +++ b/gtsam/inference/FactorGraph.h @@ -128,6 +128,11 @@ class FactorGraph { /** Collection of factors */ FastVector factors_; + /// Check exact equality of the factor pointers. Useful for derived ==. + bool isEqual(const FactorGraph& other) const { + return factors_ == other.factors_; + } + /// @name Standard Constructors /// @{ @@ -290,11 +295,11 @@ class FactorGraph { /// @name Testable /// @{ - /// print out graph + /// Print out graph to std::cout, with optional key formatter. virtual void print(const std::string& s = "FactorGraph", const KeyFormatter& formatter = DefaultKeyFormatter) const; - /** Check equality */ + /// Check equality up to tolerance. bool equals(const This& fg, double tol = 1e-9) const; /// @} From f2a65a0531edd0e6af9c63677f2aea56ce7a55a4 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:08:34 -0500 Subject: [PATCH 57/91] expose advanced interface --- gtsam/inference/Factor.h | 1 - 1 file changed, 1 deletion(-) diff --git a/gtsam/inference/Factor.h b/gtsam/inference/Factor.h index e6a8dcc604..27b85ef67c 100644 --- a/gtsam/inference/Factor.h +++ b/gtsam/inference/Factor.h @@ -158,7 +158,6 @@ typedef FastSet FactorIndexSet; /// @} - public: /// @name Advanced Interface /// @{ From 600f05ae2c4dc1ec2b32282918ec014204f41e75 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:08:46 -0500 Subject: [PATCH 58/91] exact equality --- gtsam/linear/GaussianFactorGraph.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gtsam/linear/GaussianFactorGraph.h b/gtsam/linear/GaussianFactorGraph.h index f392221222..0d5057aa88 100644 --- a/gtsam/linear/GaussianFactorGraph.h +++ b/gtsam/linear/GaussianFactorGraph.h @@ -99,6 +99,12 @@ namespace gtsam { /// @} + /// Check exact equality. + friend bool operator==(const GaussianFactorGraph& lhs, + const GaussianFactorGraph& rhs) { + return lhs.isEqual(rhs); + } + /** Add a factor by value - makes a copy */ void add(const GaussianFactor& factor) { push_back(factor.clone()); } @@ -414,7 +420,7 @@ namespace gtsam { */ GTSAM_EXPORT bool hasConstraints(const GaussianFactorGraph& factors); - /****** Linear Algebra Opeations ******/ + /****** Linear Algebra Operations ******/ ///* matrix-vector operations */ //GTSAM_EXPORT void residual(const GaussianFactorGraph& fg, const VectorValues &x, VectorValues &r); From 465976211925f9e239f98ac3d4eda7d7a20119cf Mon Sep 17 00:00:00 2001 From: Varun Agrawal Date: Sat, 22 Jan 2022 10:17:12 -0500 Subject: [PATCH 59/91] Update python install in CI to use pip --- .github/scripts/python.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/python.sh b/.github/scripts/python.sh index 6cc62d2b06..0855dbc21b 100644 --- a/.github/scripts/python.sh +++ b/.github/scripts/python.sh @@ -83,6 +83,6 @@ cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ make -j2 install cd $GITHUB_WORKSPACE/build/python -$PYTHON setup.py install --user --prefix= +$PYTHON -m pip install --user . cd $GITHUB_WORKSPACE/python/gtsam/tests $PYTHON -m unittest discover -v From d0ff3ab97ecdfb8466eae676a096baf55693aacd Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 12:40:29 -0500 Subject: [PATCH 60/91] Fix most lint errors --- gtsam/discrete/DecisionTree-inl.h | 166 +++++++++++++++--------------- gtsam/discrete/DecisionTree.h | 68 ++++++------ 2 files changed, 120 insertions(+), 114 deletions(-) diff --git a/gtsam/discrete/DecisionTree-inl.h b/gtsam/discrete/DecisionTree-inl.h index 84116ccd5f..01c7b689c1 100644 --- a/gtsam/discrete/DecisionTree-inl.h +++ b/gtsam/discrete/DecisionTree-inl.h @@ -21,42 +21,44 @@ #include +#include #include #include +#include #include #include #include #include #include -#include #include #include #include +#include +#include #include +#include +#include using boost::assign::operator+=; namespace gtsam { - /*********************************************************************************/ + /****************************************************************************/ // Node - /*********************************************************************************/ + /****************************************************************************/ #ifdef DT_DEBUG_MEMORY template int DecisionTree::Node::nrNodes = 0; #endif - /*********************************************************************************/ + /****************************************************************************/ // Leaf - /*********************************************************************************/ - template - class DecisionTree::Leaf: public DecisionTree::Node { - + /****************************************************************************/ + template + struct DecisionTree::Leaf : public DecisionTree::Node { /** constant stored in this leaf */ Y constant_; - public: - /** Constructor from constant */ Leaf(const Y& constant) : constant_(constant) {} @@ -96,7 +98,7 @@ namespace gtsam { std::string value = valueFormatter(constant_); if (showZero || value.compare("0")) os << "\"" << this->id() << "\" [label=\"" << value - << "\", shape=box, rank=sink, height=0.35, fixedsize=true]\n"; // width=0.55, + << "\", shape=box, rank=sink, height=0.35, fixedsize=true]\n"; } /** evaluate */ @@ -121,13 +123,13 @@ namespace gtsam { // Applying binary operator to two leaves results in a leaf NodePtr apply_g_op_fL(const Leaf& fL, const Binary& op) const override { - NodePtr h(new Leaf(op(fL.constant_, constant_))); // fL op gL + NodePtr h(new Leaf(op(fL.constant_, constant_))); // fL op gL return h; } // If second argument is a Choice node, call it's apply with leaf as second NodePtr apply_g_op_fC(const Choice& fC, const Binary& op) const override { - return fC.apply_fC_op_gL(*this, op); // operand order back to normal + return fC.apply_fC_op_gL(*this, op); // operand order back to normal } /** choose a branch, create new memory ! */ @@ -136,32 +138,30 @@ namespace gtsam { } bool isLeaf() const override { return true; } + }; // Leaf - }; // Leaf - - /*********************************************************************************/ + /****************************************************************************/ // Choice - /*********************************************************************************/ + /****************************************************************************/ template - class DecisionTree::Choice: public DecisionTree::Node { - + struct DecisionTree::Choice: public DecisionTree::Node { /** the label of the variable on which we split */ L label_; /** The children of this Choice node. */ std::vector branches_; - private: + private: /** incremental allSame */ size_t allSame_; using ChoicePtr = boost::shared_ptr; - public: - + public: ~Choice() override { #ifdef DT_DEBUG_MEMORY - std::std::cout << Node::nrNodes << " destructing (Choice) " << this->id() << std::std::endl; + std::std::cout << Node::nrNodes << " destructing (Choice) " << this->id() + << std::std::endl; #endif } @@ -172,7 +172,8 @@ namespace gtsam { assert(f->branches().size() > 0); NodePtr f0 = f->branches_[0]; assert(f0->isLeaf()); - NodePtr newLeaf(new Leaf(boost::dynamic_pointer_cast(f0)->constant())); + NodePtr newLeaf( + new Leaf(boost::dynamic_pointer_cast(f0)->constant())); return newLeaf; } else #endif @@ -192,7 +193,6 @@ namespace gtsam { */ Choice(const Choice& f, const Choice& g, const Binary& op) : allSame_(true) { - // Choose what to do based on label if (f.label() > g.label()) { // f higher than g @@ -318,10 +318,8 @@ namespace gtsam { */ Choice(const L& label, const Choice& f, const Unary& op) : label_(label), allSame_(true) { - - branches_.reserve(f.branches_.size()); // reserve space - for (const NodePtr& branch: f.branches_) - push_back(branch->apply(op)); + branches_.reserve(f.branches_.size()); // reserve space + for (const NodePtr& branch : f.branches_) push_back(branch->apply(op)); } /** apply unary operator */ @@ -364,8 +362,7 @@ namespace gtsam { /** choose a branch, recursively */ NodePtr choose(const L& label, size_t index) const override { - if (label_ == label) - return branches_[index]; // choose branch + if (label_ == label) return branches_[index]; // choose branch // second case, not label of interest, just recurse auto r = boost::make_shared(label_, branches_.size()); @@ -373,12 +370,11 @@ namespace gtsam { r->push_back(branch->choose(label, index)); return Unique(r); } + }; // Choice - }; // Choice - - /*********************************************************************************/ + /****************************************************************************/ // DecisionTree - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree() { } @@ -388,13 +384,13 @@ namespace gtsam { root_(root) { } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree(const Y& y) { root_ = NodePtr(new Leaf(y)); } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree(const L& label, const Y& y1, const Y& y2) { auto a = boost::make_shared(label, 2); @@ -404,7 +400,7 @@ namespace gtsam { root_ = Choice::Unique(a); } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree(const LabelC& labelC, const Y& y1, const Y& y2) { @@ -417,7 +413,7 @@ namespace gtsam { root_ = Choice::Unique(a); } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree(const std::vector& labelCs, const std::vector& ys) { @@ -425,29 +421,28 @@ namespace gtsam { root_ = create(labelCs.begin(), labelCs.end(), ys.begin(), ys.end()); } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree(const std::vector& labelCs, const std::string& table) { - // Convert std::string to values of type Y std::vector ys; std::istringstream iss(table); copy(std::istream_iterator(iss), std::istream_iterator(), - back_inserter(ys)); + back_inserter(ys)); // now call recursive Create root_ = create(labelCs.begin(), labelCs.end(), ys.begin(), ys.end()); } - /*********************************************************************************/ + /****************************************************************************/ template template DecisionTree::DecisionTree( Iterator begin, Iterator end, const L& label) { root_ = compose(begin, end, label); } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree::DecisionTree(const L& label, const DecisionTree& f0, const DecisionTree& f1) { @@ -456,17 +451,17 @@ namespace gtsam { root_ = compose(functions.begin(), functions.end(), label); } - /*********************************************************************************/ + /****************************************************************************/ template template DecisionTree::DecisionTree(const DecisionTree& other, Func Y_of_X) { // Define functor for identity mapping of node label. - auto L_of_L = [](const L& label) { return label; }; + auto L_of_L = [](const L& label) { return label; }; root_ = convertFrom(other.root_, L_of_L, Y_of_X); } - /*********************************************************************************/ + /****************************************************************************/ template template DecisionTree::DecisionTree(const DecisionTree& other, @@ -475,16 +470,16 @@ namespace gtsam { root_ = convertFrom(other.root_, L_of_M, Y_of_X); } - /*********************************************************************************/ + /****************************************************************************/ // Called by two constructors above. - // Takes a label and a corresponding range of decision trees, and creates a new - // decision tree. However, the order of the labels needs to be respected, so we - // cannot just create a root Choice node on the label: if the label is not the - // highest label, we need to do a complicated and expensive recursive call. - template template - typename DecisionTree::NodePtr DecisionTree::compose(Iterator begin, - Iterator end, const L& label) const { - + // Takes a label and a corresponding range of decision trees, and creates a + // new decision tree. However, the order of the labels needs to be respected, + // so we cannot just create a root Choice node on the label: if the label is + // not the highest label, we need a complicated/ expensive recursive call. + template + template + typename DecisionTree::NodePtr DecisionTree::compose( + Iterator begin, Iterator end, const L& label) const { // find highest label among branches boost::optional highestLabel; size_t nrChoices = 0; @@ -527,7 +522,7 @@ namespace gtsam { } } - /*********************************************************************************/ + /****************************************************************************/ // "create" is a bit of a complicated thing, but very useful. // It takes a range of labels and a corresponding range of values, // and creates a decision tree, as follows: @@ -552,7 +547,6 @@ namespace gtsam { template typename DecisionTree::NodePtr DecisionTree::create( It begin, It end, ValueIt beginY, ValueIt endY) const { - // get crucial counts size_t nrChoices = begin->second; size_t size = endY - beginY; @@ -564,7 +558,11 @@ namespace gtsam { // Create a simple choice node with values as leaves. if (size != nrChoices) { std::cout << "Trying to create DD on " << begin->first << std::endl; - std::cout << boost::format("DecisionTree::create: expected %d values but got %d instead") % nrChoices % size << std::endl; + std::cout << boost::format( + "DecisionTree::create: expected %d values but got %d " + "instead") % + nrChoices % size + << std::endl; throw std::invalid_argument("DecisionTree::create invalid argument"); } auto choice = boost::make_shared(begin->first, endY - beginY); @@ -585,7 +583,7 @@ namespace gtsam { return compose(functions.begin(), functions.end(), begin->first); } - /*********************************************************************************/ + /****************************************************************************/ template template typename DecisionTree::NodePtr DecisionTree::convertFrom( @@ -594,11 +592,11 @@ namespace gtsam { std::function Y_of_X) const { using LY = DecisionTree; - // ugliness below because apparently we can't have templated virtual functions - // If leaf, apply unary conversion "op" and create a unique leaf + // ugliness below because apparently we can't have templated virtual + // functions If leaf, apply unary conversion "op" and create a unique leaf using MXLeaf = typename DecisionTree::Leaf; if (auto leaf = boost::dynamic_pointer_cast(f)) - return NodePtr(new Leaf(Y_of_X(leaf->constant()))); + return NodePtr(new Leaf(Y_of_X(leaf->constant()))); // Check if Choice using MXChoice = typename DecisionTree::Choice; @@ -612,19 +610,19 @@ namespace gtsam { // put together via Shannon expansion otherwise not sorted. std::vector functions; - for(auto && branch: choice->branches()) { + for (auto&& branch : choice->branches()) { functions.emplace_back(convertFrom(branch, L_of_M, Y_of_X)); } return LY::compose(functions.begin(), functions.end(), newLabel); } - /*********************************************************************************/ + /****************************************************************************/ // Functor performing depth-first visit without Assignment argument. template struct Visit { using F = std::function; - Visit(F f) : f(f) {} ///< Construct from folding function. - F f; ///< folding function object. + explicit Visit(F f) : f(f) {} ///< Construct from folding function. + F f; ///< folding function object. /// Do a depth-first visit on the tree rooted at node. void operator()(const typename DecisionTree::NodePtr& node) const { @@ -647,15 +645,15 @@ namespace gtsam { visit(root_); } - /*********************************************************************************/ + /****************************************************************************/ // Functor performing depth-first visit with Assignment argument. template struct VisitWith { using Choices = Assignment; using F = std::function; - VisitWith(F f) : f(f) {} ///< Construct from folding function. - Choices choices; ///< Assignment, mutating through recursion. - F f; ///< folding function object. + explicit VisitWith(F f) : f(f) {} ///< Construct from folding function. + Choices choices; ///< Assignment, mutating through recursion. + F f; ///< folding function object. /// Do a depth-first visit on the tree rooted at node. void operator()(const typename DecisionTree::NodePtr& node) { @@ -681,7 +679,7 @@ namespace gtsam { visit(root_); } - /*********************************************************************************/ + /****************************************************************************/ // fold is just done with a visit template template @@ -690,7 +688,7 @@ namespace gtsam { return x0; } - /*********************************************************************************/ + /****************************************************************************/ // labels is just done with a visit template std::set DecisionTree::labels() const { @@ -702,7 +700,7 @@ namespace gtsam { return unique; } -/*********************************************************************************/ +/****************************************************************************/ template bool DecisionTree::equals(const DecisionTree& other, const CompareFunc& compare) const { @@ -736,7 +734,7 @@ namespace gtsam { return DecisionTree(root_->apply(op)); } - /*********************************************************************************/ + /****************************************************************************/ template DecisionTree DecisionTree::apply(const DecisionTree& g, const Binary& op) const { @@ -752,7 +750,7 @@ namespace gtsam { return result; } - /*********************************************************************************/ + /****************************************************************************/ // The way this works: // We have an ADT, picture it as a tree. // At a certain depth, we have a branch on "label". @@ -772,7 +770,7 @@ namespace gtsam { return result; } - /*********************************************************************************/ + /****************************************************************************/ template void DecisionTree::dot(std::ostream& os, const LabelFormatter& labelFormatter, @@ -790,9 +788,11 @@ namespace gtsam { bool showZero) const { std::ofstream os((name + ".dot").c_str()); dot(os, labelFormatter, valueFormatter, showZero); - int result = system( - ("dot -Tpdf " + name + ".dot -o " + name + ".pdf >& /dev/null").c_str()); - if (result==-1) throw std::runtime_error("DecisionTree::dot system call failed"); + int result = + system(("dot -Tpdf " + name + ".dot -o " + name + ".pdf >& /dev/null") + .c_str()); + if (result == -1) + throw std::runtime_error("DecisionTree::dot system call failed"); } template @@ -804,8 +804,6 @@ namespace gtsam { return ss.str(); } -/*********************************************************************************/ - -} // namespace gtsam - +/******************************************************************************/ + } // namespace gtsam diff --git a/gtsam/discrete/DecisionTree.h b/gtsam/discrete/DecisionTree.h index 78f3a75b72..53782ef5e3 100644 --- a/gtsam/discrete/DecisionTree.h +++ b/gtsam/discrete/DecisionTree.h @@ -26,9 +26,11 @@ #include #include #include +#include #include +#include +#include #include -#include namespace gtsam { @@ -39,15 +41,13 @@ namespace gtsam { */ template class DecisionTree { - protected: /// Default method for comparison of two objects of type Y. static bool DefaultCompare(const Y& a, const Y& b) { return a == b; } - public: - + public: using LabelFormatter = std::function; using ValueFormatter = std::function; using CompareFunc = std::function; @@ -57,15 +57,14 @@ namespace gtsam { using Binary = std::function; /** A label annotated with cardinality */ - using LabelC = std::pair; + using LabelC = std::pair; /** DTs consist of Leaf and Choice nodes, both subclasses of Node */ - class Leaf; - class Choice; + struct Leaf; + struct Choice; /** ------------------------ Node base class --------------------------- */ - class Node { - public: + struct Node { using Ptr = boost::shared_ptr; #ifdef DT_DEBUG_MEMORY @@ -75,14 +74,16 @@ namespace gtsam { // Constructor Node() { #ifdef DT_DEBUG_MEMORY - std::cout << ++nrNodes << " constructed " << id() << std::endl; std::cout.flush(); + std::cout << ++nrNodes << " constructed " << id() << std::endl; + std::cout.flush(); #endif } // Destructor virtual ~Node() { #ifdef DT_DEBUG_MEMORY - std::cout << --nrNodes << " destructed " << id() << std::endl; std::cout.flush(); + std::cout << --nrNodes << " destructed " << id() << std::endl; + std::cout.flush(); #endif } @@ -110,17 +111,17 @@ namespace gtsam { }; /** ------------------------ Node base class --------------------------- */ - public: - + public: /** A function is a shared pointer to the root of a DT */ using NodePtr = typename Node::Ptr; /// A DecisionTree just contains the root. TODO(dellaert): make protected. NodePtr root_; - protected: - - /** Internal recursive function to create from keys, cardinalities, and Y values */ + protected: + /** Internal recursive function to create from keys, cardinalities, + * and Y values + */ template NodePtr create(It begin, It end, ValueIt beginY, ValueIt endY) const; @@ -140,7 +141,6 @@ namespace gtsam { std::function Y_of_X) const; public: - /// @name Standard Constructors /// @{ @@ -148,7 +148,7 @@ namespace gtsam { DecisionTree(); /** Create a constant */ - DecisionTree(const Y& y); + explicit DecisionTree(const Y& y); /** Create a new leaf function splitting on a variable */ DecisionTree(const L& label, const Y& y1, const Y& y2); @@ -167,8 +167,8 @@ namespace gtsam { DecisionTree(Iterator begin, Iterator end, const L& label); /** Create DecisionTree from two others */ - DecisionTree(const L& label, // - const DecisionTree& f0, const DecisionTree& f1); + DecisionTree(const L& label, const DecisionTree& f0, + const DecisionTree& f1); /** * @brief Convert from a different value type. @@ -289,7 +289,8 @@ namespace gtsam { } /** combine subtrees on key with binary operation "op" */ - DecisionTree combine(const L& label, size_t cardinality, const Binary& op) const; + DecisionTree combine(const L& label, size_t cardinality, + const Binary& op) const; /** combine with LabelC for convenience */ DecisionTree combine(const LabelC& labelC, const Binary& op) const { @@ -313,15 +314,14 @@ namespace gtsam { /// @{ // internal use only - DecisionTree(const NodePtr& root); + explicit DecisionTree(const NodePtr& root); // internal use only template NodePtr compose(Iterator begin, Iterator end, const L& label) const; /// @} - - }; // DecisionTree + }; // DecisionTree /** free versions of apply */ @@ -340,11 +340,19 @@ namespace gtsam { return f.apply(g, op); } - /// unzip a DecisionTree if its leaves are `std::pair` - template - std::pair, DecisionTree > unzip(const DecisionTree > &input) { - return std::make_pair(DecisionTree(input, [](std::pair i) { return i.first; }), - DecisionTree(input, [](std::pair i) { return i.second; })); + /** + * @brief unzip a DecisionTree with `std::pair` values. + * + * @param input the DecisionTree with `(T1,T2)` values. + * @return a pair of DecisionTree on T1 and T2, respectively. + */ + template + std::pair, DecisionTree > unzip( + const DecisionTree >& input) { + return std::make_pair( + DecisionTree(input, [](std::pair i) { return i.first; }), + DecisionTree(input, + [](std::pair i) { return i.second; })); } -} // namespace gtsam +} // namespace gtsam From 9317e94452c5374bd25fa6764dc315d54e68b5a8 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 09:04:27 -0500 Subject: [PATCH 61/91] Fix formatting --- gtsam/discrete/tests/testDecisionTree.cpp | 122 ++++++++++------------ 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/gtsam/discrete/tests/testDecisionTree.cpp b/gtsam/discrete/tests/testDecisionTree.cpp index 1029417764..c157a25433 100644 --- a/gtsam/discrete/tests/testDecisionTree.cpp +++ b/gtsam/discrete/tests/testDecisionTree.cpp @@ -31,14 +31,14 @@ using namespace boost::assign; using namespace std; using namespace gtsam; -template -void dot(const T&f, const string& filename) { +template +void dot(const T& f, const string& filename) { #ifndef DISABLE_DOT f.dot(filename); #endif } -#define DOT(x)(dot(x,#x)) +#define DOT(x) (dot(x, #x)) struct Crazy { int a; @@ -65,14 +65,15 @@ struct CrazyDecisionTree : public DecisionTree { // traits namespace gtsam { -template<> struct traits : public Testable {}; -} +template <> +struct traits : public Testable {}; +} // namespace gtsam GTSAM_CONCEPT_TESTABLE_INST(CrazyDecisionTree) -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test string labels and int range -/* ******************************************************************************** */ +/* ************************************************************************** */ struct DT : public DecisionTree { using Base = DecisionTree; @@ -98,30 +99,21 @@ struct DT : public DecisionTree { // traits namespace gtsam { -template<> struct traits
: public Testable
{}; -} +template <> +struct traits
: public Testable
{}; +} // namespace gtsam GTSAM_CONCEPT_TESTABLE_INST(DT) struct Ring { - static inline int zero() { - return 0; - } - static inline int one() { - return 1; - } - static inline int id(const int& a) { - return a; - } - static inline int add(const int& a, const int& b) { - return a + b; - } - static inline int mul(const int& a, const int& b) { - return a * b; - } + static inline int zero() { return 0; } + static inline int one() { return 1; } + static inline int id(const int& a) { return a; } + static inline int add(const int& a, const int& b) { return a + b; } + static inline int mul(const int& a, const int& b) { return a * b; } }; -/* ******************************************************************************** */ +/* ************************************************************************** */ // test DT TEST(DecisionTree, example) { // Create labels @@ -139,20 +131,20 @@ TEST(DecisionTree, example) { // A DT a(A, 0, 5); - LONGS_EQUAL(0,a(x00)) - LONGS_EQUAL(5,a(x10)) + LONGS_EQUAL(0, a(x00)) + LONGS_EQUAL(5, a(x10)) DOT(a); // pruned DT p(A, 2, 2); - LONGS_EQUAL(2,p(x00)) - LONGS_EQUAL(2,p(x10)) + LONGS_EQUAL(2, p(x00)) + LONGS_EQUAL(2, p(x10)) DOT(p); // \neg B DT notb(B, 5, 0); - LONGS_EQUAL(5,notb(x00)) - LONGS_EQUAL(5,notb(x10)) + LONGS_EQUAL(5, notb(x00)) + LONGS_EQUAL(5, notb(x10)) DOT(notb); // Check supplying empty trees yields an exception @@ -162,34 +154,34 @@ TEST(DecisionTree, example) { // apply, two nodes, in natural order DT anotb = apply(a, notb, &Ring::mul); - LONGS_EQUAL(0,anotb(x00)) - LONGS_EQUAL(0,anotb(x01)) - LONGS_EQUAL(25,anotb(x10)) - LONGS_EQUAL(0,anotb(x11)) + LONGS_EQUAL(0, anotb(x00)) + LONGS_EQUAL(0, anotb(x01)) + LONGS_EQUAL(25, anotb(x10)) + LONGS_EQUAL(0, anotb(x11)) DOT(anotb); // check pruning DT pnotb = apply(p, notb, &Ring::mul); - LONGS_EQUAL(10,pnotb(x00)) - LONGS_EQUAL( 0,pnotb(x01)) - LONGS_EQUAL(10,pnotb(x10)) - LONGS_EQUAL( 0,pnotb(x11)) + LONGS_EQUAL(10, pnotb(x00)) + LONGS_EQUAL(0, pnotb(x01)) + LONGS_EQUAL(10, pnotb(x10)) + LONGS_EQUAL(0, pnotb(x11)) DOT(pnotb); // check pruning DT zeros = apply(DT(A, 0, 0), notb, &Ring::mul); - LONGS_EQUAL(0,zeros(x00)) - LONGS_EQUAL(0,zeros(x01)) - LONGS_EQUAL(0,zeros(x10)) - LONGS_EQUAL(0,zeros(x11)) + LONGS_EQUAL(0, zeros(x00)) + LONGS_EQUAL(0, zeros(x01)) + LONGS_EQUAL(0, zeros(x10)) + LONGS_EQUAL(0, zeros(x11)) DOT(zeros); // apply, two nodes, in switched order DT notba = apply(a, notb, &Ring::mul); - LONGS_EQUAL(0,notba(x00)) - LONGS_EQUAL(0,notba(x01)) - LONGS_EQUAL(25,notba(x10)) - LONGS_EQUAL(0,notba(x11)) + LONGS_EQUAL(0, notba(x00)) + LONGS_EQUAL(0, notba(x01)) + LONGS_EQUAL(25, notba(x10)) + LONGS_EQUAL(0, notba(x11)) DOT(notba); // Test choose 0 @@ -204,10 +196,10 @@ TEST(DecisionTree, example) { // apply, two nodes at same level DT a_and_a = apply(a, a, &Ring::mul); - LONGS_EQUAL(0,a_and_a(x00)) - LONGS_EQUAL(0,a_and_a(x01)) - LONGS_EQUAL(25,a_and_a(x10)) - LONGS_EQUAL(25,a_and_a(x11)) + LONGS_EQUAL(0, a_and_a(x00)) + LONGS_EQUAL(0, a_and_a(x01)) + LONGS_EQUAL(25, a_and_a(x10)) + LONGS_EQUAL(25, a_and_a(x11)) DOT(a_and_a); // create a function on C @@ -219,16 +211,16 @@ TEST(DecisionTree, example) { // mul notba with C DT notbac = apply(notba, c, &Ring::mul); - LONGS_EQUAL(125,notbac(x101)) + LONGS_EQUAL(125, notbac(x101)) DOT(notbac); // mul now in different order DT acnotb = apply(apply(a, c, &Ring::mul), notb, &Ring::mul); - LONGS_EQUAL(125,acnotb(x101)) + LONGS_EQUAL(125, acnotb(x101)) DOT(acnotb); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // test Conversion of values bool bool_of_int(const int& y) { return y != 0; }; typedef DecisionTree StringBoolTree; @@ -249,11 +241,9 @@ TEST(DecisionTree, ConvertValuesOnly) { EXPECT(!f2(x00)); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // test Conversion of both values and labels. -enum Label { - U, V, X, Y, Z -}; +enum Label { U, V, X, Y, Z }; typedef DecisionTree LabelBoolTree; TEST(DecisionTree, ConvertBoth) { @@ -281,7 +271,7 @@ TEST(DecisionTree, ConvertBoth) { EXPECT(!f2(x11)); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // test Compose expansion TEST(DecisionTree, Compose) { // Create labels @@ -292,7 +282,7 @@ TEST(DecisionTree, Compose) { // Create from string vector keys; - keys += DT::LabelC(A,2), DT::LabelC(B,2); + keys += DT::LabelC(A, 2), DT::LabelC(B, 2); DT f2(keys, "0 2 1 3"); EXPECT(assert_equal(f2, f1, 1e-9)); @@ -302,13 +292,13 @@ TEST(DecisionTree, Compose) { DOT(f4); // a bigger tree - keys += DT::LabelC(C,2); + keys += DT::LabelC(C, 2); DT f5(keys, "0 4 2 6 1 5 3 7"); EXPECT(assert_equal(f5, f4, 1e-9)); DOT(f5); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Check we can create a decision tree of containers. TEST(DecisionTree, Containers) { using Container = std::vector; @@ -330,7 +320,7 @@ TEST(DecisionTree, Containers) { StringContainerTree converted(stringIntTree, container_of_int); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test visit. TEST(DecisionTree, visit) { // Create small two-level tree @@ -342,7 +332,7 @@ TEST(DecisionTree, visit) { EXPECT_DOUBLES_EQUAL(6.0, sum, 1e-9); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test visit, with Choices argument. TEST(DecisionTree, visitWith) { // Create small two-level tree @@ -354,7 +344,7 @@ TEST(DecisionTree, visitWith) { EXPECT_DOUBLES_EQUAL(6.0, sum, 1e-9); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test fold. TEST(DecisionTree, fold) { // Create small two-level tree @@ -365,7 +355,7 @@ TEST(DecisionTree, fold) { EXPECT_DOUBLES_EQUAL(6.0, sum, 1e-9); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test retrieving all labels. TEST(DecisionTree, labels) { // Create small two-level tree From 241906d2c95f9dac6ed0034cdc73fd2fa597eb54 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 10:43:46 -0500 Subject: [PATCH 62/91] Thresholding test --- gtsam/discrete/tests/testDecisionTree.cpp | 35 +++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/gtsam/discrete/tests/testDecisionTree.cpp b/gtsam/discrete/tests/testDecisionTree.cpp index c157a25433..c338bb86fa 100644 --- a/gtsam/discrete/tests/testDecisionTree.cpp +++ b/gtsam/discrete/tests/testDecisionTree.cpp @@ -308,7 +308,7 @@ TEST(DecisionTree, Containers) { StringContainerTree tree; // Create small two-level tree - string A("A"), B("B"), C("C"); + string A("A"), B("B"); DT stringIntTree(B, DT(A, 0, 1), DT(A, 2, 3)); // Check conversion @@ -324,7 +324,7 @@ TEST(DecisionTree, Containers) { // Test visit. TEST(DecisionTree, visit) { // Create small two-level tree - string A("A"), B("B"), C("C"); + string A("A"), B("B"); DT tree(B, DT(A, 0, 1), DT(A, 2, 3)); double sum = 0.0; auto visitor = [&](int y) { sum += y; }; @@ -336,7 +336,7 @@ TEST(DecisionTree, visit) { // Test visit, with Choices argument. TEST(DecisionTree, visitWith) { // Create small two-level tree - string A("A"), B("B"), C("C"); + string A("A"), B("B"); DT tree(B, DT(A, 0, 1), DT(A, 2, 3)); double sum = 0.0; auto visitor = [&](const Assignment& choices, int y) { sum += y; }; @@ -348,7 +348,7 @@ TEST(DecisionTree, visitWith) { // Test fold. TEST(DecisionTree, fold) { // Create small two-level tree - string A("A"), B("B"), C("C"); + string A("A"), B("B"); DT tree(B, DT(A, 0, 1), DT(A, 2, 3)); auto add = [](const int& y, double x) { return y + x; }; double sum = tree.fold(add, 0.0); @@ -359,14 +359,14 @@ TEST(DecisionTree, fold) { // Test retrieving all labels. TEST(DecisionTree, labels) { // Create small two-level tree - string A("A"), B("B"), C("C"); + string A("A"), B("B"); DT tree(B, DT(A, 0, 1), DT(A, 2, 3)); auto labels = tree.labels(); EXPECT_LONGS_EQUAL(2, labels.size()); } /* ******************************************************************************** */ -// Test retrieving all labels. +// Test unzip method. TEST(DecisionTree, unzip) { using DTP = DecisionTree>; using DT1 = DecisionTree; @@ -390,6 +390,29 @@ TEST(DecisionTree, unzip) { EXPECT(tree2.equals(dt2)); } +/* ************************************************************************** */ +// Test thresholding. +TEST(DecisionTree, threshold) { + // Create three level tree + vector keys; + keys += DT::LabelC("C", 2), DT::LabelC("B", 2), DT::LabelC("A", 2); + DT tree(keys, "0 1 2 3 4 5 6 7"); + + // Check number of elements equal to zero + auto count = [](const int& value, int count) { + return value == 0 ? count + 1 : count; + }; + EXPECT_LONGS_EQUAL(1, tree.fold(count, 0)); + + // Now threshold + auto threshold = [](int value) { return value < 5 ? 0 : value; }; + DT thresholded(tree, threshold); + + // Check number of elements equal to zero now = 5 + // TODO(frank): it is 2, because the pruned branches are counted as 1! + EXPECT_LONGS_EQUAL(5, thresholded.fold(count, 0)); +} + /* ************************************************************************* */ int main() { TestResult tr; From 94c692ddd1eb1e62067bd44d3237c4dbd6e15559 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 11:59:48 -0500 Subject: [PATCH 63/91] New test on marginal --- .../tests/testDiscreteConditional.cpp | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/gtsam/discrete/tests/testDiscreteConditional.cpp b/gtsam/discrete/tests/testDiscreteConditional.cpp index c2d941eaa7..13a34dd19d 100644 --- a/gtsam/discrete/tests/testDiscreteConditional.cpp +++ b/gtsam/discrete/tests/testDiscreteConditional.cpp @@ -191,20 +191,36 @@ TEST(DiscreteConditional, marginals) { DiscreteConditional prior(B % "1/2"); DiscreteConditional pAB = prior * conditional; + // P(A=0) = P(A=0|B=0)P(B=0) + P(A=0|B=1)P(B=1) = 1*1 + 2*2 = 5 + // P(A=1) = P(A=1|B=0)P(B=0) + P(A=1|B=1)P(B=1) = 2*1 + 1*2 = 4 DiscreteConditional actualA = pAB.marginal(A.first); DiscreteConditional pA(A % "5/4"); EXPECT(assert_equal(pA, actualA)); - EXPECT_LONGS_EQUAL(1, actualA.nrFrontals()); + EXPECT(actualA.frontals() == KeyVector{1}); EXPECT_LONGS_EQUAL(0, actualA.nrParents()); - KeyVector frontalsA(actualA.beginFrontals(), actualA.endFrontals()); - EXPECT((frontalsA == KeyVector{1})); DiscreteConditional actualB = pAB.marginal(B.first); EXPECT(assert_equal(prior, actualB)); - EXPECT_LONGS_EQUAL(1, actualB.nrFrontals()); + EXPECT(actualB.frontals() == KeyVector{0}); EXPECT_LONGS_EQUAL(0, actualB.nrParents()); - KeyVector frontalsB(actualB.beginFrontals(), actualB.endFrontals()); - EXPECT((frontalsB == KeyVector{0})); +} + +/* ************************************************************************* */ +// Check calculation of marginals in case branches are pruned +TEST(DiscreteConditional, marginals2) { + DiscreteKey A(0, 2), B(1, 2); // changing keys need to make pruning happen! + DiscreteConditional conditional(A | B = "2/2 3/1"); + DiscreteConditional prior(B % "1/2"); + DiscreteConditional pAB = prior * conditional; + GTSAM_PRINT(pAB); + // P(A=0) = P(A=0|B=0)P(B=0) + P(A=0|B=1)P(B=1) = 2*1 + 3*2 = 8 + // P(A=1) = P(A=1|B=0)P(B=0) + P(A=1|B=1)P(B=1) = 2*1 + 1*2 = 4 + DiscreteConditional actualA = pAB.marginal(A.first); + DiscreteConditional pA(A % "8/4"); + EXPECT(assert_equal(pA, actualA)); + + DiscreteConditional actualB = pAB.marginal(B.first); + EXPECT(assert_equal(prior, actualB)); } /* ************************************************************************* */ From ca329daa13dd93cc2d284951bd1da1f8595a6b6a Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 12:50:35 -0500 Subject: [PATCH 64/91] linting --- gtsam/discrete/DecisionTreeFactor.cpp | 146 ++++++++++++++------------ gtsam/discrete/DecisionTreeFactor.h | 82 ++++++--------- 2 files changed, 106 insertions(+), 122 deletions(-) diff --git a/gtsam/discrete/DecisionTreeFactor.cpp b/gtsam/discrete/DecisionTreeFactor.cpp index 1e8f5aa3e7..ef4cc48f69 100644 --- a/gtsam/discrete/DecisionTreeFactor.cpp +++ b/gtsam/discrete/DecisionTreeFactor.cpp @@ -17,9 +17,9 @@ * @author Frank Dellaert */ +#include #include #include -#include #include #include @@ -29,42 +29,42 @@ using namespace std; namespace gtsam { - /* ******************************************************************************** */ - DecisionTreeFactor::DecisionTreeFactor() { - } + /* ************************************************************************ */ + DecisionTreeFactor::DecisionTreeFactor() {} - /* ******************************************************************************** */ + /* ************************************************************************ */ DecisionTreeFactor::DecisionTreeFactor(const DiscreteKeys& keys, - const ADT& potentials) : - DiscreteFactor(keys.indices()), ADT(potentials), - cardinalities_(keys.cardinalities()) { - } + const ADT& potentials) + : DiscreteFactor(keys.indices()), + ADT(potentials), + cardinalities_(keys.cardinalities()) {} - /* *************************************************************************/ - DecisionTreeFactor::DecisionTreeFactor(const DiscreteConditional& c) : - DiscreteFactor(c.keys()), AlgebraicDecisionTree(c), cardinalities_(c.cardinalities_) { - } + /* ************************************************************************ */ + DecisionTreeFactor::DecisionTreeFactor(const DiscreteConditional& c) + : DiscreteFactor(c.keys()), + AlgebraicDecisionTree(c), + cardinalities_(c.cardinalities_) {} - /* ************************************************************************* */ - bool DecisionTreeFactor::equals(const DiscreteFactor& other, double tol) const { - if(!dynamic_cast(&other)) { + /* ************************************************************************ */ + bool DecisionTreeFactor::equals(const DiscreteFactor& other, + double tol) const { + if (!dynamic_cast(&other)) { return false; - } - else { + } else { const auto& f(static_cast(other)); return ADT::equals(f, tol); } } - /* ************************************************************************* */ - double DecisionTreeFactor::safe_div(const double &a, const double &b) { + /* ************************************************************************ */ + double DecisionTreeFactor::safe_div(const double& a, const double& b) { // The use for safe_div is when we divide the product factor by the sum // factor. If the product or sum is zero, we accord zero probability to the // event. return (a == 0 || b == 0) ? 0 : (a / b); } - /* ************************************************************************* */ + /* ************************************************************************ */ void DecisionTreeFactor::print(const string& s, const KeyFormatter& formatter) const { cout << s; @@ -75,31 +75,32 @@ namespace gtsam { ADT::print("", formatter); } - /* ************************************************************************* */ + /* ************************************************************************ */ DecisionTreeFactor DecisionTreeFactor::apply(const DecisionTreeFactor& f, - ADT::Binary op) const { - map cs; // new cardinalities + ADT::Binary op) const { + map cs; // new cardinalities // make unique key-cardinality map - for(Key j: keys()) cs[j] = cardinality(j); - for(Key j: f.keys()) cs[j] = f.cardinality(j); + for (Key j : keys()) cs[j] = cardinality(j); + for (Key j : f.keys()) cs[j] = f.cardinality(j); // Convert map into keys DiscreteKeys keys; - for(const std::pair& key: cs) - keys.push_back(key); + for (const std::pair& key : cs) keys.push_back(key); // apply operand ADT result = ADT::apply(f, op); // Make a new factor return DecisionTreeFactor(keys, result); } - /* ************************************************************************* */ - DecisionTreeFactor::shared_ptr DecisionTreeFactor::combine(size_t nrFrontals, - ADT::Binary op) const { - - if (nrFrontals > size()) throw invalid_argument( - (boost::format( - "DecisionTreeFactor::combine: invalid number of frontal keys %d, nr.keys=%d") - % nrFrontals % size()).str()); + /* ************************************************************************ */ + DecisionTreeFactor::shared_ptr DecisionTreeFactor::combine( + size_t nrFrontals, ADT::Binary op) const { + if (nrFrontals > size()) + throw invalid_argument( + (boost::format( + "DecisionTreeFactor::combine: invalid number of frontal " + "keys %d, nr.keys=%d") % + nrFrontals % size()) + .str()); // sum over nrFrontals keys size_t i; @@ -113,20 +114,21 @@ namespace gtsam { DiscreteKeys dkeys; for (; i < keys().size(); i++) { Key j = keys()[i]; - dkeys.push_back(DiscreteKey(j,cardinality(j))); + dkeys.push_back(DiscreteKey(j, cardinality(j))); } return boost::make_shared(dkeys, result); } - - /* ************************************************************************* */ - DecisionTreeFactor::shared_ptr DecisionTreeFactor::combine(const Ordering& frontalKeys, - ADT::Binary op) const { - - if (frontalKeys.size() > size()) throw invalid_argument( - (boost::format( - "DecisionTreeFactor::combine: invalid number of frontal keys %d, nr.keys=%d") - % frontalKeys.size() % size()).str()); + /* ************************************************************************ */ + DecisionTreeFactor::shared_ptr DecisionTreeFactor::combine( + const Ordering& frontalKeys, ADT::Binary op) const { + if (frontalKeys.size() > size()) + throw invalid_argument( + (boost::format( + "DecisionTreeFactor::combine: invalid number of frontal " + "keys %d, nr.keys=%d") % + frontalKeys.size() % size()) + .str()); // sum over nrFrontals keys size_t i; @@ -137,20 +139,22 @@ namespace gtsam { } // create new factor, note we collect keys that are not in frontalKeys - // TODO: why do we need this??? result should contain correct keys!!! + // TODO(frank): why do we need this??? result should contain correct keys!!! DiscreteKeys dkeys; for (i = 0; i < keys().size(); i++) { Key j = keys()[i]; - // TODO: inefficient! - if (std::find(frontalKeys.begin(), frontalKeys.end(), j) != frontalKeys.end()) + // TODO(frank): inefficient! + if (std::find(frontalKeys.begin(), frontalKeys.end(), j) != + frontalKeys.end()) continue; - dkeys.push_back(DiscreteKey(j,cardinality(j))); + dkeys.push_back(DiscreteKey(j, cardinality(j))); } return boost::make_shared(dkeys, result); } - /* ************************************************************************* */ - std::vector> DecisionTreeFactor::enumerate() const { + /* ************************************************************************ */ + std::vector> DecisionTreeFactor::enumerate() + const { // Get all possible assignments std::vector> pairs; for (auto& key : keys()) { @@ -168,7 +172,7 @@ namespace gtsam { return result; } - /* ************************************************************************* */ + /* ************************************************************************ */ DiscreteKeys DecisionTreeFactor::discreteKeys() const { DiscreteKeys result; for (auto&& key : keys()) { @@ -180,7 +184,7 @@ namespace gtsam { return result; } - /* ************************************************************************* */ + /* ************************************************************************ */ static std::string valueFormatter(const double& v) { return (boost::format("%4.2g") % v).str(); } @@ -194,8 +198,8 @@ namespace gtsam { /** output to graphviz format, open a file */ void DecisionTreeFactor::dot(const std::string& name, - const KeyFormatter& keyFormatter, - bool showZero) const { + const KeyFormatter& keyFormatter, + bool showZero) const { ADT::dot(name, keyFormatter, valueFormatter, showZero); } @@ -205,8 +209,8 @@ namespace gtsam { return ADT::dot(keyFormatter, valueFormatter, showZero); } - // Print out header. - /* ************************************************************************* */ + // Print out header. + /* ************************************************************************ */ string DecisionTreeFactor::markdown(const KeyFormatter& keyFormatter, const Names& names) const { stringstream ss; @@ -271,17 +275,19 @@ namespace gtsam { return ss.str(); } - /* ************************************************************************* */ - DecisionTreeFactor::DecisionTreeFactor(const DiscreteKeys &keys, const vector &table) : - DiscreteFactor(keys.indices()), AlgebraicDecisionTree(keys, table), - cardinalities_(keys.cardinalities()) { - } + /* ************************************************************************ */ + DecisionTreeFactor::DecisionTreeFactor(const DiscreteKeys& keys, + const vector& table) + : DiscreteFactor(keys.indices()), + AlgebraicDecisionTree(keys, table), + cardinalities_(keys.cardinalities()) {} - /* ************************************************************************* */ - DecisionTreeFactor::DecisionTreeFactor(const DiscreteKeys &keys, const string &table) : - DiscreteFactor(keys.indices()), AlgebraicDecisionTree(keys, table), - cardinalities_(keys.cardinalities()) { - } + /* ************************************************************************ */ + DecisionTreeFactor::DecisionTreeFactor(const DiscreteKeys& keys, + const string& table) + : DiscreteFactor(keys.indices()), + AlgebraicDecisionTree(keys, table), + cardinalities_(keys.cardinalities()) {} - /* ************************************************************************* */ -} // namespace gtsam + /* ************************************************************************ */ +} // namespace gtsam diff --git a/gtsam/discrete/DecisionTreeFactor.h b/gtsam/discrete/DecisionTreeFactor.h index 751b8c62c4..91fa7c4849 100644 --- a/gtsam/discrete/DecisionTreeFactor.h +++ b/gtsam/discrete/DecisionTreeFactor.h @@ -18,16 +18,18 @@ #pragma once +#include #include #include -#include #include +#include #include - -#include -#include +#include #include +#include +#include +#include namespace gtsam { @@ -36,21 +38,19 @@ namespace gtsam { /** * A discrete probabilistic factor */ - class GTSAM_EXPORT DecisionTreeFactor: public DiscreteFactor, public AlgebraicDecisionTree { - - public: - + class GTSAM_EXPORT DecisionTreeFactor : public DiscreteFactor, + public AlgebraicDecisionTree { + public: // typedefs needed to play nice with gtsam typedef DecisionTreeFactor This; - typedef DiscreteFactor Base; ///< Typedef to base class + typedef DiscreteFactor Base; ///< Typedef to base class typedef boost::shared_ptr shared_ptr; typedef AlgebraicDecisionTree ADT; - protected: - std::map cardinalities_; - - public: + protected: + std::map cardinalities_; + public: /// @name Standard Constructors /// @{ @@ -61,7 +61,8 @@ namespace gtsam { DecisionTreeFactor(const DiscreteKeys& keys, const ADT& potentials); /** Constructor from doubles */ - DecisionTreeFactor(const DiscreteKeys& keys, const std::vector& table); + DecisionTreeFactor(const DiscreteKeys& keys, + const std::vector& table); /** Constructor from string */ DecisionTreeFactor(const DiscreteKeys& keys, const std::string& table); @@ -86,7 +87,8 @@ namespace gtsam { bool equals(const DiscreteFactor& other, double tol = 1e-9) const override; // print - void print(const std::string& s = "DecisionTreeFactor:\n", + void print( + const std::string& s = "DecisionTreeFactor:\n", const KeyFormatter& formatter = DefaultKeyFormatter) const override; /// @} @@ -105,7 +107,7 @@ namespace gtsam { static double safe_div(const double& a, const double& b); - size_t cardinality(Key j) const { return cardinalities_.at(j);} + size_t cardinality(Key j) const { return cardinalities_.at(j); } /// divide by factor f (safely) DecisionTreeFactor operator/(const DecisionTreeFactor& f) const { @@ -113,9 +115,7 @@ namespace gtsam { } /// Convert into a decisiontree - DecisionTreeFactor toDecisionTreeFactor() const override { - return *this; - } + DecisionTreeFactor toDecisionTreeFactor() const override { return *this; } /// Create new factor by summing all values with the same separator values shared_ptr sum(size_t nrFrontals) const { @@ -164,27 +164,6 @@ namespace gtsam { */ shared_ptr combine(const Ordering& keys, ADT::Binary op) const; - -// /** -// * @brief Permutes the keys in Potentials and DiscreteFactor -// * -// * This re-implements the permuteWithInverse() in both Potentials -// * and DiscreteFactor by doing both of them together. -// */ -// -// void permuteWithInverse(const Permutation& inversePermutation){ -// DiscreteFactor::permuteWithInverse(inversePermutation); -// Potentials::permuteWithInverse(inversePermutation); -// } -// -// /** -// * Apply a reduction, which is a remapping of variable indices. -// */ -// virtual void reduceWithInverse(const internal::Reduction& inverseReduction) { -// DiscreteFactor::reduceWithInverse(inverseReduction); -// Potentials::reduceWithInverse(inverseReduction); -// } - /// Enumerate all values into a map from values to double. std::vector> enumerate() const; @@ -194,16 +173,16 @@ namespace gtsam { /// @} /// @name Wrapper support /// @{ - + /** output to graphviz format, stream version */ void dot(std::ostream& os, - const KeyFormatter& keyFormatter = DefaultKeyFormatter, - bool showZero = true) const; + const KeyFormatter& keyFormatter = DefaultKeyFormatter, + bool showZero = true) const; /** output to graphviz format, open a file */ void dot(const std::string& name, - const KeyFormatter& keyFormatter = DefaultKeyFormatter, - bool showZero = true) const; + const KeyFormatter& keyFormatter = DefaultKeyFormatter, + bool showZero = true) const; /** output to graphviz format string */ std::string dot(const KeyFormatter& keyFormatter = DefaultKeyFormatter, @@ -217,7 +196,7 @@ namespace gtsam { * @return std::string a markdown string. */ std::string markdown(const KeyFormatter& keyFormatter = DefaultKeyFormatter, - const Names& names = {}) const override; + const Names& names = {}) const override; /** * @brief Render as html table @@ -227,14 +206,13 @@ namespace gtsam { * @return std::string a html string. */ std::string html(const KeyFormatter& keyFormatter = DefaultKeyFormatter, - const Names& names = {}) const override; + const Names& names = {}) const override; /// @} - -}; -// DecisionTreeFactor + }; // traits -template<> struct traits : public Testable {}; +template <> +struct traits : public Testable {}; -}// namespace gtsam +} // namespace gtsam From 8acf67d4c86838fbe1401bea98ab7db013bb2d80 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 12:58:12 -0500 Subject: [PATCH 65/91] linting --- gtsam/discrete/AlgebraicDecisionTree.h | 87 ++++++++++++-------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/gtsam/discrete/AlgebraicDecisionTree.h b/gtsam/discrete/AlgebraicDecisionTree.h index 566357a485..6ce36a688a 100644 --- a/gtsam/discrete/AlgebraicDecisionTree.h +++ b/gtsam/discrete/AlgebraicDecisionTree.h @@ -20,6 +20,10 @@ #include +#include +#include +#include +#include namespace gtsam { /** @@ -27,13 +31,14 @@ namespace gtsam { * Just has some nice constructors and some syntactic sugar * TODO: consider eliminating this class altogether? */ - template - class GTSAM_EXPORT AlgebraicDecisionTree: public DecisionTree { + template + class GTSAM_EXPORT AlgebraicDecisionTree : public DecisionTree { /** - * @brief Default method used by `labelFormatter` or `valueFormatter` when printing. - * + * @brief Default method used by `labelFormatter` or `valueFormatter` when + * printing. + * * @param x The value passed to format. - * @return std::string + * @return std::string */ static std::string DefaultFormatter(const L& x) { std::stringstream ss; @@ -42,17 +47,12 @@ namespace gtsam { } public: - using Base = DecisionTree; /** The Real ring with addition and multiplication */ struct Ring { - static inline double zero() { - return 0.0; - } - static inline double one() { - return 1.0; - } + static inline double zero() { return 0.0; } + static inline double one() { return 1.0; } static inline double add(const double& a, const double& b) { return a + b; } @@ -65,54 +65,49 @@ namespace gtsam { static inline double div(const double& a, const double& b) { return a / b; } - static inline double id(const double& x) { - return x; - } + static inline double id(const double& x) { return x; } }; - AlgebraicDecisionTree() : - Base(1.0) { - } + AlgebraicDecisionTree() : Base(1.0) {} - AlgebraicDecisionTree(const Base& add) : - Base(add) { - } + explicit AlgebraicDecisionTree(const Base& add) : Base(add) {} /** Create a new leaf function splitting on a variable */ - AlgebraicDecisionTree(const L& label, double y1, double y2) : - Base(label, y1, y2) { - } + AlgebraicDecisionTree(const L& label, double y1, double y2) + : Base(label, y1, y2) {} /** Create a new leaf function splitting on a variable */ - AlgebraicDecisionTree(const typename Base::LabelC& labelC, double y1, double y2) : - Base(labelC, y1, y2) { - } + AlgebraicDecisionTree(const typename Base::LabelC& labelC, double y1, + double y2) + : Base(labelC, y1, y2) {} /** Create from keys and vector table */ - AlgebraicDecisionTree // - (const std::vector& labelCs, const std::vector& ys) { - this->root_ = Base::create(labelCs.begin(), labelCs.end(), ys.begin(), - ys.end()); + AlgebraicDecisionTree // + (const std::vector& labelCs, + const std::vector& ys) { + this->root_ = + Base::create(labelCs.begin(), labelCs.end(), ys.begin(), ys.end()); } /** Create from keys and string table */ - AlgebraicDecisionTree // - (const std::vector& labelCs, const std::string& table) { + AlgebraicDecisionTree // + (const std::vector& labelCs, + const std::string& table) { // Convert string to doubles std::vector ys; std::istringstream iss(table); std::copy(std::istream_iterator(iss), - std::istream_iterator(), std::back_inserter(ys)); + std::istream_iterator(), std::back_inserter(ys)); // now call recursive Create - this->root_ = Base::create(labelCs.begin(), labelCs.end(), ys.begin(), - ys.end()); + this->root_ = + Base::create(labelCs.begin(), labelCs.end(), ys.begin(), ys.end()); } /** Create a new function splitting on a variable */ - template - AlgebraicDecisionTree(Iterator begin, Iterator end, const L& label) : - Base(nullptr) { + template + AlgebraicDecisionTree(Iterator begin, Iterator end, const L& label) + : Base(nullptr) { this->root_ = compose(begin, end, label); } @@ -122,7 +117,7 @@ namespace gtsam { * @param other: The AlgebraicDecisionTree with label type M to convert. * @param map: Map from label type M to label type L. */ - template + template AlgebraicDecisionTree(const AlgebraicDecisionTree& other, const std::map& map) { // Functor for label conversion so we can use `convertFrom`. @@ -160,8 +155,8 @@ namespace gtsam { /// print method customized to value type `double`. void print(const std::string& s, - const typename Base::LabelFormatter& labelFormatter = - &DefaultFormatter) const { + const typename Base::LabelFormatter& labelFormatter = + &DefaultFormatter) const { auto valueFormatter = [](const double& v) { return (boost::format("%4.4g") % v).str(); }; @@ -177,8 +172,8 @@ namespace gtsam { return Base::equals(other, compare); } }; -// AlgebraicDecisionTree -template struct traits> : public Testable> {}; -} -// namespace gtsam +template +struct traits> + : public Testable> {}; +} // namespace gtsam From 289382ea7654658158d212ef87543eb43ab5159a Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 13:07:20 -0500 Subject: [PATCH 66/91] linting --- .../tests/testAlgebraicDecisionTree.cpp | 150 +++++++++--------- 1 file changed, 71 insertions(+), 79 deletions(-) diff --git a/gtsam/discrete/tests/testAlgebraicDecisionTree.cpp b/gtsam/discrete/tests/testAlgebraicDecisionTree.cpp index 910515b5c4..9d130a1f66 100644 --- a/gtsam/discrete/tests/testAlgebraicDecisionTree.cpp +++ b/gtsam/discrete/tests/testAlgebraicDecisionTree.cpp @@ -17,38 +17,39 @@ */ #include -#include // make sure we have traits +#include // make sure we have traits #include // headers first to make sure no missing headers //#define DT_NO_PRUNING #include -#include // for convert only +#include // for convert only #define DISABLE_TIMING -#include #include #include +#include using namespace boost::assign; #include -#include #include +#include using namespace std; using namespace gtsam; -/* ******************************************************************************** */ +/* ************************************************************************** */ typedef AlgebraicDecisionTree ADT; // traits namespace gtsam { -template<> struct traits : public Testable {}; -} +template <> +struct traits : public Testable {}; +} // namespace gtsam #define DISABLE_DOT -template -void dot(const T&f, const string& filename) { +template +void dot(const T& f, const string& filename) { #ifndef DISABLE_DOT f.dot(filename); #endif @@ -63,8 +64,8 @@ void dot(const T&f, const string& filename) { // If second argument of binary op is Leaf template - typename DecisionTree::Node::Ptr DecisionTree::Choice::apply_fC_op_gL( - Cache& cache, const Leaf& gL, Mul op) const { + typename DecisionTree::Node::Ptr DecisionTree::Choice::apply_fC_op_gL( Cache& cache, const Leaf& gL, Mul op) const { Ptr h(new Choice(label(), cardinality())); for(const NodePtr& branch: branches_) h->push_back(branch->apply_f_op_g(cache, gL, op)); @@ -72,9 +73,9 @@ void dot(const T&f, const string& filename) { } */ -/* ******************************************************************************** */ +/* ************************************************************************** */ // instrumented operators -/* ******************************************************************************** */ +/* ************************************************************************** */ size_t muls = 0, adds = 0; double elapsed; void resetCounts() { @@ -83,8 +84,9 @@ void resetCounts() { } void printCounts(const string& s) { #ifndef DISABLE_TIMING - cout << boost::format("%s: %3d muls, %3d adds, %g ms.") % s % muls % adds - % (1000 * elapsed) << endl; + cout << boost::format("%s: %3d muls, %3d adds, %g ms.") % s % muls % adds % + (1000 * elapsed) + << endl; #endif resetCounts(); } @@ -97,12 +99,11 @@ double add_(const double& a, const double& b) { return a + b; } -/* ******************************************************************************** */ +/* ************************************************************************** */ // test ADT -TEST(ADT, example3) -{ +TEST(ADT, example3) { // Create labels - DiscreteKey A(0,2), B(1,2), C(2,2), D(3,2), E(4,2); + DiscreteKey A(0, 2), B(1, 2), C(2, 2), D(3, 2), E(4, 2); // Literals ADT a(A, 0.5, 0.5); @@ -114,22 +115,21 @@ TEST(ADT, example3) ADT cnotb = c * notb; dot(cnotb, "ADT-cnotb"); -// a.print("a: "); -// cnotb.print("cnotb: "); + // a.print("a: "); + // cnotb.print("cnotb: "); ADT acnotb = a * cnotb; -// acnotb.print("acnotb: "); -// acnotb.printCache("acnotb Cache:"); + // acnotb.print("acnotb: "); + // acnotb.printCache("acnotb Cache:"); dot(acnotb, "ADT-acnotb"); - ADT big = apply(apply(d, note, &mul), acnotb, &add_); dot(big, "ADT-big"); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Asia Bayes Network -/* ******************************************************************************** */ +/* ************************************************************************** */ /** Convert Signature into CPT */ ADT create(const Signature& signature) { @@ -143,9 +143,9 @@ ADT create(const Signature& signature) { /* ************************************************************************* */ // test Asia Joint -TEST(ADT, joint) -{ - DiscreteKey A(0, 2), S(1, 2), T(2, 2), L(3, 2), B(4, 2), E(5, 2), X(6, 2), D(7, 2); +TEST(ADT, joint) { + DiscreteKey A(0, 2), S(1, 2), T(2, 2), L(3, 2), B(4, 2), E(5, 2), X(6, 2), + D(7, 2); resetCounts(); gttic_(asiaCPTs); @@ -204,10 +204,9 @@ TEST(ADT, joint) /* ************************************************************************* */ // test Inference with joint -TEST(ADT, inference) -{ - DiscreteKey A(0,2), D(1,2),// - B(2,2), L(3,2), E(4,2), S(5,2), T(6,2), X(7,2); +TEST(ADT, inference) { + DiscreteKey A(0, 2), D(1, 2), // + B(2, 2), L(3, 2), E(4, 2), S(5, 2), T(6, 2), X(7, 2); resetCounts(); gttic_(infCPTs); @@ -244,7 +243,7 @@ TEST(ADT, inference) dot(joint, "Joint-Product-ASTLBEX"); joint = apply(joint, pD, &mul); dot(joint, "Joint-Product-ASTLBEXD"); - EXPECT_LONGS_EQUAL(370, (long)muls); // different ordering + EXPECT_LONGS_EQUAL(370, (long)muls); // different ordering gttoc_(asiaProd); tictoc_getNode(asiaProdNode, asiaProd); elapsed = asiaProdNode->secs() + asiaProdNode->wall(); @@ -271,9 +270,8 @@ TEST(ADT, inference) } /* ************************************************************************* */ -TEST(ADT, factor_graph) -{ - DiscreteKey B(0,2), L(1,2), E(2,2), S(3,2), T(4,2), X(5,2); +TEST(ADT, factor_graph) { + DiscreteKey B(0, 2), L(1, 2), E(2, 2), S(3, 2), T(4, 2), X(5, 2); resetCounts(); gttic_(createCPTs); @@ -403,18 +401,19 @@ TEST(ADT, factor_graph) /* ************************************************************************* */ // test equality -TEST(ADT, equality_noparser) -{ - DiscreteKey A(0,2), B(1,2); +TEST(ADT, equality_noparser) { + DiscreteKey A(0, 2), B(1, 2); Signature::Table tableA, tableB; Signature::Row rA, rB; - rA += 80, 20; rB += 60, 40; - tableA += rA; tableB += rB; + rA += 80, 20; + rB += 60, 40; + tableA += rA; + tableB += rB; // Check straight equality ADT pA1 = create(A % tableA); ADT pA2 = create(A % tableA); - EXPECT(pA1.equals(pA2)); // should be equal + EXPECT(pA1.equals(pA2)); // should be equal // Check equality after apply ADT pB = create(B % tableB); @@ -425,13 +424,12 @@ TEST(ADT, equality_noparser) /* ************************************************************************* */ // test equality -TEST(ADT, equality_parser) -{ - DiscreteKey A(0,2), B(1,2); +TEST(ADT, equality_parser) { + DiscreteKey A(0, 2), B(1, 2); // Check straight equality ADT pA1 = create(A % "80/20"); ADT pA2 = create(A % "80/20"); - EXPECT(pA1.equals(pA2)); // should be equal + EXPECT(pA1.equals(pA2)); // should be equal // Check equality after apply ADT pB = create(B % "60/40"); @@ -440,12 +438,11 @@ TEST(ADT, equality_parser) EXPECT(pAB2.equals(pAB1)); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Factor graph construction // test constructor from strings -TEST(ADT, constructor) -{ - DiscreteKey v0(0,2), v1(1,3); +TEST(ADT, constructor) { + DiscreteKey v0(0, 2), v1(1, 3); DiscreteValues x00, x01, x02, x10, x11, x12; x00[0] = 0, x00[1] = 0; x01[0] = 0, x01[1] = 1; @@ -470,11 +467,10 @@ TEST(ADT, constructor) EXPECT_DOUBLES_EQUAL(3, f2(x11), 1e-9); EXPECT_DOUBLES_EQUAL(5, f2(x12), 1e-9); - DiscreteKey z0(0,5), z1(1,4), z2(2,3), z3(3,2); + DiscreteKey z0(0, 5), z1(1, 4), z2(2, 3), z3(3, 2); vector table(5 * 4 * 3 * 2); double x = 0; - for(double& t: table) - t = x++; + for (double& t : table) t = x++; ADT f3(z0 & z1 & z2 & z3, table); DiscreteValues assignment; assignment[0] = 0; @@ -487,9 +483,8 @@ TEST(ADT, constructor) /* ************************************************************************* */ // test conversion to integer indices // Only works if DiscreteKeys are binary, as size_t has binary cardinality! -TEST(ADT, conversion) -{ - DiscreteKey X(0,2), Y(1,2); +TEST(ADT, conversion) { + DiscreteKey X(0, 2), Y(1, 2); ADT fDiscreteKey(X & Y, "0.2 0.5 0.3 0.6"); dot(fDiscreteKey, "conversion-f1"); @@ -513,11 +508,10 @@ TEST(ADT, conversion) EXPECT_DOUBLES_EQUAL(0.6, fIndexKey(x11), 1e-9); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // test operations in elimination -TEST(ADT, elimination) -{ - DiscreteKey A(0,2), B(1,3), C(2,2); +TEST(ADT, elimination) { + DiscreteKey A(0, 2), B(1, 3), C(2, 2); ADT f1(A & B & C, "1 2 3 4 5 6 1 8 3 3 5 5"); dot(f1, "elimination-f1"); @@ -525,53 +519,51 @@ TEST(ADT, elimination) // sum out lower key ADT actualSum = f1.sum(C); ADT expectedSum(A & B, "3 7 11 9 6 10"); - CHECK(assert_equal(expectedSum,actualSum)); + CHECK(assert_equal(expectedSum, actualSum)); // normalize ADT actual = f1 / actualSum; vector cpt; - cpt += 1.0 / 3, 2.0 / 3, 3.0 / 7, 4.0 / 7, 5.0 / 11, 6.0 / 11, // - 1.0 / 9, 8.0 / 9, 3.0 / 6, 3.0 / 6, 5.0 / 10, 5.0 / 10; + cpt += 1.0 / 3, 2.0 / 3, 3.0 / 7, 4.0 / 7, 5.0 / 11, 6.0 / 11, // + 1.0 / 9, 8.0 / 9, 3.0 / 6, 3.0 / 6, 5.0 / 10, 5.0 / 10; ADT expected(A & B & C, cpt); - CHECK(assert_equal(expected,actual)); + CHECK(assert_equal(expected, actual)); } { // sum out lower 2 keys ADT actualSum = f1.sum(C).sum(B); ADT expectedSum(A, 21, 25); - CHECK(assert_equal(expectedSum,actualSum)); + CHECK(assert_equal(expectedSum, actualSum)); // normalize ADT actual = f1 / actualSum; vector cpt; - cpt += 1.0 / 21, 2.0 / 21, 3.0 / 21, 4.0 / 21, 5.0 / 21, 6.0 / 21, // - 1.0 / 25, 8.0 / 25, 3.0 / 25, 3.0 / 25, 5.0 / 25, 5.0 / 25; + cpt += 1.0 / 21, 2.0 / 21, 3.0 / 21, 4.0 / 21, 5.0 / 21, 6.0 / 21, // + 1.0 / 25, 8.0 / 25, 3.0 / 25, 3.0 / 25, 5.0 / 25, 5.0 / 25; ADT expected(A & B & C, cpt); - CHECK(assert_equal(expected,actual)); + CHECK(assert_equal(expected, actual)); } } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test non-commutative op -TEST(ADT, div) -{ - DiscreteKey A(0,2), B(1,2); +TEST(ADT, div) { + DiscreteKey A(0, 2), B(1, 2); // Literals ADT a(A, 8, 16); ADT b(B, 2, 4); - ADT expected_a_div_b(A & B, "4 2 8 4"); // 8/2 8/4 16/2 16/4 - ADT expected_b_div_a(A & B, "0.25 0.5 0.125 0.25"); // 2/8 4/8 2/16 4/16 + ADT expected_a_div_b(A & B, "4 2 8 4"); // 8/2 8/4 16/2 16/4 + ADT expected_b_div_a(A & B, "0.25 0.5 0.125 0.25"); // 2/8 4/8 2/16 4/16 EXPECT(assert_equal(expected_a_div_b, a / b)); EXPECT(assert_equal(expected_b_div_a, b / a)); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // test zero shortcut -TEST(ADT, zero) -{ - DiscreteKey A(0,2), B(1,2); +TEST(ADT, zero) { + DiscreteKey A(0, 2), B(1, 2); // Literals ADT a(A, 0, 1); From beb3985c8c0e363b034702fa941647a0b16627f8 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 13:28:40 -0500 Subject: [PATCH 67/91] Added missing header --- gtsam/discrete/AlgebraicDecisionTree.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gtsam/discrete/AlgebraicDecisionTree.h b/gtsam/discrete/AlgebraicDecisionTree.h index 6ce36a688a..828f0b1a27 100644 --- a/gtsam/discrete/AlgebraicDecisionTree.h +++ b/gtsam/discrete/AlgebraicDecisionTree.h @@ -18,6 +18,7 @@ #pragma once +#include #include #include @@ -70,7 +71,8 @@ namespace gtsam { AlgebraicDecisionTree() : Base(1.0) {} - explicit AlgebraicDecisionTree(const Base& add) : Base(add) {} + // Explicitly non-explicit constructor + AlgebraicDecisionTree(const Base& add) : Base(add) {} /** Create a new leaf function splitting on a variable */ AlgebraicDecisionTree(const L& label, double y1, double y2) From fa1cde2f602c6aecf039d225eb35749cafa1bf6a Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 13:28:56 -0500 Subject: [PATCH 68/91] Added cautionary notes about fold/visit --- gtsam/discrete/DecisionTree.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gtsam/discrete/DecisionTree.h b/gtsam/discrete/DecisionTree.h index 53782ef5e3..d655756b86 100644 --- a/gtsam/discrete/DecisionTree.h +++ b/gtsam/discrete/DecisionTree.h @@ -234,6 +234,8 @@ namespace gtsam { * * @param f side-effect taking a value. * + * @note Due to pruning, leaves might not exhaust choices. + * * Example: * int sum = 0; * auto visitor = [&](int y) { sum += y; }; @@ -247,6 +249,8 @@ namespace gtsam { * * @param f side-effect taking an assignment and a value. * + * @note Due to pruning, leaves might not exhaust choices. + * * Example: * int sum = 0; * auto visitor = [&](const Assignment& choices, int y) { sum += y; }; @@ -264,6 +268,7 @@ namespace gtsam { * @return X final value for accumulator. * * @note X is always passed by value. + * @note Due to pruning, leaves might not exhaust choices. * * Example: * auto add = [](const double& y, double x) { return y + x; }; From 8db7f250216fbe37e4d14dee1842bde7113d6722 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Sat, 22 Jan 2022 13:29:16 -0500 Subject: [PATCH 69/91] Fixed thresholding and fold example --- gtsam/discrete/tests/testDecisionTree.cpp | 26 +++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/gtsam/discrete/tests/testDecisionTree.cpp b/gtsam/discrete/tests/testDecisionTree.cpp index c338bb86fa..dbfb2dc403 100644 --- a/gtsam/discrete/tests/testDecisionTree.cpp +++ b/gtsam/discrete/tests/testDecisionTree.cpp @@ -24,8 +24,8 @@ using namespace boost::assign; #include #include -//#define DT_DEBUG_MEMORY -//#define DT_NO_PRUNING +// #define DT_DEBUG_MEMORY +// #define DT_NO_PRUNING #define DISABLE_DOT #include using namespace std; @@ -349,10 +349,10 @@ TEST(DecisionTree, visitWith) { TEST(DecisionTree, fold) { // Create small two-level tree string A("A"), B("B"); - DT tree(B, DT(A, 0, 1), DT(A, 2, 3)); + DT tree(B, DT(A, 1, 1), DT(A, 2, 3)); auto add = [](const int& y, double x) { return y + x; }; double sum = tree.fold(add, 0.0); - EXPECT_DOUBLES_EQUAL(6.0, sum, 1e-9); + EXPECT_DOUBLES_EQUAL(6.0, sum, 1e-9); // Note, not 7, due to pruning! } /* ************************************************************************** */ @@ -365,7 +365,7 @@ TEST(DecisionTree, labels) { EXPECT_LONGS_EQUAL(2, labels.size()); } -/* ******************************************************************************** */ +/* ************************************************************************** */ // Test unzip method. TEST(DecisionTree, unzip) { using DTP = DecisionTree>; @@ -374,15 +374,13 @@ TEST(DecisionTree, unzip) { // Create small two-level tree string A("A"), B("B"), C("C"); - DTP tree(B, - DTP(A, {0, "zero"}, {1, "one"}), - DTP(A, {2, "two"}, {1337, "l33t"}) - ); + DTP tree(B, DTP(A, {0, "zero"}, {1, "one"}), + DTP(A, {2, "two"}, {1337, "l33t"})); DT1 dt1; DT2 dt2; std::tie(dt1, dt2) = unzip(tree); - + DT1 tree1(B, DT1(A, 0, 1), DT1(A, 2, 1337)); DT2 tree2(B, DT2(A, "zero", "one"), DT2(A, "two", "l33t")); @@ -398,7 +396,7 @@ TEST(DecisionTree, threshold) { keys += DT::LabelC("C", 2), DT::LabelC("B", 2), DT::LabelC("A", 2); DT tree(keys, "0 1 2 3 4 5 6 7"); - // Check number of elements equal to zero + // Check number of leaves equal to zero auto count = [](const int& value, int count) { return value == 0 ? count + 1 : count; }; @@ -408,9 +406,9 @@ TEST(DecisionTree, threshold) { auto threshold = [](int value) { return value < 5 ? 0 : value; }; DT thresholded(tree, threshold); - // Check number of elements equal to zero now = 5 - // TODO(frank): it is 2, because the pruned branches are counted as 1! - EXPECT_LONGS_EQUAL(5, thresholded.fold(count, 0)); + // Check number of leaves equal to zero now = 2 + // Note: it is 2, because the pruned branches are counted as 1! + EXPECT_LONGS_EQUAL(2, thresholded.fold(count, 0)); } /* ************************************************************************* */ From 9cf8c4477b7972beb55c396ae31ee32ab31b594c Mon Sep 17 00:00:00 2001 From: senselessDev Date: Sat, 22 Jan 2022 22:08:32 +0100 Subject: [PATCH 70/91] add position to Point2 nodes in GraphViz --- gtsam/nonlinear/GraphvizFormatting.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gtsam/nonlinear/GraphvizFormatting.cpp b/gtsam/nonlinear/GraphvizFormatting.cpp index c37f07c8a8..e5b81c66b9 100644 --- a/gtsam/nonlinear/GraphvizFormatting.cpp +++ b/gtsam/nonlinear/GraphvizFormatting.cpp @@ -53,6 +53,17 @@ boost::optional GraphvizFormatting::operator()( } else if (const GenericValue* p = dynamic_cast*>(&value)) { t << p->value().x(), p->value().y(), 0; + } else if (const GenericValue* p = + dynamic_cast*>(&value)) { + if (p->dim() == 2) { + const Eigen::Ref p_2d(p->value()); + t << p_2d.x(), p_2d.y(), 0; + } else if (p->dim() == 3) { + const Eigen::Ref p_3d(p->value()); + t = p_3d; + } else { + return boost::none; + } } else if (const GenericValue* p = dynamic_cast*>(&value)) { t = p->value().translation(); From 020071719ed47cee93b3d81e055486644e7d9bdb Mon Sep 17 00:00:00 2001 From: senselessDev Date: Sun, 23 Jan 2022 13:39:21 +0100 Subject: [PATCH 71/91] expose GraphvizFormatting and test it in Python --- gtsam/discrete/discrete.i | 6 + gtsam/nonlinear/nonlinear.i | 19 ++- python/gtsam/tests/test_GraphvizFormatting.py | 139 ++++++++++++++++++ 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 python/gtsam/tests/test_GraphvizFormatting.py diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index e2310f4344..1bee25379f 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -222,6 +222,12 @@ class DotWriter { DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, bool plotFactorPoints = true, bool connectKeysToFactor = true, bool binaryEdges = true); + + double figureWidthInches; + double figureHeightInches; + bool plotFactorPoints; + bool connectKeysToFactor; + bool binaryEdges; }; #include diff --git a/gtsam/nonlinear/nonlinear.i b/gtsam/nonlinear/nonlinear.i index 84c4939f49..b6ab086c45 100644 --- a/gtsam/nonlinear/nonlinear.i +++ b/gtsam/nonlinear/nonlinear.i @@ -133,6 +133,18 @@ class Ordering { void serialize() const; }; +#include +class GraphvizFormatting : gtsam::DotWriter { + GraphvizFormatting(); + + enum Axis { X, Y, Z, NEGX, NEGY, NEGZ }; + Axis paperHorizontalAxis; + Axis paperVerticalAxis; + + double scale; + bool mergeSimilarFactors; +}; + #include class NonlinearFactorGraph { NonlinearFactorGraph(); @@ -195,10 +207,13 @@ class NonlinearFactorGraph { string dot( const gtsam::Values& values, - const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const GraphvizFormatting& writer = GraphvizFormatting()); void saveGraph(const string& s, const gtsam::Values& values, const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter) const; + gtsam::DefaultKeyFormatter, + const GraphvizFormatting& writer = + GraphvizFormatting()) const; }; #include diff --git a/python/gtsam/tests/test_GraphvizFormatting.py b/python/gtsam/tests/test_GraphvizFormatting.py new file mode 100644 index 0000000000..bd675b988f --- /dev/null +++ b/python/gtsam/tests/test_GraphvizFormatting.py @@ -0,0 +1,139 @@ +""" +GTSAM Copyright 2010-2021, Georgia Tech Research Corporation, +Atlanta, Georgia 30332-0415 +All Rights Reserved + +See LICENSE for the license information + +Unit tests for Graphviz formatting of NonlinearFactorGraph. +Author: Frank Dellaert & senselessDev +""" + +# pylint: disable=no-member, invalid-name + +import unittest +import textwrap + +import numpy as np + +import gtsam +from gtsam.utils.test_case import GtsamTestCase + + +class TestGraphvizFormatting(GtsamTestCase): + """Tests for saving NonlinearFactorGraph to GraphViz format.""" + + def setUp(self): + self.graph = gtsam.NonlinearFactorGraph() + + odometry = gtsam.Pose2(2.0, 0.0, 0.0) + odometryNoise = gtsam.noiseModel.Diagonal.Sigmas( + np.array([0.2, 0.2, 0.1])) + self.graph.add(gtsam.BetweenFactorPose2(0, 1, odometry, odometryNoise)) + self.graph.add(gtsam.BetweenFactorPose2(1, 2, odometry, odometryNoise)) + + self.values = gtsam.Values() + self.values.insert_pose2(0, gtsam.Pose2(0., 0., 0.)) + self.values.insert_pose2(1, gtsam.Pose2(2., 0., 0.)) + self.values.insert_pose2(2, gtsam.Pose2(4., 0., 0.)) + + def test_default(self): + """Test with default GraphvizFormatting""" + expected_result = """\ + graph { + size="5,5"; + + var0[label="0", pos="0,0!"]; + var1[label="1", pos="0,2!"]; + var2[label="2", pos="0,4!"]; + + factor0[label="", shape=point]; + var0--factor0; + var1--factor0; + factor1[label="", shape=point]; + var1--factor1; + var2--factor1; + } + """ + + self.assertEqual(self.graph.dot(self.values), + textwrap.dedent(expected_result)) + + def test_swapped_axes(self): + """Test with user-defined GraphvizFormatting swapping x and y""" + expected_result = """\ + graph { + size="5,5"; + + var0[label="0", pos="0,0!"]; + var1[label="1", pos="2,0!"]; + var2[label="2", pos="4,0!"]; + + factor0[label="", shape=point]; + var0--factor0; + var1--factor0; + factor1[label="", shape=point]; + var1--factor1; + var2--factor1; + } + """ + + graphviz_formatting = gtsam.GraphvizFormatting() + graphviz_formatting.paperHorizontalAxis = gtsam.GraphvizFormatting.Axis.X + graphviz_formatting.paperVerticalAxis = gtsam.GraphvizFormatting.Axis.Y + self.assertEqual(self.graph.dot(self.values, + writer=graphviz_formatting), + textwrap.dedent(expected_result)) + + def test_factor_points(self): + """Test with user-defined GraphvizFormatting without factor points""" + expected_result = """\ + graph { + size="5,5"; + + var0[label="0", pos="0,0!"]; + var1[label="1", pos="0,2!"]; + var2[label="2", pos="0,4!"]; + + var0--var1; + var1--var2; + } + """ + + graphviz_formatting = gtsam.GraphvizFormatting() + graphviz_formatting.plotFactorPoints = False + + self.assertEqual(self.graph.dot(self.values, + writer=graphviz_formatting), + textwrap.dedent(expected_result)) + + def test_width_height(self): + """Test with user-defined GraphvizFormatting for width and height""" + expected_result = """\ + graph { + size="20,10"; + + var0[label="0", pos="0,0!"]; + var1[label="1", pos="0,2!"]; + var2[label="2", pos="0,4!"]; + + factor0[label="", shape=point]; + var0--factor0; + var1--factor0; + factor1[label="", shape=point]; + var1--factor1; + var2--factor1; + } + """ + + graphviz_formatting = gtsam.GraphvizFormatting() + graphviz_formatting.figureWidthInches = 20 + graphviz_formatting.figureHeightInches = 10 + + self.assertEqual(self.graph.dot(self.values, + writer=graphviz_formatting), + textwrap.dedent(expected_result)) + + +if __name__ == "__main__": + unittest.main() From 67ca0b9c4eb4fd59a05fb6fbe6b5cf29d7e62e4a Mon Sep 17 00:00:00 2001 From: senselessDev Date: Sun, 23 Jan 2022 14:19:26 +0100 Subject: [PATCH 72/91] add python test files to test target dependencies --- python/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index b39e067b07..f42e330b2c 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -181,5 +181,5 @@ add_custom_target( ${CMAKE_COMMAND} -E env # add package to python path so no need to install "PYTHONPATH=${GTSAM_PYTHON_BUILD_DIRECTORY}/$ENV{PYTHONPATH}" ${PYTHON_EXECUTABLE} -m unittest discover -v -s . - DEPENDS ${GTSAM_PYTHON_DEPENDENCIES} + DEPENDS ${GTSAM_PYTHON_DEPENDENCIES} ${GTSAM_PYTHON_TEST_FILES} WORKING_DIRECTORY "${GTSAM_PYTHON_BUILD_DIRECTORY}/gtsam/tests") From fb0575720cddb639e2edc4caaeec4cd3d679a613 Mon Sep 17 00:00:00 2001 From: senselessDev Date: Sun, 23 Jan 2022 14:48:06 +0100 Subject: [PATCH 73/91] consider CMAKE_INSTALL_PREFIX for python-install target --- python/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index f42e330b2c..56411f96cf 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -170,7 +170,7 @@ endif() # Add custom target so we can install with `make python-install` set(GTSAM_PYTHON_INSTALL_TARGET python-install) add_custom_target(${GTSAM_PYTHON_INSTALL_TARGET} - COMMAND ${PYTHON_EXECUTABLE} ${GTSAM_PYTHON_BUILD_DIRECTORY}/setup.py install + COMMAND ${PYTHON_EXECUTABLE} ${GTSAM_PYTHON_BUILD_DIRECTORY}/setup.py install --prefix ${CMAKE_INSTALL_PREFIX} DEPENDS ${GTSAM_PYTHON_DEPENDENCIES} WORKING_DIRECTORY ${GTSAM_PYTHON_BUILD_DIRECTORY}) From 79038b1b46221a6a3c5ff74cafbbfc9019c45d20 Mon Sep 17 00:00:00 2001 From: senselessDev Date: Mon, 24 Jan 2022 09:21:48 +0100 Subject: [PATCH 74/91] dont copy GT copyright --- python/gtsam/tests/test_GraphvizFormatting.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/python/gtsam/tests/test_GraphvizFormatting.py b/python/gtsam/tests/test_GraphvizFormatting.py index bd675b988f..ecdc23b450 100644 --- a/python/gtsam/tests/test_GraphvizFormatting.py +++ b/python/gtsam/tests/test_GraphvizFormatting.py @@ -1,12 +1,8 @@ """ -GTSAM Copyright 2010-2021, Georgia Tech Research Corporation, -Atlanta, Georgia 30332-0415 -All Rights Reserved - See LICENSE for the license information Unit tests for Graphviz formatting of NonlinearFactorGraph. -Author: Frank Dellaert & senselessDev +Author: senselessDev (contact by mentioning on GitHub, e.g. in PR#1059) """ # pylint: disable=no-member, invalid-name From b35ed166758e54c72b9062dba2ca54cfd5865545 Mon Sep 17 00:00:00 2001 From: senselessDev Date: Mon, 24 Jan 2022 13:59:58 +0100 Subject: [PATCH 75/91] remove --prefix for setup.py --- python/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 56411f96cf..f42e330b2c 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -170,7 +170,7 @@ endif() # Add custom target so we can install with `make python-install` set(GTSAM_PYTHON_INSTALL_TARGET python-install) add_custom_target(${GTSAM_PYTHON_INSTALL_TARGET} - COMMAND ${PYTHON_EXECUTABLE} ${GTSAM_PYTHON_BUILD_DIRECTORY}/setup.py install --prefix ${CMAKE_INSTALL_PREFIX} + COMMAND ${PYTHON_EXECUTABLE} ${GTSAM_PYTHON_BUILD_DIRECTORY}/setup.py install DEPENDS ${GTSAM_PYTHON_DEPENDENCIES} WORKING_DIRECTORY ${GTSAM_PYTHON_BUILD_DIRECTORY}) From 9eea6cf21af426598b41a72bed8bc8022bf9149c Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 25 Jan 2022 17:15:52 -0500 Subject: [PATCH 76/91] Added sumProduct as a convenient alias --- gtsam/discrete/DiscreteFactorGraph.cpp | 17 +++++++++++++++ gtsam/discrete/DiscreteFactorGraph.h | 21 +++++++++++++++++-- .../tests/testDiscreteFactorGraph.cpp | 10 +++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.cpp b/gtsam/discrete/DiscreteFactorGraph.cpp index f8e1b4bb89..b4b65f885d 100644 --- a/gtsam/discrete/DiscreteFactorGraph.cpp +++ b/gtsam/discrete/DiscreteFactorGraph.cpp @@ -144,6 +144,23 @@ namespace gtsam { boost::dynamic_pointer_cast(lookup), max); } + /* ************************************************************************ */ + // sumProduct is just an alias for regular eliminateSequential. + DiscreteBayesNet DiscreteFactorGraph::sumProduct( + OptionalOrderingType orderingType) const { + gttic(DiscreteFactorGraph_sumProduct); + auto bayesNet = BaseEliminateable::eliminateSequential(orderingType); + return *bayesNet; + } + + DiscreteLookupDAG DiscreteFactorGraph::sumProduct( + const Ordering& ordering) const { + gttic(DiscreteFactorGraph_sumProduct); + auto bayesNet = + BaseEliminateable::eliminateSequential(ordering, EliminateForMPE); + return DiscreteLookupDAG::FromBayesNet(*bayesNet); + } + /* ************************************************************************ */ // The max-product solution below is a bit clunky: the elimination machinery // does not allow for differently *typed* versions of elimination, so we diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 1ba39ff9d0..2e9b40823f 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -132,11 +132,28 @@ class GTSAM_EXPORT DiscreteFactorGraph const std::string& s = "DiscreteFactorGraph", const KeyFormatter& formatter = DefaultKeyFormatter) const override; + /** + * @brief Implement the sum-product algorithm + * + * @param orderingType : one of COLAMD, METIS, NATURAL, CUSTOM + * @return DiscreteBayesNet encoding posterior P(X|Z) + */ + DiscreteBayesNet sumProduct( + OptionalOrderingType orderingType = boost::none) const; + + /** + * @brief Implement the sum-product algorithm + * + * @param ordering + * @return DiscreteBayesNet encoding posterior P(X|Z) + */ + DiscreteLookupDAG sumProduct(const Ordering& ordering) const; + /** * @brief Implement the max-product algorithm * * @param orderingType : one of COLAMD, METIS, NATURAL, CUSTOM - * @return DiscreteLookupDAG::shared_ptr DAG with lookup tables + * @return DiscreteLookupDAG DAG with lookup tables */ DiscreteLookupDAG maxProduct( OptionalOrderingType orderingType = boost::none) const; @@ -145,7 +162,7 @@ class GTSAM_EXPORT DiscreteFactorGraph * @brief Implement the max-product algorithm * * @param ordering - * @return DiscreteLookupDAG::shared_ptr `DAG with lookup tables + * @return DiscreteLookupDAG `DAG with lookup tables */ DiscreteLookupDAG maxProduct(const Ordering& ordering) const; diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index f4819dab54..63f5b73194 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -154,6 +154,16 @@ TEST(DiscreteFactorGraph, test) { auto actualMPE = graph.optimize(); EXPECT(assert_equal(mpe, actualMPE)); EXPECT_DOUBLES_EQUAL(9, graph(mpe), 1e-5); // regression + + // Test sumProduct alias with all orderings: + auto mpeProbability = expectedBayesNet(mpe); + EXPECT_DOUBLES_EQUAL(0.28125, mpeProbability, 1e-5); // regression + for (Ordering::OrderingType orderingType : + {Ordering::COLAMD, Ordering::METIS, Ordering::NATURAL, + Ordering::CUSTOM}) { + auto bayesNet = graph.sumProduct(orderingType); + EXPECT_DOUBLES_EQUAL(mpeProbability, bayesNet(mpe), 1e-5); + } } /* ************************************************************************* */ From 09fa002bd76ab98abfc32b0b1579e6642710124e Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 25 Jan 2022 17:31:49 -0500 Subject: [PATCH 77/91] Python --- gtsam/discrete/discrete.i | 5 ++ gtsam/nonlinear/nonlinear.i | 5 ++ .../gtsam/tests/test_DiscreteFactorGraph.py | 52 ++++++++++++++++--- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 3f2c3e0602..0dcbcc1cfc 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -277,7 +277,12 @@ class DiscreteFactorGraph { double operator()(const gtsam::DiscreteValues& values) const; gtsam::DiscreteValues optimize() const; + gtsam::DiscreteLookupDAG sumProduct(); + gtsam::DiscreteLookupDAG sumProduct(gtsam::Ordering::OrderingType type); + gtsam::DiscreteLookupDAG sumProduct(const gtsam::Ordering& ordering); + gtsam::DiscreteLookupDAG maxProduct(); + gtsam::DiscreteLookupDAG maxProduct(gtsam::Ordering::OrderingType type); gtsam::DiscreteLookupDAG maxProduct(const gtsam::Ordering& ordering); gtsam::DiscreteBayesNet eliminateSequential(); diff --git a/gtsam/nonlinear/nonlinear.i b/gtsam/nonlinear/nonlinear.i index b6ab086c45..a6883d38b8 100644 --- a/gtsam/nonlinear/nonlinear.i +++ b/gtsam/nonlinear/nonlinear.i @@ -111,6 +111,11 @@ size_t mrsymbolIndex(size_t key); #include class Ordering { + /// Type of ordering to use + enum OrderingType { + COLAMD, METIS, NATURAL, CUSTOM + }; + // Standard Constructors and Named Constructors Ordering(); Ordering(const gtsam::Ordering& other); diff --git a/python/gtsam/tests/test_DiscreteFactorGraph.py b/python/gtsam/tests/test_DiscreteFactorGraph.py index 1ba145e096..ef85fc7534 100644 --- a/python/gtsam/tests/test_DiscreteFactorGraph.py +++ b/python/gtsam/tests/test_DiscreteFactorGraph.py @@ -13,9 +13,11 @@ import unittest -from gtsam import DiscreteFactorGraph, DiscreteKeys, DiscreteValues +from gtsam import DiscreteFactorGraph, DiscreteKeys, DiscreteValues, Ordering from gtsam.utils.test_case import GtsamTestCase +OrderingType = Ordering.OrderingType + class TestDiscreteFactorGraph(GtsamTestCase): """Tests for Discrete Factor Graphs.""" @@ -108,14 +110,50 @@ def test_MPE(self): graph.add([C, A], "0.2 0.8 0.3 0.7") graph.add([C, B], "0.1 0.9 0.4 0.6") - actualMPE = graph.optimize() + # We know MPE + mpe = DiscreteValues() + mpe[0] = 0 + mpe[1] = 1 + mpe[2] = 1 - expectedMPE = DiscreteValues() - expectedMPE[0] = 0 - expectedMPE[1] = 1 - expectedMPE[2] = 1 + # Use maxProduct + dag = graph.maxProduct(OrderingType.COLAMD) + actualMPE = dag.argmax() self.assertEqual(list(actualMPE.items()), - list(expectedMPE.items())) + list(mpe.items())) + + # All in one + actualMPE2 = graph.optimize() + self.assertEqual(list(actualMPE2.items()), + list(mpe.items())) + + def test_sumProduct(self): + """Test sumProduct.""" + + # Declare a bunch of keys + C, A, B = (0, 2), (1, 2), (2, 2) + + # Create Factor graph + graph = DiscreteFactorGraph() + graph.add([C, A], "0.2 0.8 0.3 0.7") + graph.add([C, B], "0.1 0.9 0.4 0.6") + + # We know MPE + mpe = DiscreteValues() + mpe[0] = 0 + mpe[1] = 1 + mpe[2] = 1 + + # Use default sumProduct + bayesNet = graph.sumProduct() + mpeProbability = bayesNet(mpe) + self.assertAlmostEqual(mpeProbability, 0.36) # regression + + # Use sumProduct + for ordering_type in [OrderingType.COLAMD, OrderingType.METIS, OrderingType.NATURAL, + OrderingType.CUSTOM]: + bayesNet = graph.sumProduct(ordering_type) + self.assertEqual(bayesNet(mpe), mpeProbability) if __name__ == "__main__": From d6b977927e204c2817d2f3e23295a05e435f94da Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Tue, 25 Jan 2022 23:47:53 -0500 Subject: [PATCH 78/91] Fix return type --- gtsam/discrete/DiscreteFactorGraph.cpp | 15 ++++++--------- gtsam/discrete/DiscreteFactorGraph.h | 2 +- gtsam/discrete/discrete.i | 6 +++--- gtsam/discrete/tests/testDiscreteFactorGraph.cpp | 5 +++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/gtsam/discrete/DiscreteFactorGraph.cpp b/gtsam/discrete/DiscreteFactorGraph.cpp index b4b65f885d..ebcac445c5 100644 --- a/gtsam/discrete/DiscreteFactorGraph.cpp +++ b/gtsam/discrete/DiscreteFactorGraph.cpp @@ -149,16 +149,15 @@ namespace gtsam { DiscreteBayesNet DiscreteFactorGraph::sumProduct( OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_sumProduct); - auto bayesNet = BaseEliminateable::eliminateSequential(orderingType); + auto bayesNet = eliminateSequential(orderingType); return *bayesNet; } - DiscreteLookupDAG DiscreteFactorGraph::sumProduct( + DiscreteBayesNet DiscreteFactorGraph::sumProduct( const Ordering& ordering) const { gttic(DiscreteFactorGraph_sumProduct); - auto bayesNet = - BaseEliminateable::eliminateSequential(ordering, EliminateForMPE); - return DiscreteLookupDAG::FromBayesNet(*bayesNet); + auto bayesNet = eliminateSequential(ordering); + return *bayesNet; } /* ************************************************************************ */ @@ -170,16 +169,14 @@ namespace gtsam { DiscreteLookupDAG DiscreteFactorGraph::maxProduct( OptionalOrderingType orderingType) const { gttic(DiscreteFactorGraph_maxProduct); - auto bayesNet = - BaseEliminateable::eliminateSequential(orderingType, EliminateForMPE); + auto bayesNet = eliminateSequential(orderingType, EliminateForMPE); return DiscreteLookupDAG::FromBayesNet(*bayesNet); } DiscreteLookupDAG DiscreteFactorGraph::maxProduct( const Ordering& ordering) const { gttic(DiscreteFactorGraph_maxProduct); - auto bayesNet = - BaseEliminateable::eliminateSequential(ordering, EliminateForMPE); + auto bayesNet = eliminateSequential(ordering, EliminateForMPE); return DiscreteLookupDAG::FromBayesNet(*bayesNet); } diff --git a/gtsam/discrete/DiscreteFactorGraph.h b/gtsam/discrete/DiscreteFactorGraph.h index 2e9b40823f..f962b1802d 100644 --- a/gtsam/discrete/DiscreteFactorGraph.h +++ b/gtsam/discrete/DiscreteFactorGraph.h @@ -147,7 +147,7 @@ class GTSAM_EXPORT DiscreteFactorGraph * @param ordering * @return DiscreteBayesNet encoding posterior P(X|Z) */ - DiscreteLookupDAG sumProduct(const Ordering& ordering) const; + DiscreteBayesNet sumProduct(const Ordering& ordering) const; /** * @brief Implement the max-product algorithm diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 0dcbcc1cfc..2582869019 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -277,9 +277,9 @@ class DiscreteFactorGraph { double operator()(const gtsam::DiscreteValues& values) const; gtsam::DiscreteValues optimize() const; - gtsam::DiscreteLookupDAG sumProduct(); - gtsam::DiscreteLookupDAG sumProduct(gtsam::Ordering::OrderingType type); - gtsam::DiscreteLookupDAG sumProduct(const gtsam::Ordering& ordering); + gtsam::DiscreteBayesNet sumProduct(); + gtsam::DiscreteBayesNet sumProduct(gtsam::Ordering::OrderingType type); + gtsam::DiscreteBayesNet sumProduct(const gtsam::Ordering& ordering); gtsam::DiscreteLookupDAG maxProduct(); gtsam::DiscreteLookupDAG maxProduct(gtsam::Ordering::OrderingType type); diff --git a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp index 63f5b73194..0a7d869ec5 100644 --- a/gtsam/discrete/tests/testDiscreteFactorGraph.cpp +++ b/gtsam/discrete/tests/testDiscreteFactorGraph.cpp @@ -158,6 +158,11 @@ TEST(DiscreteFactorGraph, test) { // Test sumProduct alias with all orderings: auto mpeProbability = expectedBayesNet(mpe); EXPECT_DOUBLES_EQUAL(0.28125, mpeProbability, 1e-5); // regression + + // Using custom ordering + DiscreteBayesNet bayesNet = graph.sumProduct(ordering); + EXPECT_DOUBLES_EQUAL(mpeProbability, bayesNet(mpe), 1e-5); + for (Ordering::OrderingType orderingType : {Ordering::COLAMD, Ordering::METIS, Ordering::NATURAL, Ordering::CUSTOM}) { From 11c5bb97666877fa1e6a8f50e07ed3c9127ca539 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 01:16:25 -0500 Subject: [PATCH 79/91] Fix docs (#1064) --- doc/Doxyfile.in | 2 +- doc/gtsam.lyx | 35 +++++++++++++++++++++++++++-------- doc/gtsam.pdf | Bin 826064 -> 825916 bytes doc/math.lyx | 8 ++++---- doc/math.pdf | Bin 273096 -> 273104 bytes python/gtsam/utils/plot.py | 14 +++++++++----- 6 files changed, 41 insertions(+), 18 deletions(-) diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index fd7f4e5f60..12193d0be5 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -1188,7 +1188,7 @@ USE_MATHJAX = YES # MathJax, but it is strongly recommended to install a local copy of MathJax # before deployment. -MATHJAX_RELPATH = https://cdn.mathjax.org/mathjax/latest +# MATHJAX_RELPATH = https://cdn.mathjax.org/mathjax/latest # The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension # names that should be enabled during MathJax rendering. diff --git a/doc/gtsam.lyx b/doc/gtsam.lyx index a5adc2b609..29d03cd357 100644 --- a/doc/gtsam.lyx +++ b/doc/gtsam.lyx @@ -1,5 +1,5 @@ -#LyX 2.2 created this file. For more info see http://www.lyx.org/ -\lyxformat 508 +#LyX 2.3 created this file. For more info see http://www.lyx.org/ +\lyxformat 544 \begin_document \begin_header \save_transient_properties true @@ -62,6 +62,8 @@ \font_osf false \font_sf_scale 100 100 \font_tt_scale 100 100 +\use_microtype false +\use_dash_ligatures true \graphics default \default_output_format default \output_sync 0 @@ -91,6 +93,7 @@ \suppress_date false \justification true \use_refstyle 0 +\use_minted 0 \index Index \shortcut idx \color #008000 @@ -105,7 +108,10 @@ \tocdepth 3 \paragraph_separation indent \paragraph_indentation default -\quotes_language english +\is_math_indent 0 +\math_numbering_side default +\quotes_style english +\dynamic_quotes 0 \papercolumns 1 \papersides 1 \paperpagestyle default @@ -168,6 +174,7 @@ Factor graphs \begin_inset CommandInset citation LatexCommand citep key "Koller09book" +literal "true" \end_inset @@ -270,6 +277,7 @@ Let us start with a one-page primer on factor graphs, which in no way replaces \begin_inset CommandInset citation LatexCommand citet key "Kschischang01it" +literal "true" \end_inset @@ -277,6 +285,7 @@ key "Kschischang01it" \begin_inset CommandInset citation LatexCommand citet key "Loeliger04spm" +literal "true" \end_inset @@ -1321,6 +1330,7 @@ r in a pre-existing map, or indeed the presence of absence of ceiling lights \begin_inset CommandInset citation LatexCommand citet key "Dellaert99b" +literal "true" \end_inset @@ -1542,6 +1552,7 @@ which is done on line 12. \begin_inset CommandInset citation LatexCommand citealt key "Dellaert06ijrr" +literal "true" \end_inset @@ -1936,8 +1947,8 @@ reference "fig:CompareMarginals" \end_inset -, where I show the marginals on position as covariance ellipses that contain - 68.26% of all probability mass. +, where I show the marginals on position as 5-sigma covariance ellipses + that contain 99.9996% of all probability mass. For the odometry marginals, it is immediately apparent from the figure that (1) the uncertainty on pose keeps growing, and (2) the uncertainty on angular odometry translates into increasing uncertainty on y. @@ -1992,6 +2003,7 @@ PoseSLAM \begin_inset CommandInset citation LatexCommand citep key "DurrantWhyte06ram" +literal "true" \end_inset @@ -2190,9 +2202,9 @@ reference "fig:example" \end_inset , along with covariance ellipses shown in green. - These covariance ellipses in 2D indicate the marginal over position, over - all possible orientations, and show the area which contain 68.26% of the - probability mass (in 1D this would correspond to one standard deviation). + These 5-sigma covariance ellipses in 2D indicate the marginal over position, + over all possible orientations, and show the area which contain 99.9996% + of the probability mass. The graph shows in a clear manner that the uncertainty on pose \begin_inset Formula $x_{5}$ \end_inset @@ -3076,6 +3088,7 @@ reference "fig:Victoria-1" \begin_inset CommandInset citation LatexCommand citep key "Kaess09ras" +literal "true" \end_inset @@ -3088,6 +3101,7 @@ key "Kaess09ras" \begin_inset CommandInset citation LatexCommand citep key "Kaess08tro" +literal "true" \end_inset @@ -3355,6 +3369,7 @@ iSAM \begin_inset CommandInset citation LatexCommand citet key "Kaess08tro,Kaess12ijrr" +literal "true" \end_inset @@ -3606,6 +3621,7 @@ subgraph preconditioning \begin_inset CommandInset citation LatexCommand citet key "Dellaert10iros,Jian11iccv" +literal "true" \end_inset @@ -3638,6 +3654,7 @@ Visual Odometry \begin_inset CommandInset citation LatexCommand citet key "Nister04cvpr2" +literal "true" \end_inset @@ -3661,6 +3678,7 @@ Visual SLAM \begin_inset CommandInset citation LatexCommand citet key "Davison03iccv" +literal "true" \end_inset @@ -3711,6 +3729,7 @@ Filtering \begin_inset CommandInset citation LatexCommand citep key "Smith87b" +literal "true" \end_inset diff --git a/doc/gtsam.pdf b/doc/gtsam.pdf index c6a39a79c4f06bb23d4b8b26d53ee5dda5912389..d4cb8908f51fef25d5cbbc7f29868238b152be2b 100644 GIT binary patch delta 89836 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⋙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 delta 90022 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

NYH;( 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 diff --git a/doc/math.lyx b/doc/math.lyx index 4ee89a9cc2..86ed2b2200 100644 --- a/doc/math.lyx +++ b/doc/math.lyx @@ -2668,7 +2668,7 @@ reference "eq:pushforward" \begin{eqnarray*} \varphi(a)e^{\yhat} & = & \varphi(ae^{\xhat})\\ a^{-1}e^{\yhat} & = & \left(ae^{\xhat}\right)^{-1}\\ -e^{\yhat} & = & -ae^{\xhat}a^{-1}\\ +e^{\yhat} & = & ae^{-\xhat}a^{-1}\\ \yhat & = & -\Ad a\xhat \end{eqnarray*} @@ -3003,8 +3003,8 @@ between \begin_inset Formula \begin{align} \varphi(g,h)e^{\yhat} & =\varphi(ge^{\xhat},h)\nonumber \\ -g^{-1}he^{\yhat} & =\left(ge^{\xhat}\right)^{-1}h=-e^{\xhat}g^{-1}h\nonumber \\ -e^{\yhat} & =-\left(h^{-1}g\right)e^{\xhat}\left(h^{-1}g\right)^{-1}=-\exp\Ad{\left(h^{-1}g\right)}\xhat\nonumber \\ +g^{-1}he^{\yhat} & =\left(ge^{\xhat}\right)^{-1}h=e^{-\xhat}g^{-1}h\nonumber \\ +e^{\yhat} & =\left(h^{-1}g\right)e^{-\xhat}\left(h^{-1}g\right)^{-1}=\exp\Ad{\left(h^{-1}g\right)}(-\xhat)\nonumber \\ \yhat & =-\Ad{\left(h^{-1}g\right)}\xhat=-\Ad{\varphi\left(h,g\right)}\xhat\label{eq:Dbetween1} \end{align} @@ -6674,7 +6674,7 @@ One representation of a line is through 2 vectors \begin_inset Formula $d$ \end_inset - points from the orgin to the closest point on the line. + points from the origin to the closest point on the line. \end_layout \begin_layout Standard diff --git a/doc/math.pdf b/doc/math.pdf index 40980354ead4a2339ffb1ef0636825a77fad1cac..71533e1e830e173ccca6dcd665f6adccb35d4d6d 100644 GIT binary patch delta 10465 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 HB;bDl>9r_1 delta 10459 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 p = 99.9996 % def set_axes_equal(fignum: int) -> None: """ @@ -125,10 +132,7 @@ def plot_point2_on_axes(axes, if P is not None: w, v = np.linalg.eig(P) - # "Sigma" value for drawing the uncertainty ellipse. 5 sigma corresponds - # to a 99.9999% confidence, i.e. assuming the estimation has been - # computed properly, there is a 99.999% chance that the true position - # of the point will lie within the uncertainty ellipse. + # 5 sigma corresponds to 99.9996%, see note above k = 5.0 angle = np.arctan2(v[1, 0], v[0, 0]) @@ -205,7 +209,7 @@ def plot_pose2_on_axes(axes, w, v = np.linalg.eig(gPp) - # k = 2.296 + # 5 sigma corresponds to 99.9996%, see note above k = 5.0 angle = np.arctan2(v[1, 0], v[0, 0]) From 2fda2a1c0026cc28fcb3cf01ab0a71f88a1c144c Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 08:02:12 -0500 Subject: [PATCH 80/91] Added inference module --- gtsam/discrete/discrete.i | 13 -- gtsam/inference/inference.i | 163 +++++++++++++++++++++++ gtsam/nonlinear/nonlinear.i | 112 ---------------- gtsam/symbolic/symbolic.i | 30 ----- python/CMakeLists.txt | 1 + python/gtsam/preamble/inference.h | 15 +++ python/gtsam/specializations/inference.h | 13 ++ 7 files changed, 192 insertions(+), 155 deletions(-) create mode 100644 gtsam/inference/inference.i create mode 100644 python/gtsam/preamble/inference.h create mode 100644 python/gtsam/specializations/inference.h diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 2582869019..80b8df1bc1 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -228,19 +228,6 @@ class DiscreteLookupDAG { gtsam::DiscreteValues argmax(gtsam::DiscreteValues given) const; }; -#include -class DotWriter { - DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, - bool plotFactorPoints = true, bool connectKeysToFactor = true, - bool binaryEdges = true); - - double figureWidthInches; - double figureHeightInches; - bool plotFactorPoints; - bool connectKeysToFactor; - bool binaryEdges; -}; - #include class DiscreteFactorGraph { DiscreteFactorGraph(); diff --git a/gtsam/inference/inference.i b/gtsam/inference/inference.i new file mode 100644 index 0000000000..5b9cef7efd --- /dev/null +++ b/gtsam/inference/inference.i @@ -0,0 +1,163 @@ +//************************************************************************* +// inference +//************************************************************************* + +namespace gtsam { + +#include + +// Default keyformatter +void PrintKeyList( + const gtsam::KeyList& keys, const string& s = "", + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); +void PrintKeyVector( + const gtsam::KeyVector& keys, const string& s = "", + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); +void PrintKeySet( + const gtsam::KeySet& keys, const string& s = "", + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); + +#include +class Symbol { + Symbol(); + Symbol(char c, uint64_t j); + Symbol(size_t key); + + size_t key() const; + void print(const string& s = "") const; + bool equals(const gtsam::Symbol& expected, double tol) const; + + char chr() const; + uint64_t index() const; + string string() const; +}; + +size_t symbol(char chr, size_t index); +char symbolChr(size_t key); +size_t symbolIndex(size_t key); + +namespace symbol_shorthand { +size_t A(size_t j); +size_t B(size_t j); +size_t C(size_t j); +size_t D(size_t j); +size_t E(size_t j); +size_t F(size_t j); +size_t G(size_t j); +size_t H(size_t j); +size_t I(size_t j); +size_t J(size_t j); +size_t K(size_t j); +size_t L(size_t j); +size_t M(size_t j); +size_t N(size_t j); +size_t O(size_t j); +size_t P(size_t j); +size_t Q(size_t j); +size_t R(size_t j); +size_t S(size_t j); +size_t T(size_t j); +size_t U(size_t j); +size_t V(size_t j); +size_t W(size_t j); +size_t X(size_t j); +size_t Y(size_t j); +size_t Z(size_t j); +} // namespace symbol_shorthand + +#include +class LabeledSymbol { + LabeledSymbol(size_t full_key); + LabeledSymbol(const gtsam::LabeledSymbol& key); + LabeledSymbol(unsigned char valType, unsigned char label, size_t j); + + size_t key() const; + unsigned char label() const; + unsigned char chr() const; + size_t index() const; + + gtsam::LabeledSymbol upper() const; + gtsam::LabeledSymbol lower() const; + gtsam::LabeledSymbol newChr(unsigned char c) const; + gtsam::LabeledSymbol newLabel(unsigned char label) const; + + void print(string s = "") const; +}; + +size_t mrsymbol(unsigned char c, unsigned char label, size_t j); +unsigned char mrsymbolChr(size_t key); +unsigned char mrsymbolLabel(size_t key); +size_t mrsymbolIndex(size_t key); + +#include +class Ordering { + /// Type of ordering to use + enum OrderingType { COLAMD, METIS, NATURAL, CUSTOM }; + + // Standard Constructors and Named Constructors + Ordering(); + Ordering(const gtsam::Ordering& other); + + template + static gtsam::Ordering Colamd(const FACTOR_GRAPH& graph); + + // Testable + void print(string s = "", const gtsam::KeyFormatter& keyFormatter = + gtsam::DefaultKeyFormatter) const; + bool equals(const gtsam::Ordering& ord, double tol) const; + + // Standard interface + size_t size() const; + size_t at(size_t key) const; + void push_back(size_t key); + + // enabling serialization functionality + void serialize() const; +}; + +#include +class DotWriter { + DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, + bool plotFactorPoints = true, bool connectKeysToFactor = true, + bool binaryEdges = true); + + double figureWidthInches; + double figureHeightInches; + bool plotFactorPoints; + bool connectKeysToFactor; + bool binaryEdges; +}; + +#include + +// Headers for overloaded methods below, break hierarchy :-/ +#include +#include +#include + +class VariableIndex { + // Standard Constructors and Named Constructors + VariableIndex(); + // TODO: Templetize constructor when wrap supports it + // template + // VariableIndex(const T& factorGraph, size_t nVariables); + // VariableIndex(const T& factorGraph); + VariableIndex(const gtsam::SymbolicFactorGraph& sfg); + VariableIndex(const gtsam::GaussianFactorGraph& gfg); + VariableIndex(const gtsam::NonlinearFactorGraph& fg); + VariableIndex(const gtsam::VariableIndex& other); + + // Testable + bool equals(const gtsam::VariableIndex& other, double tol) const; + void print(string s = "VariableIndex: ", + const gtsam::KeyFormatter& keyFormatter = + gtsam::DefaultKeyFormatter) const; + + // Standard interface + size_t size() const; + size_t nFactors() const; + size_t nEntries() const; +}; + +} // namespace gtsam diff --git a/gtsam/nonlinear/nonlinear.i b/gtsam/nonlinear/nonlinear.i index a6883d38b8..159261713a 100644 --- a/gtsam/nonlinear/nonlinear.i +++ b/gtsam/nonlinear/nonlinear.i @@ -23,121 +23,9 @@ namespace gtsam { #include #include #include -#include #include #include -class Symbol { - Symbol(); - Symbol(char c, uint64_t j); - Symbol(size_t key); - - size_t key() const; - void print(const string& s = "") const; - bool equals(const gtsam::Symbol& expected, double tol) const; - - char chr() const; - uint64_t index() const; - string string() const; -}; - -size_t symbol(char chr, size_t index); -char symbolChr(size_t key); -size_t symbolIndex(size_t key); - -namespace symbol_shorthand { -size_t A(size_t j); -size_t B(size_t j); -size_t C(size_t j); -size_t D(size_t j); -size_t E(size_t j); -size_t F(size_t j); -size_t G(size_t j); -size_t H(size_t j); -size_t I(size_t j); -size_t J(size_t j); -size_t K(size_t j); -size_t L(size_t j); -size_t M(size_t j); -size_t N(size_t j); -size_t O(size_t j); -size_t P(size_t j); -size_t Q(size_t j); -size_t R(size_t j); -size_t S(size_t j); -size_t T(size_t j); -size_t U(size_t j); -size_t V(size_t j); -size_t W(size_t j); -size_t X(size_t j); -size_t Y(size_t j); -size_t Z(size_t j); -} // namespace symbol_shorthand - -// Default keyformatter -void PrintKeyList( - const gtsam::KeyList& keys, const string& s = "", - const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); -void PrintKeyVector( - const gtsam::KeyVector& keys, const string& s = "", - const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); -void PrintKeySet( - const gtsam::KeySet& keys, const string& s = "", - const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter); - -#include -class LabeledSymbol { - LabeledSymbol(size_t full_key); - LabeledSymbol(const gtsam::LabeledSymbol& key); - LabeledSymbol(unsigned char valType, unsigned char label, size_t j); - - size_t key() const; - unsigned char label() const; - unsigned char chr() const; - size_t index() const; - - gtsam::LabeledSymbol upper() const; - gtsam::LabeledSymbol lower() const; - gtsam::LabeledSymbol newChr(unsigned char c) const; - gtsam::LabeledSymbol newLabel(unsigned char label) const; - - void print(string s = "") const; -}; - -size_t mrsymbol(unsigned char c, unsigned char label, size_t j); -unsigned char mrsymbolChr(size_t key); -unsigned char mrsymbolLabel(size_t key); -size_t mrsymbolIndex(size_t key); - -#include -class Ordering { - /// Type of ordering to use - enum OrderingType { - COLAMD, METIS, NATURAL, CUSTOM - }; - - // Standard Constructors and Named Constructors - Ordering(); - Ordering(const gtsam::Ordering& other); - - template - static gtsam::Ordering Colamd(const FACTOR_GRAPH& graph); - - // Testable - void print(string s = "", const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter) const; - bool equals(const gtsam::Ordering& ord, double tol) const; - - // Standard interface - size_t size() const; - size_t at(size_t key) const; - void push_back(size_t key); - - // enabling serialization functionality - void serialize() const; -}; - #include class GraphvizFormatting : gtsam::DotWriter { GraphvizFormatting(); diff --git a/gtsam/symbolic/symbolic.i b/gtsam/symbolic/symbolic.i index 4e7cca68ae..771e5309ad 100644 --- a/gtsam/symbolic/symbolic.i +++ b/gtsam/symbolic/symbolic.i @@ -3,11 +3,6 @@ //************************************************************************* namespace gtsam { -#include -#include - -// ################### - #include virtual class SymbolicFactor { // Standard Constructors and Named Constructors @@ -173,29 +168,4 @@ class SymbolicBayesTreeClique { void deleteCachedShortcuts(); }; -#include -class VariableIndex { - // Standard Constructors and Named Constructors - VariableIndex(); - // TODO: Templetize constructor when wrap supports it - // template - // VariableIndex(const T& factorGraph, size_t nVariables); - // VariableIndex(const T& factorGraph); - VariableIndex(const gtsam::SymbolicFactorGraph& sfg); - VariableIndex(const gtsam::GaussianFactorGraph& gfg); - VariableIndex(const gtsam::NonlinearFactorGraph& fg); - VariableIndex(const gtsam::VariableIndex& other); - - // Testable - bool equals(const gtsam::VariableIndex& other, double tol) const; - void print(string s = "VariableIndex: ", - const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter) const; - - // Standard interface - size_t size() const; - size_t nFactors() const; - size_t nEntries() const; -}; - } // namespace gtsam diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index f42e330b2c..56062c5be5 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -53,6 +53,7 @@ set(ignore set(interface_headers ${PROJECT_SOURCE_DIR}/gtsam/gtsam.i ${PROJECT_SOURCE_DIR}/gtsam/base/base.i + ${PROJECT_SOURCE_DIR}/gtsam/inference/inference.i ${PROJECT_SOURCE_DIR}/gtsam/discrete/discrete.i ${PROJECT_SOURCE_DIR}/gtsam/geometry/geometry.i ${PROJECT_SOURCE_DIR}/gtsam/linear/linear.i diff --git a/python/gtsam/preamble/inference.h b/python/gtsam/preamble/inference.h new file mode 100644 index 0000000000..320e0ac718 --- /dev/null +++ b/python/gtsam/preamble/inference.h @@ -0,0 +1,15 @@ +/* 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++. + */ + +#include + diff --git a/python/gtsam/specializations/inference.h b/python/gtsam/specializations/inference.h new file mode 100644 index 0000000000..22fe3beff6 --- /dev/null +++ b/python/gtsam/specializations/inference.h @@ -0,0 +1,13 @@ +/* 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 + * ================= + * + * `py::bind_vector` and similar machinery gives the std container a Python-like + * interface, but without the `` copying mechanism. Combined + * with `PYBIND11_MAKE_OPAQUE` this allows the types to be modified with Python, + * and saves one copy operation. + */ + From a07f1497c7a17778393da812ae706cd964c742d5 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 12:03:25 -0500 Subject: [PATCH 81/91] Made all Bayesnets derive from BayesNet --- gtsam/discrete/DiscreteBayesNet.h | 31 +++++++++-------- gtsam/discrete/tests/testDiscreteBayesNet.cpp | 5 +-- gtsam/inference/BayesNet-inst.h | 3 +- gtsam/inference/BayesNet.h | 8 ++--- gtsam/linear/GaussianBayesNet.h | 29 ++++++++++------ gtsam/symbolic/SymbolicBayesNet.h | 33 +++++++++++-------- 6 files changed, 65 insertions(+), 44 deletions(-) diff --git a/gtsam/discrete/DiscreteBayesNet.h b/gtsam/discrete/DiscreteBayesNet.h index 4916cad7c0..df94d6908a 100644 --- a/gtsam/discrete/DiscreteBayesNet.h +++ b/gtsam/discrete/DiscreteBayesNet.h @@ -31,11 +31,12 @@ namespace gtsam { -/** A Bayes net made from discrete conditional distributions. */ - class GTSAM_EXPORT DiscreteBayesNet: public BayesNet - { - public: - +/** + * A Bayes net made from discrete conditional distributions. + * @addtogroup discrete + */ +class GTSAM_EXPORT DiscreteBayesNet: public BayesNet { + public: typedef BayesNet Base; typedef DiscreteBayesNet This; typedef DiscreteConditional ConditionalType; @@ -49,16 +50,20 @@ namespace gtsam { DiscreteBayesNet() {} /** Construct from iterator over conditionals */ - template - DiscreteBayesNet(ITERATOR firstConditional, ITERATOR lastConditional) : Base(firstConditional, lastConditional) {} + template + DiscreteBayesNet(ITERATOR firstConditional, ITERATOR lastConditional) + : Base(firstConditional, lastConditional) {} /** Construct from container of factors (shared_ptr or plain objects) */ - template - explicit DiscreteBayesNet(const CONTAINER& conditionals) : Base(conditionals) {} - - /** Implicit copy/downcast constructor to override explicit template container constructor */ - template - DiscreteBayesNet(const FactorGraph& graph) : Base(graph) {} + template + explicit DiscreteBayesNet(const CONTAINER& conditionals) + : Base(conditionals) {} + + /** Implicit copy/downcast constructor to override explicit template + * container constructor */ + template + DiscreteBayesNet(const FactorGraph& graph) + : Base(graph) {} /// Destructor virtual ~DiscreteBayesNet() {} diff --git a/gtsam/discrete/tests/testDiscreteBayesNet.cpp b/gtsam/discrete/tests/testDiscreteBayesNet.cpp index c35d4742c0..42aeb6092a 100644 --- a/gtsam/discrete/tests/testDiscreteBayesNet.cpp +++ b/gtsam/discrete/tests/testDiscreteBayesNet.cpp @@ -150,12 +150,13 @@ TEST(DiscreteBayesNet, Dot) { fragment.add((Either | Tuberculosis, LungCancer) = "F T T T"); string actual = fragment.dot(); + cout << actual << endl; EXPECT(actual == "digraph G{\n" - "0->3\n" - "4->6\n" "3->5\n" "6->5\n" + "4->6\n" + "0->3\n" "}"); } diff --git a/gtsam/inference/BayesNet-inst.h b/gtsam/inference/BayesNet-inst.h index be34b2928f..0b1c69d50d 100644 --- a/gtsam/inference/BayesNet-inst.h +++ b/gtsam/inference/BayesNet-inst.h @@ -23,6 +23,7 @@ #include #include +#include namespace gtsam { @@ -39,7 +40,7 @@ void BayesNet::dot(std::ostream& os, const KeyFormatter& keyFormatter) const { os << "digraph G{\n"; - for (auto conditional : *this) { + for (auto conditional : boost::adaptors::reverse(*this)) { auto frontals = conditional->frontals(); const Key me = frontals.front(); auto parents = conditional->parents(); diff --git a/gtsam/inference/BayesNet.h b/gtsam/inference/BayesNet.h index f987ad51be..6dfe60dfeb 100644 --- a/gtsam/inference/BayesNet.h +++ b/gtsam/inference/BayesNet.h @@ -18,17 +18,17 @@ #pragma once +#include + #include -#include +#include namespace gtsam { /** * A BayesNet is a tree of conditionals, stored in elimination order. - * - * todo: how to handle Bayes nets with an optimize function? Currently using global functions. - * \nosubgrouping + * @addtogroup inference */ template class BayesNet : public FactorGraph { diff --git a/gtsam/linear/GaussianBayesNet.h b/gtsam/linear/GaussianBayesNet.h index e55a89bcda..0e51902c31 100644 --- a/gtsam/linear/GaussianBayesNet.h +++ b/gtsam/linear/GaussianBayesNet.h @@ -21,17 +21,21 @@ #pragma once #include +#include #include #include namespace gtsam { - /** A Bayes net made from linear-Gaussian densities */ - class GTSAM_EXPORT GaussianBayesNet: public FactorGraph + /** + * GaussianBayesNet is a Bayes net made from linear-Gaussian conditionals. + * @addtogroup linear + */ + class GTSAM_EXPORT GaussianBayesNet: public BayesNet { public: - typedef FactorGraph Base; + typedef BayesNet Base; typedef GaussianBayesNet This; typedef GaussianConditional ConditionalType; typedef boost::shared_ptr shared_ptr; @@ -44,16 +48,21 @@ namespace gtsam { GaussianBayesNet() {} /** Construct from iterator over conditionals */ - template - GaussianBayesNet(ITERATOR firstConditional, ITERATOR lastConditional) : Base(firstConditional, lastConditional) {} + template + GaussianBayesNet(ITERATOR firstConditional, ITERATOR lastConditional) + : Base(firstConditional, lastConditional) {} /** Construct from container of factors (shared_ptr or plain objects) */ - template - explicit GaussianBayesNet(const CONTAINER& conditionals) : Base(conditionals) {} + template + explicit GaussianBayesNet(const CONTAINER& conditionals) { + push_back(conditionals); + } - /** Implicit copy/downcast constructor to override explicit template container constructor */ - template - GaussianBayesNet(const FactorGraph& graph) : Base(graph) {} + /** Implicit copy/downcast constructor to override explicit template + * container constructor */ + template + explicit GaussianBayesNet(const FactorGraph& graph) + : Base(graph) {} /// Destructor virtual ~GaussianBayesNet() {} diff --git a/gtsam/symbolic/SymbolicBayesNet.h b/gtsam/symbolic/SymbolicBayesNet.h index 464af060b6..cbb14ffbd9 100644 --- a/gtsam/symbolic/SymbolicBayesNet.h +++ b/gtsam/symbolic/SymbolicBayesNet.h @@ -19,19 +19,19 @@ #pragma once #include +#include #include #include namespace gtsam { - /** Symbolic Bayes Net - * \nosubgrouping + /** + * A SymbolicBayesNet is a Bayes Net of purely symbolic conditionals. + * @addtogroup symbolic */ - class SymbolicBayesNet : public FactorGraph { - - public: - - typedef FactorGraph Base; + class SymbolicBayesNet : public BayesNet { + public: + typedef BayesNet Base; typedef SymbolicBayesNet This; typedef SymbolicConditional ConditionalType; typedef boost::shared_ptr shared_ptr; @@ -44,16 +44,21 @@ namespace gtsam { SymbolicBayesNet() {} /** Construct from iterator over conditionals */ - template - SymbolicBayesNet(ITERATOR firstConditional, ITERATOR lastConditional) : Base(firstConditional, lastConditional) {} + template + SymbolicBayesNet(ITERATOR firstConditional, ITERATOR lastConditional) + : Base(firstConditional, lastConditional) {} /** Construct from container of factors (shared_ptr or plain objects) */ - template - explicit SymbolicBayesNet(const CONTAINER& conditionals) : Base(conditionals) {} + template + explicit SymbolicBayesNet(const CONTAINER& conditionals) { + push_back(conditionals); + } - /** Implicit copy/downcast constructor to override explicit template container constructor */ - template - SymbolicBayesNet(const FactorGraph& graph) : Base(graph) {} + /** Implicit copy/downcast constructor to override explicit template + * container constructor */ + template + explicit SymbolicBayesNet(const FactorGraph& graph) + : Base(graph) {} /// Destructor virtual ~SymbolicBayesNet() {} From 62b188473b83cc37a58c104015fcbcfdb99e6401 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 18:45:19 -0500 Subject: [PATCH 82/91] Remove obsolete definitions --- gtsam/linear/GaussianBayesNet.cpp | 18 ------------- gtsam/linear/GaussianBayesNet.h | 25 ++++++------------ gtsam/symbolic/SymbolicBayesNet.cpp | 39 ++++++----------------------- gtsam/symbolic/SymbolicBayesNet.h | 7 ------ 4 files changed, 15 insertions(+), 74 deletions(-) diff --git a/gtsam/linear/GaussianBayesNet.cpp b/gtsam/linear/GaussianBayesNet.cpp index 1e790d0f11..8fd4f2c26d 100644 --- a/gtsam/linear/GaussianBayesNet.cpp +++ b/gtsam/linear/GaussianBayesNet.cpp @@ -205,23 +205,5 @@ namespace gtsam { } /* ************************************************************************* */ - void GaussianBayesNet::saveGraph(const std::string& s, - const KeyFormatter& keyFormatter) const { - std::ofstream of(s.c_str()); - of << "digraph G{\n"; - - for (auto conditional : boost::adaptors::reverse(*this)) { - typename GaussianConditional::Frontals frontals = conditional->frontals(); - Key me = frontals.front(); - typename GaussianConditional::Parents parents = conditional->parents(); - for (Key p : parents) - of << keyFormatter(p) << "->" << keyFormatter(me) << std::endl; - } - - of << "}"; - of.close(); - } - - /* ************************************************************************* */ } // namespace gtsam diff --git a/gtsam/linear/GaussianBayesNet.h b/gtsam/linear/GaussianBayesNet.h index 0e51902c31..6d906d65e3 100644 --- a/gtsam/linear/GaussianBayesNet.h +++ b/gtsam/linear/GaussianBayesNet.h @@ -25,6 +25,7 @@ #include #include +#include namespace gtsam { /** @@ -75,6 +76,13 @@ namespace gtsam { /** Check equality */ bool equals(const This& bn, double tol = 1e-9) const; + /// print graph + void print( + const std::string& s = "", + const KeyFormatter& formatter = DefaultKeyFormatter) const override { + Base::print(s, formatter); + } + /// @} /// @name Standard Interface @@ -189,23 +197,6 @@ namespace gtsam { */ VectorValues backSubstituteTranspose(const VectorValues& gx) const; - /// print graph - void print( - const std::string& s = "", - const KeyFormatter& formatter = DefaultKeyFormatter) const override { - Base::print(s, formatter); - } - - /** - * @brief Save the GaussianBayesNet as an image. Requires `dot` to be - * installed. - * - * @param s The name of the figure. - * @param keyFormatter Formatter to use for styling keys in the graph. - */ - void saveGraph(const std::string& s, const KeyFormatter& keyFormatter = - DefaultKeyFormatter) const; - /// @} private: diff --git a/gtsam/symbolic/SymbolicBayesNet.cpp b/gtsam/symbolic/SymbolicBayesNet.cpp index 5bc20ad127..f7113b23a5 100644 --- a/gtsam/symbolic/SymbolicBayesNet.cpp +++ b/gtsam/symbolic/SymbolicBayesNet.cpp @@ -16,41 +16,16 @@ * @author Richard Roberts */ -#include -#include #include - -#include -#include +#include namespace gtsam { - // Instantiate base class - template class FactorGraph; - - /* ************************************************************************* */ - bool SymbolicBayesNet::equals(const This& bn, double tol) const - { - return Base::equals(bn, tol); - } - - /* ************************************************************************* */ - void SymbolicBayesNet::saveGraph(const std::string &s, const KeyFormatter& keyFormatter) const - { - std::ofstream of(s.c_str()); - of << "digraph G{\n"; - - for (auto conditional: boost::adaptors::reverse(*this)) { - SymbolicConditional::Frontals frontals = conditional->frontals(); - Key me = frontals.front(); - SymbolicConditional::Parents parents = conditional->parents(); - for(Key p: parents) - of << p << "->" << me << std::endl; - } - - of << "}"; - of.close(); - } - +// Instantiate base class +template class FactorGraph; +/* ************************************************************************* */ +bool SymbolicBayesNet::equals(const This& bn, double tol) const { + return Base::equals(bn, tol); } +} // namespace gtsam diff --git a/gtsam/symbolic/SymbolicBayesNet.h b/gtsam/symbolic/SymbolicBayesNet.h index cbb14ffbd9..2f66b80e22 100644 --- a/gtsam/symbolic/SymbolicBayesNet.h +++ b/gtsam/symbolic/SymbolicBayesNet.h @@ -80,13 +80,6 @@ namespace gtsam { /// @} - /// @name Standard Interface - /// @{ - - GTSAM_EXPORT void saveGraph(const std::string &s, const KeyFormatter& keyFormatter = DefaultKeyFormatter) const; - - /// @} - private: /** Serialization function */ friend class boost::serialization::access; From ebe3aadada22742a41f24c1378ae4b414616fa81 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 18:46:06 -0500 Subject: [PATCH 83/91] Variable positions for Bayes nets --- gtsam/inference/BayesNet-inst.h | 40 +++++---- gtsam/inference/BayesNet.h | 98 +++++++++++---------- gtsam/inference/DotWriter.cpp | 8 +- gtsam/inference/DotWriter.h | 25 +++++- gtsam/inference/FactorGraph-inst.h | 2 +- gtsam/linear/tests/testGaussianBayesNet.cpp | 29 +++++- gtsam/nonlinear/GraphvizFormatting.cpp | 8 +- gtsam/nonlinear/GraphvizFormatting.h | 12 +-- gtsam/nonlinear/NonlinearFactorGraph.cpp | 2 +- gtsam/nonlinear/NonlinearFactorGraph.h | 29 +++--- 10 files changed, 157 insertions(+), 96 deletions(-) diff --git a/gtsam/inference/BayesNet-inst.h b/gtsam/inference/BayesNet-inst.h index 0b1c69d50d..bd90f4e4b3 100644 --- a/gtsam/inference/BayesNet-inst.h +++ b/gtsam/inference/BayesNet-inst.h @@ -10,16 +10,16 @@ * -------------------------------------------------------------------------- */ /** -* @file BayesNet.h -* @brief Bayes network -* @author Frank Dellaert -* @author Richard Roberts -*/ + * @file BayesNet.h + * @brief Bayes network + * @author Frank Dellaert + * @author Richard Roberts + */ #pragma once -#include #include +#include #include #include @@ -29,23 +29,31 @@ namespace gtsam { /* ************************************************************************* */ template -void BayesNet::print( - const std::string& s, const KeyFormatter& formatter) const { +void BayesNet::print(const std::string& s, + const KeyFormatter& formatter) const { Base::print(s, formatter); } /* ************************************************************************* */ template void BayesNet::dot(std::ostream& os, - const KeyFormatter& keyFormatter) const { - os << "digraph G{\n"; + const KeyFormatter& keyFormatter, + const DotWriter& writer) const { + writer.digraphPreamble(&os); + + // Create nodes for each variable in the graph + for (Key key : this->keys()) { + auto position = writer.variablePos(key); + writer.DrawVariable(key, keyFormatter, position, &os); + } + os << "\n"; for (auto conditional : boost::adaptors::reverse(*this)) { auto frontals = conditional->frontals(); const Key me = frontals.front(); auto parents = conditional->parents(); for (const Key& p : parents) - os << keyFormatter(p) << "->" << keyFormatter(me) << "\n"; + os << " var" << keyFormatter(p) << "->var" << keyFormatter(me) << "\n"; } os << "}"; @@ -54,18 +62,20 @@ void BayesNet::dot(std::ostream& os, /* ************************************************************************* */ template -std::string BayesNet::dot(const KeyFormatter& keyFormatter) const { +std::string BayesNet::dot(const KeyFormatter& keyFormatter, + const DotWriter& writer) const { std::stringstream ss; - dot(ss, keyFormatter); + dot(ss, keyFormatter, writer); return ss.str(); } /* ************************************************************************* */ template void BayesNet::saveGraph(const std::string& filename, - const KeyFormatter& keyFormatter) const { + const KeyFormatter& keyFormatter, + const DotWriter& writer) const { std::ofstream of(filename.c_str()); - dot(of, keyFormatter); + dot(of, keyFormatter, writer); of.close(); } diff --git a/gtsam/inference/BayesNet.h b/gtsam/inference/BayesNet.h index 6dfe60dfeb..219864c547 100644 --- a/gtsam/inference/BayesNet.h +++ b/gtsam/inference/BayesNet.h @@ -10,77 +10,79 @@ * -------------------------------------------------------------------------- */ /** -* @file BayesNet.h -* @brief Bayes network -* @author Frank Dellaert -* @author Richard Roberts -*/ + * @file BayesNet.h + * @brief Bayes network + * @author Frank Dellaert + * @author Richard Roberts + */ #pragma once #include #include - #include namespace gtsam { - /** - * A BayesNet is a tree of conditionals, stored in elimination order. - * @addtogroup inference - */ - template - class BayesNet : public FactorGraph { - - private: +/** + * A BayesNet is a tree of conditionals, stored in elimination order. + * @addtogroup inference + */ +template +class BayesNet : public FactorGraph { + private: + typedef FactorGraph Base; - typedef FactorGraph Base; + public: + typedef typename boost::shared_ptr + sharedConditional; ///< A shared pointer to a conditional - public: - typedef typename boost::shared_ptr sharedConditional; ///< A shared pointer to a conditional + protected: + /// @name Standard Constructors + /// @{ - protected: - /// @name Standard Constructors - /// @{ + /** Default constructor as an empty BayesNet */ + BayesNet() {} - /** Default constructor as an empty BayesNet */ - BayesNet() {}; + /** Construct from iterator over conditionals */ + template + BayesNet(ITERATOR firstConditional, ITERATOR lastConditional) + : Base(firstConditional, lastConditional) {} - /** Construct from iterator over conditionals */ - template - BayesNet(ITERATOR firstConditional, ITERATOR lastConditional) : Base(firstConditional, lastConditional) {} + /// @} - /// @} + public: + /// @name Testable + /// @{ - public: - /// @name Testable - /// @{ + /** print out graph */ + void print( + const std::string& s = "BayesNet", + const KeyFormatter& formatter = DefaultKeyFormatter) const override; - /** print out graph */ - void print( - const std::string& s = "BayesNet", - const KeyFormatter& formatter = DefaultKeyFormatter) const override; + /// @} - /// @} + /// @name Graph Display + /// @{ - /// @name Graph Display - /// @{ + /// Output to graphviz format, stream version. + void dot(std::ostream& os, + const KeyFormatter& keyFormatter = DefaultKeyFormatter, + const DotWriter& writer = DotWriter()) const; - /// Output to graphviz format, stream version. - void dot(std::ostream& os, const KeyFormatter& keyFormatter = DefaultKeyFormatter) const; + /// Output to graphviz format string. + std::string dot(const KeyFormatter& keyFormatter = DefaultKeyFormatter, + const DotWriter& writer = DotWriter()) const; - /// Output to graphviz format string. - std::string dot( - const KeyFormatter& keyFormatter = DefaultKeyFormatter) const; + /// output to file with graphviz format. + void saveGraph(const std::string& filename, + const KeyFormatter& keyFormatter = DefaultKeyFormatter, + const DotWriter& writer = DotWriter()) const; - /// output to file with graphviz format. - void saveGraph(const std::string& filename, - const KeyFormatter& keyFormatter = DefaultKeyFormatter) const; - - /// @} - }; + /// @} +}; -} +} // namespace gtsam #include diff --git a/gtsam/inference/DotWriter.cpp b/gtsam/inference/DotWriter.cpp index 18130c35d7..a45482efbb 100644 --- a/gtsam/inference/DotWriter.cpp +++ b/gtsam/inference/DotWriter.cpp @@ -25,12 +25,18 @@ using namespace std; namespace gtsam { -void DotWriter::writePreamble(ostream* os) const { +void DotWriter::graphPreamble(ostream* os) const { *os << "graph {\n"; *os << " size=\"" << figureWidthInches << "," << figureHeightInches << "\";\n\n"; } +void DotWriter::digraphPreamble(ostream* os) const { + *os << "digraph {\n"; + *os << " size=\"" << figureWidthInches << "," << figureHeightInches + << "\";\n\n"; +} + void DotWriter::DrawVariable(Key key, const KeyFormatter& keyFormatter, const boost::optional& position, ostream* os) { diff --git a/gtsam/inference/DotWriter.h b/gtsam/inference/DotWriter.h index 93c229c2b1..ad420b1817 100644 --- a/gtsam/inference/DotWriter.h +++ b/gtsam/inference/DotWriter.h @@ -23,10 +23,14 @@ #include #include +#include namespace gtsam { -/// Graphviz formatter. +/** + * @brief DotWriter is a helper class for writing graphviz .dot files. + * @addtogroup inference + */ struct GTSAM_EXPORT DotWriter { double figureWidthInches; ///< The figure width on paper in inches double figureHeightInches; ///< The figure height on paper in inches @@ -35,6 +39,9 @@ struct GTSAM_EXPORT DotWriter { ///< the dot of the factor bool binaryEdges; ///< just use non-dotted edges for binary factors + /// (optional for each variable) Manually specify variable node positions + std::map variablePositions; + explicit DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, bool plotFactorPoints = true, @@ -45,8 +52,11 @@ struct GTSAM_EXPORT DotWriter { connectKeysToFactor(connectKeysToFactor), binaryEdges(binaryEdges) {} - /// Write out preamble, including size. - void writePreamble(std::ostream* os) const; + /// Write out preamble for graph, including size. + void graphPreamble(std::ostream* os) const; + + /// Write out preamble for digraph, including size. + void digraphPreamble(std::ostream* os) const; /// Create a variable dot fragment. static void DrawVariable(Key key, const KeyFormatter& keyFormatter, @@ -57,6 +67,15 @@ struct GTSAM_EXPORT DotWriter { static void DrawFactor(size_t i, const boost::optional& position, std::ostream* os); + /// Return variable position or none + boost::optional variablePos(Key key) const { + auto it = variablePositions.find(key); + if (it == variablePositions.end()) + return boost::none; + else + return it->second; + } + /// Draw a single factor, specified by its index i and its variable keys. void processFactor(size_t i, const KeyVector& keys, const KeyFormatter& keyFormatter, diff --git a/gtsam/inference/FactorGraph-inst.h b/gtsam/inference/FactorGraph-inst.h index 3ea17fc7ff..2034fdcb67 100644 --- a/gtsam/inference/FactorGraph-inst.h +++ b/gtsam/inference/FactorGraph-inst.h @@ -131,7 +131,7 @@ template void FactorGraph::dot(std::ostream& os, const KeyFormatter& keyFormatter, const DotWriter& writer) const { - writer.writePreamble(&os); + writer.graphPreamble(&os); // Create nodes for each variable in the graph for (Key key : keys()) { diff --git a/gtsam/linear/tests/testGaussianBayesNet.cpp b/gtsam/linear/tests/testGaussianBayesNet.cpp index 00a338e547..11fc7e7f7f 100644 --- a/gtsam/linear/tests/testGaussianBayesNet.cpp +++ b/gtsam/linear/tests/testGaussianBayesNet.cpp @@ -301,5 +301,32 @@ TEST(GaussianBayesNet, ComputeSteepestDescentPoint) { } /* ************************************************************************* */ -int main() { TestResult tr; return TestRegistry::runAllTests(tr);} +TEST(GaussianBayesNet, Dot) { + GaussianBayesNet fragment; + DotWriter writer; + writer.variablePositions.emplace(_x_, Vector2(10, 20)); + writer.variablePositions.emplace(_y_, Vector2(50, 20)); + + auto position = writer.variablePos(_x_); + CHECK(position); + EXPECT(assert_equal(Vector2(10, 20), *position, 1e-5)); + + string actual = noisyBayesNet.dot(DefaultKeyFormatter, writer); + noisyBayesNet.saveGraph("noisyBayesNet.dot", DefaultKeyFormatter, writer); + EXPECT(actual == + "digraph {\n" + " size=\"5,5\";\n" + "\n" + " var11[label=\"11\", pos=\"10,20!\"];\n" + " var22[label=\"22\", pos=\"50,20!\"];\n" + "\n" + " var22->var11\n" + "}"); +} + +/* ************************************************************************* */ +int main() { + TestResult tr; + return TestRegistry::runAllTests(tr); +} /* ************************************************************************* */ diff --git a/gtsam/nonlinear/GraphvizFormatting.cpp b/gtsam/nonlinear/GraphvizFormatting.cpp index e5b81c66b9..1f0b3a8758 100644 --- a/gtsam/nonlinear/GraphvizFormatting.cpp +++ b/gtsam/nonlinear/GraphvizFormatting.cpp @@ -34,7 +34,7 @@ Vector2 GraphvizFormatting::findBounds(const Values& values, min.y() = std::numeric_limits::infinity(); for (const Key& key : keys) { if (values.exists(key)) { - boost::optional xy = operator()(values.at(key)); + boost::optional xy = extractPosition(values.at(key)); if (xy) { if (xy->x() < min.x()) min.x() = xy->x(); if (xy->y() < min.y()) min.y() = xy->y(); @@ -44,7 +44,7 @@ Vector2 GraphvizFormatting::findBounds(const Values& values, return min; } -boost::optional GraphvizFormatting::operator()( +boost::optional GraphvizFormatting::extractPosition( const Value& value) const { Vector3 t; if (const GenericValue* p = @@ -121,12 +121,11 @@ boost::optional GraphvizFormatting::operator()( return Vector2(x, y); } -// Return affinely transformed variable position if it exists. boost::optional GraphvizFormatting::variablePos(const Values& values, const Vector2& min, Key key) const { if (!values.exists(key)) return boost::none; - boost::optional xy = operator()(values.at(key)); + boost::optional xy = extractPosition(values.at(key)); if (xy) { xy->x() = scale * (xy->x() - min.x()); xy->y() = scale * (xy->y() - min.y()); @@ -134,7 +133,6 @@ boost::optional GraphvizFormatting::variablePos(const Values& values, return xy; } -// Return affinely transformed factor position if it exists. boost::optional GraphvizFormatting::factorPos(const Vector2& min, size_t i) const { if (factorPositions.size() == 0) return boost::none; diff --git a/gtsam/nonlinear/GraphvizFormatting.h b/gtsam/nonlinear/GraphvizFormatting.h index c36b09a8fc..d71e73f318 100644 --- a/gtsam/nonlinear/GraphvizFormatting.h +++ b/gtsam/nonlinear/GraphvizFormatting.h @@ -33,10 +33,10 @@ struct GTSAM_EXPORT GraphvizFormatting : public DotWriter { /// World axes to be assigned to paper axes enum Axis { X, Y, Z, NEGX, NEGY, NEGZ }; - Axis paperHorizontalAxis; ///< The world axis assigned to the horizontal - ///< paper axis - Axis paperVerticalAxis; ///< The world axis assigned to the vertical paper - ///< axis + Axis paperHorizontalAxis; ///< The world axis assigned to the horizontal + ///< paper axis + Axis paperVerticalAxis; ///< The world axis assigned to the vertical paper + ///< axis double scale; ///< Scale all positions to reduce / increase density bool mergeSimilarFactors; ///< Merge multiple factors that have the same ///< connectivity @@ -55,8 +55,8 @@ struct GTSAM_EXPORT GraphvizFormatting : public DotWriter { // Find bounds Vector2 findBounds(const Values& values, const KeySet& keys) const; - /// Extract a Vector2 from either Vector2, Pose2, Pose3, or Point3 - boost::optional operator()(const Value& value) const; + /// Extract a Vector2 from either Vector2, Pose2, Pose3, or Point3 + boost::optional extractPosition(const Value& value) const; /// Return affinely transformed variable position if it exists. boost::optional variablePos(const Values& values, const Vector2& min, diff --git a/gtsam/nonlinear/NonlinearFactorGraph.cpp b/gtsam/nonlinear/NonlinearFactorGraph.cpp index da8935d5fc..c03caed754 100644 --- a/gtsam/nonlinear/NonlinearFactorGraph.cpp +++ b/gtsam/nonlinear/NonlinearFactorGraph.cpp @@ -102,7 +102,7 @@ bool NonlinearFactorGraph::equals(const NonlinearFactorGraph& other, double tol) void NonlinearFactorGraph::dot(std::ostream& os, const Values& values, const KeyFormatter& keyFormatter, const GraphvizFormatting& writer) const { - writer.writePreamble(&os); + writer.graphPreamble(&os); // Find bounds (imperative) KeySet keys = this->keys(); diff --git a/gtsam/nonlinear/NonlinearFactorGraph.h b/gtsam/nonlinear/NonlinearFactorGraph.h index ea8748f63b..6f083a3239 100644 --- a/gtsam/nonlinear/NonlinearFactorGraph.h +++ b/gtsam/nonlinear/NonlinearFactorGraph.h @@ -215,20 +215,19 @@ namespace gtsam { /// Output to graphviz format, stream version, with Values/extra options. void dot(std::ostream& os, const Values& values, const KeyFormatter& keyFormatter = DefaultKeyFormatter, - const GraphvizFormatting& graphvizFormatting = - GraphvizFormatting()) const; + const GraphvizFormatting& writer = GraphvizFormatting()) const; /// Output to graphviz format string, with Values/extra options. - std::string dot(const Values& values, - const KeyFormatter& keyFormatter = DefaultKeyFormatter, - const GraphvizFormatting& graphvizFormatting = - GraphvizFormatting()) const; + std::string dot( + const Values& values, + const KeyFormatter& keyFormatter = DefaultKeyFormatter, + const GraphvizFormatting& writer = GraphvizFormatting()) const; /// output to file with graphviz format, with Values/extra options. - void saveGraph(const std::string& filename, const Values& values, - const KeyFormatter& keyFormatter = DefaultKeyFormatter, - const GraphvizFormatting& graphvizFormatting = - GraphvizFormatting()) const; + void saveGraph( + const std::string& filename, const Values& values, + const KeyFormatter& keyFormatter = DefaultKeyFormatter, + const GraphvizFormatting& writer = GraphvizFormatting()) const; /// @} private: @@ -262,16 +261,16 @@ namespace gtsam { {return updateCholesky(values, dampen);} /** @deprecated */ - void GTSAM_DEPRECATED saveGraph( - std::ostream& os, const Values& values = Values(), - const GraphvizFormatting& graphvizFormatting = GraphvizFormatting(), - const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { + void GTSAM_DEPRECATED + saveGraph(std::ostream& os, const Values& values = Values(), + const GraphvizFormatting& writer = GraphvizFormatting(), + const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { dot(os, values, keyFormatter, graphvizFormatting); } /** @deprecated */ void GTSAM_DEPRECATED saveGraph(const std::string& filename, const Values& values, - const GraphvizFormatting& graphvizFormatting, + const GraphvizFormatting& writer, const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { saveGraph(filename, values, keyFormatter, graphvizFormatting); } From 733f3e5f8608742e2bf6fcffc595b5092b412583 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 23:19:38 -0500 Subject: [PATCH 84/91] Expose things in the wrapper --- gtsam/discrete/discrete.i | 12 ++++++--- gtsam/inference/inference.i | 2 ++ gtsam/linear/linear.i | 51 ++++++++++++++++++++----------------- gtsam/symbolic/symbolic.i | 1 + 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 80b8df1bc1..9db1cb7f63 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -102,6 +102,7 @@ virtual class DiscreteConditional : gtsam::DecisionTreeFactor { const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; bool equals(const gtsam::DiscreteConditional& other, double tol = 1e-9) const; + gtsam::Key firstFrontalKey() const; size_t nrFrontals() const; size_t nrParents() const; void printSignature( @@ -156,10 +157,13 @@ class DiscreteBayesNet { const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; bool equals(const gtsam::DiscreteBayesNet& other, double tol = 1e-9) const; - string dot(const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter) const; - void saveGraph(string s, const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter) const; + string dot( + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + void saveGraph( + string s, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; double operator()(const gtsam::DiscreteValues& values) const; gtsam::DiscreteValues sample() const; gtsam::DiscreteValues sample(gtsam::DiscreteValues given) const; diff --git a/gtsam/inference/inference.i b/gtsam/inference/inference.i index 5b9cef7efd..862c491787 100644 --- a/gtsam/inference/inference.i +++ b/gtsam/inference/inference.i @@ -127,6 +127,8 @@ class DotWriter { bool plotFactorPoints; bool connectKeysToFactor; bool binaryEdges; + + std::map variablePositions; }; #include diff --git a/gtsam/linear/linear.i b/gtsam/linear/linear.i index d2a86ddc8d..f17e95620c 100644 --- a/gtsam/linear/linear.i +++ b/gtsam/linear/linear.i @@ -443,36 +443,39 @@ class GaussianFactorGraph { #include virtual class GaussianConditional : gtsam::JacobianFactor { - //Constructors - GaussianConditional(size_t key, Vector d, Matrix R, const gtsam::noiseModel::Diagonal* sigmas); + // Constructors + GaussianConditional(size_t key, Vector d, Matrix R, + const gtsam::noiseModel::Diagonal* sigmas); GaussianConditional(size_t key, Vector d, Matrix R, size_t name1, Matrix S, - const gtsam::noiseModel::Diagonal* sigmas); + const gtsam::noiseModel::Diagonal* sigmas); GaussianConditional(size_t key, Vector d, Matrix R, size_t name1, Matrix S, - size_t name2, Matrix T, const gtsam::noiseModel::Diagonal* sigmas); + size_t name2, Matrix T, + const gtsam::noiseModel::Diagonal* sigmas); - //Constructors with no noise model + // Constructors with no noise model GaussianConditional(size_t key, Vector d, Matrix R); - GaussianConditional(size_t key, Vector d, Matrix R, size_t name1, Matrix S); - GaussianConditional(size_t key, Vector d, Matrix R, size_t name1, Matrix S, - size_t name2, Matrix T); + GaussianConditional(size_t key, Vector d, Matrix R, size_t name1, Matrix S); + GaussianConditional(size_t key, Vector d, Matrix R, size_t name1, Matrix S, + size_t name2, Matrix T); - //Standard Interface - void print(string s = "GaussianConditional", - const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter) const; - bool equals(const gtsam::GaussianConditional& cg, double tol) const; - - // Advanced Interface - gtsam::VectorValues solve(const gtsam::VectorValues& parents) const; - gtsam::VectorValues solveOtherRHS(const gtsam::VectorValues& parents, - const gtsam::VectorValues& rhs) const; - void solveTransposeInPlace(gtsam::VectorValues& gy) const; - Matrix R() const; - Matrix S() const; - Vector d() const; + // Standard Interface + void print(string s = "GaussianConditional", + const gtsam::KeyFormatter& keyFormatter = + gtsam::DefaultKeyFormatter) const; + bool equals(const gtsam::GaussianConditional& cg, double tol) const; + gtsam::Key firstFrontalKey() const; + + // Advanced Interface + gtsam::VectorValues solve(const gtsam::VectorValues& parents) const; + gtsam::VectorValues solveOtherRHS(const gtsam::VectorValues& parents, + const gtsam::VectorValues& rhs) const; + void solveTransposeInPlace(gtsam::VectorValues& gy) const; + Matrix R() const; + Matrix S() const; + Vector d() const; - // enabling serialization functionality - void serialize() const; + // enabling serialization functionality + void serialize() const; }; #include diff --git a/gtsam/symbolic/symbolic.i b/gtsam/symbolic/symbolic.i index 771e5309ad..0297b6c4ac 100644 --- a/gtsam/symbolic/symbolic.i +++ b/gtsam/symbolic/symbolic.i @@ -98,6 +98,7 @@ virtual class SymbolicConditional : gtsam::SymbolicFactor { bool equals(const gtsam::SymbolicConditional& other, double tol) const; // Standard interface + gtsam::Key firstFrontalKey() const; size_t nrFrontals() const; size_t nrParents() const; }; From 7ccee875fe17b051ca0648d7aaa2f3324851e0be Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Wed, 26 Jan 2022 23:25:18 -0500 Subject: [PATCH 85/91] fix unit test --- gtsam/discrete/tests/testDiscreteBayesNet.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/gtsam/discrete/tests/testDiscreteBayesNet.cpp b/gtsam/discrete/tests/testDiscreteBayesNet.cpp index 42aeb6092a..cfc9c1bb50 100644 --- a/gtsam/discrete/tests/testDiscreteBayesNet.cpp +++ b/gtsam/discrete/tests/testDiscreteBayesNet.cpp @@ -152,11 +152,19 @@ TEST(DiscreteBayesNet, Dot) { string actual = fragment.dot(); cout << actual << endl; EXPECT(actual == - "digraph G{\n" - "3->5\n" - "6->5\n" - "4->6\n" - "0->3\n" + "digraph {\n" + " size=\"5,5\";\n" + "\n" + " var0[label=\"0\"];\n" + " var3[label=\"3\"];\n" + " var4[label=\"4\"];\n" + " var5[label=\"5\"];\n" + " var6[label=\"6\"];\n" + "\n" + " var3->var5\n" + " var6->var5\n" + " var4->var6\n" + " var0->var3\n" "}"); } From 87eeb0d27e7526d564900093ae4bc6a2c68cd1ce Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 27 Jan 2022 12:31:25 -0500 Subject: [PATCH 86/91] Added position hints --- gtsam/inference/DotWriter.cpp | 27 ++++++++-- gtsam/inference/DotWriter.h | 20 +++++--- gtsam/inference/inference.i | 1 + gtsam/linear/tests/testGaussianBayesNet.cpp | 1 - gtsam/nonlinear/NonlinearFactorGraph.h | 39 +++++++++++---- gtsam/symbolic/tests/testSymbolicBayesNet.cpp | 50 ++++++++++++++----- 6 files changed, 102 insertions(+), 36 deletions(-) diff --git a/gtsam/inference/DotWriter.cpp b/gtsam/inference/DotWriter.cpp index a45482efbb..9220bd1686 100644 --- a/gtsam/inference/DotWriter.cpp +++ b/gtsam/inference/DotWriter.cpp @@ -16,9 +16,11 @@ * @date December, 2021 */ -#include #include +#include +#include + #include using namespace std; @@ -59,18 +61,35 @@ void DotWriter::DrawFactor(size_t i, const boost::optional& position, } static void ConnectVariables(Key key1, Key key2, - const KeyFormatter& keyFormatter, - ostream* os) { + const KeyFormatter& keyFormatter, ostream* os) { *os << " var" << keyFormatter(key1) << "--" << "var" << keyFormatter(key2) << ";\n"; } static void ConnectVariableFactor(Key key, const KeyFormatter& keyFormatter, - size_t i, ostream* os) { + size_t i, ostream* os) { *os << " var" << keyFormatter(key) << "--" << "factor" << i << ";\n"; } +/// Return variable position or none +boost::optional DotWriter::variablePos(Key key) const { + boost::optional result = boost::none; + + // Check position hint + Symbol symbol(key); + auto hint = positionHints.find(symbol.chr()); + if (hint != positionHints.end()) + result.reset(Vector2(symbol.index(), hint->second)); + + // Override with explicit position, if given. + auto pos = variablePositions.find(key); + if (pos != variablePositions.end()) + result.reset(pos->second); + + return result; +} + void DotWriter::processFactor(size_t i, const KeyVector& keys, const KeyFormatter& keyFormatter, const boost::optional& position, diff --git a/gtsam/inference/DotWriter.h b/gtsam/inference/DotWriter.h index ad420b1817..a00cf4b071 100644 --- a/gtsam/inference/DotWriter.h +++ b/gtsam/inference/DotWriter.h @@ -39,9 +39,19 @@ struct GTSAM_EXPORT DotWriter { ///< the dot of the factor bool binaryEdges; ///< just use non-dotted edges for binary factors - /// (optional for each variable) Manually specify variable node positions + /** + * Variable positions can be optionally specified and will be included in the + * dor file with a "!' sign, so "neato" can use it to render them. + */ std::map variablePositions; + /** + * The position hints allow one to use symbol character and index to specify + * position. Unless variable positions are specified, if a hint is present for + * a given symbol, it will be used to calculate the positions as (index,hint). + */ + std::map positionHints; + explicit DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, bool plotFactorPoints = true, @@ -68,13 +78,7 @@ struct GTSAM_EXPORT DotWriter { std::ostream* os); /// Return variable position or none - boost::optional variablePos(Key key) const { - auto it = variablePositions.find(key); - if (it == variablePositions.end()) - return boost::none; - else - return it->second; - } + boost::optional variablePos(Key key) const; /// Draw a single factor, specified by its index i and its variable keys. void processFactor(size_t i, const KeyVector& keys, diff --git a/gtsam/inference/inference.i b/gtsam/inference/inference.i index 862c491787..30f51ea234 100644 --- a/gtsam/inference/inference.i +++ b/gtsam/inference/inference.i @@ -129,6 +129,7 @@ class DotWriter { bool binaryEdges; std::map variablePositions; + std::map positionHints; }; #include diff --git a/gtsam/linear/tests/testGaussianBayesNet.cpp b/gtsam/linear/tests/testGaussianBayesNet.cpp index 11fc7e7f7f..f62da15dde 100644 --- a/gtsam/linear/tests/testGaussianBayesNet.cpp +++ b/gtsam/linear/tests/testGaussianBayesNet.cpp @@ -312,7 +312,6 @@ TEST(GaussianBayesNet, Dot) { EXPECT(assert_equal(Vector2(10, 20), *position, 1e-5)); string actual = noisyBayesNet.dot(DefaultKeyFormatter, writer); - noisyBayesNet.saveGraph("noisyBayesNet.dot", DefaultKeyFormatter, writer); EXPECT(actual == "digraph {\n" " size=\"5,5\";\n" diff --git a/gtsam/nonlinear/NonlinearFactorGraph.h b/gtsam/nonlinear/NonlinearFactorGraph.h index 6f083a3239..3237d7c1e0 100644 --- a/gtsam/nonlinear/NonlinearFactorGraph.h +++ b/gtsam/nonlinear/NonlinearFactorGraph.h @@ -43,12 +43,14 @@ namespace gtsam { class ExpressionFactor; /** - * A non-linear factor graph is a graph of non-Gaussian, i.e. non-linear factors, - * which derive from NonlinearFactor. The values structures are typically (in SAM) more general - * than just vectors, e.g., Rot3 or Pose3, which are objects in non-linear manifolds. - * Linearizing the non-linear factor graph creates a linear factor graph on the - * tangent vector space at the linearization point. Because the tangent space is a true - * vector space, the config type will be an VectorValues in that linearized factor graph. + * A NonlinearFactorGraph is a graph of non-Gaussian, i.e. non-linear factors, + * which derive from NonlinearFactor. The values structures are typically (in + * SAM) more general than just vectors, e.g., Rot3 or Pose3, which are objects + * in non-linear manifolds. Linearizing the non-linear factor graph creates a + * linear factor graph on the tangent vector space at the linearization point. + * Because the tangent space is a true vector space, the config type will be + * an VectorValues in that linearized factor graph. + * @addtogroup nonlinear */ class GTSAM_EXPORT NonlinearFactorGraph: public FactorGraph { @@ -58,6 +60,9 @@ namespace gtsam { typedef NonlinearFactorGraph This; typedef boost::shared_ptr shared_ptr; + /// @name Standard Constructors + /// @{ + /** Default constructor */ NonlinearFactorGraph() {} @@ -76,6 +81,10 @@ namespace gtsam { /// Destructor virtual ~NonlinearFactorGraph() {} + /// @} + /// @name Testable + /// @{ + /** print */ void print( const std::string& str = "NonlinearFactorGraph: ", @@ -90,6 +99,10 @@ namespace gtsam { /** Test equality */ bool equals(const NonlinearFactorGraph& other, double tol = 1e-9) const; + /// @} + /// @name Standard Interface + /// @{ + /** unnormalized error, \f$ \sum_i 0.5 (h_i(X_i)-z)^2 / \sigma^2 \f$ in the most common case */ double error(const Values& values) const; @@ -206,6 +219,7 @@ namespace gtsam { emplace_shared>(key, prior, covariance); } + /// @} /// @name Graph Display /// @{ @@ -250,6 +264,8 @@ namespace gtsam { public: #ifdef GTSAM_ALLOW_DEPRECATED_SINCE_V42 + /// @name Deprecated + /// @{ /** @deprecated */ boost::shared_ptr GTSAM_DEPRECATED linearizeToHessianFactor( const Values& values, boost::none_t, const Dampen& dampen = nullptr) const @@ -261,19 +277,20 @@ namespace gtsam { {return updateCholesky(values, dampen);} /** @deprecated */ - void GTSAM_DEPRECATED - saveGraph(std::ostream& os, const Values& values = Values(), - const GraphvizFormatting& writer = GraphvizFormatting(), - const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { + void GTSAM_DEPRECATED saveGraph( + std::ostream& os, const Values& values = Values(), + const GraphvizFormatting& graphvizFormatting = GraphvizFormatting(), + const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { dot(os, values, keyFormatter, graphvizFormatting); } /** @deprecated */ void GTSAM_DEPRECATED saveGraph(const std::string& filename, const Values& values, - const GraphvizFormatting& writer, + const GraphvizFormatting& graphvizFormatting, const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { saveGraph(filename, values, keyFormatter, graphvizFormatting); } + /// @} #endif }; diff --git a/gtsam/symbolic/tests/testSymbolicBayesNet.cpp b/gtsam/symbolic/tests/testSymbolicBayesNet.cpp index a92d66f686..f9cd07d236 100644 --- a/gtsam/symbolic/tests/testSymbolicBayesNet.cpp +++ b/gtsam/symbolic/tests/testSymbolicBayesNet.cpp @@ -15,13 +15,16 @@ * @author Frank Dellaert */ -#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include using namespace std; using namespace gtsam; @@ -30,7 +33,6 @@ static const Key _L_ = 0; static const Key _A_ = 1; static const Key _B_ = 2; static const Key _C_ = 3; -static const Key _D_ = 4; static SymbolicConditional::shared_ptr B(new SymbolicConditional(_B_)), @@ -78,14 +80,38 @@ TEST( SymbolicBayesNet, combine ) } /* ************************************************************************* */ -TEST(SymbolicBayesNet, saveGraph) { +TEST(SymbolicBayesNet, Dot) { + using symbol_shorthand::A; + using symbol_shorthand::X; SymbolicBayesNet bn; - bn += SymbolicConditional(_A_, _B_); - KeyVector keys {_B_, _C_, _D_}; - bn += SymbolicConditional::FromKeys(keys,2); - bn += SymbolicConditional(_D_); - - bn.saveGraph("SymbolicBayesNet.dot"); + bn += SymbolicConditional(X(3), X(2), A(2)); + bn += SymbolicConditional(X(2), X(1), A(1)); + bn += SymbolicConditional(X(1)); + + DotWriter writer; + writer.positionHints.emplace('a', 2); + writer.positionHints.emplace('x', 1); + + auto position = writer.variablePos(A(1)); + CHECK(position); + EXPECT(assert_equal(Vector2(1, 2), *position, 1e-5)); + + string actual = bn.dot(DefaultKeyFormatter, writer); + EXPECT(actual == + "digraph {\n" + " size=\"5,5\";\n" + "\n" + " vara1[label=\"a1\", pos=\"1,2!\"];\n" + " vara2[label=\"a2\", pos=\"2,2!\"];\n" + " varx1[label=\"x1\", pos=\"1,1!\"];\n" + " varx2[label=\"x2\", pos=\"2,1!\"];\n" + " varx3[label=\"x3\", pos=\"3,1!\"];\n" + "\n" + " varx1->varx2\n" + " vara1->varx2\n" + " varx2->varx3\n" + " vara2->varx3\n" + "}"); } /* ************************************************************************* */ From c0f6cd247b61cb6dc2c8971e041ab3a7374b61eb Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 27 Jan 2022 12:53:27 -0500 Subject: [PATCH 87/91] allow for boxes! --- gtsam/inference/BayesNet-inst.h | 2 +- gtsam/inference/DotWriter.cpp | 7 +++++-- gtsam/inference/DotWriter.h | 12 ++++++++---- gtsam/inference/FactorGraph-inst.h | 2 +- gtsam/inference/inference.i | 1 + gtsam/nonlinear/NonlinearFactorGraph.cpp | 2 +- gtsam/symbolic/tests/testSymbolicBayesNet.cpp | 7 +++++-- 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/gtsam/inference/BayesNet-inst.h b/gtsam/inference/BayesNet-inst.h index bd90f4e4b3..c201475c5d 100644 --- a/gtsam/inference/BayesNet-inst.h +++ b/gtsam/inference/BayesNet-inst.h @@ -44,7 +44,7 @@ void BayesNet::dot(std::ostream& os, // Create nodes for each variable in the graph for (Key key : this->keys()) { auto position = writer.variablePos(key); - writer.DrawVariable(key, keyFormatter, position, &os); + writer.drawVariable(key, keyFormatter, position, &os); } os << "\n"; diff --git a/gtsam/inference/DotWriter.cpp b/gtsam/inference/DotWriter.cpp index 9220bd1686..a6a33bc74d 100644 --- a/gtsam/inference/DotWriter.cpp +++ b/gtsam/inference/DotWriter.cpp @@ -39,15 +39,18 @@ void DotWriter::digraphPreamble(ostream* os) const { << "\";\n\n"; } -void DotWriter::DrawVariable(Key key, const KeyFormatter& keyFormatter, +void DotWriter::drawVariable(Key key, const KeyFormatter& keyFormatter, const boost::optional& position, - ostream* os) { + ostream* os) const { // Label the node with the label from the KeyFormatter *os << " var" << keyFormatter(key) << "[label=\"" << keyFormatter(key) << "\""; if (position) { *os << ", pos=\"" << position->x() << "," << position->y() << "!\""; } + if (boxes.count(key)) { + *os << ", shape=box"; + } *os << "];\n"; } diff --git a/gtsam/inference/DotWriter.h b/gtsam/inference/DotWriter.h index a00cf4b071..13683e338c 100644 --- a/gtsam/inference/DotWriter.h +++ b/gtsam/inference/DotWriter.h @@ -24,6 +24,7 @@ #include #include +#include namespace gtsam { @@ -43,7 +44,7 @@ struct GTSAM_EXPORT DotWriter { * Variable positions can be optionally specified and will be included in the * dor file with a "!' sign, so "neato" can use it to render them. */ - std::map variablePositions; + std::map variablePositions; /** * The position hints allow one to use symbol character and index to specify @@ -52,6 +53,9 @@ struct GTSAM_EXPORT DotWriter { */ std::map positionHints; + /** A set of keys that will be displayed as a box */ + std::set boxes; + explicit DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, bool plotFactorPoints = true, @@ -69,9 +73,9 @@ struct GTSAM_EXPORT DotWriter { void digraphPreamble(std::ostream* os) const; /// Create a variable dot fragment. - static void DrawVariable(Key key, const KeyFormatter& keyFormatter, - const boost::optional& position, - std::ostream* os); + void drawVariable(Key key, const KeyFormatter& keyFormatter, + const boost::optional& position, + std::ostream* os) const; /// Create factor dot. static void DrawFactor(size_t i, const boost::optional& position, diff --git a/gtsam/inference/FactorGraph-inst.h b/gtsam/inference/FactorGraph-inst.h index 2034fdcb67..3d85be49ec 100644 --- a/gtsam/inference/FactorGraph-inst.h +++ b/gtsam/inference/FactorGraph-inst.h @@ -135,7 +135,7 @@ void FactorGraph::dot(std::ostream& os, // Create nodes for each variable in the graph for (Key key : keys()) { - writer.DrawVariable(key, keyFormatter, boost::none, &os); + writer.drawVariable(key, keyFormatter, boost::none, &os); } os << "\n"; diff --git a/gtsam/inference/inference.i b/gtsam/inference/inference.i index 30f51ea234..9dd5b98126 100644 --- a/gtsam/inference/inference.i +++ b/gtsam/inference/inference.i @@ -130,6 +130,7 @@ class DotWriter { std::map variablePositions; std::map positionHints; + std::set boxes; }; #include diff --git a/gtsam/nonlinear/NonlinearFactorGraph.cpp b/gtsam/nonlinear/NonlinearFactorGraph.cpp index c03caed754..dfa54f26f0 100644 --- a/gtsam/nonlinear/NonlinearFactorGraph.cpp +++ b/gtsam/nonlinear/NonlinearFactorGraph.cpp @@ -111,7 +111,7 @@ void NonlinearFactorGraph::dot(std::ostream& os, const Values& values, // Create nodes for each variable in the graph for (Key key : keys) { auto position = writer.variablePos(values, min, key); - writer.DrawVariable(key, keyFormatter, position, &os); + writer.drawVariable(key, keyFormatter, position, &os); } os << "\n"; diff --git a/gtsam/symbolic/tests/testSymbolicBayesNet.cpp b/gtsam/symbolic/tests/testSymbolicBayesNet.cpp index f9cd07d236..2e13be10eb 100644 --- a/gtsam/symbolic/tests/testSymbolicBayesNet.cpp +++ b/gtsam/symbolic/tests/testSymbolicBayesNet.cpp @@ -91,18 +91,21 @@ TEST(SymbolicBayesNet, Dot) { DotWriter writer; writer.positionHints.emplace('a', 2); writer.positionHints.emplace('x', 1); + writer.boxes.emplace(A(1)); + writer.boxes.emplace(A(2)); auto position = writer.variablePos(A(1)); CHECK(position); EXPECT(assert_equal(Vector2(1, 2), *position, 1e-5)); string actual = bn.dot(DefaultKeyFormatter, writer); + bn.saveGraph("bn.dot", DefaultKeyFormatter, writer); EXPECT(actual == "digraph {\n" " size=\"5,5\";\n" "\n" - " vara1[label=\"a1\", pos=\"1,2!\"];\n" - " vara2[label=\"a2\", pos=\"2,2!\"];\n" + " vara1[label=\"a1\", pos=\"1,2!\", shape=box];\n" + " vara2[label=\"a2\", pos=\"2,2!\", shape=box];\n" " varx1[label=\"x1\", pos=\"1,1!\"];\n" " varx2[label=\"x2\", pos=\"2,1!\"];\n" " varx3[label=\"x3\", pos=\"3,1!\"];\n" From 828abe2fc065d7a9561e41d1e0889cc778a5f13e Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 27 Jan 2022 14:28:32 -0500 Subject: [PATCH 88/91] Add dot in all wrappers --- gtsam/discrete/discrete.i | 23 ++++++++++++----------- gtsam/inference/inference.i | 1 + gtsam/linear/linear.i | 16 ++++++++++++++++ gtsam/nonlinear/nonlinear.i | 15 +++++++-------- gtsam/symbolic/symbolic.i | 16 ++++++++++++++++ 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/gtsam/discrete/discrete.i b/gtsam/discrete/discrete.i index 9db1cb7f63..56e7248a3f 100644 --- a/gtsam/discrete/discrete.i +++ b/gtsam/discrete/discrete.i @@ -157,6 +157,10 @@ class DiscreteBayesNet { const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; bool equals(const gtsam::DiscreteBayesNet& other, double tol = 1e-9) const; + double operator()(const gtsam::DiscreteValues& values) const; + gtsam::DiscreteValues sample() const; + gtsam::DiscreteValues sample(gtsam::DiscreteValues given) const; + string dot( const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, const gtsam::DotWriter& writer = gtsam::DotWriter()) const; @@ -164,9 +168,6 @@ class DiscreteBayesNet { string s, const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, const gtsam::DotWriter& writer = gtsam::DotWriter()) const; - double operator()(const gtsam::DiscreteValues& values) const; - gtsam::DiscreteValues sample() const; - gtsam::DiscreteValues sample(gtsam::DiscreteValues given) const; string markdown(const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; string markdown(const gtsam::KeyFormatter& keyFormatter, @@ -256,14 +257,6 @@ class DiscreteFactorGraph { void print(string s = "") const; bool equals(const gtsam::DiscreteFactorGraph& fg, double tol = 1e-9) const; - string dot( - const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, - const gtsam::DotWriter& dotWriter = gtsam::DotWriter()) const; - void saveGraph( - string s, - const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, - const gtsam::DotWriter& dotWriter = gtsam::DotWriter()) const; - gtsam::DecisionTreeFactor product() const; double operator()(const gtsam::DiscreteValues& values) const; gtsam::DiscreteValues optimize() const; @@ -285,6 +278,14 @@ class DiscreteFactorGraph { std::pair eliminatePartialMultifrontal(const gtsam::Ordering& ordering); + string dot( + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + void saveGraph( + string s, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + string markdown(const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter) const; string markdown(const gtsam::KeyFormatter& keyFormatter, diff --git a/gtsam/inference/inference.i b/gtsam/inference/inference.i index 9dd5b98126..5a661d5cf2 100644 --- a/gtsam/inference/inference.i +++ b/gtsam/inference/inference.i @@ -131,6 +131,7 @@ class DotWriter { std::map variablePositions; std::map positionHints; std::set boxes; + std::map factorPositions; }; #include diff --git a/gtsam/linear/linear.i b/gtsam/linear/linear.i index f17e95620c..b079c3dd18 100644 --- a/gtsam/linear/linear.i +++ b/gtsam/linear/linear.i @@ -437,6 +437,14 @@ class GaussianFactorGraph { pair hessian() const; pair hessian(const gtsam::Ordering& ordering) const; + string dot( + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + void saveGraph( + string s, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + // enabling serialization functionality void serialize() const; }; @@ -527,6 +535,14 @@ virtual class GaussianBayesNet { double logDeterminant() const; gtsam::VectorValues backSubstitute(const gtsam::VectorValues& gx) const; gtsam::VectorValues backSubstituteTranspose(const gtsam::VectorValues& gx) const; + + string dot( + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + void saveGraph( + string s, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; }; #include diff --git a/gtsam/nonlinear/nonlinear.i b/gtsam/nonlinear/nonlinear.i index 159261713a..055fbd75b6 100644 --- a/gtsam/nonlinear/nonlinear.i +++ b/gtsam/nonlinear/nonlinear.i @@ -95,18 +95,17 @@ class NonlinearFactorGraph { gtsam::GaussianFactorGraph* linearize(const gtsam::Values& values) const; gtsam::NonlinearFactorGraph clone() const; - // enabling serialization functionality - void serialize() const; - string dot( const gtsam::Values& values, const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, const GraphvizFormatting& writer = GraphvizFormatting()); - void saveGraph(const string& s, const gtsam::Values& values, - const gtsam::KeyFormatter& keyFormatter = - gtsam::DefaultKeyFormatter, - const GraphvizFormatting& writer = - GraphvizFormatting()) const; + void saveGraph( + const string& s, const gtsam::Values& values, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const GraphvizFormatting& writer = GraphvizFormatting()) const; + + // enabling serialization functionality + void serialize() const; }; #include diff --git a/gtsam/symbolic/symbolic.i b/gtsam/symbolic/symbolic.i index 0297b6c4ac..1f1d4b48f9 100644 --- a/gtsam/symbolic/symbolic.i +++ b/gtsam/symbolic/symbolic.i @@ -77,6 +77,14 @@ virtual class SymbolicFactorGraph { const gtsam::KeyVector& key_vector, const gtsam::Ordering& marginalizedVariableOrdering); gtsam::SymbolicFactorGraph* marginal(const gtsam::KeyVector& key_vector); + + string dot( + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + void saveGraph( + string s, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; }; #include @@ -121,6 +129,14 @@ class SymbolicBayesNet { gtsam::SymbolicConditional* back() const; void push_back(gtsam::SymbolicConditional* conditional); void push_back(const gtsam::SymbolicBayesNet& bayesNet); + + string dot( + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; + void saveGraph( + string s, + const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, + const gtsam::DotWriter& writer = gtsam::DotWriter()) const; }; #include From 34e92995e7bc4d08099832374a7e66951435ae8e Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 27 Jan 2022 14:34:24 -0500 Subject: [PATCH 89/91] Distinguish writer from formatting --- gtsam/nonlinear/nonlinear.i | 4 ++-- python/gtsam/tests/test_GraphvizFormatting.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gtsam/nonlinear/nonlinear.i b/gtsam/nonlinear/nonlinear.i index 055fbd75b6..eedf421bc7 100644 --- a/gtsam/nonlinear/nonlinear.i +++ b/gtsam/nonlinear/nonlinear.i @@ -98,11 +98,11 @@ class NonlinearFactorGraph { string dot( const gtsam::Values& values, const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, - const GraphvizFormatting& writer = GraphvizFormatting()); + const GraphvizFormatting& formatting = GraphvizFormatting()); void saveGraph( const string& s, const gtsam::Values& values, const gtsam::KeyFormatter& keyFormatter = gtsam::DefaultKeyFormatter, - const GraphvizFormatting& writer = GraphvizFormatting()) const; + const GraphvizFormatting& formatting = GraphvizFormatting()) const; // enabling serialization functionality void serialize() const; diff --git a/python/gtsam/tests/test_GraphvizFormatting.py b/python/gtsam/tests/test_GraphvizFormatting.py index ecdc23b450..5962366efa 100644 --- a/python/gtsam/tests/test_GraphvizFormatting.py +++ b/python/gtsam/tests/test_GraphvizFormatting.py @@ -78,7 +78,7 @@ def test_swapped_axes(self): graphviz_formatting.paperHorizontalAxis = gtsam.GraphvizFormatting.Axis.X graphviz_formatting.paperVerticalAxis = gtsam.GraphvizFormatting.Axis.Y self.assertEqual(self.graph.dot(self.values, - writer=graphviz_formatting), + formatting=graphviz_formatting), textwrap.dedent(expected_result)) def test_factor_points(self): @@ -100,7 +100,7 @@ def test_factor_points(self): graphviz_formatting.plotFactorPoints = False self.assertEqual(self.graph.dot(self.values, - writer=graphviz_formatting), + formatting=graphviz_formatting), textwrap.dedent(expected_result)) def test_width_height(self): @@ -127,7 +127,7 @@ def test_width_height(self): graphviz_formatting.figureHeightInches = 10 self.assertEqual(self.graph.dot(self.values, - writer=graphviz_formatting), + formatting=graphviz_formatting), textwrap.dedent(expected_result)) From 21232252806a947c5ac2d284d778b318449dd301 Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 27 Jan 2022 14:34:38 -0500 Subject: [PATCH 90/91] allow factorPositions --- gtsam/inference/BayesNet-inst.h | 1 + gtsam/inference/DotWriter.cpp | 5 ++++- gtsam/inference/DotWriter.h | 8 +++++++- gtsam/inference/FactorGraph-inst.h | 3 ++- gtsam/nonlinear/GraphvizFormatting.cpp | 2 +- gtsam/nonlinear/GraphvizFormatting.h | 3 --- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/gtsam/inference/BayesNet-inst.h b/gtsam/inference/BayesNet-inst.h index c201475c5d..afde5498dc 100644 --- a/gtsam/inference/BayesNet-inst.h +++ b/gtsam/inference/BayesNet-inst.h @@ -48,6 +48,7 @@ void BayesNet::dot(std::ostream& os, } os << "\n"; + // Reverse order as typically Bayes nets stored in reverse topological sort. for (auto conditional : boost::adaptors::reverse(*this)) { auto frontals = conditional->frontals(); const Key me = frontals.front(); diff --git a/gtsam/inference/DotWriter.cpp b/gtsam/inference/DotWriter.cpp index a6a33bc74d..ad53305757 100644 --- a/gtsam/inference/DotWriter.cpp +++ b/gtsam/inference/DotWriter.cpp @@ -102,7 +102,10 @@ void DotWriter::processFactor(size_t i, const KeyVector& keys, ConnectVariables(keys[0], keys[1], keyFormatter, os); } else { // Create dot for the factor. - DrawFactor(i, position, os); + if (!position && factorPositions.count(i)) + DrawFactor(i, factorPositions.at(i), os); + else + DrawFactor(i, position, os); // Make factor-variable connections if (connectKeysToFactor) { diff --git a/gtsam/inference/DotWriter.h b/gtsam/inference/DotWriter.h index 13683e338c..23302ee60e 100644 --- a/gtsam/inference/DotWriter.h +++ b/gtsam/inference/DotWriter.h @@ -42,7 +42,7 @@ struct GTSAM_EXPORT DotWriter { /** * Variable positions can be optionally specified and will be included in the - * dor file with a "!' sign, so "neato" can use it to render them. + * dot file with a "!' sign, so "neato" can use it to render them. */ std::map variablePositions; @@ -56,6 +56,12 @@ struct GTSAM_EXPORT DotWriter { /** A set of keys that will be displayed as a box */ std::set boxes; + /** + * Factor positions can be optionally specified and will be included in the + * dot file with a "!' sign, so "neato" can use it to render them. + */ + std::map factorPositions; + explicit DotWriter(double figureWidthInches = 5, double figureHeightInches = 5, bool plotFactorPoints = true, diff --git a/gtsam/inference/FactorGraph-inst.h b/gtsam/inference/FactorGraph-inst.h index 3d85be49ec..a2ae071016 100644 --- a/gtsam/inference/FactorGraph-inst.h +++ b/gtsam/inference/FactorGraph-inst.h @@ -135,7 +135,8 @@ void FactorGraph::dot(std::ostream& os, // Create nodes for each variable in the graph for (Key key : keys()) { - writer.drawVariable(key, keyFormatter, boost::none, &os); + auto position = writer.variablePos(key); + writer.drawVariable(key, keyFormatter, position, &os); } os << "\n"; diff --git a/gtsam/nonlinear/GraphvizFormatting.cpp b/gtsam/nonlinear/GraphvizFormatting.cpp index 1f0b3a8758..ca3466b6a1 100644 --- a/gtsam/nonlinear/GraphvizFormatting.cpp +++ b/gtsam/nonlinear/GraphvizFormatting.cpp @@ -124,7 +124,7 @@ boost::optional GraphvizFormatting::extractPosition( boost::optional GraphvizFormatting::variablePos(const Values& values, const Vector2& min, Key key) const { - if (!values.exists(key)) return boost::none; + if (!values.exists(key)) return DotWriter::variablePos(key); boost::optional xy = extractPosition(values.at(key)); if (xy) { xy->x() = scale * (xy->x() - min.x()); diff --git a/gtsam/nonlinear/GraphvizFormatting.h b/gtsam/nonlinear/GraphvizFormatting.h index d71e73f318..03cdb34694 100644 --- a/gtsam/nonlinear/GraphvizFormatting.h +++ b/gtsam/nonlinear/GraphvizFormatting.h @@ -41,9 +41,6 @@ struct GTSAM_EXPORT GraphvizFormatting : public DotWriter { bool mergeSimilarFactors; ///< Merge multiple factors that have the same ///< connectivity - /// (optional for each factor) Manually specify factor "dot" positions: - std::map factorPositions; - /// Default constructor sets up robot coordinates. Paper horizontal is robot /// Y, paper vertical is robot X. Default figure size of 5x5 in. GraphvizFormatting() From 48488a9b4f080ca404525e82abfa776d18c400ec Mon Sep 17 00:00:00 2001 From: Frank Dellaert Date: Thu, 27 Jan 2022 15:58:43 -0500 Subject: [PATCH 91/91] Bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c37099a45..a79e812efb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ endif() set (GTSAM_VERSION_MAJOR 4) set (GTSAM_VERSION_MINOR 2) set (GTSAM_VERSION_PATCH 0) -set (GTSAM_PRERELEASE_VERSION "a3") +set (GTSAM_PRERELEASE_VERSION "a4") math (EXPR GTSAM_VERSION_NUMERIC "10000 * ${GTSAM_VERSION_MAJOR} + 100 * ${GTSAM_VERSION_MINOR} + ${GTSAM_VERSION_PATCH}") if (${GTSAM_VERSION_PATCH} EQUAL 0)