Coverage for pySDC/tutorial/step_7/F_pySDC_with_Gusto.py: 91%

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 F: pySDC and Gusto 

14# 

15# [Gusto](https://github.com/firedrakeproject/gusto) is a toolkit for geophysical simulations that uses 

16# [Firedrake](https://github.com/firedrakeproject/firedrake) for the spatial discretization. This example is an 

17# adaptation of the Williamson 5 test case as implemented in 

18# [Gusto](https://github.com/firedrakeproject/gusto/blob/main/examples/shallow_water/williamson_5.py): the shallow 

19# water equations, with flow over a mountain (Test Case 5 of Williamson et al., 1992, *A standard test set for 

20# numerical approximations to the shallow water equations in spherical geometry*, JCP). It may take a considerable 

21# amount of time. 

22# 

23# This coupling works slightly differently from the other examples of this step, as time stepping is part of Gusto. 

24# The aim of the coupling is not a spatial discretization, but to use the equations that are implemented in Gusto. A 

25# Gusto equation includes the basic form of the equation set, but a crucial part is to modify terms in the discretized 

26# equations with spatial methods, such as upwinding schemes. 

27# 

28# We get the finished equation set into pySDC by setting up pySDC as a time discretization for Gusto: a pySDC 

29# description and controller parameters as usual, passed to the pySDC time discretization, which then goes into a 

30# Gusto time stepper. During this instantiation, the equation, and the residual used for solving systems, is modified 

31# with all the spatial methods. Afterwards, there are two choices: 

32# 

33# - use the Gusto time stepper for running, or 

34# - take its `.scheme.controller`, which is the pySDC controller, and run with pySDC. 

35# 

36# That is why a Gusto time stepper is needed even to run with pySDC: the spatial methods modify the residual of the 

37# equations when it is created. Once they have, we can continue in Gusto or in pySDC. 

38 

39# %% 

40import firedrake as fd 

41from pySDC.helpers.pySDC_as_gusto_time_discretization import pySDC_integrator 

42from pySDC.helpers.firedrake_ensemble_communicator import FiredrakeEnsembleCommunicator 

43from gusto import SDC, BackwardEuler 

44from gusto.core.labels import implicit, time_derivative 

45from gusto.core.logging import logger, INFO 

46 

47logger.setLevel(INFO) 

48 

49 

50from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter 

51from firedrake import SpatialCoordinate, as_vector, pi, sqrt, min_value, Function 

52from gusto import ( 

53 Domain, 

54 IO, 

55 OutputParameters, 

56 DGUpwind, 

57 ShallowWaterParameters, 

58 ShallowWaterEquations, 

59 Sum, 

60 lonlatr_from_xyz, 

61 GeneralIcosahedralSphereMesh, 

62 ZonalComponent, 

63 MeridionalComponent, 

64 RelativeVorticity, 

65 Timestepper, 

66) 

67 

68williamson_5_defaults = { 

69 'ncells_per_edge': 12, # number of cells per icosahedron edge 

70 'dt': 900.0, 

71 'tmax': 50.0 * 24.0 * 60.0 * 60.0, # 50 days 

72 'dumpfreq': 10, # output every <dumpfreq> steps 

73 'dirname': 'williamson_5', # results will go into ./results/<dirname> 

74 'time_parallelism': False, # use parallel diagonal SDC or not 

75 'QI': 'MIN-SR-S', # implicit preconditioner 

76 'M': '3', # number of collocation nodes 

77 'kmax': '5', # use fixed number of iteration up to this value 

78 'use_pySDC': True, # whether to use pySDC for time integration 

79 'use_adaptivity': True, # whether to use adaptive step size selection 

80 'Nlevels': 1, # number of levels in SDC 

81 'logger_level': 15, # pySDC logger level 

82} 

83 

84# %% [markdown] 

85# ## Williamson 5 

86# 

87# The whole test case, with the Gusto SDC implementation or the pySDC-Gusto coupling, optionally with adaptivity, 

88# several levels in space, or space-time parallelism. 

89 

90 

91# %% 

92def williamson_5( 

93 ncells_per_edge=williamson_5_defaults['ncells_per_edge'], 

94 dt=williamson_5_defaults['dt'], 

95 tmax=williamson_5_defaults['tmax'], 

96 dumpfreq=williamson_5_defaults['dumpfreq'], 

97 dirname=williamson_5_defaults['dirname'], 

98 time_parallelism=williamson_5_defaults['time_parallelism'], 

99 QI=williamson_5_defaults['QI'], 

100 M=williamson_5_defaults['M'], 

101 kmax=williamson_5_defaults['kmax'], 

102 use_pySDC=williamson_5_defaults['use_pySDC'], 

103 use_adaptivity=williamson_5_defaults['use_adaptivity'], 

104 Nlevels=williamson_5_defaults['Nlevels'], 

105 logger_level=williamson_5_defaults['logger_level'], 

106 mesh=None, 

107 _ML_is_setup=True, 

108): 

109 """ 

110 Run the Williamson 5 test case. 

111 

112 Args: 

113 ncells_per_edge (int): number of cells per icosahedron edge 

114 dt (float): Initial step size 

115 tmax (float): Time to integrate to 

116 dumpfreq (int): Output every <dumpfreq> time steps 

117 dirname (str): Output will go into ./results/<dirname> 

118 time_parallelism (bool): True for parallel SDC, False for serial 

119 M (int): Number of collocation nodes 

120 kmax (int): Max number of SDC iterations 

121 use_pySDC (bool): Use pySDC as Gusto time integrator or Gusto SDC implementation 

122 Nlevels (int): Number of SDC levels 

123 logger_level (int): Logger level 

124 """ 

125 if not use_pySDC and use_adaptivity: 

126 raise NotImplementedError('Adaptive step size selection not yet implemented in Gusto') 

127 if not use_pySDC and Nlevels > 1: 

128 raise NotImplementedError('Multi-level SDC not yet implemented in Gusto') 

129 if time_parallelism and Nlevels > 1: 

130 raise NotImplementedError('Multi-level SDC does not work with MPI parallel sweeper yet') 

131 

132 # ------------------------------------------------------------------------ # 

133 # Parameters for test case 

134 # ------------------------------------------------------------------------ # 

135 

136 radius = 6371220.0 # planetary radius (m) 

137 mean_depth = 5960 # reference depth (m) 

138 g = 9.80616 # acceleration due to gravity (m/s^2) 

139 u_max = 20.0 # max amplitude of the zonal wind (m/s) 

140 mountain_height = 2000.0 # height of mountain (m) 

141 R0 = pi / 9.0 # radius of mountain (rad) 

142 lamda_c = -pi / 2.0 # longitudinal centre of mountain (rad) 

143 phi_c = pi / 6.0 # latitudinal centre of mountain (rad) 

144 

145 # ------------------------------------------------------------------------ # 

146 # Our settings for this set up 

147 # ------------------------------------------------------------------------ # 

148 

149 element_order = 1 

150 

151 # ------------------------------------------------------------------------ # 

152 # Set up model objects 

153 # ------------------------------------------------------------------------ # 

154 

155 # parallelism 

156 if time_parallelism: 

157 ensemble_comm = FiredrakeEnsembleCommunicator(fd.COMM_WORLD, fd.COMM_WORLD.size // M) 

158 space_comm = ensemble_comm.space_comm 

159 from pySDC.implementations.sweeper_classes.generic_implicit_MPI import generic_implicit_MPI as sweeper_class 

160 

161 if ensemble_comm.time_comm.rank > 0: 

162 dirname = f'{dirname}-{ensemble_comm.time_comm.rank}' 

163 else: 

164 ensemble_comm = None 

165 space_comm = fd.COMM_WORLD 

166 from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit as sweeper_class 

167 

168 # Domain 

169 mesh = GeneralIcosahedralSphereMesh(radius, ncells_per_edge, degree=2, comm=space_comm) if mesh is None else mesh 

170 if Nlevels > 1: 

171 hierarchy = fd.MeshHierarchy(mesh, Nlevels - 1) 

172 mesh = hierarchy[-1] 

173 domain = Domain(mesh, dt, 'BDM', element_order) 

174 x, y, z = SpatialCoordinate(mesh) 

175 lamda, phi, _ = lonlatr_from_xyz(x, y, z) 

176 

177 # Equation: topography 

178 rsq = min_value(R0**2, (lamda - lamda_c) ** 2 + (phi - phi_c) ** 2) 

179 r = sqrt(rsq) 

180 tpexpr = mountain_height * (1 - r / R0) 

181 parameters = ShallowWaterParameters(mesh, H=mean_depth, g=g, topog_expr=tpexpr) 

182 eqns = ShallowWaterEquations(domain, parameters) 

183 

184 eqns.label_terms(lambda t: not t.has_label(time_derivative), implicit) 

185 

186 # I/O 

187 output = OutputParameters( 

188 dirname=dirname, 

189 dumplist_latlon=['D'], 

190 dumpfreq=dumpfreq, 

191 dump_vtus=True, 

192 dump_nc=True, 

193 dumplist=['D', 'topography'], 

194 ) 

195 diagnostic_fields = [Sum('D', 'topography'), RelativeVorticity(), MeridionalComponent('u'), ZonalComponent('u')] 

196 io = IO(domain, output, diagnostic_fields=diagnostic_fields) 

197 

198 # Transport schemes 

199 transport_methods = [DGUpwind(eqns, "u"), DGUpwind(eqns, "D")] 

200 

201 # ------------------------------------------------------------------------ # 

202 # pySDC parameters: description and controller parameters 

203 # ------------------------------------------------------------------------ # 

204 

205 level_params = dict() 

206 level_params['restol'] = -1 

207 level_params['dt'] = dt 

208 level_params['residual_type'] = 'full_rel' 

209 

210 step_params = dict() 

211 step_params['maxiter'] = kmax 

212 

213 sweeper_params = dict() 

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

215 sweeper_params['node_type'] = 'LEGENDRE' 

216 sweeper_params['num_nodes'] = M 

217 sweeper_params['QI'] = QI 

218 sweeper_params['QE'] = 'PIC' 

219 sweeper_params['comm'] = ensemble_comm 

220 sweeper_params['initial_guess'] = 'copy' 

221 

222 problem_params = dict() 

223 

224 convergence_controllers = {} 

225 if use_adaptivity: 

226 from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity 

227 from pySDC.implementations.convergence_controller_classes.spread_step_sizes import ( 

228 SpreadStepSizesBlockwiseNonMPI, 

229 ) 

230 

231 convergence_controllers[Adaptivity] = {'e_tol': 1e-6, 'rel_error': True, 'dt_max': 1e4, 'dt_rel_min_slope': 0.5} 

232 # this is needed because the coupling runs on the controller level and this will almost always overwrite 

233 convergence_controllers[SpreadStepSizesBlockwiseNonMPI] = {'overwrite_to_reach_Tend': False} 

234 

235 controller_params = dict() 

236 controller_params['logger_level'] = logger_level if fd.COMM_WORLD.rank == 0 else 30 

237 controller_params['mssdc_jac'] = False 

238 

239 description = dict() 

240 description['problem_params'] = problem_params 

241 description['sweeper_class'] = sweeper_class 

242 description['sweeper_params'] = sweeper_params 

243 description['level_params'] = level_params 

244 description['step_params'] = step_params 

245 description['convergence_controllers'] = convergence_controllers 

246 

247 from pySDC.implementations.transfer_classes.TransferFiredrakeMesh import MeshToMeshFiredrakeHierarchy 

248 

249 description['space_transfer_class'] = MeshToMeshFiredrakeHierarchy 

250 

251 # ------------------------------------------------------------------------ # 

252 # petsc solver parameters 

253 # ------------------------------------------------------------------------ # 

254 

255 solver_parameters = { 

256 'snes_type': 'newtonls', 

257 'ksp_type': 'gmres', 

258 'pc_type': 'bjacobi', 

259 'sub_pc_type': 'ilu', 

260 'ksp_rtol': 1e-12, 

261 'snes_rtol': 1e-12, 

262 'ksp_atol': 1e-30, 

263 'snes_atol': 1e-30, 

264 'ksp_divtol': 1e30, 

265 'snes_divtol': 1e30, 

266 'snes_max_it': 999, 

267 'ksp_max_it': 999, 

268 } 

269 

270 # ------------------------------------------------------------------------ # 

271 # Set Gusto SDC parameters to match the pySDC ones 

272 # ------------------------------------------------------------------------ # 

273 

274 SDC_params = { 

275 'base_scheme': BackwardEuler(domain, solver_parameters=solver_parameters), 

276 'M': sweeper_params['num_nodes'], 

277 'maxk': step_params['maxiter'], 

278 'quad_type': sweeper_params['quad_type'], 

279 'node_type': sweeper_params['node_type'], 

280 'qdelta_imp': sweeper_params['QI'], 

281 'qdelta_exp': sweeper_params['QE'], 

282 'formulation': 'Z2N', 

283 'initial_guess': 'copy', 

284 'nonlinear_solver_parameters': solver_parameters, 

285 'linear_solver_parameters': solver_parameters, 

286 'final_update': False, 

287 } 

288 

289 # ------------------------------------------------------------------------ # 

290 # Setup time stepper 

291 # ------------------------------------------------------------------------ # 

292 

293 if use_pySDC: 

294 method = pySDC_integrator(description, controller_params, domain=domain, solver_parameters=solver_parameters) 

295 else: 

296 method = SDC(**SDC_params, domain=domain) 

297 

298 stepper = Timestepper(eqns, method, io, spatial_methods=transport_methods) 

299 

300 # ------------------------------------------------------------------------ # 

301 # Setup multi-level SDC 

302 # ------------------------------------------------------------------------ # 

303 

304 if not _ML_is_setup: 

305 return stepper 

306 

307 if Nlevels > 1: 

308 steppers = [ 

309 None, 

310 ] * (Nlevels) 

311 steppers[0] = stepper 

312 

313 # get different steppers on the different levels 

314 # recall that the setup of the problems is only finished when the stepper is setup 

315 for i in range(1, Nlevels): 

316 steppers[i] = williamson_5( 

317 ncells_per_edge=ncells_per_edge, 

318 dt=dt, 

319 tmax=tmax, 

320 dumpfreq=dumpfreq, 

321 dirname=f'{dirname}_unused_{i}', 

322 time_parallelism=time_parallelism, 

323 QI=QI, 

324 M=M, 

325 kmax=kmax, 

326 use_pySDC=use_pySDC, 

327 use_adaptivity=use_adaptivity, 

328 Nlevels=1, 

329 mesh=hierarchy[-i - 1], # mind that the finest level in pySDC is 0, but -1 in hierarchy 

330 logger_level=50, 

331 _ML_is_setup=False, 

332 ) 

333 

334 # update description and setup pySDC again with the discretizations from different steppers 

335 description['problem_params']['residual'] = [me.scheme.residual for me in steppers] 

336 description['problem_params']['equation'] = [me.scheme.equation for me in steppers] 

337 method = pySDC_integrator(description, controller_params, domain=domain, solver_parameters=solver_parameters) 

338 stepper = Timestepper(eqns, method, io, spatial_methods=transport_methods) 

339 

340 # ------------------------------------------------------------------------ # 

341 # Initial conditions 

342 # ------------------------------------------------------------------------ # 

343 

344 u0 = stepper.fields('u') 

345 D0 = stepper.fields('D') 

346 uexpr = as_vector([-u_max * y / radius, u_max * x / radius, 0.0]) 

347 Dexpr = mean_depth - tpexpr - (radius * parameters.Omega * u_max + 0.5 * u_max**2) * (z / radius) ** 2 / g 

348 

349 u0.project(uexpr) 

350 D0.interpolate(Dexpr) 

351 

352 Dbar = Function(D0.function_space()).assign(mean_depth) 

353 stepper.set_reference_profiles([('D', Dbar)]) 

354 

355 # ------------------------------------------------------------------------ # 

356 # Run 

357 # ------------------------------------------------------------------------ # 

358 

359 if use_pySDC and use_adaptivity: 

360 # we have to do this for adaptive time stepping, because it is a bit of a mess 

361 method.timestepper = stepper 

362 

363 stepper.run(t=0, tmax=tmax) 

364 return stepper, mesh 

365 

366 

367# ---------------------------------------------------------------------------- # 

368# MAIN 

369# ---------------------------------------------------------------------------- # 

370 

371 

372# %% [markdown] 

373# ## Running it 

374# 

375# The script supports space-time parallelism, as well as running the Gusto SDC implementation or the pySDC-Gusto 

376# coupling. Run it with `--help` to learn how to configure it. Afterwards, 

377# [`F_2_plot_pySDC_with_Gusto_result.py`](https://github.com/Parallel-in-Time/pySDC/blob/master/pySDC/tutorial/step_7/F_2_plot_pySDC_with_Gusto_result.py), 

378# next to this tutorial, plots the results. 

379 

380# %% 

381if __name__ == "__main__": 

382 

383 parser = ArgumentParser( 

384 description='Williamson 5 (flow over a mountain) with pySDC as the time discretization of Gusto, or with ' 

385 "Gusto's own SDC implementation for comparison.", 

386 formatter_class=ArgumentDefaultsHelpFormatter, 

387 ) 

388 parser.add_argument( 

389 '--ncells_per_edge', 

390 help="The number of cells per edge of icosahedron", 

391 type=int, 

392 default=williamson_5_defaults['ncells_per_edge'], 

393 ) 

394 parser.add_argument('--dt', help="The time step in seconds.", type=float, default=williamson_5_defaults['dt']) 

395 parser.add_argument( 

396 "--tmax", help="The end time for the simulation in seconds.", type=float, default=williamson_5_defaults['tmax'] 

397 ) 

398 parser.add_argument( 

399 '--dumpfreq', 

400 help="The frequency at which to dump field output.", 

401 type=int, 

402 default=williamson_5_defaults['dumpfreq'], 

403 ) 

404 parser.add_argument( 

405 '--dirname', help="The name of the directory to write to.", type=str, default=williamson_5_defaults['dirname'] 

406 ) 

407 parser.add_argument( 

408 '--time_parallelism', 

409 help="Whether to use parallel diagonal SDC or not.", 

410 type=str, 

411 default=williamson_5_defaults['time_parallelism'], 

412 ) 

413 parser.add_argument('--kmax', help='SDC iteration count', type=int, default=williamson_5_defaults['kmax']) 

414 parser.add_argument('-M', help='SDC node count', type=int, default=williamson_5_defaults['M']) 

415 parser.add_argument( 

416 '--use_pySDC', 

417 help='whether to use pySDC or Gusto SDC implementation', 

418 type=str, 

419 default=williamson_5_defaults['use_pySDC'], 

420 ) 

421 parser.add_argument( 

422 '--use_adaptivity', 

423 help='whether to use adaptive step size selection', 

424 type=str, 

425 default=williamson_5_defaults['use_adaptivity'], 

426 ) 

427 parser.add_argument('--QI', help='Implicit preconditioner', type=str, default=williamson_5_defaults['QI']) 

428 parser.add_argument( 

429 '--Nlevels', 

430 help="Number of SDC levels.", 

431 type=int, 

432 default=williamson_5_defaults['Nlevels'], 

433 ) 

434 args, unknown = parser.parse_known_args() 

435 

436 options = vars(args) 

437 for key in ['use_pySDC', 'use_adaptivity', 'time_parallelism']: 

438 options[key] = options[key] not in ['False', 0, False, 'false'] 

439 

440 williamson_5(**options) 

441 

442# %% [markdown] 

443# Firedrake and Gusto do not run in the browser, nor in the environment this website is built in. Our CI runs this part 

444# in a Firedrake container with Gusto: it checks that the pySDC coupling gives the same result as Gusto's own SDC 

445# after a few time steps, and that the multilevel version converges faster than the single-level one. These are the 

446# results of the run that built this page, for a few time steps only, to keep the CI fast: 

447# 

448# :::{literalinclude} /../../data_firedrake/step_7_F_out.txt 

449# :language: text 

450# :::