Coverage for pySDC/implementations/problem_classes/GenericGusto.py: 93%

110 statements  

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

1from pySDC.core.problem import Problem, WorkCounter 

2from pySDC.implementations.datatype_classes.firedrake_mesh import firedrake_mesh, IMEX_firedrake_mesh 

3from gusto.core.labels import ( 

4 time_derivative, 

5 implicit, 

6 explicit, 

7) 

8from firedrake.fml import replace_subject, all_terms, drop 

9import firedrake as fd 

10import numpy as np 

11 

12 

13class GenericGusto(Problem): 

14 """ 

15 Problem class wrapping a Gusto (Firedrake) equation, with all terms of its residual treated implicitly. 

16 

17 Keep in mind that you probably want to use the pySDC-Gusto coupling via 

18 the `pySDC_integrator` class in the helpers in order to get spatial methods rather than interfacing with this 

19 class directly. 

20 

21 Gusto equations work by a residual, which is minimized in nonlinear solvers to obtain the right hand side 

22 evaluation or the solution to (IMEX) Euler steps. You control what you solve for by manipulating labeled parts 

23 of the residual. 

24 """ 

25 

26 dtype_u = firedrake_mesh 

27 dtype_f = firedrake_mesh 

28 rhs_n_labels = 1 

29 

30 def __init__( 

31 self, 

32 equation, 

33 apply_bcs=True, 

34 solver_parameters=None, 

35 stop_at_divergence=False, 

36 LHS_cache_size=12, 

37 residual=None, 

38 *active_labels, 

39 ): 

40 """ 

41 Initialisation 

42 

43 Args: 

44 equation (:class:`PrognosticEquation`): the model's equation. 

45 apply_bcs (bool, optional): whether to apply the equation's boundary 

46 conditions. Defaults to True. 

47 solver_parameters (dict, optional): Solver parameters for the nonlinear variational problems. 

48 Defaults to GMRES with a block Jacobi preconditioner with ILU on the blocks. 

49 stop_at_divergence (bool, optional): Whether to raise an error when the variational problems do not converge. Defaults to False 

50 LHS_cache_size (int, optional): Size of the cache for solvers. Defaults to 12. 

51 residual (Firedrake.form, optional): Overwrite the residual of the equation, e.g. after adding spatial methods. Defaults to None. 

52 *active_labels (:class:`Label`): labels indicating which terms of 

53 the equation to include. 

54 """ 

55 

56 self.equation = equation 

57 self.residual = equation.residual if residual is None else residual 

58 self.field_name = equation.field_name 

59 self.fs = equation.function_space 

60 self.idx = None 

61 if solver_parameters is None: 

62 # default solver parameters 

63 solver_parameters = {'ksp_type': 'gmres', 'pc_type': 'bjacobi', 'sub_pc_type': 'ilu'} 

64 self.solver_parameters = solver_parameters 

65 self.stop_at_divergence = stop_at_divergence 

66 

67 # -------------------------------------------------------------------- # 

68 # Setup caches 

69 # -------------------------------------------------------------------- # 

70 

71 self.x_out = fd.Function(self.fs) 

72 self.solvers = {} 

73 self._u = fd.Function(self.fs) 

74 

75 super().__init__(self.fs) 

76 self._makeAttributeAndRegister('LHS_cache_size', 'apply_bcs', localVars=locals(), readOnly=True) 

77 self.work_counters['rhs'] = WorkCounter() 

78 self.work_counters['ksp'] = WorkCounter() 

79 self.work_counters['solver_setup'] = WorkCounter() 

80 self.work_counters['solver'] = WorkCounter() 

81 

82 @property 

83 def bcs(self): 

84 """ 

85 Boundary conditions of the prognostic field of the equation. 

86 

87 Returns: 

88 list of :class:`DirichletBC`: The boundary conditions, or None if `apply_bcs` is False 

89 """ 

90 if not self.apply_bcs: 

91 return None 

92 else: 

93 return self.equation.bcs[self.equation.field_name] 

94 

95 def eval_f(self, u, *args): 

96 """ 

97 Evaluate the right hand side f(u) by solving M f = -R(u), where M is the mass form (the time derivative terms of 

98 the residual) and R(u) are all other terms of the residual evaluated at `u`. The solver is set up once and 

99 cached. 

100 

101 Args: 

102 u (dtype_u): Solution at which to evaluate 

103 *args: Not used, the time is not passed on to Gusto 

104 

105 Returns: 

106 dtype_f: The right hand side 

107 """ 

108 self._u.assign(u.functionspace) 

109 

110 if 'eval_rhs' not in self.solvers.keys(): 

111 residual = self.residual.label_map( 

112 lambda t: t.has_label(time_derivative), 

113 map_if_false=replace_subject(self._u, old_idx=self.idx), 

114 map_if_true=drop, 

115 ) 

116 mass_form = self.residual.label_map( 

117 lambda t: t.has_label(time_derivative), 

118 map_if_true=replace_subject(self.x_out, old_idx=self.idx), 

119 map_if_false=drop, 

120 ) 

121 

122 problem = fd.NonlinearVariationalProblem((mass_form + residual).form, self.x_out, bcs=self.bcs) 

123 solver_name = self.field_name + self.__class__.__name__ 

124 self.solvers['eval_rhs'] = fd.NonlinearVariationalSolver( 

125 problem, solver_parameters=self.solver_parameters, options_prefix=solver_name 

126 ) 

127 self.work_counters['solver_setup']() 

128 

129 self.solvers['eval_rhs'].solve() 

130 self.work_counters['rhs']() 

131 

132 return self.dtype_f(self.x_out) 

133 

134 def solve_system(self, rhs, factor, u0, *args): 

135 """ 

136 Solve the nonlinear system (M - factor f)(u) = M rhs, i.e. M u + factor R(u) = M rhs with R the terms of the 

137 residual other than the time derivative, with a Firedrake nonlinear variational solver starting from `u0`. 

138 

139 The solvers are cached per `factor`. When the cache is full, the oldest one for a factor is evicted. If the 

140 solver does not converge, the error is raised if `stop_at_divergence` is set and only logged otherwise. 

141 

142 Args: 

143 rhs (dtype_u): Right hand side 

144 factor (float): Prefactor of the right hand side f, e.g. the node-to-node step size 

145 u0 (dtype_u): Initial guess 

146 *args: Not used, typically the time 

147 

148 Returns: 

149 dtype_u: The solution 

150 """ 

151 self.x_out.assign(u0.functionspace) # set initial guess 

152 self._u.assign(rhs.functionspace) 

153 

154 if factor not in self.solvers.keys(): 

155 if len(self.solvers) >= self.LHS_cache_size + self.rhs_n_labels: 

156 self.solvers.pop( 

157 [me for me in self.solvers.keys() if type(me) in [float, int, np.float64, np.float32]][0] 

158 ) 

159 

160 # setup left hand side (M - factor*f)(u) 

161 # put in output variable 

162 residual = self.residual.label_map(all_terms, map_if_true=replace_subject(self.x_out, old_idx=self.idx)) 

163 # multiply f by factor 

164 residual = residual.label_map( 

165 lambda t: t.has_label(time_derivative), map_if_false=lambda t: fd.Constant(factor) * t 

166 ) 

167 

168 # subtract right hand side 

169 mass_form = self.residual.label_map(lambda t: t.has_label(time_derivative), map_if_false=drop) 

170 residual -= mass_form.label_map(all_terms, map_if_true=replace_subject(self._u, old_idx=self.idx)) 

171 

172 # construct solver 

173 problem = fd.NonlinearVariationalProblem(residual.form, self.x_out, bcs=self.bcs) 

174 solver_name = f'{self.field_name}-{self.__class__.__name__}-{factor}' 

175 self.solvers[factor] = fd.NonlinearVariationalSolver( 

176 problem, solver_parameters=self.solver_parameters, options_prefix=solver_name 

177 ) 

178 self.work_counters['solver_setup']() 

179 

180 try: 

181 self.solvers[factor].solve() 

182 except fd.exceptions.ConvergenceError as error: 

183 if self.stop_at_divergence: 

184 raise error 

185 else: 

186 self.logger.debug(error) 

187 

188 self.work_counters['ksp'].niter += self.solvers[factor].snes.getLinearSolveIterations() 

189 self.work_counters['solver']() 

190 return self.dtype_u(self.x_out) 

191 

192 

193class GenericGustoImex(GenericGusto): 

194 """ 

195 Problem class wrapping a Gusto (Firedrake) equation, IMEX with the terms labeled implicit and explicit split. 

196 """ 

197 

198 dtype_f = IMEX_firedrake_mesh 

199 rhs_n_labels = 2 

200 

201 def evaluate_labeled_term(self, u, label): 

202 """ 

203 Evaluate the part of the right hand side made of the terms carrying `label`, by solving M x = -R_label(u), where 

204 M is the mass form and R_label are the terms of the residual with `label` and without the time derivative label. 

205 One solver per label is set up and cached. 

206 

207 Args: 

208 u (dtype_u): Solution at which to evaluate 

209 label (:class:`Label`): Gusto label, e.g. `implicit` or `explicit` 

210 

211 Returns: 

212 firedrake.Function: The result, in the output buffer `self.x_out` that is overwritten by the next solve 

213 """ 

214 self._u.assign(u.functionspace) 

215 

216 if label not in self.solvers.keys(): 

217 residual = self.residual.label_map( 

218 lambda t: t.has_label(label) and not t.has_label(time_derivative), 

219 map_if_true=replace_subject(self._u, old_idx=self.idx), 

220 map_if_false=drop, 

221 ) 

222 mass_form = self.residual.label_map( 

223 lambda t: t.has_label(time_derivative), 

224 map_if_true=replace_subject(self.x_out, old_idx=self.idx), 

225 map_if_false=drop, 

226 ) 

227 

228 problem = fd.NonlinearVariationalProblem((mass_form + residual).form, self.x_out, bcs=self.bcs) 

229 solver_name = self.field_name + self.__class__.__name__ 

230 self.solvers[label] = fd.NonlinearVariationalSolver( 

231 problem, solver_parameters=self.solver_parameters, options_prefix=solver_name 

232 ) 

233 self.work_counters['solver_setup']() 

234 

235 self.solvers[label].solve() 

236 return self.x_out 

237 

238 def eval_f(self, u, *args): 

239 """ 

240 Evaluate the right hand side with the terms labeled `implicit` in the implicit part and the terms labeled 

241 `explicit` in the explicit part. Terms carrying neither label are left out. 

242 

243 Args: 

244 u (dtype_u): Solution at which to evaluate 

245 *args: Not used, the time is not passed on to Gusto 

246 

247 Returns: 

248 dtype_f: The right hand side, with `impl` and `expl` parts 

249 """ 

250 me = self.dtype_f(self.init) 

251 me.impl.assign(self.evaluate_labeled_term(u, implicit)) 

252 me.expl.assign(self.evaluate_labeled_term(u, explicit)) 

253 self.work_counters['rhs']() 

254 return me 

255 

256 def solve_system(self, rhs, factor, u0, *args): 

257 """ 

258 Solve the nonlinear system (M - factor f_I)(u) = M rhs, i.e. M u + factor R_I(u) = M rhs with R_I the terms of 

259 the residual labeled `implicit`, with a Firedrake nonlinear variational solver starting from `u0`. The explicit 

260 terms are left out. Caching and convergence failures are handled as in `GenericGusto.solve_system`. 

261 

262 Args: 

263 rhs (dtype_u): Right hand side 

264 factor (float): Prefactor of the implicit part f_I, e.g. the node-to-node step size 

265 u0 (dtype_u): Initial guess 

266 *args: Not used, typically the time 

267 

268 Returns: 

269 dtype_u: The solution 

270 """ 

271 self.x_out.assign(u0.functionspace) # set initial guess 

272 self._u.assign(rhs.functionspace) 

273 

274 if factor not in self.solvers.keys(): 

275 if len(self.solvers) >= self.LHS_cache_size + self.rhs_n_labels: 

276 self.solvers.pop( 

277 [me for me in self.solvers.keys() if type(me) in [float, int, np.float64, np.float32]][0] 

278 ) 

279 

280 # setup left hand side (M - factor*f_I)(u) 

281 # put in output variable 

282 residual = self.residual.label_map( 

283 lambda t: t.has_label(time_derivative) or t.has_label(implicit), 

284 map_if_true=replace_subject(self.x_out, old_idx=self.idx), 

285 map_if_false=drop, 

286 ) 

287 # multiply f_I by factor 

288 residual = residual.label_map( 

289 lambda t: t.has_label(implicit) and not t.has_label(time_derivative), 

290 map_if_true=lambda t: fd.Constant(factor) * t, 

291 ) 

292 

293 # subtract right hand side 

294 mass_form = self.residual.label_map(lambda t: t.has_label(time_derivative), map_if_false=drop) 

295 residual -= mass_form.label_map(all_terms, map_if_true=replace_subject(self._u, old_idx=self.idx)) 

296 

297 # construct solver 

298 problem = fd.NonlinearVariationalProblem(residual.form, self.x_out, bcs=self.bcs) 

299 solver_name = f'{self.field_name}-{self.__class__.__name__}-{factor}' 

300 self.solvers[factor] = fd.NonlinearVariationalSolver( 

301 problem, solver_parameters=self.solver_parameters, options_prefix=solver_name 

302 ) 

303 self.work_counters['solver_setup']() 

304 

305 try: 

306 self.solvers[factor].solve() 

307 except fd.exceptions.ConvergenceError as error: 

308 if self.stop_at_divergence: 

309 raise error 

310 else: 

311 self.logger.debug(error) 

312 

313 self.work_counters['ksp'].niter += self.solvers[factor].snes.getLinearSolveIterations() 

314 self.work_counters['solver']() 

315 return self.dtype_u(self.x_out)