Coverage for pySDC/tutorial/step_5/B_my_first_PFASST_run.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 12:30 +0000

1# --- 

2# jupyter: 

3# jupytext: 

4# formats: py:percent 

5# kernelspec: 

6# display_name: Python 3 

7# name: python3 

8# --- 

9 

10# %% [markdown] 

11# # Part B: My first PFASST run 

12# 

13# With the multistep multilevel hierarchy of [Part A](A_multistep_multilevel_hierarchy), we are ready for PFASST, 

14# starting with the heat equation. One of the most important characteristics of a parallel-in-time method is how it 

15# behaves when more time steps are computed in parallel for the same problem, i.e. with `dt` and `Tend` fixed. So we 

16# run the 16 time steps of $[0, 4]$ with 1, 2, 4, 8 and 16 parallel steps. 

17 

18# %% 

19import matplotlib.pyplot as plt 

20import numpy as np 

21 

22from pySDC.helpers.stats_helper import get_sorted 

23from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI 

24from pySDC.implementations.problem_classes.HeatEquation_ND_FD import heatNd_forced 

25from pySDC.implementations.sweeper_classes.imex_1st_order import imex_1st_order 

26from pySDC.implementations.transfer_classes.TransferMesh import mesh_to_mesh 

27 

28# initialize level parameters 

29level_params = {'restol': 1e-10, 'dt': 0.25} 

30 

31# initialize sweeper parameters 

32sweeper_params = { 

33 'quad_type': 'RADAU-RIGHT', 

34 'num_nodes': [3], 

35 'QI': 'LU', # For the IMEX sweeper, the LU-trick can be activated for the implicit part 

36} 

37 

38# initialize problem parameters 

39problem_params = { 

40 'nu': 0.1, # diffusion coefficient 

41 'freq': 8, # frequency for the test value 

42 'nvars': [511, 255], # number of degrees of freedom for each level 

43 'bc': 'dirichlet-zero', # boundary conditions 

44} 

45 

46# initialize step parameters 

47step_params = {'maxiter': 50} 

48 

49# initialize space transfer parameters 

50space_transfer_params = {'rorder': 2, 'iorder': 6} 

51 

52# initialize controller parameters 

53controller_params = { 

54 'logger_level': 30, 

55 'predict_type': 'pfasst_burnin', # PFASST's coarse-level predictor, see step 4, part C 

56} 

57 

58# fill description dictionary for easy step instantiation 

59description = { 

60 'problem_class': heatNd_forced, # pass problem class 

61 'problem_params': problem_params, # pass problem parameters 

62 'sweeper_class': imex_1st_order, # pass sweeper 

63 'sweeper_params': sweeper_params, # pass sweeper parameters 

64 'level_params': level_params, # pass level parameters 

65 'step_params': step_params, # pass step parameters 

66 'space_transfer_class': mesh_to_mesh, # pass spatial transfer class 

67 'space_transfer_params': space_transfer_params, # pass parameters for spatial transfer 

68} 

69 

70# set time parameters 

71t0 = 0.0 

72Tend = 4.0 

73 

74# set up list of parallel time-steps to run PFASST with 

75nsteps = int(Tend / level_params['dt']) 

76num_proc_list = [2**i for i in range(int(np.log2(nsteps) + 1))] 

77print('numbers of parallel time steps:', num_proc_list) 

78 

79# %% [markdown] 

80# ## The runs 

81# 

82# One controller for each number of parallel steps; the rest is what we did in [Step 3](../step_3), for each run: 

83# the error, and statistics of the iteration counts. 

84 

85# %% tags=["scroll-output"] 

86results = {} 

87# loop over different number of processes and check results 

88for num_proc in num_proc_list: 

89 print('Working with %2i processes...' % num_proc) 

90 # instantiate controller 

91 controller = controller_nonMPI(num_procs=num_proc, controller_params=controller_params, description=description) 

92 

93 # get initial values on finest level 

94 P = controller.MS[0].levels[0].prob 

95 uinit = P.u_exact(t0) 

96 

97 # call main function to get things done... 

98 uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend) 

99 

100 # compute exact solution and compare 

101 uex = P.u_exact(Tend) 

102 err = abs(uex - uend) 

103 

104 # filter statistics by type (number of iterations) 

105 iter_counts = get_sorted(stats, type='niter', sortby='time') 

106 

107 # compute and print statistics 

108 for item in iter_counts: 

109 print('Number of iterations for time %4.2f: %2i' % item) 

110 print() 

111 niters = np.array([item[1] for item in iter_counts]) 

112 print(' Mean number of iterations: %4.2f' % np.mean(niters)) 

113 print(' Range of values for number of iterations: %2i ' % np.ptp(niters)) 

114 print(' Position of max/min number of iterations: %2i -- %2i' % (int(np.argmax(niters)), int(np.argmin(niters)))) 

115 print(' Std and var for number of iterations: %4.2f -- %4.2f' % (float(np.std(niters)), float(np.var(niters)))) 

116 print() 

117 print() 

118 

119 results[num_proc] = (err, niters) 

120 

121# %% [markdown] 

122# The iteration counts of all runs at a glance, one row per number of parallel steps: 

123 

124# %% tags=["hide-input"] 

125fig, ax = plt.subplots(figsize=(8, 2.6), constrained_layout=True) 

126image = ax.imshow([results[n][1] for n in num_proc_list], cmap='viridis', aspect='auto', vmin=0) 

127ax.set_yticks(range(len(num_proc_list))) 

128ax.set_yticklabels(num_proc_list) 

129ax.set_ylabel('parallel steps') 

130ax.set_xlabel('time step') 

131fig.colorbar(image, label='iterations') 

132 

133# %% [markdown] 

134# PFASST performs very well here: whether the 16 steps are computed one after another or all at once, they need 

135# about 5 iterations each. 

136# 

137# :::{admonition} Important things to note 

138# - In the IMEX sweeper, `QI = 'LU'` activates the LU-trick for the implicit part. For stiff parabolic problems 

139# with Gauss-Radau nodes, this is usually a very good idea. 

140# - As usual for MLSDC and PFASST, success depends heavily on the parameters. A more complicated problem, a more 

141# or less stiff one, a different order of the spatial interpolation, etc. can give completely different results. 

142# [Part C](C_advection_and_PFASST) has an example. 

143# ::: 

144# 

145# The checks the tests run, for every number of parallel steps: 

146 

147# %% 

148for err, niters in results.values(): 

149 assert err < 1.3505e-04, f"ERROR: error is too high, got {err}" 

150 assert np.ptp(niters) <= 1, f"ERROR: range of number of iterations is too high, got {np.ptp(niters)}" 

151 assert np.mean(niters) <= 5.0, f"ERROR: mean number of iterations is too high, got {np.mean(niters)}"