Coverage for pySDC/projects/StroemungsRaum/problem_classes/NavierStokes_2D_TaylorGreen_monolithic_FEniCS.py: 100%

136 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-25 20:28 +0000

1import logging 

2 

3import dolfin as df 

4import numpy as np 

5 

6from pySDC.core.problem import Problem 

7from pySDC.implementations.datatype_classes.fenics_mesh import fenics_mesh 

8from pySDC.projects.StroemungsRaum.problem_classes.newton_step import NewtonStep 

9 

10 

11class _PeriodicX(df.SubDomain): 

12 """ 

13 Identifies the right boundary :math:`x = 0.5` with the left boundary :math:`x = -0.5`. 

14 

15 Passed to ``FunctionSpace`` as ``constrained_domain``, so periodicity is handled by 

16 the dof map itself and the periodic dofs never enter the linear system. 

17 """ 

18 

19 def inside(self, x, on_boundary): 

20 return bool(df.near(x[0], -0.5) and on_boundary) 

21 

22 def map(self, x, y): 

23 y[0] = x[0] - 1.0 

24 y[1] = x[1] 

25 

26 

27class fenics_NSE_2D_TaylorGreen(Problem): 

28 r""" 

29 Forced two-dimensional incompressible Navier-Stokes equations on :math:`\Omega = [-0.5, 0.5]^2`, 

30 set up to expose the order reduction caused by time-dependent Dirichlet boundary conditions. 

31 

32 .. math:: 

33 \frac{\partial u}{\partial t} = - u \cdot \nabla u + \nu \Delta u - \nabla p + g, 

34 \qquad \nabla \cdot u = 0 

35 

36 The forcing :math:`g` is manufactured from the analytical solution 

37 

38 .. math:: 

39 u(x, y, t) &= 1 - e^{-8\pi^2\nu t}\sin(2\pi(x - t))\sin(\pi y)\cos(\pi y) \\ 

40 v(x, y, t) &= - e^{-8\pi^2\nu t}\cos(2\pi(x - t))\cos^2(\pi y) \\ 

41 p(x, y, t) &= 1 + \frac{4}{17}e^{-16\pi^2\nu t}\cos(4\pi(x - t))\cos(\pi y) 

42 

43 which is divergence free and exactly one-periodic in :math:`x`. On :math:`y = \pm 0.5` it 

44 collapses to the constants :math:`u = (1, 0)`, :math:`p = 1`, so the top and bottom boundary 

45 data is time-independent. 

46 

47 Because the solution is genuinely periodic in :math:`x`, the *same* exact solution satisfies 

48 both variants selected by ``periodic``: 

49 

50 - ``periodic=False``: time-dependent Dirichlet conditions on :math:`x = \pm 0.5`, 

51 - ``periodic=True``: periodic conditions on :math:`x = \pm 0.5`. 

52 

53 The only difference between the two runs is therefore the presence of time-dependent boundary 

54 data, which is what isolates the order reduction. 

55 

56 On :math:`x = \pm 0.5` the *pressure* is prescribed from the exact solution as well. That is 

57 not neutral: it acts as a partial lifting of the algebraic constraint, and it lifts the 

58 observed pressure order from :math:`M` to :math:`M+1`. Dropping it gives order :math:`M` 

59 and a roughly 20 times larger error, so the gap measured here understates what a setup 

60 without prescribed boundary pressure would show. 

61 

62 Setting ``differentiated_bc`` imposes the time-dependent data in differentiated form and 

63 recovers most of the lost order, following the remedy explored for a time-dependent 

64 *constraint* in pull request #641. It requires the ``generic_implicit_mass_diffbc`` sweeper. 

65 See :meth:`prepare_step` for the construction and its measured effect. 

66 

67 Note that the number of collocation nodes decides whether anything can be seen at all. 

68 RADAU-RIGHT with :math:`M` nodes has design order :math:`2M-1` and falls back to the stiff 

69 order :math:`M+1` in the presence of time-dependent boundary data, so the gap on offer is 

70 :math:`M-2`: **zero for M = 2**, where both are 3. Use :math:`M \geq 4`; at :math:`M = 4` 

71 the measured pressure orders are 7 (periodic) against 5 (Dirichlet). 

72 

73 The problem is discretized in space with Taylor-Hood elements on a mixed velocity-pressure 

74 space and solved monolithically, so the semi-discrete system is the differential-algebraic 

75 system :math:`M \dot{w} = f(w, t)` with the singular mass matrix :math:`M = \mathrm{diag}(M_v, 0)`. 

76 It therefore requires ``generic_implicit_mass`` as sweeper, which applies :math:`M` where needed 

77 instead of inverting it. 

78 

79 Parameters 

80 ---------- 

81 nelems : int, optional 

82 Number of elements per spatial direction. 

83 t0 : float, optional 

84 Starting time. 

85 order : int, optional 

86 Polynomial degree of the velocity space; the pressure space uses ``order - 1``. 

87 nu : float, optional 

88 Kinematic viscosity :math:`\nu`. 

89 periodic : bool, optional 

90 Use periodic instead of time-dependent Dirichlet conditions on :math:`x = \pm 0.5`. 

91 differentiated_bc : bool, optional 

92 Impose the time-dependent boundary data in differentiated form; needs ``periodic=False`` 

93 and the ``generic_implicit_mass_diffbc`` sweeper. 

94 Sol_tol : float, optional 

95 Absolute tolerance for the Newton solver. 

96 

97 Attributes 

98 ---------- 

99 V : FunctionSpace 

100 Velocity space. 

101 Q : FunctionSpace 

102 Pressure space. 

103 W : FunctionSpace 

104 Mixed velocity-pressure space. 

105 M : Matrix 

106 The velocity mass matrix :math:`\mathrm{diag}(M_v, 0)` on the mixed space. 

107 g : Expression 

108 Manufactured forcing term. 

109 bc : list of DirichletBC 

110 Dirichlet boundary conditions, time-dependent unless ``periodic``. 

111 bc_hom : list of DirichletBC 

112 Homogeneous conditions on the Dirichlet part of the boundary only, used to fix the residual. 

113 fix_bc_for_residual : bool 

114 Flag indicating that the residual requires special treatment due to boundary conditions. 

115 

116 References 

117 ---------- 

118 .. [1] The FEniCS Project Version 1.5. M. S. Alnaes, J. Blechta, J. Hake, A. Johansson, B. Kehlet, A. Logg, 

119 C. Richardson, J. Ring, M. E. Rognes, G. N. Wells. Archive of Numerical Software (2015). 

120 """ 

121 

122 dtype_u = fenics_mesh 

123 dtype_f = fenics_mesh 

124 

125 df.set_log_active(False) 

126 

127 def __init__(self, nelems=32, t0=0.0, order=2, nu=0.02, periodic=False, differentiated_bc=False, Sol_tol=1e-10): 

128 

129 # set logger level for FFC and dolfin 

130 logging.getLogger('FFC').setLevel(logging.WARNING) 

131 logging.getLogger('UFL').setLevel(logging.WARNING) 

132 

133 # set solver and form parameters 

134 df.parameters["form_compiler"]["optimize"] = True 

135 df.parameters["form_compiler"]["cpp_optimize"] = True 

136 

137 mesh = df.RectangleMesh(df.Point(-0.5, -0.5), df.Point(0.5, 0.5), nelems, nelems) 

138 

139 # define function spaces (Taylor-Hood); periodicity is baked into the dof map 

140 P2 = df.VectorElement("P", mesh.ufl_cell(), order) 

141 P1 = df.FiniteElement("P", mesh.ufl_cell(), order - 1) 

142 constraint = _PeriodicX() if periodic else None 

143 self.W = df.FunctionSpace(mesh, df.MixedElement([P2, P1]), constrained_domain=constraint) 

144 self.V = df.FunctionSpace(mesh, P2, constrained_domain=constraint) 

145 self.Q = df.FunctionSpace(mesh, P1, constrained_domain=constraint) 

146 

147 super().__init__(self.W) 

148 self._makeAttributeAndRegister( 

149 'nelems', 

150 't0', 

151 'order', 

152 'nu', 

153 'periodic', 

154 'differentiated_bc', 

155 'Sol_tol', 

156 localVars=locals(), 

157 readOnly=True, 

158 ) 

159 

160 self.logger.debug('DoFs on this level: %d', self.W.dim()) 

161 

162 # trial and test functions on the mixed space 

163 self.u, self.p = df.TrialFunctions(self.W) 

164 self.v, self.q = df.TestFunctions(self.W) 

165 

166 # velocity mass matrix on the mixed space, i.e. diag(M_v, 0) 

167 self.M = df.assemble(df.inner(self.u, self.v) * df.dx) 

168 

169 # manufactured solution and the forcing term derived from it 

170 self.u_ex = df.Expression( 

171 ( 

172 '1.0 - exp(-8*pi*pi*nu*t)*sin(2*pi*(x[0] - t))*sin(pi*x[1])*cos(pi*x[1])', 

173 '-exp(-8*pi*pi*nu*t)*cos(2*pi*(x[0] - t))*cos(pi*x[1])*cos(pi*x[1])', 

174 ), 

175 pi=np.pi, 

176 nu=nu, 

177 t=t0, 

178 degree=order + 2, 

179 ) 

180 self.p_ex = df.Expression( 

181 '1.0 + (4.0/17.0)*exp(-16*pi*pi*nu*t)*cos(4*pi*(x[0] - t))*cos(pi*x[1])', 

182 pi=np.pi, 

183 nu=nu, 

184 t=t0, 

185 degree=order + 2, 

186 ) 

187 self.g = df.Expression( 

188 ( 

189 'pi/34.0*exp(-16*pi*pi*nu*t)*sin(4*pi*(t - x[0]))*cos(pi*x[1])*(32.0 - 17.0*cos(pi*x[1]))', 

190 '2*pi*pi*nu*exp(-8*pi*pi*nu*t)*cos(2*pi*(t - x[0]))' 

191 ' - pi*exp(-16*pi*pi*nu*t)*sin(pi*x[1])' 

192 '*(2.0*pow(cos(pi*x[1]), 3) + 4.0/17.0*cos(4*pi*(t - x[0])))', 

193 ), 

194 pi=np.pi, 

195 nu=nu, 

196 t=t0, 

197 degree=order + 2, 

198 ) 

199 

200 # on y = +-0.5 the exact solution is constant in space and time 

201 top_bottom = 'near(x[1], -0.5) || near(x[1], 0.5)' 

202 self.left_right = 'near(x[0], -0.5) || near(x[0], 0.5)' 

203 self.bc_fixed = [ 

204 df.DirichletBC(self.W.sub(0), df.Constant((1.0, 0.0)), top_bottom), 

205 df.DirichletBC(self.W.sub(1), df.Constant(1.0), top_bottom), 

206 ] 

207 self.bc = list(self.bc_fixed) 

208 if not periodic: 

209 self.bc += [ 

210 df.DirichletBC(self.W.sub(0), self.u_ex, self.left_right), 

211 df.DirichletBC(self.W.sub(1), self.p_ex, self.left_right), 

212 ] 

213 

214 # boundary conditions per collocation node, filled in by prepare_step 

215 self._node_times = None 

216 self._node_bcs = None 

217 if differentiated_bc: 

218 if periodic: 

219 raise ValueError('differentiated_bc has no effect without time-dependent boundary data') 

220 self.u_dot, self.p_dot = self._boundary_derivatives(nu, order, t0) 

221 

222 # the residual is meaningless where the solution is prescribed, but only there: with 

223 # periodicity the dofs on x = +-0.5 are unknowns and their residual has to be kept 

224 dirichlet = top_bottom if periodic else 'on_boundary' 

225 self.bc_hom = [ 

226 df.DirichletBC(self.W.sub(0), df.Constant((0.0, 0.0)), dirichlet), 

227 df.DirichletBC(self.W.sub(1), df.Constant(0.0), dirichlet), 

228 ] 

229 self.fix_bc_for_residual = True 

230 

231 # residual form for a single node-to-node step, assembled once; `factor` and the 

232 # boundary/forcing expressions carry the time dependence 

233 self.factor = df.Constant(0.0) 

234 self.w = df.Function(self.W) 

235 u, p = df.split(self.w) 

236 

237 F = df.dot(u, self.v) * df.dx 

238 F += self.factor * df.dot(df.dot(u, df.nabla_grad(u)), self.v) * df.dx 

239 F += self.factor * self.nu * df.inner(df.nabla_grad(u), df.nabla_grad(self.v)) * df.dx 

240 F -= self.factor * df.dot(p, df.div(self.v)) * df.dx 

241 F -= self.factor * df.dot(self.g, self.v) * df.dx 

242 F -= self.factor * df.dot(df.div(u), self.q) * df.dx 

243 

244 self.step = NewtonStep(F, df.derivative(F, self.w)) 

245 self.newton = df.NewtonSolver() 

246 self.newton.parameters['absolute_tolerance'] = Sol_tol 

247 

248 @staticmethod 

249 def _boundary_derivatives(nu, order, t0): 

250 r""" 

251 Time derivatives of the boundary data, needed to impose it in differentiated form. 

252 

253 Returns 

254 ------- 

255 u_dot, p_dot : Expression 

256 :math:`\partial_t u` and :math:`\partial_t p` of the manufactured solution. 

257 """ 

258 kwargs = dict(pi=np.pi, nu=nu, t=t0, degree=order + 2) 

259 u_dot = df.Expression( 

260 ( 

261 '8*pi*pi*nu*exp(-8*pi*pi*nu*t)*sin(2*pi*(x[0] - t))*sin(pi*x[1])*cos(pi*x[1])' 

262 ' + 2*pi*exp(-8*pi*pi*nu*t)*cos(2*pi*(x[0] - t))*sin(pi*x[1])*cos(pi*x[1])', 

263 '8*pi*pi*nu*exp(-8*pi*pi*nu*t)*cos(2*pi*(x[0] - t))*cos(pi*x[1])*cos(pi*x[1])' 

264 ' - 2*pi*exp(-8*pi*pi*nu*t)*sin(2*pi*(x[0] - t))*cos(pi*x[1])*cos(pi*x[1])', 

265 ), 

266 **kwargs, 

267 ) 

268 p_dot = df.Expression( 

269 '(4.0/17.0)*cos(pi*x[1])*(' 

270 '-16*pi*pi*nu*exp(-16*pi*pi*nu*t)*cos(4*pi*(x[0] - t))' 

271 ' + 4*pi*exp(-16*pi*pi*nu*t)*sin(4*pi*(x[0] - t)))', 

272 **kwargs, 

273 ) 

274 return u_dot, p_dot 

275 

276 def prepare_step(self, t0, dt, coll): 

277 r""" 

278 Build the differentiated boundary conditions for every collocation node of a step. 

279 

280 Rather than evaluating the boundary data pointwise at the node, :math:`u_B(\tau_m) = 

281 g(\tau_m)`, the condition is imposed on the *derivative* and the stage value recovered 

282 by the collocation quadrature, 

283 

284 .. math:: 

285 u_B(\tau_m) = g(t_0) + \Delta t \sum_j Q_{mj}\, \dot{g}(\tau_j). 

286 

287 The two differ by the quadrature error :math:`O(\Delta t^{M+1})`, but the second is 

288 consistent with the collocation polynomial instead of pointwise exact, which is what 

289 recovers the order lost to time-dependent boundary data. 

290 

291 Measured at :math:`M = 4`, ``nelems=24``, ``nu=0.1``, orders and errors in the pressure 

292 taken from consecutive step sizes: 

293 

294 ========================= ========== ===================== 

295 boundary condition order error at ``dt = 0.1`` 

296 ========================= ========== ===================== 

297 periodic (best possible) 6.32 7.4e-08 

298 pointwise 5.74 9.7e-07 

299 differentiated 6.30 1.3e-07 

300 ========================= ========== ===================== 

301 

302 The remaining factor of 1.8 against the periodic case is a constant, not a rate. Note 

303 that the observed orders here are pre-asymptotic -- the periodic reference does not 

304 reach its design order 7 either -- so these numbers show that the remedy works, not 

305 that it restores exactly :math:`2M-1`. 

306 

307 Called once per step by :class:`generic_implicit_mass_diffbc`; ``solve_system`` then 

308 picks the condition belonging to the node it is asked to solve at. 

309 

310 Parameters 

311 ---------- 

312 t0 : float 

313 Left end of the step. 

314 dt : float 

315 Step size. 

316 coll : pySDC.core.collocation.CollBase 

317 Collocation rule of the sweeper, supplying the nodes and the matrix Q. 

318 """ 

319 if not self.differentiated_bc: 

320 raise RuntimeError( 

321 'prepare_step builds the differentiated boundary conditions, which this problem ' 

322 'was not set up for; use generic_implicit_mass or pass differentiated_bc=True' 

323 ) 

324 

325 M = coll.num_nodes 

326 Q = coll.Qmat[1:, 1:] 

327 self._node_times = t0 + dt * np.asarray(coll.nodes) 

328 

329 u_rate, p_rate = [], [] 

330 for j in range(M): 

331 self.u_dot.t = self._node_times[j] 

332 self.p_dot.t = self._node_times[j] 

333 u_rate.append(df.interpolate(self.u_dot, self.V)) 

334 p_rate.append(df.interpolate(self.p_dot, self.Q)) 

335 

336 self.u_ex.t = t0 

337 self.p_ex.t = t0 

338 u_base = df.interpolate(self.u_ex, self.V) 

339 p_base = df.interpolate(self.p_ex, self.Q) 

340 

341 self._node_bcs = [] 

342 for m in range(M): 

343 gu, gp = df.Function(self.V), df.Function(self.Q) 

344 gu.assign(u_base) 

345 gp.assign(p_base) 

346 for j in range(M): 

347 gu.vector().axpy(dt * Q[m, j], u_rate[j].vector()) 

348 gp.vector().axpy(dt * Q[m, j], p_rate[j].vector()) 

349 self._node_bcs.append( 

350 self.bc_fixed 

351 + [ 

352 df.DirichletBC(self.W.sub(0), gu, self.left_right), 

353 df.DirichletBC(self.W.sub(1), gp, self.left_right), 

354 ] 

355 ) 

356 

357 def solve_system(self, rhs, factor, u0, t): 

358 r""" 

359 Newton solver for :math:`M w + factor \cdot N(w, t) = rhs`, where :math:`N` collects the 

360 convective, viscous, pressure and divergence terms and ``rhs`` is the mass-weighted 

361 right-hand side assembled by the sweeper. 

362 

363 Parameters 

364 ---------- 

365 rhs : dtype_f 

366 Right-hand side for the nonlinear system. 

367 factor : float 

368 Abbrev. for the node-to-node stepsize (or any other factor required). 

369 u0 : dtype_u 

370 Initial guess for the iterative solver. 

371 t : float 

372 Current time. 

373 

374 Returns 

375 ------- 

376 w : dtype_u 

377 Solution. 

378 """ 

379 self.factor.assign(factor) 

380 self.u_ex.t = t 

381 self.p_ex.t = t 

382 self.g.t = t 

383 

384 if self.differentiated_bc: 

385 if self._node_bcs is None: 

386 raise RuntimeError( 

387 'differentiated_bc requires the generic_implicit_mass_diffbc sweeper, ' 

388 'which calls prepare_step once per step' 

389 ) 

390 node = np.flatnonzero(self._node_times == t) 

391 if node.size != 1: 

392 raise RuntimeError( 

393 f'no collocation node of the prepared step is at t = {t}; the prepared step ' 

394 f'covers {self._node_times}' 

395 ) 

396 self.bc = self._node_bcs[node[0]] 

397 

398 self.w.vector()[:] = u0.values.vector()[:] 

399 self.step.rhs = rhs.values.vector() 

400 self.step.bcs = self.bc 

401 self.newton.solve(self.step, self.w.vector()) 

402 

403 me = self.dtype_u(self.W) 

404 me.values.vector()[:] = self.w.vector()[:] 

405 return me 

406 

407 def eval_f(self, w, t): 

408 r""" 

409 Routine to evaluate the right-hand side of the problem in weak form, i.e. *without* 

410 applying :math:`M^{-1}`. 

411 

412 Parameters 

413 ---------- 

414 w : dtype_u 

415 Current values of the numerical solution. 

416 t : float 

417 Current time at which the numerical solution is computed. 

418 

419 Returns 

420 ------- 

421 f : dtype_f 

422 The right-hand side. 

423 """ 

424 u, p = df.split(w.values) 

425 self.g.t = t 

426 

427 F = -df.dot(df.dot(u, df.nabla_grad(u)), self.v) * df.dx 

428 F -= self.nu * df.inner(df.nabla_grad(u), df.nabla_grad(self.v)) * df.dx 

429 F += df.dot(p, df.div(self.v)) * df.dx 

430 F += df.dot(self.g, self.v) * df.dx 

431 F += df.dot(df.div(u), self.q) * df.dx 

432 

433 f = self.dtype_f(self.W) 

434 df.assemble(F, tensor=f.values.vector()) 

435 return f 

436 

437 def apply_mass_matrix(self, w): 

438 r""" 

439 Routine to apply the velocity mass matrix. 

440 

441 Parameters 

442 ---------- 

443 w : dtype_u 

444 Current values of the numerical solution. 

445 

446 Returns 

447 ------- 

448 me : dtype_u 

449 The product :math:`M \vec{w}`. 

450 """ 

451 me = self.dtype_u(self.W) 

452 self.M.mult(w.values.vector(), me.values.vector()) 

453 return me 

454 

455 def u_exact(self, t): 

456 r""" 

457 Routine to compute the exact solution at time :math:`t`. 

458 

459 Parameters 

460 ---------- 

461 t : float 

462 Time of the exact solution. 

463 

464 Returns 

465 ------- 

466 me : dtype_u 

467 Exact solution. 

468 """ 

469 self.u_ex.t = t 

470 self.p_ex.t = t 

471 

472 me = self.dtype_u(self.W) 

473 df.assign(me.values.sub(0), df.interpolate(self.u_ex, self.V)) 

474 df.assign(me.values.sub(1), df.interpolate(self.p_ex, self.Q)) 

475 return me 

476 

477 def fix_residual(self, res): 

478 """ 

479 Applies homogeneous Dirichlet boundary conditions to the residual, on the Dirichlet part 

480 of the boundary only. 

481 

482 Parameters 

483 ---------- 

484 res : dtype_u 

485 Residual. 

486 """ 

487 for bc in self.bc_hom: 

488 bc.apply(res.values.vector()) 

489 return None