Part B: Multilevel hierarchy#

How does a step get several levels? From the description: wherever a problem, sweeper or level parameter is a list instead of a single value, the step creates one level per entry, the first entry being the finest level. There are two generic ways to coarsen:

  • in space: make the problem parameter nvars a list,

  • in the collocation order: make the sweeper parameter num_nodes a list.

A third way is to make an entry of the description itself a list, e.g. a list of problem classes; we use that in Part D.

from pySDC.core.step import Step
from pySDC.implementations.problem_classes.HeatEquation_ND_FD import heatNd_unforced
from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit
from pySDC.implementations.transfer_classes.TransferMesh import mesh_to_mesh

# initialize level parameters
level_params = {'restol': 1e-10, 'dt': 0.1}

# initialize sweeper parameters
sweeper_params = {
    'quad_type': 'RADAU-RIGHT',
    'num_nodes': [5, 3],  # number of collocation nodes for each level
    'QI': 'LU',
}

# initialize problem parameters
problem_params = {
    'nu': 0.1,  # diffusion coefficient
    'freq': 4,  # frequency for the test value
    'nvars': [31, 15, 7],  # number of degrees of freedom for each level
    'bc': 'dirichlet-zero',  # boundary conditions
}

# initialize step parameters
step_params = {'maxiter': 20}

# initialize space transfer parameters
space_transfer_params = {'rorder': 2, 'iorder': 2}

# fill description dictionary for easy step instantiation
description = {
    'problem_class': heatNd_unforced,
    'problem_params': problem_params,
    'sweeper_class': generic_implicit,
    'sweeper_params': sweeper_params,
    'level_params': level_params,
    'step_params': step_params,
    'space_transfer_class': mesh_to_mesh,  # the transfer between levels is part of the description, too
    'space_transfer_params': space_transfer_params,
}

# now the description contains more or less everything we need to create a step with multiple levels
S = Step(description=description)

During the setup, dictionaries with list entries are turned into lists of dictionaries, one for each level:

for l in range(len(S.levels)):
    L = S.levels[l]
    print('Level %2i: nvars = %4i -- nnodes = %2i' % (l, L.prob.nvars[0], L.sweep.coll.num_nodes))
Level  0: nvars =   31 -- nnodes =  5
Level  1: nvars =   15 -- nnodes =  3
Level  2: nvars =    7 -- nnodes =  3

Three levels, although num_nodes has only two entries: the longest list decides how many levels there are, and levels beyond the end of a shorter list get its last entry. That is why levels 1 and 2 both have 3 nodes.

Important things to note

  • Not all lists need the same length: the longest defines the number of levels, shorter ones are extended with their last entry.

  • Like most other parameters, space_transfer_class and space_transfer_params are part of the description.

  • For advanced users: base_transfer_params passes parameters to the class that ties two levels together (BaseTransfer, which uses the space transfer class), and base_transfer_class replaces that class.

The checks the tests run:

for l in range(len(S.levels)):
    L = S.levels[l]
    assert (
        L.prob.nvars[0] == problem_params['nvars'][min(l, len(problem_params['nvars']) - 1)]
    ), f"ERROR: number of DOFs is not correct on this level, got {L.prob.nvars}"
    assert (
        L.sweep.coll.num_nodes == sweeper_params['num_nodes'][min(l, len(sweeper_params['num_nodes']) - 1)]
    ), f"ERROR: number of nodes is not correct on this level, got {L.sweep.coll.num_nodes}"