Skip to content

Commit f4f67fa

Browse files
authored
Merge pull request #2408 from DanMcGann/feature/risam-isam2-updates
iSAM2 Updates to Support riSAM Implementation
2 parents 9abbd45 + fa9b4e7 commit f4f67fa

File tree

10 files changed

+542
-7
lines changed

10 files changed

+542
-7
lines changed

gtsam/inference/BayesTree-inst.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,5 +573,34 @@ namespace gtsam {
573573
return cliques;
574574
}
575575

576+
/* *********************************************************************** */
577+
template <class CLIQUE>
578+
void BayesTree<CLIQUE>::collectAffectedPathKeys(
579+
gtsam::KeySet& traversedKeys, const sharedClique& clique) const {
580+
// base case is nullptr, if so we do nothing and return empties above
581+
if (clique) {
582+
// traverse me
583+
traversedKeys.insert(clique->conditional()->frontals().begin(),
584+
clique->conditional()->frontals().end());
585+
// traverse path above me
586+
this->collectAffectedPathKeys(traversedKeys, clique->parent_.lock());
587+
}
588+
}
589+
590+
/* *********************************************************************** */
591+
template <class CLIQUE>
592+
gtsam::KeySet BayesTree<CLIQUE>::collectAffectedKeys(
593+
const gtsam::KeyVector& keys) const {
594+
gtsam::KeySet traversedKeys;
595+
// process each key of the new factor
596+
for (const gtsam::Key& j : keys) {
597+
typename Nodes::const_iterator node = nodes_.find(j);
598+
if (node != nodes_.end()) {
599+
// traverse path from clique to root
600+
this->collectAffectedPathKeys(traversedKeys, node->second);
601+
}
602+
}
603+
return traversedKeys;
604+
}
576605
}
577606
/// namespace gtsam

gtsam/inference/BayesTree.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,17 @@ namespace gtsam {
245245
/** Add all cliques in this BayesTree to the specified factor graph */
246246
void addFactorsToGraph(FactorGraph<FactorType>* graph) const;
247247

248-
protected:
248+
/**
249+
* @brief Returns the set of keys from the tree that are affected by a
250+
* update to 'keys'
251+
* @param keys: The keys updated and in-turn affecting the tree
252+
* @returns The set of keys from the tree that are affected
253+
* @note Note: Return matches contents of BayesNet from removeTop without
254+
* affecting the tree
255+
*/
256+
gtsam::KeySet collectAffectedKeys(const gtsam::KeyVector& keys) const;
257+
258+
protected:
249259

250260
/** private helper method for saving the Tree to a text file in GraphViz format */
251261
void dot(std::ostream &s, sharedClique clique, const KeyFormatter& keyFormatter,
@@ -260,6 +270,11 @@ namespace gtsam {
260270
/** Fill the nodes index for a subtree */
261271
void fillNodesIndex(const sharedClique& subtree);
262272

273+
/// @brief Helper for collectAffectedKeys that recursively aggregates
274+
/// affected keys from a path from 'clique' to the root of tree
275+
void collectAffectedPathKeys(gtsam::KeySet& traversedKeys,
276+
const sharedClique& clique) const;
277+
263278
// Friend JunctionTree because it directly fills roots and nodes index.
264279
template<class BAYESTREE, class GRAPH> friend class EliminatableClusterTree;
265280

gtsam/nonlinear/DoglegOptimizerImpl.h

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,124 @@ typename DoglegOptimizerImpl::IterationResult DoglegOptimizerImpl::Iterate(
252252
return result;
253253
}
254254

255+
/** This class contains an extension of the Dogleg Algorithm where a line search
256+
* is performed across the Dogleg arc (interpolation of gradient and
257+
* Gauss-Newton directions). This algorithm is applicable to cases where the
258+
* trust region is not correlated between algorithm iterations.
259+
*/
260+
struct GTSAM_EXPORT DoglegLineSearchImpl {
261+
struct Params {
262+
double minDelta; ///< Minimum allowed small delta
263+
double maxDelta; ///< Maximum allowed delta
264+
double stepSize; ///< Increase of trust region for outward steps
265+
double sufficientDecreaseCoeff; ///< Coefficient for suff. decrease check
266+
bool verbose; ///< Whether to print debug information
267+
};
268+
269+
/**
270+
* Compute the update point for one iteration of the Dogleg Line Search
271+
* algorithm, starting with a trust region of |N| * minDelta the algorithm
272+
* searches trust regions from |N| * minDelta to maxDelta where |N| is the
273+
* number of variables in the system. The algorithm returns the search point
274+
* with minimum cost that meets the Wolfe Conditions. Evaluation points for
275+
* the line search are computed according to a geometric series step_{k+1} =
276+
* stepSize * step_k.
277+
*
278+
*
279+
* @tparam M The type of the Bayes' net or tree, currently
280+
* either BayesNet<GaussianConditional> (or GaussianBayesNet) or
281+
* BayesTree<GaussianConditional>.
282+
* @tparam F For normal usage this will be NonlinearFactorGraph<VALUES>.
283+
* @tparam VALUES The Values or TupleValues to pass to F::error() to evaluate
284+
* the error function.
285+
* @param params The parameters for dogleg line search
286+
* @param Rd The Bayes' net or tree as described above.
287+
* @param f The original nonlinear factor graph with which to evaluate the
288+
* accuracy of \f$ M(\delta x) \f$ to adjust \f$ \delta \f$.
289+
* @param x0 The linearization point about which \f$ \bayesNet \f$ was created
290+
* @param verbose Flag to write debug information.
291+
* @return A DoglegIterationResult containing the new \c delta, the linear
292+
* update \c dx_d, and the resulting nonlinear error \c f_error.
293+
*/
294+
template <class M, class F, class VALUES>
295+
static DoglegOptimizerImpl::IterationResult Iterate(const Params& params,
296+
const VectorValues& dx_u,
297+
const VectorValues& dx_n,
298+
const M& Rd, const F& f,
299+
const VALUES& x0);
300+
};
301+
302+
/* ************************************************************************* */
303+
template <class M, class F, class VALUES>
304+
typename DoglegOptimizerImpl::IterationResult DoglegLineSearchImpl::Iterate(
305+
const Params& params, const VectorValues& dx_u, const VectorValues& dx_n,
306+
const M& Rd, const F& f, const VALUES& x0) {
307+
if (params.verbose)
308+
std::cout << "DoglegLineSearch | minDelta: " << params.minDelta
309+
<< " maxDelta: " << params.maxDelta
310+
<< " stepSize: " << params.stepSize << std::endl;
311+
const double fInitError = f.error(x0);
312+
const double gnStep = dx_n.norm();
313+
const size_t numVars = dx_n.size();
314+
315+
double maxStep, step;
316+
/// The search bounds are scaled by the number of variables in the system
317+
maxStep = std::min(params.maxDelta * numVars, gnStep);
318+
step = std::min(params.minDelta * numVars, gnStep);
319+
step = std::min(step, maxStep); // Edge case min_step > maxStep
320+
321+
if (params.verbose)
322+
std::cout << "Search Region: [ " << step << " -> " << maxStep << "]"
323+
<< std::endl;
324+
325+
DoglegOptimizerImpl::IterationResult result;
326+
result.dx_d =
327+
DoglegOptimizerImpl::ComputeDoglegPoint(step, dx_u, dx_n, params.verbose);
328+
result.f_error = f.error(x0.retract(result.dx_d));
329+
result.delta = step;
330+
if (params.verbose)
331+
std::cout << "Initial Step Error: " << result.f_error << std::endl;
332+
333+
// Validate Step size will terminate search
334+
if (step < 1e-12 || params.stepSize < 1.0)
335+
throw std::runtime_error(
336+
"Invalid DoglegLineSearch configuration. Would cause infinite search.");
337+
338+
// Search Increase delta
339+
double eps = std::numeric_limits<double>::epsilon();
340+
while (step < maxStep - eps) {
341+
// Compute the trust region for the evaluation point according to the
342+
// Geometric Series
343+
step = std::min(maxStep, step * params.stepSize);
344+
345+
// Compute the Evaluation Point and evaluate the error
346+
VectorValues dx_d = DoglegOptimizerImpl::ComputeDoglegPoint(
347+
step, dx_u, dx_n, params.verbose);
348+
VALUES x_d(x0.retract(dx_d));
349+
double fNewError = f.error(x_d);
350+
351+
if (params.verbose)
352+
std::cout << "Step: " << step << " | Error: " << fNewError << std::endl;
353+
354+
// Check step acceptance conditions
355+
bool updateDecreasedError = fNewError < result.f_error;
356+
bool updateHasSufficientDecrease =
357+
fNewError < (fInitError - params.sufficientDecreaseCoeff * step);
358+
359+
if (params.verbose)
360+
std::cout << "Decrease Error: " << updateDecreasedError
361+
<< " | Suff. Dec.: " << updateHasSufficientDecrease
362+
<< std::endl;
363+
364+
if (updateDecreasedError && updateHasSufficientDecrease) {
365+
result.f_error = fNewError;
366+
result.dx_d = dx_d;
367+
result.delta = step;
368+
369+
if (params.verbose) std::cout << "Step Accepted" << std::endl;
370+
}
371+
}
372+
373+
return result;
374+
}
255375
}

gtsam/nonlinear/ISAM2.cpp

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ ISAM2Result ISAM2::update(const NonlinearFactorGraph& newFactors,
431431
addVariables(newTheta, result.details());
432432

433433
// Update delta if we need it to check relinearization later
434-
if (update.relinarizationNeeded(update_count_))
434+
if (update.relinarizationNeeded(update_count_) && !deltaReplacedMask_.empty())
435435
updateDelta(updateParams.forceFullSolve);
436436

437437
// 1. Add any new factors \Factors:=\Factors\cup\Factors'.
@@ -756,9 +756,9 @@ void ISAM2::updateDelta(bool forceFullSolve) const {
756756
const VectorValues gradAtZero = this->gradientAtZero(); // Compute gradient
757757
DeltaImpl::UpdateRgProd(roots_, deltaReplacedMask_, gradAtZero,
758758
&RgProd_); // Update RgProd
759-
const VectorValues dx_u = DeltaImpl::ComputeGradientSearch(
760-
gradAtZero, RgProd_); // Compute gradient search point
761-
759+
const VectorValues dx_u =
760+
VectorValues::Zero(deltaNewton_)
761+
.update(DeltaImpl::ComputeGradientSearch(gradAtZero, RgProd_));
762762
// Clear replaced keys mask because now we've updated deltaNewton_ and
763763
// RgProd_
764764
deltaReplacedMask_.clear();
@@ -777,6 +777,39 @@ void ISAM2::updateDelta(bool forceFullSolve) const {
777777
// Copy the VectorValues containing with the linear solution
778778
delta_ = doglegResult.dx_d;
779779
gttoc(Copy_dx_d);
780+
} else if (std::holds_alternative<ISAM2DoglegLineSearchParams>(
781+
params_.optimizationParams)) {
782+
const ISAM2DoglegLineSearchParams& isamDllsParams =
783+
std::get<ISAM2DoglegLineSearchParams>(params_.optimizationParams);
784+
const double effectiveWildfireThreshold =
785+
forceFullSolve ? 0.0 : isamDllsParams.getWildfireThreshold();
786+
787+
// Timer start
788+
gttic(DoglegLineSearch_Iterate);
789+
790+
// Compute Newton's method step
791+
gttic(Wildfire_update);
792+
DeltaImpl::UpdateGaussNewtonDelta(
793+
roots_, deltaReplacedMask_, effectiveWildfireThreshold, &deltaNewton_);
794+
gttoc(Wildfire_update);
795+
796+
// Compute steepest descent step
797+
const VectorValues gradAtZero = this->gradientAtZero();
798+
DeltaImpl::UpdateRgProd(roots_, deltaReplacedMask_, gradAtZero, &RgProd_);
799+
const VectorValues dx_u =
800+
VectorValues::Zero(deltaNewton_)
801+
.update(DeltaImpl::ComputeGradientSearch(gradAtZero, RgProd_));
802+
deltaReplacedMask_.clear();
803+
804+
// Do the DogLeg Line Search
805+
DoglegOptimizerImpl::IterationResult doglegResult(
806+
DoglegLineSearchImpl::Iterate(isamDllsParams.dllsParams, dx_u,
807+
deltaNewton_, *this, nonlinearFactors_,
808+
theta_));
809+
810+
// Update Delta and linear step
811+
delta_ = doglegResult.dx_d;
812+
gttoc(DoglegLineSearch_Iterate);
780813
} else {
781814
throw std::runtime_error("iSAM2: unknown ISAM2Params type");
782815
}
@@ -832,4 +865,41 @@ VectorValues ISAM2::gradientAtZero() const {
832865
return g;
833866
}
834867

868+
/* ************************************************************************* */
869+
std::pair<KeySet, bool> ISAM2::predictUpdateInfo(
870+
const NonlinearFactorGraph& newFactors, const Values& newTheta,
871+
const ISAM2UpdateParams& updateParams) const {
872+
// Temp variables required by inputs below
873+
ISAM2Result result;
874+
UpdateImpl update(params_, updateParams);
875+
876+
if (update.relinarizationNeeded(update_count_) && !deltaReplacedMask_.empty())
877+
updateDelta(updateParams.forceFullSolve);
878+
879+
// Gather the Keys involved from update params
880+
update.computeUnusedKeys(newFactors, variableIndex_,
881+
result.keysWithRemovedFactors, &result.unusedKeys);
882+
update.gatherInvolvedKeys(newFactors, nonlinearFactors_,
883+
result.keysWithRemovedFactors, &result.markedKeys);
884+
update.updateKeys(result.markedKeys, &result);
885+
886+
// Gather keys involved due to relinearization threshold
887+
// Note: increment by one as we are performing a one step look ahead
888+
KeySet relinKeys;
889+
if (update.relinarizationNeeded(update_count_ + 1)) {
890+
relinKeys = update.gatherRelinearizeKeys(roots_, delta_, fixedVariables_,
891+
&result.markedKeys);
892+
if (!relinKeys.empty())
893+
update.findFluid(roots_, relinKeys, &result.markedKeys, result.details());
894+
}
895+
// Get the top of the bayes tree affected by all the involved keys
896+
KeySet affectedKeys = collectAffectedKeys(
897+
KeyVector(result.markedKeys.begin(), result.markedKeys.end()));
898+
// Add the new keys to get the entire set of keys affected by the update
899+
KeySet newKeys = newFactors.keys();
900+
affectedKeys.insert(newKeys.begin(), newKeys.end());
901+
// Return the affected keys and whether or not this will be a batch update
902+
return {affectedKeys, affectedKeys.size() >= theta_.size() * 0.65};
903+
}
904+
835905
} // namespace gtsam

gtsam/nonlinear/ISAM2.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,19 @@ class GTSAM_EXPORT ISAM2 : public BayesTree<ISAM2Clique> {
290290
*/
291291
VectorValues gradientAtZero() const;
292292

293+
/** @brief Predicts the updated variables for a hypothetical update.
294+
* @param newFactors The factors for the hypothetical update
295+
* @param newTheta The estimates for new variables in the hypothetical update
296+
* @param updateParams The update params for the hypothetical update
297+
* @returns The set of all affected keys, and a flag indicating if this would
298+
* be a batch update
299+
*
300+
* NOTE: Update may mutate the mutable field delta_
301+
*/
302+
std::pair<KeySet, bool> predictUpdateInfo(
303+
const NonlinearFactorGraph& newFactors, const Values& newTheta,
304+
const ISAM2UpdateParams& updateParams) const;
305+
293306
/// @}
294307

295308
protected:

gtsam/nonlinear/ISAM2Params.h

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,69 @@ struct GTSAM_EXPORT ISAM2DoglegParams {
126126
const std::string& adaptationMode) const;
127127
};
128128

129+
/**
130+
* @ingroup isam2
131+
* Parameters for ISAM2 using Dogleg Line Search optimization. Either this
132+
* class, ISAM2GaussNewtonParams, or ISAM2DoglegParams should be specified as
133+
* the optimizationParams in ISAM2Params, which should in turn be passed to
134+
* ISAM2(const ISAM2Params&).
135+
*/
136+
struct GTSAM_EXPORT ISAM2DoglegLineSearchParams {
137+
DoglegLineSearchImpl::Params dllsParams; ///< Params for DoglegLineSearch
138+
double wildfireThreshold; ///< Update delta when changes are above thresh
139+
140+
/** Specify parameters as constructor arguments */
141+
ISAM2DoglegLineSearchParams(double minDelta = 0.02, double maxDelta = 0.5,
142+
double stepSize = 1.5,
143+
double sufficientDecreaseCoeff = 1e-3,
144+
bool verbose = false,
145+
double wildfireThreshold = 1e-4)
146+
: dllsParams{minDelta, maxDelta, stepSize, sufficientDecreaseCoeff,
147+
verbose},
148+
wildfireThreshold(wildfireThreshold) {
149+
if (minDelta < 1e-12 || maxDelta < 1e-12 || stepSize < 1.0) {
150+
throw std::invalid_argument(
151+
"ISAM2DoglegLineSearchParams constructed with invalid configuration. "
152+
"Search Bounds [minDelta, maxDelta] ~ 0 or stepSize < 1.0");
153+
}
154+
}
155+
156+
void print(const std::string str = "") const {
157+
using std::cout;
158+
cout << str << "type: ISAM2DoglegLineSearchParams\n";
159+
cout << str << "minDelta: " << dllsParams.minDelta << "\n";
160+
cout << str << "maxDelta: " << dllsParams.maxDelta << "\n";
161+
cout << str << "stepSize: " << dllsParams.stepSize << "\n";
162+
cout << str
163+
<< "sufficientDecreaseCoeff: " << dllsParams.sufficientDecreaseCoeff
164+
<< "\n";
165+
cout << str << "verbose: " << dllsParams.verbose << "\n";
166+
cout << str << "wildfireThreshold: " << wildfireThreshold << "\n";
167+
cout.flush();
168+
}
169+
/** Getters **/
170+
double getMinDelta() const { return dllsParams.minDelta; }
171+
double getMaxDelta() const { return dllsParams.maxDelta; }
172+
double getStepSize() const { return dllsParams.stepSize; }
173+
double getSufficientDecreaseCoeff() const {
174+
return dllsParams.sufficientDecreaseCoeff;
175+
}
176+
bool isVerbose() const { return dllsParams.verbose; }
177+
double getWildfireThreshold() const { return wildfireThreshold; }
178+
179+
/** Setters */
180+
void setMinDelta(double minDelta) { this->dllsParams.minDelta = minDelta; }
181+
void setMaxDelta(double maxDelta) { this->dllsParams.maxDelta = maxDelta; }
182+
void setStepSize(double stepSize) { this->dllsParams.stepSize = stepSize; }
183+
void setSufficientDecreaseCoeff(double sufficientDecreaseCoeff) {
184+
this->dllsParams.sufficientDecreaseCoeff = sufficientDecreaseCoeff;
185+
}
186+
void setVerbose(bool verbose) { this->dllsParams.verbose = verbose; }
187+
void setWildfireThreshold(double wildfireThreshold) {
188+
this->wildfireThreshold = wildfireThreshold;
189+
}
190+
};
191+
129192
/**
130193
* @ingroup isam2
131194
* Parameters for the ISAM2 algorithm. Default parameter values are listed
@@ -134,9 +197,11 @@ struct GTSAM_EXPORT ISAM2DoglegParams {
134197
typedef FastMap<char, Vector> ISAM2ThresholdMap;
135198
typedef ISAM2ThresholdMap::value_type ISAM2ThresholdMapValue;
136199
struct GTSAM_EXPORT ISAM2Params {
137-
typedef std::variant<ISAM2GaussNewtonParams, ISAM2DoglegParams>
200+
typedef std::variant<ISAM2GaussNewtonParams, ISAM2DoglegParams,
201+
ISAM2DoglegLineSearchParams>
138202
OptimizationParams; ///< Either ISAM2GaussNewtonParams or
139-
///< ISAM2DoglegParams
203+
///< ISAM2DoglegParams or
204+
///< ISAM2DoglegLineSearchParams
140205
typedef std::variant<double, FastMap<char, Vector> >
141206
RelinearizationThreshold; ///< Either a constant relinearization
142207
///< threshold or a per-variable-type set of

0 commit comments

Comments
 (0)