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

114 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-20 10:09 +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: coriolis 

158 parameters = ShallowWaterParameters(H=mean_depth, g=g) 

159 Omega = parameters.Omega 

160 fexpr = 2 * Omega * z / radius 

161 

162 # Equation: topography 

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

164 r = sqrt(rsq) 

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

166 eqns = ShallowWaterEquations(domain, parameters, fexpr=fexpr, topog_expr=tpexpr) 

167 

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

169 

170 # I/O 

171 output = OutputParameters( 

172 dirname=dirname, 

173 dumplist_latlon=['D'], 

174 dumpfreq=dumpfreq, 

175 dump_vtus=True, 

176 dump_nc=True, 

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

178 ) 

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

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

181 

182 # Transport schemes 

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

184 

185 # ------------------------------------------------------------------------ # 

186 # pySDC parameters: description and controller parameters 

187 # ------------------------------------------------------------------------ # 

188 

189 level_params = dict() 

190 level_params['restol'] = -1 

191 level_params['dt'] = dt 

192 level_params['residual_type'] = 'full_rel' 

193 

194 step_params = dict() 

195 step_params['maxiter'] = kmax 

196 

197 sweeper_params = dict() 

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

199 sweeper_params['node_type'] = 'LEGENDRE' 

200 sweeper_params['num_nodes'] = M 

201 sweeper_params['QI'] = QI 

202 sweeper_params['QE'] = 'PIC' 

203 sweeper_params['comm'] = ensemble_comm 

204 sweeper_params['initial_guess'] = 'copy' 

205 

206 problem_params = dict() 

207 

208 convergence_controllers = {} 

209 if use_adaptivity: 

210 from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity 

211 from pySDC.implementations.convergence_controller_classes.spread_step_sizes import ( 

212 SpreadStepSizesBlockwiseNonMPI, 

213 ) 

214 

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

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

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

218 

219 controller_params = dict() 

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

221 controller_params['mssdc_jac'] = False 

222 

223 description = dict() 

224 description['problem_params'] = problem_params 

225 description['sweeper_class'] = sweeper_class 

226 description['sweeper_params'] = sweeper_params 

227 description['level_params'] = level_params 

228 description['step_params'] = step_params 

229 description['convergence_controllers'] = convergence_controllers 

230 

231 from pySDC.implementations.transfer_classes.TransferFiredrakeMesh import MeshToMeshFiredrakeHierarchy 

232 

233 description['space_transfer_class'] = MeshToMeshFiredrakeHierarchy 

234 

235 # ------------------------------------------------------------------------ # 

236 # petsc solver parameters 

237 # ------------------------------------------------------------------------ # 

238 

239 solver_parameters = { 

240 'snes_type': 'newtonls', 

241 'ksp_type': 'gmres', 

242 'pc_type': 'bjacobi', 

243 'sub_pc_type': 'ilu', 

244 'ksp_rtol': 1e-12, 

245 'snes_rtol': 1e-12, 

246 'ksp_atol': 1e-30, 

247 'snes_atol': 1e-30, 

248 'ksp_divtol': 1e30, 

249 'snes_divtol': 1e30, 

250 'snes_max_it': 999, 

251 'ksp_max_it': 999, 

252 } 

253 

254 # ------------------------------------------------------------------------ # 

255 # Set Gusto SDC parameters to match the pySDC ones 

256 # ------------------------------------------------------------------------ # 

257 

258 SDC_params = { 

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

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

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

262 'quad_type': sweeper_params['quad_type'], 

263 'node_type': sweeper_params['node_type'], 

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

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

266 'formulation': 'Z2N', 

267 'initial_guess': 'copy', 

268 'nonlinear_solver_parameters': solver_parameters, 

269 'linear_solver_parameters': solver_parameters, 

270 'final_update': False, 

271 } 

272 

273 # ------------------------------------------------------------------------ # 

274 # Setup time stepper 

275 # ------------------------------------------------------------------------ # 

276 

277 if use_pySDC: 

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

279 else: 

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

281 

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

283 

284 # ------------------------------------------------------------------------ # 

285 # Setup multi-level SDC 

286 # ------------------------------------------------------------------------ # 

287 

288 if not _ML_is_setup: 

289 return stepper 

290 

291 if Nlevels > 1: 

292 steppers = [ 

293 None, 

294 ] * (Nlevels) 

295 steppers[0] = stepper 

296 

297 # get different steppers on the different levels 

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

299 for i in range(1, Nlevels): 

300 steppers[i] = williamson_5( 

301 ncells_per_edge=ncells_per_edge, 

302 dt=dt, 

303 tmax=tmax, 

304 dumpfreq=dumpfreq, 

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

306 time_parallelism=time_parallelism, 

307 QI=QI, 

308 M=M, 

309 kmax=kmax, 

310 use_pySDC=use_pySDC, 

311 use_adaptivity=use_adaptivity, 

312 Nlevels=1, 

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

314 logger_level=50, 

315 _ML_is_setup=False, 

316 ) 

317 

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

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

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

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

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

323 

324 # ------------------------------------------------------------------------ # 

325 # Initial conditions 

326 # ------------------------------------------------------------------------ # 

327 

328 u0 = stepper.fields('u') 

329 D0 = stepper.fields('D') 

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

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

332 

333 u0.project(uexpr) 

334 D0.interpolate(Dexpr) 

335 

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

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

338 

339 # ------------------------------------------------------------------------ # 

340 # Run 

341 # ------------------------------------------------------------------------ # 

342 

343 if use_pySDC and use_adaptivity: 

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

345 method.timestepper = stepper 

346 

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

348 return stepper, mesh 

349 

350 

351# ---------------------------------------------------------------------------- # 

352# MAIN 

353# ---------------------------------------------------------------------------- # 

354 

355 

356if __name__ == "__main__": 

357 

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

359 parser.add_argument( 

360 '--ncells_per_edge', 

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

362 type=int, 

363 default=williamson_5_defaults['ncells_per_edge'], 

364 ) 

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

366 parser.add_argument( 

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

368 ) 

369 parser.add_argument( 

370 '--dumpfreq', 

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

372 type=int, 

373 default=williamson_5_defaults['dumpfreq'], 

374 ) 

375 parser.add_argument( 

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

377 ) 

378 parser.add_argument( 

379 '--time_parallelism', 

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

381 type=str, 

382 default=williamson_5_defaults['time_parallelism'], 

383 ) 

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

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

386 parser.add_argument( 

387 '--use_pySDC', 

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

389 type=str, 

390 default=williamson_5_defaults['use_pySDC'], 

391 ) 

392 parser.add_argument( 

393 '--use_adaptivity', 

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

395 type=str, 

396 default=williamson_5_defaults['use_adaptivity'], 

397 ) 

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

399 parser.add_argument( 

400 '--Nlevels', 

401 help="Number of SDC levels.", 

402 type=int, 

403 default=williamson_5_defaults['Nlevels'], 

404 ) 

405 args, unknown = parser.parse_known_args() 

406 

407 options = vars(args) 

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

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

410 

411 williamson_5(**options)