Matrix-based versions of PFASST

In this project, we use and test two matrix-based version of PFASST for linear problems: we write PFASST as a standard twogrid method and we derive the propagaton matrix of PFASST. Both approaches are compared to the standard multigrid controller of PFASST and with each other. This includes tests with the heat, the advection as well as the test equation.

Matrix-based PFASST

When written as a multigrid method for a linear composite collocation problem, PFASST can be compactly defined via a smoothing and a coarse-grid correction step, just as a standard two-grid method. In the new (and very specialized) controller allinclusive_matrix_nonMPI.py, this concept is exploited to obtain a PFASST (and MLSDC and SDC) controller closely resembling this idea and notation. In compare_to_matrixbased.py, this controller is tested against the standard PFASST implementation.

Full code: pySDC/projects/matrixPFASST/compare_to_matrixbased.py

import numpy as np
from pathlib import Path

from pySDC.helpers.stats_helper import get_sorted

from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI
from pySDC.implementations.problem_classes.AdvectionEquation_ND_FD import advectionNd
from pySDC.implementations.problem_classes.HeatEquation_ND_FD import heatNd_unforced
from pySDC.implementations.problem_classes.TestEquation_0D import testequation0d
from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit
from pySDC.implementations.transfer_classes.TransferMesh import mesh_to_mesh
from pySDC.implementations.transfer_classes.TransferMesh_NoCoarse import mesh_to_mesh as mesh_to_mesh_nocoarse
from pySDC.projects.matrixPFASST.controller_matrix_nonMPI import controller_matrix_nonMPI


def diffusion_setup(par=0.0):
    """
    Setup routine for advection test

    Args:
        par (float): parameter for controlling stiffness
    """
    # initialize level parameters
    level_params = dict()
    level_params['restol'] = 1e-08
    level_params['dt'] = 0.25
    level_params['nsweeps'] = [3, 1]

    # initialize sweeper parameters
    sweeper_params = dict()
    sweeper_params['quad_type'] = 'RADAU-RIGHT'
    sweeper_params['num_nodes'] = 3
    sweeper_params['QI'] = 'LU'  # For the IMEX sweeper, the LU-trick can be activated for the implicit part
    sweeper_params['initial_guess'] = 'spread'

    # initialize problem parameters
    problem_params = dict()
    problem_params['nu'] = par  # diffusion coefficient
    problem_params['freq'] = 4  # frequency for the test value
    problem_params['nvars'] = [127, 63]  # number of degrees of freedom for each level
    problem_params['bc'] = 'dirichlet-zero'  # boundary conditions

    # initialize step parameters
    step_params = dict()
    step_params['maxiter'] = 50

    # initialize space transfer parameters
    space_transfer_params = dict()
    space_transfer_params['rorder'] = 2
    space_transfer_params['iorder'] = 2

    # initialize controller parameters
    controller_params = dict()
    controller_params['logger_level'] = 30
    controller_params['all_to_done'] = True

    # fill description dictionary for easy step instantiation
    description = dict()
    description['problem_class'] = heatNd_unforced  # pass problem class
    description['problem_params'] = problem_params  # pass problem parameters
    description['sweeper_class'] = generic_implicit  # pass sweeper
    description['sweeper_params'] = sweeper_params  # pass sweeper parameters
    description['level_params'] = level_params  # pass level parameters
    description['step_params'] = step_params  # pass step parameters
    description['space_transfer_class'] = mesh_to_mesh  # pass spatial transfer class
    description['space_transfer_params'] = space_transfer_params  # pass paramters for spatial transfer

    return description, controller_params


def advection_setup(par=0.0):
    """
    Setup routine for advection test

    Args:
        par (float): parameter for controlling stiffness
    """
    # initialize level parameters
    level_params = dict()
    level_params['restol'] = 1e-08
    level_params['dt'] = 0.25
    level_params['nsweeps'] = [3, 1]

    # initialize sweeper parameters
    sweeper_params = dict()
    sweeper_params['quad_type'] = 'RADAU-RIGHT'
    sweeper_params['num_nodes'] = [3]
    sweeper_params['QI'] = ['LU']  # For the IMEX sweeper, the LU-trick can be activated for the implicit part
    sweeper_params['initial_guess'] = 'spread'

    # initialize problem parameters
    problem_params = dict()
    problem_params['c'] = par
    problem_params['freq'] = 4  # frequency for the test value
    problem_params['nvars'] = [128, 64]  # number of degrees of freedom for each level
    problem_params['order'] = 2
    problem_params['stencil_type'] = 'center'
    problem_params['bc'] = 'periodic'  # boundary conditions

    # initialize step parameters
    step_params = dict()
    step_params['maxiter'] = 50

    # initialize space transfer parameters
    space_transfer_params = dict()
    space_transfer_params['rorder'] = 2
    space_transfer_params['iorder'] = 2
    space_transfer_params['periodic'] = True

    # initialize controller parameters
    controller_params = dict()
    controller_params['logger_level'] = 30
    controller_params['all_to_done'] = True

    # fill description dictionary for easy step instantiation
    description = dict()
    description['problem_class'] = advectionNd  # pass problem class
    description['problem_params'] = problem_params
    description['sweeper_class'] = generic_implicit  # pass sweeper (see part B)
    description['sweeper_params'] = sweeper_params  # pass sweeper parameters
    description['level_params'] = level_params  # pass level parameters
    description['step_params'] = step_params  # pass step parameters
    description['space_transfer_class'] = mesh_to_mesh  # pass spatial transfer class
    description['space_transfer_params'] = space_transfer_params  # pass paramters for spatial transfer

    return description, controller_params


def testequation_setup():
    """
    Setup routine for the test equation

    Args:
        par (float): parameter for controlling stiffness
    """
    # initialize level parameters
    level_params = dict()
    level_params['restol'] = 1e-08
    level_params['dt'] = 0.25
    level_params['nsweeps'] = [3, 1]

    # initialize sweeper parameters
    sweeper_params = dict()
    sweeper_params['quad_type'] = 'RADAU-RIGHT'
    sweeper_params['num_nodes'] = [3, 2]
    sweeper_params['QI'] = 'LU'
    sweeper_params['initial_guess'] = 'spread'

    # initialize problem parameters
    problem_params = dict()
    problem_params['u0'] = 1.0  # initial value (for all instances)
    # use single values like this...
    # problem_params['lambdas'] = [[-1.0]]
    # .. or a list of values like this ...
    # problem_params['lambdas'] = [[-1.0, -2.0, 1j, -1j]]
    # .. or a whole block of values like this
    ilim_left = -11
    ilim_right = 0
    rlim_left = 0
    rlim_right = 11
    ilam = 1j * np.logspace(ilim_left, ilim_right, 11)
    rlam = -1 * np.logspace(rlim_left, rlim_right, 11)
    lambdas = []
    for rl in rlam:
        for il in ilam:
            lambdas.append(rl + il)
    problem_params['lambdas'] = [lambdas]
    # note: PFASST will do all of those at once, but without interaction (realized via diagonal matrix).
    # The propagation matrix will be diagonal too, corresponding to the respective lambda value.

    # initialize step parameters
    step_params = dict()
    step_params['maxiter'] = 50

    # initialize controller parameters
    controller_params = dict()
    controller_params['logger_level'] = 30
    controller_params['all_to_done'] = True

    # fill description dictionary for easy step instantiation
    description = dict()
    description['problem_class'] = testequation0d  # pass problem class
    description['problem_params'] = problem_params  # pass problem parameters
    description['sweeper_class'] = generic_implicit  # pass sweeper
    description['sweeper_params'] = sweeper_params  # pass sweeper parameters
    description['level_params'] = level_params  # pass level parameters
    description['step_params'] = step_params  # pass step parameters
    description['space_transfer_class'] = mesh_to_mesh_nocoarse  # pass spatial transfer class
    description['space_transfer_params'] = dict()  # pass paramters for spatial transfer

    return description, controller_params


def compare_controllers(type=None, par=0.0, f=None):
    """
    A simple test program to compare PFASST runs with matrix-based and matrix-free controllers

    Args:
        type (str): setup type
        par (float) parameter for controlling stiffness
        f: file handler
    """

    # set time parameters
    t0 = 0.0
    Tend = 1.0

    if type == 'diffusion':
        description, controller_params = diffusion_setup(par)
    elif type == 'advection':
        description, controller_params = advection_setup(par)
    elif type == 'testequation':
        description, controller_params = testequation_setup()
    else:
        raise ValueError('No valis setup type provided, aborting..')

    out = '\nWorking with %s setup and parameter %3.1e..' % (type, par)
    f.write(out + '\n')
    print(out)

    # instantiate controller
    controller_mat = controller_matrix_nonMPI(num_procs=4, controller_params=controller_params, description=description)

    controller_nomat = controller_nonMPI(num_procs=4, controller_params=controller_params, description=description)

    # get initial values on finest level
    P = controller_nomat.MS[0].levels[0].prob
    uinit = P.u_exact(t0)
    uex = P.u_exact(Tend)

    # this is where the iteration is happening
    uend_mat, stats_mat = controller_mat.run(u0=uinit, t0=t0, Tend=Tend)
    uend_nomat, stats_nomat = controller_nomat.run(u0=uinit, t0=t0, Tend=Tend)

    diff = abs(uend_mat - uend_nomat)

    err_mat = abs(uend_mat - uex)
    err_nomat = abs(uend_nomat - uex)

    out = '  Error (mat/nomat) vs. exact solution: %6.4e -- %6.4e' % (err_mat, err_nomat)
    f.write(out + '\n')
    print(out)
    out = '  Difference between both results: %6.4e' % diff
    f.write(out + '\n')
    print(out)

    assert diff < 2.3e-15, 'ERROR: difference between matrix-based and matrix-free result is too large, got %s' % diff

    # get and convert statistics to list of iterations count, sorted by process
    iter_counts_mat = get_sorted(stats_mat, type='niter', sortby='time')
    iter_counts_nomat = get_sorted(stats_nomat, type='niter', sortby='time')

    out = '  Iteration counts for matrix-based version: %s' % iter_counts_mat
    f.write(out + '\n')
    print(out)
    out = '  Iteration counts for matrix-free version: %s' % iter_counts_nomat
    f.write(out + '\n')
    print(out)

    assert (
        iter_counts_nomat == iter_counts_mat
    ), 'ERROR: number of iterations differ between matrix-based and matrix-free controller'


def main():
    par_list = [1e-02, 1.0, 1e02]

    Path("data").mkdir(parents=True, exist_ok=True)
    f = open('data/comparison_matrix_vs_nomat_detail.txt', 'w')
    for par in par_list:
        compare_controllers(type='diffusion', par=par, f=f)
        compare_controllers(type='advection', par=par, f=f)
    compare_controllers(type='testequation', par=0.0, f=f)
    f.close()


if __name__ == "__main__":
    main()

Results:


Working with diffusion setup and parameter 1.0e-02..
  Error (mat/nomat) vs. exact solution: 4.0681e-07 -- 4.0681e-07
  Difference between both results: 1.1102e-15
  Iteration counts for matrix-based version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]
  Iteration counts for matrix-free version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]

Working with advection setup and parameter 1.0e-02..
  Error (mat/nomat) vs. exact solution: 2.0169e-04 -- 2.0169e-04
  Difference between both results: 4.4409e-16
  Iteration counts for matrix-based version: [(0.0, 2), (0.25, 2), (0.5, 2), (0.75, 2)]
  Iteration counts for matrix-free version: [(0.0, 2), (0.25, 2), (0.5, 2), (0.75, 2)]

Working with diffusion setup and parameter 1.0e+00..
  Error (mat/nomat) vs. exact solution: 5.8573e-06 -- 5.8573e-06
  Difference between both results: 2.1781e-18
  Iteration counts for matrix-based version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]
  Iteration counts for matrix-free version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]

Working with advection setup and parameter 1.0e+00..
  Error (mat/nomat) vs. exact solution: 2.9363e-01 -- 2.9363e-01
  Difference between both results: 1.4433e-15
  Iteration counts for matrix-based version: [(0.0, 7), (0.25, 7), (0.5, 7), (0.75, 7)]
  Iteration counts for matrix-free version: [(0.0, 7), (0.25, 7), (0.5, 7), (0.75, 7)]

Working with diffusion setup and parameter 1.0e+02..
  Error (mat/nomat) vs. exact solution: 3.2887e-13 -- 3.2887e-13
  Difference between both results: 9.7511e-22
  Iteration counts for matrix-based version: [(0.0, 2), (0.25, 2), (0.5, 2), (0.75, 2)]
  Iteration counts for matrix-free version: [(0.0, 2), (0.25, 2), (0.5, 2), (0.75, 2)]

Working with advection setup and parameter 1.0e+02..
  Error (mat/nomat) vs. exact solution: 1.0000e+00 -- 1.0000e+00
  Difference between both results: 4.3996e-17
  Iteration counts for matrix-based version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]
  Iteration counts for matrix-free version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]

Working with testequation setup and parameter 0.0e+00..
  Error (mat/nomat) vs. exact solution: 5.7992e-06 -- 5.7992e-06
  Difference between both results: 5.0019e-16
  Iteration counts for matrix-based version: [(0.0, 4), (0.25, 4), (0.5, 4), (0.75, 4)]
  Iteration counts for matrix-free version: [(0.0, 4), (0.25, 4), (0.5, 4), (0.75, 4)]

Propagator-based PFASST

The second approach follows directly from the matrix formulation: instead of writing PFASST as an iterative scheme, we now derive the full propagation matrix, which takes the initial value u0 and produces the final value uend, over all steps, iterations and sweeps. This matrix can be used to analyze PFASST in yet another exciting way.

Full code: pySDC/projects/matrixPFASST/compare_to_propagator.py

import numpy as np
from pathlib import Path

from pySDC.helpers.stats_helper import get_sorted

from pySDC.implementations.problem_classes.AdvectionEquation_ND_FD import advectionNd
from pySDC.implementations.problem_classes.HeatEquation_ND_FD import heatNd_unforced
from pySDC.implementations.problem_classes.TestEquation_0D import testequation0d
from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit
from pySDC.implementations.transfer_classes.TransferMesh import mesh_to_mesh
from pySDC.implementations.transfer_classes.TransferMesh_NoCoarse import mesh_to_mesh as mesh_to_mesh_nocoarse
from pySDC.projects.matrixPFASST.controller_matrix_nonMPI import controller_matrix_nonMPI


def diffusion_setup(par=0.0):
    """
    Setup routine for advection test

    Args:
        par (float): parameter for controlling stiffness
    """
    # initialize level parameters
    level_params = dict()
    level_params['restol'] = 1e-08
    level_params['dt'] = 0.25
    level_params['nsweeps'] = [3, 1]

    # initialize sweeper parameters
    sweeper_params = dict()
    sweeper_params['quad_type'] = 'RADAU-RIGHT'
    sweeper_params['num_nodes'] = 3
    sweeper_params['QI'] = 'LU'
    sweeper_params['initial_guess'] = 'spread'

    # initialize problem parameters
    problem_params = dict()
    problem_params['nu'] = par  # diffusion coefficient
    problem_params['freq'] = 4  # frequency for the test value
    problem_params['nvars'] = [127]  # number of degrees of freedom for each level
    problem_params['bc'] = 'dirichlet-zero'  # boundary conditions

    # initialize step parameters
    step_params = dict()
    step_params['maxiter'] = 50

    # initialize space transfer parameters
    space_transfer_params = dict()
    space_transfer_params['rorder'] = 2
    space_transfer_params['iorder'] = 2

    # initialize controller parameters
    controller_params = dict()
    controller_params['logger_level'] = 30

    # fill description dictionary for easy step instantiation
    description = dict()
    description['problem_class'] = heatNd_unforced  # pass problem class
    description['problem_params'] = problem_params  # pass problem parameters
    description['sweeper_class'] = generic_implicit  # pass sweeper
    description['sweeper_params'] = sweeper_params  # pass sweeper parameters
    description['level_params'] = level_params  # pass level parameters
    description['step_params'] = step_params  # pass step parameters
    description['space_transfer_class'] = mesh_to_mesh  # pass spatial transfer class
    description['space_transfer_params'] = space_transfer_params  # pass paramters for spatial transfer

    return description, controller_params


def advection_setup(par=0.0):
    """
    Setup routine for advection test

    Args:
        par (float): parameter for controlling stiffness
    """
    # initialize level parameters
    level_params = dict()
    level_params['restol'] = 1e-08
    level_params['dt'] = 0.25
    level_params['nsweeps'] = [3, 1]

    # initialize sweeper parameters
    sweeper_params = dict()
    sweeper_params['quad_type'] = 'RADAU-RIGHT'
    sweeper_params['num_nodes'] = [3]
    sweeper_params['QI'] = ['LU']
    sweeper_params['initial_guess'] = 'spread'

    # initialize problem parameters
    problem_params = dict()
    problem_params['c'] = par
    problem_params['freq'] = 4  # frequency for the test value
    problem_params['nvars'] = [128, 64]  # number of degrees of freedom for each level
    problem_params['order'] = 2
    problem_params['stencil_type'] = 'center'
    problem_params['bc'] = 'periodic'  # boundary conditions

    # initialize step parameters
    step_params = dict()
    step_params['maxiter'] = 50

    # initialize space transfer parameters
    space_transfer_params = dict()
    space_transfer_params['rorder'] = 2
    space_transfer_params['iorder'] = 2
    space_transfer_params['periodic'] = True

    # initialize controller parameters
    controller_params = dict()
    controller_params['logger_level'] = 30

    # fill description dictionary for easy step instantiation
    description = dict()
    description['problem_class'] = advectionNd  # pass problem class
    description['problem_params'] = problem_params
    description['sweeper_class'] = generic_implicit  # pass sweeper (see part B)
    description['sweeper_params'] = sweeper_params  # pass sweeper parameters
    description['level_params'] = level_params  # pass level parameters
    description['step_params'] = step_params  # pass step parameters
    description['space_transfer_class'] = mesh_to_mesh  # pass spatial transfer class
    description['space_transfer_params'] = space_transfer_params  # pass paramters for spatial transfer

    return description, controller_params


def scalar_equation_setup():
    """
    Setup routine for the test equation

    Args:
        par (float): parameter for controlling stiffness
    """
    # initialize level parameters
    level_params = dict()
    level_params['restol'] = 1e-08
    level_params['dt'] = 0.25
    level_params['nsweeps'] = [3, 1]

    # initialize sweeper parameters
    sweeper_params = dict()
    sweeper_params['quad_type'] = 'RADAU-RIGHT'
    sweeper_params['num_nodes'] = [3, 2]
    sweeper_params['QI'] = 'LU'
    sweeper_params['initial_guess'] = 'spread'

    # initialize problem parameters
    problem_params = dict()
    problem_params['u0'] = 1.0  # initial value (for all instances)
    # use single values like this...
    # problem_params['lambdas'] = [[-1.0]]
    # .. or a list of values like this ...
    # problem_params['lambdas'] = [[-1.0, -2.0, 1j, -1j]]
    # .. or a whole block of values like this
    ilim_left = -11
    ilim_right = 0
    rlim_left = 0
    rlim_right = 11
    ilam = 1j * np.logspace(ilim_left, ilim_right, 11)
    rlam = -1 * np.logspace(rlim_left, rlim_right, 11)
    lambdas = []
    for rl in rlam:
        for il in ilam:
            lambdas.append(rl + il)
    problem_params['lambdas'] = [lambdas]
    # note: PFASST will do all of those at once, but without interaction (realized via diagonal matrix).
    # The propagation matrix will be diagonal too, corresponding to the respective lambda value.

    # initialize step parameters
    step_params = dict()
    step_params['maxiter'] = 50

    # initialize controller parameters
    controller_params = dict()
    controller_params['logger_level'] = 30

    # fill description dictionary for easy step instantiation
    description = dict()
    description['problem_class'] = testequation0d  # pass problem class
    description['problem_params'] = problem_params  # pass problem parameters
    description['sweeper_class'] = generic_implicit  # pass sweeper
    description['sweeper_params'] = sweeper_params  # pass sweeper parameters
    description['level_params'] = level_params  # pass level parameters
    description['step_params'] = step_params  # pass step parameters
    description['space_transfer_class'] = mesh_to_mesh_nocoarse  # pass spatial transfer class
    description['space_transfer_params'] = dict()  # pass paramters for spatial transfer

    return description, controller_params


def compare_controllers(type=None, par=0.0, f=None):
    """
    A simple test program to compare PFASST runs with matrix-based and matrix-free controllers

    Args:
        type (str): setup type
        par (float): parameter for controlling stiffness
        f: file handler
    """

    # set time parameters
    t0 = 0.0
    Tend = 1.0

    if type == 'diffusion':
        description, controller_params = diffusion_setup(par)
    elif type == 'advection':
        description, controller_params = advection_setup(par)
    elif type == 'testequation':
        description, controller_params = scalar_equation_setup()
    else:
        raise ValueError('No valis setup type provided, aborting..')

    out = '\nWorking with %s setup and parameter %3.1e..' % (type, par)
    f.write(out + '\n')
    print(out)

    # instantiate controller
    controller = controller_matrix_nonMPI(num_procs=4, controller_params=controller_params, description=description)
    # get initial values on finest level
    P = controller.MS[0].levels[0].prob
    uinit = P.u_exact(t0)
    uex = P.u_exact(Tend)

    # this is where the iteration is happening
    uend_mat, stats_mat = controller.run(u0=uinit, t0=t0, Tend=Tend)

    # filter statistics by type (number of iterations)
    iter_counts_mat = get_sorted(stats_mat, type='niter', sortby='time')

    out = '  Iteration counts for matrix-based version: %s' % iter_counts_mat
    f.write(out + '\n')
    print(out)

    # filter only iteration counts and check for equality
    niters = [item[1] for item in iter_counts_mat]
    assert niters.count(niters[0]) == len(niters), 'ERROR: not all time-steps have the same number of iterations'
    niter = niters[0]

    # build propagation matrix using the prescribed number of iterations (or any other, if needed)
    prop = controller.build_propagation_matrix(niter=niter)

    err_prop_ex = np.linalg.norm(prop.dot(uinit) - uex)
    err_mat_ex = np.linalg.norm(uend_mat - uex)
    out = '  Error (mat/prop) vs. exact solution: %6.4e -- %6.4e' % (err_mat_ex, err_prop_ex)
    f.write(out + '\n')
    print(out)
    err_mat_prop = np.linalg.norm(prop.dot(uinit) - uend_mat)
    out = '  Difference between matrix-PFASST and propagator: %6.4e' % err_mat_prop
    f.write(out + '\n')
    print(out)

    assert err_mat_prop < 2.0e-14, (
        'ERROR: difference between matrix-based and propagator result is too large, got %s' % err_mat_prop
    )


def main():
    par_list = [1e-02, 1.0, 1e02]

    Path("data").mkdir(parents=True, exist_ok=True)
    f = open('data/comparison_matrix_vs_propagator_detail.txt', 'w')
    for par in par_list:
        compare_controllers(type='diffusion', par=par, f=f)
        compare_controllers(type='advection', par=par, f=f)
    compare_controllers(type='testequation', par=0.0, f=f)
    f.close()


if __name__ == "__main__":
    main()

Results:


Working with diffusion setup and parameter 1.0e-02..
  Iteration counts for matrix-based version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]
  Error (mat/prop) vs. exact solution: 3.2542e-06 -- 3.2542e-06
  Difference between matrix-PFASST and propagator: 3.0509e-15

Working with advection setup and parameter 1.0e-02..
  Iteration counts for matrix-based version: [(0.0, 2), (0.25, 2), (0.5, 2), (0.75, 2)]
  Error (mat/prop) vs. exact solution: 1.6141e-03 -- 1.6141e-03
  Difference between matrix-PFASST and propagator: 1.7400e-15

Working with diffusion setup and parameter 1.0e+00..
  Iteration counts for matrix-based version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]
  Error (mat/prop) vs. exact solution: 4.6858e-05 -- 4.6858e-05
  Difference between matrix-PFASST and propagator: 9.2885e-18

Working with advection setup and parameter 1.0e+00..
  Iteration counts for matrix-based version: [(0.0, 7), (0.25, 7), (0.5, 7), (0.75, 7)]
  Error (mat/prop) vs. exact solution: 2.3515e+00 -- 2.3515e+00
  Difference between matrix-PFASST and propagator: 3.8462e-15

Working with diffusion setup and parameter 1.0e+02..
  Iteration counts for matrix-based version: [(0.0, 2), (0.25, 2), (0.5, 2), (0.75, 2)]
  Error (mat/prop) vs. exact solution: 2.6312e-12 -- 2.6312e-12
  Difference between matrix-PFASST and propagator: 9.8449e-21

Working with advection setup and parameter 1.0e+02..
  Iteration counts for matrix-based version: [(0.0, 3), (0.25, 3), (0.5, 3), (0.75, 3)]
  Error (mat/prop) vs. exact solution: 8.0000e+00 -- 8.0000e+00
  Difference between matrix-PFASST and propagator: 8.4603e-16

Working with testequation setup and parameter 0.0e+00..
  Iteration counts for matrix-based version: [(0.0, 4), (0.25, 4), (0.5, 4), (0.75, 4)]
  Error (mat/prop) vs. exact solution: 2.0384e-05 -- 2.0384e-05
  Difference between matrix-PFASST and propagator: 3.4674e-16