MultiGrid¶
The MultiGrid class creates a hierarchy of grids at progressively coarser
resolutions over the same domain. This is the data structure needed for
multigrid iterative solvers, which accelerate convergence of elliptic PDE
solvers by transferring corrections between resolution levels.
from numgrids import *
import numpy as np
Creating a MultiGrid¶
Pass the axes for the finest grid and an optional min_size parameter that
sets the minimum number of points per axis on the coarsest level:
ax_x = create_axis(AxisType.EQUIDISTANT, 64, 0.0, 1.0)
ax_y = create_axis(AxisType.EQUIDISTANT, 64, 0.0, 1.0)
mg = MultiGrid(ax_x, ax_y, min_size=4)
Starting from the finest grid (64 x 64), the constructor repeatedly halves
each axis until one of them would drop below min_size. The default
min_size is 2.
Accessing levels¶
The levels property returns a list of Grid objects, ordered from finest
to coarsest:
for i, level in enumerate(mg.levels):
print(f"Level {i}: shape = {level.shape}")
Output:
Level 0: shape = (64, 64)
Level 1: shape = (32, 32)
Level 2: shape = (16, 16)
Level 3: shape = (8, 8)
Level 4: shape = (4, 4)
mg.levels[0]is always the finest grid (the one you passed in).mg.levels[-1]is the coarsest grid.
Transferring data between levels¶
The transfer() method moves an array from one level to an adjacent level
using interpolation:
# Sample a function on the finest grid
X, Y = mg.levels[0].meshed_coords
f_fine = np.sin(np.pi * X) * np.cos(np.pi * Y)
# Restrict (fine -> coarse): level 0 -> level 1
f_coarse = mg.transfer(f_fine, level_from=0, level_to=1)
print(f_coarse.shape) # (32, 32)
# Prolongate (coarse -> fine): level 1 -> level 0
f_back = mg.transfer(f_coarse, level_from=1, level_to=0)
print(f_back.shape) # (64, 64)
Note
Transfers are only allowed between adjacent levels (i.e.
|level_from - level_to| == 1). To move data across multiple levels,
chain the transfers.
The method parameter controls the interpolation order used for the
transfer (default "linear"):
f_coarse = mg.transfer(f_fine, level_from=0, level_to=1, method="cubic")
Mixed axis types¶
MultiGrid supports all axis types. Each level preserves the axis type,
domain bounds, periodicity, and name of the original axes:
mg = MultiGrid(
create_axis(AxisType.CHEBYSHEV, 32, 0.0, 1.0, name="x"),
create_axis(AxisType.EQUIDISTANT_PERIODIC, 64, 0.0, 2 * np.pi, name="theta"),
min_size=4,
)
for i, level in enumerate(mg.levels):
ax0 = level.get_axis(0)
ax1 = level.get_axis(1)
print(f"Level {i}: {type(ax0).__name__}({len(ax0)}), "
f"{type(ax1).__name__}({len(ax1)}, periodic={ax1.periodic})")
Use case: multigrid solver skeleton¶
A typical V-cycle multigrid solver follows this pattern:
Restrict the residual from the fine grid to the coarse grid.
Solve (or smooth) on the coarse grid.
Prolongate the correction back to the fine grid.
Update the fine-grid solution.
MultiGrid provides the grid hierarchy and the transfer() method for
steps 1 and 3. The smoothing and solving steps are problem-specific and left
to the user.
# Pseudocode for a single V-cycle
residual = compute_residual(mg.levels[0], u, rhs)
# Restrict residual to each coarser level
r = [residual]
for k in range(len(mg.levels) - 1):
r.append(mg.transfer(r[-1], k, k + 1))
# Solve on the coarsest level
e = solve_coarse(mg.levels[-1], r[-1])
# Prolongate corrections back up
for k in range(len(mg.levels) - 2, -1, -1):
e = mg.transfer(e, k + 1, k)
u = u + e
u = smooth(mg.levels[k], u, rhs)