Coverage for pySDC/projects/FEM_with_FEniCS/run_examples.py: 98%

87 statements  

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

1""" 

2Run every declared combination and write the tables the README quotes to 

3``data/fem_with_fenics_out.txt``, and the element-order study as a plot to 

4``data/fem_with_fenics_orders.png``. 

5""" 

6 

7from pathlib import Path 

8 

9import numpy as np 

10 

11from pySDC.helpers.stats_helper import get_sorted 

12from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI 

13from pySDC.projects.FEM_with_FEniCS.setups import ( 

14 EXAMPLES, 

15 get_coarsenings, 

16 get_description, 

17 get_families, 

18 get_order_study, 

19 get_pfasst_procs, 

20) 

21 

22#: Cost of one solution restriction as a fraction of one fine-level sweep. Measured across the 

23#: examples as 0.006-0.014 for the L2 projection and 0.023-0.037 for point sampling, so it is a 

24#: 1-10%% correction either way -- small, but not zero, and the metric used to assume it was zero. 

25RESTRICTION_COST = 0.01 

26 

27 

28def run(example, nlevels=1, num_procs=1, **kwargs): 

29 """ 

30 Run one configuration. 

31 

32 Returns 

33 ------- 

34 dict 

35 ``niter`` (mean over the steps), ``uend``, ``dofs`` per level and ``work``, the number of 

36 fine-level sweep equivalents: iterations times the summed dof ratio of the hierarchy, plus 

37 the solution restrictions, which are not free -- see :data:`RESTRICTION_COST`. 

38 """ 

39 description, controller_params, t0, Tend = get_description(example, nlevels=nlevels, **kwargs) 

40 controller = controller_nonMPI(num_procs=num_procs, controller_params=controller_params, description=description) 

41 

42 step = controller.MS[0] 

43 dofs = [level.prob.init.dim() for level in step.levels] 

44 prob = step.levels[0].prob 

45 

46 uend, stats = controller.run(u0=prob.u_exact(t0), t0=t0, Tend=Tend) 

47 niter = np.mean([item[1] for item in get_sorted(stats, type='niter', sortby='time')]) 

48 

49 # one restriction per coarse level per iteration, projecting the M node values and u0 

50 num_nodes = step.levels[0].sweep.coll.num_nodes 

51 restrictions = (num_nodes + 1) * (len(dofs) - 1) * RESTRICTION_COST 

52 

53 return { 

54 'niter': niter, 

55 'uend': uend, 

56 'dofs': dofs, 

57 'work': niter * (sum(n / dofs[0] for n in dofs) + restrictions), 

58 } 

59 

60 

61def compare_mlsdc(example, family='CG', coarsening='h', out=print, **kwargs): 

62 """SDC against MLSDC with 2 and 3 levels. Returns the per-level results keyed by nlevels.""" 

63 results = { 

64 nlevels: run(example, nlevels=nlevels, family=family, coarsening=coarsening, **kwargs) for nlevels in (1, 2, 3) 

65 } 

66 ref = results[1] 

67 

68 ladder = 'mesh coarsening' if coarsening == 'h' else 'order coarsening' 

69 out(f'\n{example} [{family}, {coarsening}]: SDC vs MLSDC ({ladder}, collocation nodes kept)') 

70 out(f' {"levels":>7s} {"dofs":>22s} {"niter":>7s} {"work":>7s} {"speed-up":>9s} {"|u - u_SDC|":>12s}') 

71 for nlevels, res in results.items(): 

72 speedup = ref['work'] / res['work'] 

73 diff = abs(res['uend'] - ref['uend']) 

74 out( 

75 f' {nlevels:7d} {str(res["dofs"]):>22s} {res["niter"]:7.2f} {res["work"]:7.2f} ' 

76 f'{speedup:8.2f}x {diff:12.2e}' 

77 ) 

78 return results 

79 

80 

81def check_pfasst(example, nlevels=2, family='CG', coarsening='h', procs=None, out=print, **kwargs): 

82 """PFASST over a growing number of parallel steps. Returns the results keyed by num_procs.""" 

83 procs = get_pfasst_procs(example) if procs is None else procs 

84 results = { 

85 p: run(example, nlevels=nlevels, num_procs=p, family=family, coarsening=coarsening, **kwargs) for p in procs 

86 } 

87 ref = results[procs[0]] 

88 

89 out(f'\n{example} [{family}, {coarsening}]: PFASST with {nlevels} levels, iterations over parallel steps') 

90 out(f' {"procs":>6s} {"niter":>7s} {"|u - u_serial|":>15s}') 

91 for p, res in results.items(): 

92 out(f' {p:6d} {res["niter"]:7.2f} {abs(res["uend"] - ref["uend"]):15.2e}') 

93 return results 

94 

95 

96def compare_orders(example, orders, family='CG', out=print, **kwargs): 

97 """ 

98 MLSDC speed-up against the element order, at identical dof counts on every level. 

99 

100 This is the table behind "use high-order elements": the refinement ladder is shifted so that 

101 ``CG1``, ``CG2`` and ``CG4`` all give the same dofs per level, which leaves the *quality* of the 

102 coarse space as the only thing that varies. Returns the speed-ups keyed by (order, nlevels). 

103 """ 

104 speedups = {} 

105 for order in orders: 

106 results = { 

107 nlevels: run(example, nlevels=nlevels, family=family, coarsening='h', order=order, **kwargs) 

108 for nlevels in (1, 2, 3) 

109 } 

110 for nlevels in (2, 3): 

111 speedups[(order, nlevels)] = results[1]['work'] / results[nlevels]['work'] 

112 

113 out(f'\n{example} [{family}]: MLSDC speed-up against element order, at equal dofs per level') 

114 out(f' {"levels":>7s}' + ''.join(f'{"CG%d" % o:>12s}' for o in orders)) 

115 for nlevels in (2, 3): 

116 out(f' {nlevels:7d}' + ''.join(f'{speedups[(o, nlevels)]:11.2f}x' for o in orders)) 

117 return speedups 

118 

119 

120def plot_orders(speedups, fname='data/fem_with_fenics_orders.png', nlevels=3): 

121 """ 

122 Plot the MLSDC speed-up against the element order, one line per example. 

123 

124 Parameters 

125 ---------- 

126 speedups : dict 

127 The results of :func:`compare_orders`, keyed by example. 

128 fname : str 

129 Where to save the figure. 

130 nlevels : int 

131 Which hierarchy to show, 2 or 3 levels. 

132 """ 

133 import matplotlib.pyplot as plt 

134 

135 colors = ['#2a78d6', '#eb6834', '#1baf7a', '#eda100'] 

136 markers = ['o', 's', 'D', '^'] 

137 

138 fig, ax = plt.subplots(figsize=(4.8, 2.6), constrained_layout=True) 

139 ax.axhline(1.0, color='#8a8a85', lw=1, ls='--', zorder=1) 

140 ax.text(0.01, 0.985, 'as fast as SDC', color='#5f5e5a', fontsize=9, va='top', transform=ax.get_yaxis_transform()) 

141 for i, (example, study) in enumerate(speedups.items()): 

142 orders = sorted({order for order, _ in study}) 

143 ax.plot( 

144 range(len(orders)), 

145 [study[(order, nlevels)] for order in orders], 

146 color=colors[i], 

147 marker=markers[i], 

148 ms=8, 

149 lw=2.5, 

150 mec='white', 

151 mew=1.5, 

152 label=example, 

153 zorder=2, 

154 ) 

155 ax.set_xticks(range(len(orders)), [f'CG{order}' for order in orders]) 

156 ax.set_xlabel('element order, at equal dofs per level') 

157 ax.set_ylabel(f'speed-up over SDC,\n{nlevels}-level MLSDC') 

158 ax.grid(axis='y', color='#e6e5df', lw=0.8) 

159 ax.set_axisbelow(True) 

160 for side in ('top', 'right'): 

161 ax.spines[side].set_visible(False) 

162 ax.legend(frameon=False, loc='upper left') 

163 fig.savefig(fname, dpi=150) 

164 plt.close(fig) 

165 

166 

167def main(): 

168 Path('data').mkdir(parents=True, exist_ok=True) 

169 with open('data/fem_with_fenics_out.txt', 'w') as f: 

170 

171 def out(line=''): 

172 print(line) 

173 f.write(str(line) + '\n') 

174 

175 out('FEniCS + pySDC, mass-matrix formulation throughout (no mass inverse anywhere).') 

176 out( 

177 f'work = iterations x [sum(dofs_l / dofs_0) + (M+1)(nlevels-1) x {RESTRICTION_COST}], ' 

178 'i.e. fine-level sweep equivalents including the solution restrictions.' 

179 ) 

180 out('[family, coarsening]: CG/DG elements, h = coarser mesh, p = lower element order.') 

181 speedups = {} 

182 for example in EXAMPLES: 

183 for family in get_families(example): 

184 for coarsening in get_coarsenings(example): 

185 compare_mlsdc(example, family=family, coarsening=coarsening, out=out) 

186 check_pfasst(example, family=family, coarsening=coarsening, out=out) 

187 if get_order_study(example): 

188 speedups[example] = compare_orders(example, orders=get_order_study(example), out=out) 

189 if speedups: 

190 plot_orders(speedups) 

191 

192 

193if __name__ == '__main__': 

194 main()