Coverage for pySDC/tutorial/step_3/A_getting_statistics.py: 100%

42 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 A: Getting statistics 

12# 

13# We run the heat equation from [Step 2](../step_2/C_using_pySDCs_frontend) again, now for eight time steps, and 

14# look at what the controller returns besides the solution: the **statistics**. pySDC records residuals, iteration 

15# counts, timings and more while it runs, and this is how we find out what happened. 

16 

17# %% 

18import matplotlib.pyplot as plt 

19import numpy as np 

20 

21from pySDC.helpers.stats_helper import get_list_of_types, get_sorted 

22from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI 

23from pySDC.implementations.problem_classes.HeatEquation_ND_FD import heatNd_forced 

24from pySDC.implementations.sweeper_classes.imex_1st_order import imex_1st_order 

25 

26# initialize level parameters 

27level_params = {'restol': 1e-10, 'dt': 0.1} 

28 

29# initialize sweeper parameters 

30sweeper_params = {'quad_type': 'RADAU-RIGHT', 'num_nodes': 3} 

31 

32# initialize problem parameters 

33problem_params = { 

34 'nu': 0.1, # diffusion coefficient 

35 'freq': 4, # frequency for the test value 

36 'nvars': 1023, # number of degrees of freedom 

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

38} 

39 

40# initialize step parameters 

41step_params = {'maxiter': 20} 

42 

43# initialize controller parameters (<-- this is new!) 

44controller_params = {'logger_level': 30} # reduce verbosity of each run 

45 

46# Fill description dictionary for easy hierarchy creation 

47description = { 

48 'problem_class': heatNd_forced, 

49 'problem_params': problem_params, 

50 'sweeper_class': imex_1st_order, 

51 'sweeper_params': sweeper_params, 

52 'level_params': level_params, 

53 'step_params': step_params, 

54} 

55 

56# instantiate the controller 

57controller = controller_nonMPI(num_procs=1, controller_params=controller_params, description=description) 

58 

59# set time parameters 

60t0 = 0.1 

61Tend = 0.9 

62 

63# get initial values on finest level 

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

65uinit = P.u_exact(t0) 

66 

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

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

69 

70# %% [markdown] 

71# The controller parameter `logger_level` controls how much the run prints. It takes the 

72# [levels of Python's logging module](https://docs.python.org/3/library/logging.html#logging-levels): 20 (`INFO`, 

73# the default) shows every iteration, 30 (`WARNING`) only what goes wrong. That is why this run was silent. 

74# 

75# ## What is in `stats` 

76# 

77# `stats` is a dictionary. Each value is one number, and its key says where and when that number was recorded: 

78 

79# %% 

80key = next(iter(stats)) 

81print(f'{len(stats)} entries, for example') 

82print(f' key: {key}') 

83print(f' value: {stats[key]}') 

84 

85# %% [markdown] 

86# The keys are tuples that record the process, the time, the level, the iteration, the sweep, the type of the 

87# value and more, so every value carries a kind of time stamp. This makes `stats` a little complex, but it also 

88# means that anyone can add their own entries without changing its definition, as we do in 

89# [Part B](B_adding_statistics). Which types a run recorded, including those added by users, `get_list_of_types` 

90# tells: 

91 

92# %% 

93print('List of registered statistic types:', get_list_of_types(stats)) 

94 

95# %% [markdown] 

96# The entries are made by **hooks**, see the `Hooks` class in `pySDC/core/hooks.py` and the default ones in 

97# `pySDC/core/default_hook.py`. 

98# 

99# ## Filtering and sorting 

100# 

101# `get_sorted` picks the entries that match the keywords we give it, and returns them sorted by one field of the key, 

102# as a list of tuples (value of that field, value). Here: all residuals recorded in the first time step, sorted by 

103# iteration. 

104 

105# %% 

106# filter statistics by first time interval and type (residual) 

107residuals = get_sorted(stats, time=0.1, type='residual_post_iteration', sortby='iter') 

108 

109for item in residuals: 

110 print('Residual in iteration %2i: %8.4e' % item) 

111 

112# %% [markdown] 

113# And the number of iterations for every time step, sorted by time. `get_sorted` is a shortcut for 

114# `sort_stats(filter_stats(...))`, with the same arguments. 

115 

116# %% 

117# get and convert filtered statistics to list of iterations count, sorted by time 

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

119 

120for item in iter_counts: 

121 print('Number of iterations at time %4.2f: %2i' % item) 

122 

123# %% [markdown] 

124# :::{warning} 

125# Filtering compares exactly. The first step starts at `t0 = 0.1`, which is stored as given, but later times are 

126# sums of time steps: the third step starts at `0.30000000000000004`, so `time=0.3` finds nothing. To pick a later 

127# step, sort by time and compare with a tolerance instead: 

128# ::: 

129 

130# %% 

131print('filtered with time=0.3:', get_sorted(stats, time=0.3, type='niter')) 

132print('compared with np.isclose:', [item for item in iter_counts if np.isclose(item[0], 0.3)]) 

133 

134# %% [markdown] 

135# With a little more filtering we can see all eight steps converge the same way: 

136 

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

138fig, ax = plt.subplots(figsize=(6, 3.5)) 

139for time, _ in iter_counts: 

140 # the exact time of each step, straight from the keys, so no filtering by computed times 

141 residual = sorted( 

142 (key.iter, value) for key, value in stats.items() if key.type == 'residual_post_iteration' and key.time == time 

143 ) 

144 ax.semilogy(*zip(*residual, strict=True), label=f'$t = {time:.1f}$') 

145ax.axhline(level_params['restol'], color='k', ls='--') 

146ax.set_xlabel('iteration') 

147ax.set_ylabel('residual') 

148ax.legend(frameon=False, ncol=2, fontsize=8) 

149ax.grid(alpha=0.3) 

150fig.tight_layout() 

151 

152# %% [markdown] 

153# ## Summary 

154# 

155# - The controller returns `stats`, a dictionary whose keys record where and when each value was taken. 

156# - `get_list_of_types` shows what is in there; `get_sorted` filters and sorts it. 

157# - Filter by time only with times you did not compute; otherwise, compare with a tolerance. 

158# 

159# The check the tests run: 

160 

161# %% 

162assert all(item[1] == 12 for item in iter_counts), f'ERROR: number of iterations are not as expected, got {iter_counts}'