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

112 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-13 09:00 +0000

1""" 

2Example for running pySDC together with Gusto. This test runs a shallow water equation and may take a considerable 

3amount of time. After you have run it, move on to step F_2, which includes a plotting script. 

4 

5This is Test Case 5 (flow over a mountain) of Williamson et al, 1992: 

6``A standard test set for numerical approximations to the shallow water 

7equations in spherical geometry'', JCP. 

8 

9This script is adapted from the Gusto example: https://github.com/firedrakeproject/gusto/blob/main/examples/shallow_water/williamson_5.py 

10 

11The pySDC coupling works by setting up pySDC as a time integrator within Gusto. 

12To this end, you need to construct a pySDC description and controller parameters as usual and pass them when 

13constructing the pySDC time discretization. 

14 

15After passing this to a Gusto timestepper, you have two choices: 

16 - Access the `.scheme.controller` variable of the timestepper, which is the pySDC controller and use pySDC for 

17 running 

18 - Use the Gusto timestepper for running 

19You may wonder why it is necessary to construct a Gusto timestepper if you don't want to use it. The reason is the 

20setup of spatial methods, such as upwinding. These are passed to the Gusto timestepper and modify the residual of the 

21equations during its instantiation. Once the residual is modified, we can choose whether to continue in Gusto or pySDC. 

22 

23This script supports space-time parallelism, as well as running the Gusto SDC implementation or the pySDC-Gusto coupling. 

24Please run with `--help` to learn how to configure this script. 

25""" 

26 

27import firedrake as fd 

28from pySDC.helpers.pySDC_as_gusto_time_discretization import pySDC_integrator 

29from pySDC.helpers.firedrake_ensemble_communicator import FiredrakeEnsembleCommunicator 

30from gusto import SDC, BackwardEuler 

31from gusto.core.labels import implicit, time_derivative 

32from gusto.core.logging import logger, INFO 

33 

34logger.setLevel(INFO) 

35 

36 

37from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter 

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

39from gusto import ( 

40 Domain, 

41 IO, 

42 OutputParameters, 

43 DGUpwind, 

44 ShallowWaterParameters, 

45 ShallowWaterEquations, 

46 Sum, 

47 lonlatr_from_xyz, 

48 GeneralIcosahedralSphereMesh, 

49 ZonalComponent, 

50 MeridionalComponent, 

51 RelativeVorticity, 

52 Timestepper, 

53) 

54 

55williamson_5_defaults = { 

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

57 'dt': 900.0, 

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

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

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

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

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

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

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

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

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

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

68 'logger_level': 15, # pySDC logger level 

69} 

70 

71 

72def williamson_5( 

73 ncells_per_edge=williamson_5_defaults['ncells_per_edge'], 

74 dt=williamson_5_defaults['dt'], 

75 tmax=williamson_5_defaults['tmax'], 

76 dumpfreq=williamson_5_defaults['dumpfreq'], 

77 dirname=williamson_5_defaults['dirname'], 

78 time_parallelism=williamson_5_defaults['time_parallelism'], 

79 QI=williamson_5_defaults['QI'], 

80 M=williamson_5_defaults['M'], 

81 kmax=williamson_5_defaults['kmax'], 

82 use_pySDC=williamson_5_defaults['use_pySDC'], 

83 use_adaptivity=williamson_5_defaults['use_adaptivity'], 

84 Nlevels=williamson_5_defaults['Nlevels'], 

85 logger_level=williamson_5_defaults['logger_level'], 

86 mesh=None, 

87 _ML_is_setup=True, 

88): 

89 """ 

90 Run the Williamson 5 test case. 

91 

92 Args: 

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

94 dt (float): Initial step size 

95 tmax (float): Time to integrate to 

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

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

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

99 M (int): Number of collocation nodes 

100 kmax (int): Max number of SDC iterations 

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

102 Nlevels (int): Number of SDC levels 

103 logger_level (int): Logger level 

104 """ 

105 if not use_pySDC and use_adaptivity: 

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

107 if not use_pySDC and Nlevels > 1: 

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

109 if time_parallelism and Nlevels > 1: 

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

111 

112 # ------------------------------------------------------------------------ # 

113 # Parameters for test case 

114 # ------------------------------------------------------------------------ # 

115 

116 radius = 6371220.0 # planetary radius (m) 

117 mean_depth = 5960 # reference depth (m) 

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

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

120 mountain_height = 2000.0 # height of mountain (m) 

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

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

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

124 

125 # ------------------------------------------------------------------------ # 

126 # Our settings for this set up 

127 # ------------------------------------------------------------------------ # 

128 

129 element_order = 1 

130 

131 # ------------------------------------------------------------------------ # 

132 # Set up model objects 

133 # ------------------------------------------------------------------------ # 

134 

135 # parallelism 

136 if time_parallelism: 

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

138 space_comm = ensemble_comm.space_comm 

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

140 

141 if ensemble_comm.time_comm.rank > 0: 

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

143 else: 

144 ensemble_comm = None 

145 space_comm = fd.COMM_WORLD 

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

147 

148 # Domain 

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

150 if Nlevels > 1: 

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

152 mesh = hierarchy[-1] 

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

154 x, y, z = SpatialCoordinate(mesh) 

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

156 

157 # Equation: topography 

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

159 r = sqrt(rsq) 

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

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

162 eqns = ShallowWaterEquations(domain, parameters) 

163 

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

165 

166 # I/O 

167 output = OutputParameters( 

168 dirname=dirname, 

169 dumplist_latlon=['D'], 

170 dumpfreq=dumpfreq, 

171 dump_vtus=True, 

172 dump_nc=True, 

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

174 ) 

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

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

177 

178 # Transport schemes 

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

180 

181 # ------------------------------------------------------------------------ # 

182 # pySDC parameters: description and controller parameters 

183 # ------------------------------------------------------------------------ # 

184 

185 level_params = dict() 

186 level_params['restol'] = -1 

187 level_params['dt'] = dt 

188 level_params['residual_type'] = 'full_rel' 

189 

190 step_params = dict() 

191 step_params['maxiter'] = kmax 

192 

193 sweeper_params = dict() 

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

195 sweeper_params['node_type'] = 'LEGENDRE' 

196 sweeper_params['num_nodes'] = M 

197 sweeper_params['QI'] = QI 

198 sweeper_params['QE'] = 'PIC' 

199 sweeper_params['comm'] = ensemble_comm 

200 sweeper_params['initial_guess'] = 'copy' 

201 

202 problem_params = dict() 

203 

204 convergence_controllers = {} 

205 if use_adaptivity: 

206 from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity 

207 from pySDC.implementations.convergence_controller_classes.spread_step_sizes import ( 

208 SpreadStepSizesBlockwiseNonMPI, 

209 ) 

210 

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

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

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

214 

215 controller_params = dict() 

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

217 controller_params['mssdc_jac'] = False 

218 

219 description = dict() 

220 description['problem_params'] = problem_params 

221 description['sweeper_class'] = sweeper_class 

222 description['sweeper_params'] = sweeper_params 

223 description['level_params'] = level_params 

224 description['step_params'] = step_params 

225 description['convergence_controllers'] = convergence_controllers 

226 

227 from pySDC.implementations.transfer_classes.TransferFiredrakeMesh import MeshToMeshFiredrakeHierarchy 

228 

229 description['space_transfer_class'] = MeshToMeshFiredrakeHierarchy 

230 

231 # ------------------------------------------------------------------------ # 

232 # petsc solver parameters 

233 # ------------------------------------------------------------------------ # 

234 

235 solver_parameters = { 

236 'snes_type': 'newtonls', 

237 'ksp_type': 'gmres', 

238 'pc_type': 'bjacobi', 

239 'sub_pc_type': 'ilu', 

240 'ksp_rtol': 1e-12, 

241 'snes_rtol': 1e-12, 

242 'ksp_atol': 1e-30, 

243 'snes_atol': 1e-30, 

244 'ksp_divtol': 1e30, 

245 'snes_divtol': 1e30, 

246 'snes_max_it': 999, 

247 'ksp_max_it': 999, 

248 } 

249 

250 # ------------------------------------------------------------------------ # 

251 # Set Gusto SDC parameters to match the pySDC ones 

252 # ------------------------------------------------------------------------ # 

253 

254 SDC_params = { 

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

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

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

258 'quad_type': sweeper_params['quad_type'], 

259 'node_type': sweeper_params['node_type'], 

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

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

262 'formulation': 'Z2N', 

263 'initial_guess': 'copy', 

264 'nonlinear_solver_parameters': solver_parameters, 

265 'linear_solver_parameters': solver_parameters, 

266 'final_update': False, 

267 } 

268 

269 # ------------------------------------------------------------------------ # 

270 # Setup time stepper 

271 # ------------------------------------------------------------------------ # 

272 

273 if use_pySDC: 

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

275 else: 

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

277 

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

279 

280 # ------------------------------------------------------------------------ # 

281 # Setup multi-level SDC 

282 # ------------------------------------------------------------------------ # 

283 

284 if not _ML_is_setup: 

285 return stepper 

286 

287 if Nlevels > 1: 

288 steppers = [ 

289 None, 

290 ] * (Nlevels) 

291 steppers[0] = stepper 

292 

293 # get different steppers on the different levels 

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

295 for i in range(1, Nlevels): 

296 steppers[i] = williamson_5( 

297 ncells_per_edge=ncells_per_edge, 

298 dt=dt, 

299 tmax=tmax, 

300 dumpfreq=dumpfreq, 

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

302 time_parallelism=time_parallelism, 

303 QI=QI, 

304 M=M, 

305 kmax=kmax, 

306 use_pySDC=use_pySDC, 

307 use_adaptivity=use_adaptivity, 

308 Nlevels=1, 

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

310 logger_level=50, 

311 _ML_is_setup=False, 

312 ) 

313 

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

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

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

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

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

319 

320 # ------------------------------------------------------------------------ # 

321 # Initial conditions 

322 # ------------------------------------------------------------------------ # 

323 

324 u0 = stepper.fields('u') 

325 D0 = stepper.fields('D') 

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

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

328 

329 u0.project(uexpr) 

330 D0.interpolate(Dexpr) 

331 

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

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

334 

335 # ------------------------------------------------------------------------ # 

336 # Run 

337 # ------------------------------------------------------------------------ # 

338 

339 if use_pySDC and use_adaptivity: 

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

341 method.timestepper = stepper 

342 

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

344 return stepper, mesh 

345 

346 

347# ---------------------------------------------------------------------------- # 

348# MAIN 

349# ---------------------------------------------------------------------------- # 

350 

351 

352if __name__ == "__main__": 

353 

354 parser = ArgumentParser(description=__doc__, formatter_class=ArgumentDefaultsHelpFormatter) 

355 parser.add_argument( 

356 '--ncells_per_edge', 

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

358 type=int, 

359 default=williamson_5_defaults['ncells_per_edge'], 

360 ) 

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

362 parser.add_argument( 

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

364 ) 

365 parser.add_argument( 

366 '--dumpfreq', 

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

368 type=int, 

369 default=williamson_5_defaults['dumpfreq'], 

370 ) 

371 parser.add_argument( 

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

373 ) 

374 parser.add_argument( 

375 '--time_parallelism', 

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

377 type=str, 

378 default=williamson_5_defaults['time_parallelism'], 

379 ) 

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

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

382 parser.add_argument( 

383 '--use_pySDC', 

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

385 type=str, 

386 default=williamson_5_defaults['use_pySDC'], 

387 ) 

388 parser.add_argument( 

389 '--use_adaptivity', 

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

391 type=str, 

392 default=williamson_5_defaults['use_adaptivity'], 

393 ) 

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

395 parser.add_argument( 

396 '--Nlevels', 

397 help="Number of SDC levels.", 

398 type=int, 

399 default=williamson_5_defaults['Nlevels'], 

400 ) 

401 args, unknown = parser.parse_known_args() 

402 

403 options = vars(args) 

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

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

406 

407 williamson_5(**options)