Skip to content

Commit 29cb32c

Browse files
authored
Merge pull request #2327 from borglab/feature/multifrontalSolver
MultifrontalSolver
2 parents 7e7b7fa + 19bf514 commit 29cb32c

File tree

9 files changed

+1135
-2
lines changed

9 files changed

+1135
-2
lines changed

gtsam/base/SymmetricBlockMatrix.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ namespace gtsam {
133133
/// This method makes a copy - use the methods below if performance is critical.
134134
Matrix block(DenseIndex I, DenseIndex J) const;
135135

136+
/// Get a block view (anywhere in the matrix) without allocating a copy.
137+
/// This method returns a const reference to the block for efficient read access.
138+
/// @param I The row block index.
139+
/// @param J The column block index.
140+
/// @return A const block view into the matrix.
141+
constBlock blockView(DenseIndex I, DenseIndex J) const {
142+
return block_(I, J);
143+
}
144+
136145
/// Return the J'th diagonal block as a self adjoint view.
137146
Eigen::SelfAdjointView<Block, Eigen::Upper> diagonalBlock(DenseIndex J) {
138147
return block_(J, J).selfadjointView<Eigen::Upper>();
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/* ----------------------------------------------------------------------------
2+
3+
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
4+
* Atlanta, Georgia 30332-0415
5+
* All Rights Reserved
6+
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
7+
8+
* See LICENSE for the license information
9+
10+
* -------------------------------------------------------------------------- */
11+
12+
/**
13+
* @file MultifrontalClique.cpp
14+
* @brief Implementation of imperative multifrontal clique data structure.
15+
* @author Frank Dellaert
16+
* @date December 2025
17+
*/
18+
19+
#include <gtsam/linear/JacobianFactor.h>
20+
#include <gtsam/linear/MultifrontalClique.h>
21+
22+
#include <algorithm>
23+
#include <cassert>
24+
#include <iostream>
25+
#include <set>
26+
#include <stdexcept>
27+
28+
namespace gtsam {
29+
30+
namespace {
31+
32+
// Build a stacked separator vector x_sep in the provided scratch buffer.
33+
Vector& buildSeparatorVector(const std::vector<const Vector*>& separatorPtrs,
34+
Vector* scratch) {
35+
size_t separatorDim = 0;
36+
for (const Vector* values : separatorPtrs) {
37+
separatorDim += values->size();
38+
}
39+
if (static_cast<size_t>(scratch->size()) != separatorDim) {
40+
scratch->resize(separatorDim);
41+
}
42+
size_t offset = 0;
43+
for (const Vector* values : separatorPtrs) {
44+
scratch->segment(offset, values->size()) = *values;
45+
offset += values->size();
46+
}
47+
return *scratch;
48+
}
49+
50+
} // namespace
51+
52+
MultifrontalClique::MultifrontalClique(
53+
const SymbolicJunctionTree::sharedNode& cluster) {
54+
if (!cluster) {
55+
throw std::runtime_error("MultifrontalSolver: null cluster.");
56+
}
57+
cluster_ = cluster;
58+
const auto& frontals = cluster_->orderedFrontalKeys;
59+
if (frontals.empty()) {
60+
throw std::runtime_error(
61+
"MultifrontalSolver: cluster has no frontal keys.");
62+
}
63+
key_ = frontals.front();
64+
}
65+
66+
void MultifrontalClique::setParent(
67+
const std::weak_ptr<MultifrontalClique>& parent) {
68+
parent_ = parent;
69+
}
70+
71+
void MultifrontalClique::addChild(const shared_ptr& child) {
72+
children_.push_back(child);
73+
}
74+
75+
const KeyVector& MultifrontalClique::frontals() const {
76+
return cluster_->orderedFrontalKeys;
77+
}
78+
79+
const std::vector<MultifrontalClique::shared_ptr>&
80+
MultifrontalClique::children() const {
81+
return children_;
82+
}
83+
84+
const std::weak_ptr<MultifrontalClique>& MultifrontalClique::parent() const {
85+
return parent_;
86+
}
87+
88+
void MultifrontalClique::assignParentIndicesForChildren() {
89+
for (const auto& child : children_) {
90+
if (!child) continue;
91+
child->setParentIndices(child->parentIndicesFor(*this));
92+
}
93+
}
94+
95+
Key MultifrontalClique::key() const { return key_; }
96+
97+
size_t MultifrontalClique::factorCount() const {
98+
return cluster_->factors.size();
99+
}
100+
101+
void MultifrontalClique::calculateSeparatorKeys() {
102+
// Separator keys are computed from local factor keys and child separators.
103+
KeySet allKeys;
104+
for (const auto& factor : cluster_->factors) {
105+
if (!factor) continue;
106+
allKeys.insert(factor->begin(), factor->end());
107+
}
108+
for (const auto& child : children_) {
109+
if (!child) continue;
110+
allKeys.insert(child->separatorKeys_.begin(), child->separatorKeys_.end());
111+
}
112+
for (Key k : frontals()) {
113+
allKeys.erase(k);
114+
}
115+
separatorKeys_.assign(allKeys.begin(), allKeys.end());
116+
}
117+
118+
void MultifrontalClique::cacheValuePointers(VectorValues* values) {
119+
frontalPtrs_.clear();
120+
separatorPtrs_.clear();
121+
frontalPtrs_.reserve(frontals().size());
122+
separatorPtrs_.reserve(separatorKeys_.size());
123+
for (Key key : frontals()) {
124+
frontalPtrs_.push_back(&values->at(key));
125+
}
126+
for (Key key : separatorKeys_) {
127+
separatorPtrs_.push_back(&values->at(key));
128+
}
129+
}
130+
131+
const KeyVector& MultifrontalClique::separatorKeys() const {
132+
return separatorKeys_;
133+
}
134+
135+
std::vector<size_t> MultifrontalClique::blockDims(
136+
const std::map<Key, size_t>& dims) const {
137+
std::vector<size_t> blockDims;
138+
for (Key k : frontals()) blockDims.push_back(dims.at(k));
139+
for (Key k : separatorKeys_) blockDims.push_back(dims.at(k));
140+
return blockDims;
141+
}
142+
143+
size_t MultifrontalClique::countRows(const GaussianFactorGraph& graph) const {
144+
size_t vbmRows = 0;
145+
for (const auto& factor : cluster_->factors) {
146+
auto indexed =
147+
std::dynamic_pointer_cast<internal::IndexedSymbolicFactor>(factor);
148+
if (!indexed) continue;
149+
if (auto jacobianFactor =
150+
std::dynamic_pointer_cast<JacobianFactor>(graph[indexed->index_])) {
151+
vbmRows += jacobianFactor->rows();
152+
}
153+
}
154+
return vbmRows;
155+
}
156+
157+
std::vector<size_t> MultifrontalClique::parentIndicesFor(
158+
const MultifrontalClique& parent) const {
159+
std::vector<size_t> indices;
160+
for (Key k : separatorKeys_) {
161+
auto fIt = std::find(parent.frontals().begin(), parent.frontals().end(), k);
162+
if (fIt != parent.frontals().end()) {
163+
indices.push_back(std::distance(parent.frontals().begin(), fIt));
164+
continue;
165+
}
166+
auto sIt = std::find(parent.separatorKeys_.begin(),
167+
parent.separatorKeys_.end(), k);
168+
if (sIt != parent.separatorKeys_.end()) {
169+
indices.push_back(parent.frontals().size() +
170+
std::distance(parent.separatorKeys_.begin(), sIt));
171+
continue;
172+
}
173+
throw std::runtime_error(
174+
"MultifrontalSolver: separator key not found in parent clique");
175+
}
176+
return indices;
177+
}
178+
179+
void MultifrontalClique::initializeMatrices(
180+
const std::vector<size_t>& blockDims, size_t verticalBlockMatrixRows) {
181+
sbm_ = SymmetricBlockMatrix(blockDims, true);
182+
Ab_ = VerticalBlockMatrix(blockDims, verticalBlockMatrixRows, true);
183+
Ab_.matrix().setZero();
184+
}
185+
186+
void MultifrontalClique::fillAb(const GaussianFactorGraph& graph) {
187+
sbm_.setZero();
188+
189+
// We only overwrite the fixed sparsity pattern, so Ab must be zeroed once in
190+
// initializeMatrices and then kept consistent across loads.
191+
size_t rowOffset = 0;
192+
for (const auto& factor : cluster_->factors) {
193+
auto indexed =
194+
std::dynamic_pointer_cast<internal::IndexedSymbolicFactor>(factor);
195+
if (!indexed) continue;
196+
auto jacobianFactor =
197+
std::dynamic_pointer_cast<JacobianFactor>(graph[indexed->index_]);
198+
if (!jacobianFactor) continue;
199+
200+
for (auto it = jacobianFactor->begin(); it != jacobianFactor->end(); ++it) {
201+
Key k = *it;
202+
auto fIt = std::find(frontals().begin(), frontals().end(), k);
203+
size_t blockIdx = 0;
204+
if (fIt != frontals().end()) {
205+
blockIdx = std::distance(frontals().begin(), fIt);
206+
} else {
207+
auto sIt = std::find(separatorKeys_.begin(), separatorKeys_.end(), k);
208+
if (sIt != separatorKeys_.end()) {
209+
blockIdx =
210+
frontals().size() + std::distance(separatorKeys_.begin(), sIt);
211+
} else
212+
continue;
213+
}
214+
Ab_(blockIdx).middleRows(rowOffset, jacobianFactor->rows()) =
215+
jacobianFactor->getA(it);
216+
}
217+
size_t rhsBlockIdx = Ab_.nBlocks() - 1; // RHS block is appended by VBM.
218+
Ab_(rhsBlockIdx).middleRows(rowOffset, jacobianFactor->rows()) =
219+
jacobianFactor->getb();
220+
rowOffset += jacobianFactor->rows();
221+
}
222+
}
223+
224+
void MultifrontalClique::eliminate() {
225+
// Update SBM with the local factors, Ab^T * Ab
226+
sbm_.selfadjointView().rankUpdate(Ab_.matrix().transpose());
227+
228+
// Form normal equations and factor the frontal block (Schur complement step).
229+
sbm_.choleskyPartial(frontals().size());
230+
231+
auto parent = parent_.lock();
232+
if (!parent) return;
233+
// Expose only the separator+RHS view when contributing to the parent.
234+
sbm_.blockStart() = frontals().size();
235+
parent->updateWith(sbm_, parentIndices_);
236+
sbm_.blockStart() = 0;
237+
}
238+
239+
void MultifrontalClique::updateWith(const SymmetricBlockMatrix& separator,
240+
const std::vector<size_t>& indices) {
241+
const size_t numBlocks = indices.size();
242+
assert(separator.nBlocks() == numBlocks + 1);
243+
const size_t rhsBlock = sbm_.nBlocks() - 1;
244+
245+
for (size_t i = 0; i <= numBlocks; ++i) {
246+
const size_t p_i = (i < numBlocks) ? indices[i] : rhsBlock;
247+
sbm_.updateDiagonalBlock(p_i, separator.diagonalBlock(i));
248+
for (size_t j = i + 1; j <= numBlocks; ++j) {
249+
const size_t p_j = (j < numBlocks) ? indices[j] : rhsBlock;
250+
sbm_.updateOffDiagonalBlock(p_i, p_j, separator.aboveDiagonalBlock(i, j));
251+
}
252+
}
253+
}
254+
255+
void MultifrontalClique::solve() const {
256+
// Solve with block back-substitution on the Cholesky-stored SBM, avoiding
257+
// materializing an explicit R matrix or split representation.
258+
const size_t nFrontals = frontalPtrs_.size();
259+
const size_t nSeparators = separatorPtrs_.size();
260+
261+
const size_t rhsBlock = nFrontals + nSeparators;
262+
263+
size_t frontalDim = 0;
264+
for (const Vector* values : frontalPtrs_) {
265+
frontalDim += values->size();
266+
}
267+
if (static_cast<size_t>(rhsScratch_.size()) != frontalDim) {
268+
rhsScratch_.resize(frontalDim);
269+
}
270+
rhsScratch_.noalias() =
271+
sbm_.aboveDiagonalRange(0, nFrontals, rhsBlock, rhsBlock + 1);
272+
273+
// Eliminate separator contributions: b -= S * x_sep.
274+
if (nSeparators > 0) {
275+
const Vector& xSep =
276+
buildSeparatorVector(separatorPtrs_, &separatorScratch_);
277+
rhsScratch_.noalias() -= sbm_.aboveDiagonalRange(0, nFrontals, nFrontals,
278+
nFrontals + nSeparators) *
279+
xSep;
280+
}
281+
282+
// Solve the contiguous frontal system in one triangular solve.
283+
sbm_.triangularView(0, nFrontals).solveInPlace(rhsScratch_);
284+
285+
// Write solved frontal blocks back into the global solution.
286+
size_t offset = 0;
287+
for (Vector* values : frontalPtrs_) {
288+
const size_t dim = values->size();
289+
values->noalias() = rhsScratch_.segment(offset, dim);
290+
offset += dim;
291+
}
292+
}
293+
294+
void MultifrontalClique::print(const std::string& s,
295+
const KeyFormatter& keyFormatter) const {
296+
if (!s.empty()) std::cout << s;
297+
std::cout << "Clique(key=" << keyFormatter(key_) << ", frontals=[";
298+
for (size_t i = 0; i < frontals().size(); ++i) {
299+
std::cout << keyFormatter(frontals()[i]);
300+
if (i + 1 < frontals().size()) std::cout << ", ";
301+
}
302+
std::cout << "], separators=[";
303+
for (size_t i = 0; i < separatorKeys_.size(); ++i) {
304+
std::cout << keyFormatter(separatorKeys_[i]);
305+
if (i + 1 < separatorKeys_.size()) std::cout << ", ";
306+
}
307+
std::cout << "], factors=" << cluster_->factors.size()
308+
<< ", children=" << children_.size()
309+
<< ", sbmBlocks=" << sbm_.nBlocks()
310+
<< ", AbRows=" << Ab_.matrix().rows() << ")\n";
311+
312+
auto assembleSbm = [](const SymmetricBlockMatrix& sbm) {
313+
const size_t nBlocks = sbm.nBlocks();
314+
std::vector<size_t> offsets(nBlocks + 1, 0);
315+
for (size_t i = 0; i < nBlocks; ++i) {
316+
offsets[i + 1] = offsets[i] + sbm.getDim(i);
317+
}
318+
Matrix full = Matrix::Zero(offsets.back(), offsets.back());
319+
for (size_t i = 0; i < nBlocks; ++i) {
320+
for (size_t j = 0; j < nBlocks; ++j) {
321+
Matrix block = sbm.block(i, j);
322+
full.block(offsets[i], offsets[j], block.rows(), block.cols()) = block;
323+
}
324+
}
325+
return full;
326+
};
327+
328+
std::cout << " Ab:\n" << Ab_.matrix() << "\n";
329+
std::cout << " SBM:\n" << assembleSbm(sbm_) << "\n";
330+
}
331+
332+
std::ostream& operator<<(std::ostream& os, const MultifrontalClique& clique) {
333+
const KeyFormatter formatter = DefaultKeyFormatter;
334+
auto printKeys = [&](const KeyVector& keys) {
335+
os << "[";
336+
for (size_t i = 0; i < keys.size(); ++i) {
337+
os << formatter(keys[i]);
338+
if (i + 1 < keys.size()) os << ", ";
339+
}
340+
os << "]";
341+
};
342+
343+
os << "Clique(key=" << formatter(clique.key()) << ", frontals=";
344+
printKeys(clique.frontals());
345+
os << ", separators=";
346+
printKeys(clique.separatorKeys());
347+
os << ", factors=" << clique.factorCount();
348+
os << ", children=" << clique.children().size();
349+
os << ", sbmBlocks=" << clique.sbm().nBlocks();
350+
os << ", AbRows=" << clique.Ab().matrix().rows() << ")";
351+
return os;
352+
}
353+
354+
} // namespace gtsam

0 commit comments

Comments
 (0)