src/solve.h
Helper macros to invert (linear) spatial operators
The macros below can be used to easily invert linear systems described by stencils i.e. \mathcal{L}(a) = b For example, let us consider the Poisson equation \nabla^2 a = b where a is unknown and b is given. This can be discretised as
(a[1] + a[-1] + a[0,1] + a[0,-1] - 4.*a[])/sq(Delta) = b[];This can be solved using the solve() macro below and a multigrid solver with
solve (a, (a[1] + a[-1] + a[0,1] + a[0,-1] - 4.*a[])/sq(Delta), b[]);The macro can take the same optional arguments as mg_solve() to tune the
multigrid solver.
The macro returns multigrid statistics.
The msolve() macro generalizes
this to systems of linear equations, with multiple unknown fields. For
example let us consider the coupled reaction-diffusion equations
\begin{aligned}
\partial_tC_1 & = \mu_1\nabla^2C_1 + k_1 C_2, \\
\partial_tC_2 & = \mu_2\nabla^2C_2 + k_2 C_1
\end{aligned}
A first-order, implicit-in-time discretisation could be
\begin{aligned}
\frac{C_1(t) - C_1(t+\Delta t)}{\Delta t} + \mu_1\nabla^2C_1(t+\Delta t)
+
k_1 C_2(t+\Delta t) & = 0, \\
\frac{C_2(t) - C_2(t+\Delta t)}{\Delta t} + \mu_2\nabla^2C_2(t+\Delta t)
+
k_2 C_1(t+\Delta t) & = 0
\end{aligned}
This can be solved using
scalar C1n[], C2n[];
foreach()
C1n[] = C1[], C2n[] = C2[];
restriction ({C1n, C2n});
#define laplacian(a) ((a[1,0] + a[-1,0] + a[0,1] + a[0,-1] - 4.*a[])/sq(Delta))
msolve ({C1, C2}, {
(C1n[] - C1[])/dt + mu1*laplacian(C1) + k1*C2[],
(C2n[] - C2[])/dt + mu2*laplacian(C2) + k2*C1[]
});where the call to restriction() ensures that the values
of C1n and C2n are defined on all the levels
of the multigrid.
Note that the previous example (with only one equation and one unknown) could also have been solved using
restriction ({b});
msolve ({a}, {(a[1] + a[-1] + a[0,1] + a[0,-1] - 4.*a[])/sq(Delta) - b[]});Single unknown
Note that the macro below is a slightly simplified version of the mg_solve() and mg_cycle() functions where more documentation can be found.
#include "poisson.h"
macro
mgstats solve (scalar a, double func, double rhs,
int nrelax = 4,
int minlevel = 0,
double tolerance = TOLERANCE)
{{
mgstats _s = (mgstats){0};
scalar _res[], _da[];
scalar_clone (_da, a);
for (int b = 0; b < nboundary; b++)
_da.boundary[b] = _da.boundary_homogeneous[b];
_s.nrelax = nrelax;
double _resb;
{
double maxres = 0.;
foreach (reduction(max:maxres)) {
_res[] = rhs - func;
if (fabs (_res[]) > maxres)
maxres = fabs (_res[]);
}
_resb = _s.resb = _s.resa = maxres;
}
for (_s.i = 0; _s.i < NITERMAX && (_s.i < NITERMIN || _s.resa > tolerance); _s.i++) {
{
restriction ({_res});
int _maxlevel = grid->maxdepth;
int _minlevel = min (minlevel, _maxlevel);
for (int l = _minlevel; l <= _maxlevel; l++) {
if (l == _minlevel)
foreach_level_or_leaf (l)
foreach_blockf (_da)
_da[] = 0.;
else
foreach_level (l)
foreach_blockf (_da)
_da[] = bilinear (point, _da);
boundary_level ({_da}, l);
for (int i = 0; i < _s.nrelax; i++) {
scalar a = _da;
foreach_level_or_leaf (l) {
a[] = 0.;
double _n = _res[] - func, _d;
diagonalize(a)
_d = func;
a[] = _n/_d;
}Note that the diagonalize() operator is not necessary,
one could have written instead
foreach_level_or_leaf (l) {
a[] = 0.;
double _d = - func, _n = _res[] + _d;
a[] = 1.;
_d += func;
a[] = _n/_d;
} boundary_level ({_da}, l);
}
}
foreach()
foreach_blockf (a)
a[] += _da[];
}
{
double maxres = 0.;
foreach (reduction(max:maxres)) {
_res[] = rhs - func;
if (fabs (_res[]) > maxres)
maxres = fabs (_res[]);
}
_s.resa = maxres;
}
if (_s.resa > tolerance) {
if (_resb/_s.resa < 1.2 && _s.nrelax < 100)
_s.nrelax++;
else if (_resb/_s.resa > 10 && _s.nrelax > 2)
_s.nrelax--;
}
_resb = _s.resa;
}
_s.minlevel = minlevel;
if (_s.resa > tolerance)
fprintf (stderr,
"src/solve.h:%d: warning: convergence for %s not reached after %d iterations\n"
" res: %g nrelax: %d\n", LINENO, a.name,
_s.i, _s.resa, _s.nrelax),
fflush (ferr);
return _s;
}}Multiple unknowns
The msolve() macro takes a list of unknowns and a list
of equations, which must have the same length. It also takes the same
optional arguments as mg_solve() to tune the
multigrid solver. Note however that it does not return the multigrid
statistics but write them in the optional variable given by
mg. This is necessary to be able to use the “ellipsis”
block which can optionally be passed to the macro.
This optional ellipsis block is called after each iteration of the multigrid solver and can be used, for example, to update the coefficients of the system within the solution procedure (for example to deal with non-linear couplings).
The overall solution strategy is similar to that used in solve() and mg_solve(). Aside from the generalisation
to multiple unknowns, the main difference is that this macro does not
require manually splitting the homogeneous and the non-homogeneous parts
of the equations (i.e. the func and rhs
arguments of solve()
respectively).
macro msolve (scalar * X, double * equations,
mgstats * mg = NULL,
int nrelax = 4,
int minlevel = 0,
double tolerance = 1e-3)
{{
mgstats _s = (mgstats){0};
_s.nrelax = nrelax;
scalar * _list = (scalar *) X;
scalar * _lres = list_clone (_list), * _lds = list_clone (_list), * _lrhs = list_clone (_list);
int _len = list_len (_list);
double _resb;We compute the initial residuals in _lres, for each
equation, and store the initial solution in _lds. The
ellipsis block is inserted before this evaluation.
{
{...}
double maxres = 0.;
foreach (reduction(max:maxres)) {
double * R = equations;
int i = 0;
for (scalar res in _lres) {
res[] = R[i++];
if (fabs (res[]) > maxres)
maxres = fabs (res[]);
}
scalar s, ds;
for (s, ds in _list, _lds)
ds[] = s[];
}
_resb = _s.resb = _s.resa = maxres;
}On each level of the multigrid hierarchy, we store in
_lrhs the non-homogeneous part of each equation. This is
done by resetting the vector of unknowns to zero, and re-evaluating each
equation.
int _maxlevel = grid->maxdepth;
int _minlevel = min (minlevel, _maxlevel);
reset (_list, 0.);
for (int l = _minlevel; l <= _maxlevel; l++)
foreach_level (l) {
double * R = equations;
int i = 0;
for (scalar rhs in _lrhs)
rhs[] = R[i++];
}This is the main multigrid iteration loop.
for (_s.i = 0; _s.i < NITERMAX && (_s.i < NITERMIN || _s.resa > tolerance); _s.i++) {We need homogenous boundary conditions on the vector of unknowns, in order to compute the correction to the solution.
for (scalar s in _list)
for (int b = 0; b < nboundary; b++)
s.boundary[b] = s.boundary_homogeneous[b];After having restricted the residual on all levels, we iterate from the coarsest to the finest level. On the coarsest level the initial guess is zero. On all other levels it is bilinearly-interpolated from the coarser level.
restriction (_lres);
for (int _l = _minlevel; _l <= _maxlevel; _l++) {
if (_l == _minlevel)
foreach_level_or_leaf (_l)
for (scalar s in _list)
s[] = 0.;
else
foreach_level (_l)
for (scalar s in _list)
s[] = bilinear (point, s);
boundary_level (_list, _l);This is the relaxation loop.
for (int _iter = 0; _iter < _s.nrelax; _iter++) {
foreach_level_or_leaf (_l) {We first evaluate the off-diagonal and non-homogeneous terms
_R of each equation, by setting all diagonal unknowns to
zero.
for (scalar _s in _list)
_s[] = 0.;
double * _R = equations, _D[_len][_len];We then compute the matrix _D of diagonal coefficients
for each unknown and each equation, by setting each diagonal unknown to
one for each equation. To get only the diagonal coefficient we need to
substract the _R vector we just computed.
int _k = 0;
for (scalar _s in _list) {
_s[] = 1.;
double * _r = equations;
for (int j = 0; j < _len; j++)
_D[_k][j] = _r[j] - _R[j];
_s[] = 0.; _k++;
}The residual for each equation is then computed and stored in
_R.
_k = 0;
scalar rhs, res;
for (rhs, res in _lrhs, _lres)
_R[_k++] += res[] - rhs[];We then solve the resulting system of equations for each diagonal unknown. Note that for the moment we are limited to a maximum of two unknowns, but it should be easy to generalise using e.g. Gauss pivoting etc.
if (_len == 1) {
scalar x0 = _list[0];
assert (_D[0][0] != 0.);
x0[] = - _R[0]/_D[0][0];
}
else if (_len == 2) {
double det = _D[0][0]*_D[1][1] - _D[0][1]*_D[1][0];
assert (det != 0.);
scalar x0 = _list[0], x1 = _list[1];
x0[] = (_D[1][0]*_R[1] - _D[1][1]*_R[0])/det;
x1[] = (_D[0][1]*_R[0] - _D[0][0]*_R[1])/det;
}
else
assert (false); // not implemented yet
}
boundary_level (_list, _l);
}
}Once we have computed the correction on the finest level, we update the solution and revert to the original (non-homogeneous) boundary conditions.
foreach() {
scalar s, ds;
for (s, ds in _list, _lds)
s[] += ds[], ds[] = s[];
}
{
scalar s, ds;
for (s, ds in _list, _lds)
for (int b = 0; b < nboundary; b++)
s.boundary[b] = ds.boundary[b];
}We then compute the new residual after having applied the optional ellipsis block.
{...}
double maxres = 0.;
foreach (reduction(max:maxres)) {
double * _R = equations;
int i = 0;
for (scalar res in _lres) {
res[] = _R[i++];
if (fabs (res[]) > maxres)
maxres = fabs (res[]);
}
}We tune the number of relaxations, based on the convergence rate.
_s.resa = maxres;
if (_s.resa > tolerance) {
if (_resb/_s.resa < 1.2 && _s.nrelax < 100)
_s.nrelax++;
else if (_resb/_s.resa > 10 && _s.nrelax > 2)
_s.nrelax--;
}
_resb = _s.resa;
}We check for convergence, cleanup and return the multigrid statistics.
_s.minlevel = minlevel;
if (_s.resa > tolerance) {
fprintf (stderr, "src/solve.h:%d: warning: convergence for {", LINENO);
for (scalar a in _list)
fprintf (stderr, "%s%s", a.name, --_len ? "," : "");
fprintf (stderr, "} not reached after %d iterations\n"
" res: %g nrelax: %d\n",
_s.i, _s.resa, _s.nrelax);
fflush (stderr);
}
delete (_lres), free (_lres);
delete (_lds), free (_lds);
delete (_lrhs), free (_lrhs);
if (((long)mg) + 1 != 1) // just to avoid a warning
*mg = _s;
}}