OGS
NumLib::AndersonAcceleration Class Referencefinal

Detailed Description

Anderson acceleration of the damped Picard fixpoint iteration.

Owns a sliding window of the last depth damped steps and the Gram matrix of that window, and mixes them into an accelerated iterate. History vectors are taken from the global vector provider on demand and returned in the destructor (RAII), so no manual release is required at the call site.

The window and its Gram matrix are only ever mutated together; keeping them in one object makes "history and Gram stay in sync" a class invariant instead of a convention spread across the solver loop.

A depth below min_mixing_depth admits no mixing (the sum-to-one constraint forces unit weight on the single stored step), so such an instance is an inert no-op: it takes no vector from the global vector provider and leaves the iterate untouched. This is the plain-Picard case.

Definition at line 30 of file AndersonAcceleration.h.

#include <AndersonAcceleration.h>

Classes

struct  HistoryEntry

Public Member Functions

 AndersonAcceleration (int depth)
 ~AndersonAcceleration ()
 AndersonAcceleration (AndersonAcceleration const &)=delete
AndersonAccelerationoperator= (AndersonAcceleration const &)=delete
void accelerate (GlobalVector const &x_old, GlobalVector &x_new)
void dropLastStep ()

Static Public Attributes

static constexpr int min_mixing_depth = 2

Static Private Member Functions

static void releaseHistoryEntry (HistoryEntry const &entry)
 Returns entry's vectors to the global vector provider.

Private Attributes

int const _depth
std::vector< HistoryEntry_history
 Circular buffer of history entries, oldest first (size <= _depth).
Eigen::MatrixXd _gram

Constructor & Destructor Documentation

◆ AndersonAcceleration() [1/2]

NumLib::AndersonAcceleration::AndersonAcceleration ( int depth)
explicit
Parameters
depthnumber of previous iterates retained for mixing; a value below min_mixing_depth makes the instance an inert no-op (plain Picard).

Definition at line 150 of file AndersonAcceleration.cpp.

151 : _depth(depth), _gram(depth, depth)
152{
154 {
155 _history.reserve(_depth);
156 }
157}
static constexpr int min_mixing_depth
std::vector< HistoryEntry > _history
Circular buffer of history entries, oldest first (size <= _depth).

References _depth, _gram, _history, and min_mixing_depth.

Referenced by AndersonAcceleration(), and operator=().

◆ ~AndersonAcceleration()

NumLib::AndersonAcceleration::~AndersonAcceleration ( )

Definition at line 159 of file AndersonAcceleration.cpp.

160{
161 for (auto const& entry : _history)
162 {
163 releaseHistoryEntry(entry);
164 }
165}
static void releaseHistoryEntry(HistoryEntry const &entry)
Returns entry's vectors to the global vector provider.

References _history, and releaseHistoryEntry().

◆ AndersonAcceleration() [2/2]

NumLib::AndersonAcceleration::AndersonAcceleration ( AndersonAcceleration const & )
delete

Member Function Documentation

◆ accelerate()

void NumLib::AndersonAcceleration::accelerate ( GlobalVector const & x_old,
GlobalVector & x_new )

Records the newest damped step \( x_{\rm old} \to x_{\rm new} \) and overwrites x_new in place with the Anderson-mixed iterate.

While fewer than two steps are stored, or when the mixture is rejected as untrustworthy (see detail::computeAndersonWeights), x_new is left unchanged. For a no-op instance (depth < min_mixing_depth) this does nothing.

Parameters
x_oldthe iterate entering this step.
x_newin: the damped Picard step \( f = \beta(g(x_{\rm old}) - x_{\rm old}) \) added to x_old; out: the mixed iterate.

Definition at line 173 of file AndersonAcceleration.cpp.

175{
176 namespace LinAlg = MathLib::LinAlg;
177
178 // A depth below min_mixing_depth admits no mixing (plain Picard); nothing
179 // is stored.
181 {
182 return;
183 }
184
185 // Additionally mixes the last _depth damped steps
186 // f_i = beta*(g(x_i) - x_i) (i.e. x_new - x_old computed after the beta
187 // relaxation already applied by the caller) to find the optimal theta
188 // minimising ||sum theta_i f_i|| s.t. sum theta_i = 1, then sets
189 // x_new = sum theta_i*(x_i + f_i).
190
191 // Whether the circular buffer is full and the oldest entry is about to be
192 // evicted (needed for the incremental Gram update).
193 bool const rotated = static_cast<int>(_history.size()) == _depth;
194 if (!rotated)
195 {
196 // The id out-params are unused: the provider allocates a fresh vector
197 // on every call and never re-fetches by id.
198 std::size_t x_id = 0u;
199 std::size_t f_id = 0u;
200 _history.push_back(
203 }
204 else
205 {
206 // Recycle the oldest entry as the newest one.
207 std::rotate(_history.begin(), _history.begin() + 1, _history.end());
208 }
209
210 auto const& newest = _history.back();
211
212 // x = x_old, f = x_new - x_old
213 LinAlg::copy(x_old, *newest.x);
214 LinAlg::copy(x_new, *newest.f);
215 LinAlg::axpy(*newest.f, -1.0, x_old);
216
217 // Actual window size, <= _depth while the buffer fills.
218 int const history_size = static_cast<int>(_history.size());
219
220 // Incrementally maintain the (history_size x history_size) Gram matrix
221 // G = F^T F whose columns are the stored damped steps f_0 ...
222 // f_{history_size-1}. All steps but the newest are unchanged from the
223 // previous iteration, so only the last row/column is recomputed -
224 // history_size dot products instead of a full
225 // history_size*(history_size+1)/2 rebuild. On a rotate the oldest entry
226 // (index 0) was evicted, so the cached block is first shifted up-left by
227 // one.
228 if (rotated)
229 {
230 _gram.topLeftCorner(history_size - 1, history_size - 1) =
231 _gram.block(1, 1, history_size - 1, history_size - 1).eval();
232 }
233 for (int i = 0; i < history_size; ++i)
234 {
235 double const d = LinAlg::dot(*_history[i].f, *newest.f);
236 _gram(i, history_size - 1) = d;
237 _gram(history_size - 1, i) = d;
238 }
239
240 // A single stored step needs no mixing: the sum-to-one constraint forces
241 // theta = (1), which just reproduces the damped step already held in x_new.
242 if (history_size < min_mixing_depth)
243 {
244 return;
245 }
246
247 // Solve G theta = e (least-squares) with the constraint sum theta_i = 1 via
248 // a simple Lagrange formulation:
249 //
250 // [ G 1 ] [ theta ] = [ 0 ]
251 // [ 1 0 ] [ lambda ] [ 1 ]
252 //
253 // The beta factor scales G by beta^2 and cancels in theta, so the weights
254 // are identical to the undamped case. The Anderson update is then:
255 // x_anderson = sum_i theta_i * (x_i + f_i)
256 // = sum_i theta_i * (x_i + beta*(g(x_i)-x_i))
257 // = sum_i theta_i * ((1-beta)*x_i + beta*g(x_i))
258 Eigen::MatrixXd const G = _gram.topLeftCorner(history_size, history_size);
259
260 Eigen::VectorXd const theta = detail::computeAndersonWeights(G);
261
262 // Accumulate the Anderson mixed iterate directly into x_new. Its previous
263 // value is no longer needed: the newest step was already extracted from it
264 // above, and it is not aliased by any history entry (those are independent
265 // copies).
266 x_new.setZero();
267 for (int i = 0; i < history_size; ++i)
268 {
269 // x_new += theta_i * (x_i + f_i)
270 LinAlg::axpy(x_new, theta(i), *_history[i].x);
271 LinAlg::axpy(x_new, theta(i), *_history[i].f);
272 }
273
274 DBUG("Picard/Anderson: history size {:d}, theta=[{:.4g}]", history_size,
275 fmt::join(theta.data(), theta.data() + history_size, ", "));
276}
void DBUG(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:22
virtual GlobalVector & getVector(std::size_t &id)=0
Get an uninitialized vector with the given id.
double dot(PETScVector const &a, PETScVector const &b)
Definition LinAlg.cpp:64
void copy(PETScVector const &x, PETScVector &y)
Definition LinAlg.cpp:30
void axpy(PETScVector &y, PetscScalar const a, PETScVector const &x)
Definition LinAlg.cpp:50
Eigen::VectorXd computeAndersonWeights(Eigen::MatrixXd G)
static NUMLIB_EXPORT VectorProvider & provider

References _depth, _gram, _history, MathLib::LinAlg::axpy(), NumLib::detail::computeAndersonWeights(), MathLib::LinAlg::copy(), DBUG(), MathLib::LinAlg::dot(), min_mixing_depth, NumLib::GlobalVectorProvider::provider, and MathLib::EigenVector::setZero().

Referenced by NumLib::NonlinearSolver< NonlinearSolverTag::Picard >::solve().

◆ dropLastStep()

void NumLib::AndersonAcceleration::dropLastStep ( )

Discards the step recorded by the most recent accelerate() call, used when the current iteration is repeated. The Gram shift performed while recording is deliberately not undone (see implementation).

Definition at line 278 of file AndersonAcceleration.cpp.

279{
280 // Drop the just-added (now stale) history entry, since the iteration is
281 // being repeated. In the full-buffer case this is the recycled slot;
282 // releasing it by reference is safe because the provider tracks vectors by
283 // pointer, not by the (unused) id.
284 //
285 // The rotation and the Gram shift performed by accelerate() are not undone,
286 // and need not be: dropping the newest entry leaves the buffer holding the
287 // remaining entries in order, and the shifted top-left block of the Gram
288 // matrix is exactly their Gram matrix. The oldest entry stays evicted,
289 // which merely shortens the sliding window by one.
290 if (!_history.empty())
291 {
293 _history.pop_back();
294 }
295}

References _history, and releaseHistoryEntry().

Referenced by NumLib::NonlinearSolver< NonlinearSolverTag::Picard >::solve().

◆ operator=()

AndersonAcceleration & NumLib::AndersonAcceleration::operator= ( AndersonAcceleration const & )
delete

◆ releaseHistoryEntry()

void NumLib::AndersonAcceleration::releaseHistoryEntry ( HistoryEntry const & entry)
staticprivate

Returns entry's vectors to the global vector provider.

Definition at line 167 of file AndersonAcceleration.cpp.

168{
171}
virtual void releaseVector(GlobalVector const &x)=0

References NumLib::AndersonAcceleration::HistoryEntry::f, NumLib::GlobalVectorProvider::provider, and NumLib::AndersonAcceleration::HistoryEntry::x.

Referenced by ~AndersonAcceleration(), and dropLastStep().

Member Data Documentation

◆ _depth

int const NumLib::AndersonAcceleration::_depth
private

Maximum window size; mixing is active only for values >= min_mixing_depth.

Definition at line 81 of file AndersonAcceleration.h.

Referenced by AndersonAcceleration(), and accelerate().

◆ _gram

Eigen::MatrixXd NumLib::AndersonAcceleration::_gram
private

Gram matrix G = F^T F of the stored steps, maintained incrementally across iterations (only the newest step's row/column is recomputed). Sized once to the maximum window (_depth x _depth) so the incremental update never reallocates; only the leading history_size x history_size block is live while the window fills.

Definition at line 91 of file AndersonAcceleration.h.

Referenced by AndersonAcceleration(), and accelerate().

◆ _history

std::vector<HistoryEntry> NumLib::AndersonAcceleration::_history
private

Circular buffer of history entries, oldest first (size <= _depth).

Definition at line 84 of file AndersonAcceleration.h.

Referenced by AndersonAcceleration(), ~AndersonAcceleration(), accelerate(), and dropLastStep().

◆ min_mixing_depth

int NumLib::AndersonAcceleration::min_mixing_depth = 2
staticconstexpr

Smallest depth that admits mixing: below it the sum-to-one constraint forces unit weight on the single stored step, i.e. plain Picard.

Definition at line 35 of file AndersonAcceleration.h.

Referenced by AndersonAcceleration(), accelerate(), NumLib::createNonlinearSolver(), and NumLib::NonlinearSolver< NonlinearSolverTag::Picard >::solve().


The documentation for this class was generated from the following files: