Coverage for pySDC/tutorial/step_7/E_pySDC_with_Firedrake.py: 100%

112 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-17 13:41 +0000

1# --- 

2# jupyter: 

3# jupytext: 

4# formats: py:percent 

5# kernelspec: 

6# display_name: Python 3 

7# name: python3 

8# language_info: 

9# name: python 

10# --- 

11 

12# %% [markdown] 

13# # Part E: pySDC and Firedrake 

14# 

15# [Firedrake](https://github.com/firedrakeproject/firedrake) is a finite element library with similar features as 

16# FEniCS. This example runs the same forced heat equation as the [FEniCS example](A_pySDC_with_FEniCS), but 

17# implemented in Firedrake. The setup proceeds very much as in earlier tutorials; the interesting part is rather the 

18# problem class. See 

19# [pySDC/implementations/problem_classes/HeatFiredrake.py](https://github.com/Parallel-in-Time/pySDC/blob/master/pySDC/implementations/problem_classes/HeatFiredrake.py) 

20# as a blueprint for how to implement problems with Firedrake in a way that pySDC understands. 

21# 

22# ## Single level, serial or parallel across the nodes 

23# 

24# SDC with the diagonal preconditioner `MIN-SR-S`, whose nodes can be solved in parallel. The space-time parallelism 

25# comes from a Firedrake ensemble: with `--useMPIsweeper` and a multiple of 3 ranks, the ranks are split across the 

26# 3 collocation nodes, and the ranks of each node share the spatial problem. See the 

27# [Firedrake documentation on parallelism](https://www.firedrakeproject.org/firedrake/parallelism.html). 

28 

29# %% 

30from pathlib import Path 

31 

32import numpy as np 

33from mpi4py import MPI 

34 

35 

36def setup(useMPIsweeper): 

37 """ 

38 Helper routine to set up parameters 

39 

40 Returns: 

41 description and controller_params parameter dictionaries 

42 """ 

43 from pySDC.implementations.problem_classes.HeatFiredrake import Heat1DForcedFiredrake 

44 from pySDC.implementations.sweeper_classes.imex_1st_order import imex_1st_order 

45 from pySDC.implementations.sweeper_classes.imex_1st_order_MPI import imex_1st_order_MPI 

46 from pySDC.implementations.hooks.log_errors import LogGlobalErrorPostRun 

47 from pySDC.implementations.hooks.log_work import LogWork 

48 from pySDC.helpers.firedrake_ensemble_communicator import FiredrakeEnsembleCommunicator 

49 

50 # setup space-time parallelism via ensemble for Firedrake, see https://www.firedrakeproject.org/firedrake/parallelism.html 

51 num_nodes = 3 

52 ensemble = FiredrakeEnsembleCommunicator(MPI.COMM_WORLD, max([MPI.COMM_WORLD.size // num_nodes, 1])) 

53 

54 level_params = dict() 

55 level_params['restol'] = 5e-10 

56 level_params['dt'] = 0.2 

57 

58 step_params = dict() 

59 step_params['maxiter'] = 20 

60 

61 sweeper_params = dict() 

62 sweeper_params['quad_type'] = 'RADAU-RIGHT' 

63 sweeper_params['num_nodes'] = num_nodes 

64 sweeper_params['QI'] = 'MIN-SR-S' 

65 sweeper_params['QE'] = 'PIC' 

66 sweeper_params['comm'] = ensemble 

67 

68 problem_params = dict() 

69 problem_params['nu'] = 0.1 

70 problem_params['n'] = 128 

71 problem_params['c'] = 1.0 

72 problem_params['comm'] = ensemble.space_comm 

73 

74 controller_params = dict() 

75 controller_params['logger_level'] = 15 if MPI.COMM_WORLD.rank == 0 else 30 

76 controller_params['hook_class'] = [LogGlobalErrorPostRun, LogWork] 

77 

78 description = dict() 

79 description['problem_class'] = Heat1DForcedFiredrake 

80 description['problem_params'] = problem_params 

81 description['sweeper_class'] = imex_1st_order_MPI if useMPIsweeper else imex_1st_order 

82 description['sweeper_params'] = sweeper_params 

83 description['level_params'] = level_params 

84 description['step_params'] = step_params 

85 

86 return description, controller_params 

87 

88 

89# %% [markdown] 

90# ## Three levels 

91# 

92# Multilevel SDC, serial, coarsened in space from 128 to 32 to 4 elements, with interpolation of the right-hand side. 

93 

94 

95# %% 

96def setup_ML(): 

97 """ 

98 Helper routine to set up parameters 

99 

100 Returns: 

101 description and controller_params parameter dictionaries 

102 """ 

103 from pySDC.implementations.problem_classes.HeatFiredrake import Heat1DForcedFiredrake 

104 from pySDC.implementations.sweeper_classes.imex_1st_order import imex_1st_order 

105 from pySDC.implementations.sweeper_classes.imex_1st_order_MPI import imex_1st_order_MPI 

106 from pySDC.implementations.transfer_classes.TransferFiredrakeMesh import MeshToMeshFiredrake 

107 from pySDC.implementations.hooks.log_errors import LogGlobalErrorPostRun 

108 from pySDC.implementations.hooks.log_work import LogWork 

109 from pySDC.helpers.firedrake_ensemble_communicator import FiredrakeEnsembleCommunicator 

110 

111 level_params = dict() 

112 level_params['restol'] = 5e-10 

113 level_params['dt'] = 0.2 

114 

115 step_params = dict() 

116 step_params['maxiter'] = 20 

117 

118 sweeper_params = dict() 

119 sweeper_params['quad_type'] = 'RADAU-RIGHT' 

120 sweeper_params['num_nodes'] = 3 

121 sweeper_params['QI'] = 'MIN-SR-S' 

122 sweeper_params['QE'] = 'PIC' 

123 

124 problem_params = dict() 

125 problem_params['nu'] = 0.1 

126 problem_params['n'] = [128, 32, 4] 

127 problem_params['c'] = 1.0 

128 

129 base_transfer_params = dict() 

130 base_transfer_params['finter'] = True 

131 

132 controller_params = dict() 

133 controller_params['logger_level'] = 15 if MPI.COMM_WORLD.rank == 0 else 30 

134 controller_params['hook_class'] = [LogGlobalErrorPostRun, LogWork] 

135 

136 description = dict() 

137 description['problem_class'] = Heat1DForcedFiredrake 

138 description['problem_params'] = problem_params 

139 description['sweeper_class'] = imex_1st_order 

140 description['sweeper_params'] = sweeper_params 

141 description['level_params'] = level_params 

142 description['step_params'] = step_params 

143 description['space_transfer_class'] = MeshToMeshFiredrake 

144 description['base_transfer_params'] = base_transfer_params 

145 

146 return description, controller_params 

147 

148 

149# %% [markdown] 

150# ## Running it 

151# 

152# The run prints the error and the work: SDC iterations, solver setups, solves and right-hand side evaluations on the 

153# finest level, and checks them against what we got last time. 

154 

155 

156# %% 

157def runHeatFiredrake(useMPIsweeper=False, ML=False): 

158 """ 

159 Run the example defined by the above parameters 

160 """ 

161 from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI 

162 from pySDC.helpers.stats_helper import get_sorted 

163 

164 Tend = 1.0 

165 t0 = 0.0 

166 

167 if ML: 

168 assert not useMPIsweeper, 'MPI parallel diagonal SDC and ML SDC are not compatible at the moment' 

169 description, controller_params = setup_ML() 

170 else: 

171 description, controller_params = setup(useMPIsweeper) 

172 

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

174 

175 # get initial values 

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

177 uinit = P.u_exact(0.0) 

178 

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

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

181 

182 # see what we get 

183 error = get_sorted(stats, type='e_global_post_run') 

184 work_solver_setup = get_sorted(stats, type='work_solver_setup') 

185 work_solves = get_sorted(stats, type='work_solves') 

186 work_rhs = get_sorted(stats, type='work_rhs') 

187 niter = get_sorted(stats, type='niter') 

188 

189 tot_iter = np.sum([me[1] for me in niter]) 

190 tot_solver_setup = np.sum([me[1] for me in work_solver_setup]) 

191 tot_solves = np.sum([me[1] for me in work_solves]) 

192 tot_rhs = np.sum([me[1] for me in work_rhs]) 

193 

194 time_rank = description["sweeper_params"]["comm"].rank if useMPIsweeper else 0 

195 print( 

196 f'Finished with error {error[0][1]:.2e}. Used {tot_iter} SDC iterations, with {tot_solver_setup} solver setups, {tot_solves} solves and {tot_rhs} right hand side evaluations on the finest level of time task {time_rank}.' 

197 ) 

198 

199 # the results the website shows, from the first rank 

200 if MPI.COMM_WORLD.rank == 0: 

201 timing = get_sorted(stats, type='timing_run')[0][1] 

202 variant = 'three-level SDC' if ML else ('SDC parallel across the nodes' if useMPIsweeper else 'serial SDC') 

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

204 with open('data/step_7_E_out.txt', 'a') as file: 

205 file.write( 

206 f'{variant:30s} error {error[0][1]:.2e}, {tot_iter:2d} iterations, {tot_solves:3d} solves on the ' 

207 f'finest level, time to solution {timing:.2f} s\n' 

208 ) 

209 

210 # do tests that we got the same as last time 

211 n_nodes = 1 if useMPIsweeper else description['sweeper_params']['num_nodes'] 

212 assert error[0][1] < 2e-7 

213 assert tot_iter == (10 if ML else 29) 

214 assert tot_solver_setup == n_nodes 

215 assert tot_solves == n_nodes * tot_iter 

216 assert tot_rhs == n_nodes * tot_iter + (n_nodes + 1) * len(niter) 

217 

218 

219# %% [markdown] 

220# The script runs in three different ways: 

221# 

222# - `python E_pySDC_with_Firedrake.py` for single-level serial SDC, 

223# - `mpiexec -np 3 python E_pySDC_with_Firedrake.py --useMPIsweeper` for single-level SDC, parallel across the nodes, 

224# - `python E_pySDC_with_Firedrake.py --ML` for three-level serial SDC. 

225# 

226# Multilevel SDC reduces the number of SDC iterations quite a bit, but not necessarily the time to solution, partly 

227# because more solvers are constructed on the coarse levels, and the parallel variant does the work of one node per 

228# rank. The results below show both; mind that they come from a CI machine, not from a dedicated one. We do not claim 

229# to have found the best parameters: this is just an example of how to use it. 

230 

231# %% 

232if __name__ == "__main__": 

233 from argparse import ArgumentParser 

234 

235 parser = ArgumentParser() 

236 parser.add_argument( 

237 '--ML', 

238 help='Whether you want to run multi-level', 

239 default=False, 

240 required=False, 

241 action='store_const', 

242 const=True, 

243 ) 

244 parser.add_argument( 

245 '--useMPIsweeper', 

246 help='Whether you want to use MPI parallel diagonal SDC', 

247 default=False, 

248 required=False, 

249 action='store_const', 

250 const=True, 

251 ) 

252 

253 args = parser.parse_args() 

254 

255 runHeatFiredrake(**vars(args)) 

256 

257# %% [markdown] 

258# Firedrake does not run in the browser, nor in the environment this website is built in. Our CI runs all three 

259# variants in a Firedrake container, and checks their errors and work counts. These are its results, in the run that 

260# built this page: 

261# 

262# :::{literalinclude} /../../data_firedrake/step_7_E_out.txt 

263# :language: text 

264# :::