Coverage for pySDC/projects/StroemungsRaum/problem_classes/NavierStokes_2D_monolithic_FEniCS.py: 100%
108 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
1import logging
2import os
4import dolfin as df
5import numpy as np
7from pySDC.core.problem import Problem
8from pySDC.implementations.datatype_classes.fenics_mesh import fenics_mesh
9from pySDC.projects.StroemungsRaum.problem_classes.newton_step import NewtonStep
12class fenics_NSE_2D_Monolithic(Problem):
13 r"""
14 Example implementing the forced two-dimensional incompressible Navier-Stokes equations with
15 time-dependent Dirichlet boundary conditions
17 .. math::
18 \frac{\partial u}{\partial t} = - u \cdot \nabla u + \nu \Delta u - \nabla p + f
19 0 = \nabla \cdot u
21 for :math:`x \in \Omega`, where the forcing term :math:`f` is defined by
23 .. math::
24 f(x, t) = (0, 0).
26 This implementation follows a monolithic approach, where velocity and pressure are
27 solved simultaneously in a coupled system using a mixed finite element formulation.
29 Boundary conditions are applied on subsets of the boundary:
30 - no-slip on channel walls and cylinder surface,
31 - a time-dependent inflow profile at the inlet,
32 - pressure condition at the outflow.
34 In this class the problem is implemented in the way that the spatial part is solved using ``FEniCS`` [1]_. Hence, the problem
35 is reformulated to the *weak formulation*
37 .. math::
38 \int_\Omega u_t v\,dx = - \int_\Omega u \cdot \nabla u v\,dx - \nu \int_\Omega \nabla u \nabla v\,dx + \int_\Omega p \nabla \cdot v\,dx + \int_\Omega f v\,dx
39 \int_\Omega \nabla \cdot u q\,dx = 0
41 Parameters
42 ----------
43 t0 : float, optional
44 Starting time.
45 order : int, optional
46 Defines the order of the elements in the function space.
47 nu : float, optional
48 Diffusion coefficient :math:`\nu`.
49 Sol_tol : float, optional
50 Tolerance for the nonlinear solver.
52 Attributes
53 ----------
54 V : FunctionSpace
55 Defines the function space of the trial and test functions for velocity.
56 Q : FunctionSpace
57 Defines the function space of the trial and test functions for pressure.
58 W : FunctionSpace
59 Defines the mixed function space for the coupled velocity-pressure system.
60 M : scalar, vector, matrix or higher rank tensor
61 Denotes the expression :math:`\int_\Omega u_t v\,dx`.
62 g : Expression
63 The forcing term :math:`f` in the Navier-Stokes momentum equation.
64 bc : DirichletBC
65 Denotes the time-dependent Dirichlet boundary conditions.
66 bc_hom : DirichletBC
67 Denotes the homogeneous Dirichlet boundary conditions, potentially required for fixing the residual
68 fix_bc_for_residual: boolean
69 flag to indicate that the residual requires special treatment due to boundary conditions
71 References
72 ----------
73 .. [1] The FEniCS Project Version 1.5. M. S. Alnaes, J. Blechta, J. Hake, A. Johansson, B. Kehlet, A. Logg,
74 C. Richardson, J. Ring, M. E. Rognes, G. N. Wells. Archive of Numerical Software (2015).
75 .. [2] Automated Solution of Differential Equations by the Finite Element Method. A. Logg, K.-A. Mardal, G. N.
76 Wells and others. Springer (2012).
77 """
79 dtype_u = fenics_mesh
80 dtype_f = fenics_mesh
82 df.set_log_active(False)
84 def __init__(self, t0=0.0, order=2, nu=0.001, Sol_tol=1e-10):
86 # set logger level for FFC and dolfin
87 logging.getLogger('FFC').setLevel(logging.WARNING)
88 logging.getLogger('UFL').setLevel(logging.WARNING)
90 # set solver and form parameters
91 df.parameters["form_compiler"]["optimize"] = True
92 df.parameters["form_compiler"]["cpp_optimize"] = True
93 df.parameters['allow_extrapolation'] = True
95 # load mesh
96 path = f"{os.path.dirname(__file__)}/../meshs/"
97 mesh = df.Mesh(path + '/cylinder.xml')
99 # define function spaces for future reference (Taylor-Hood)
100 P2 = df.VectorElement("P", mesh.ufl_cell(), order)
101 P1 = df.FiniteElement("P", mesh.ufl_cell(), order - 1)
102 TH = df.MixedElement([P2, P1])
103 self.W = df.FunctionSpace(mesh, TH)
104 self.V = df.FunctionSpace(mesh, P2)
105 self.Q = df.FunctionSpace(mesh, P1)
107 # print the number of DoFs for debugging purposes
108 tmp = df.Function(self.W)
109 self.logger.debug('DoFs on this level: %d', len(tmp.vector()[:]))
111 super().__init__(self.W)
112 self._makeAttributeAndRegister('t0', 'order', 'nu', 'Sol_tol', localVars=locals(), readOnly=True)
114 # Trial and test function for the Mixed FE space
115 self.u, self.p = df.TrialFunctions(self.W)
116 self.v, self.q = df.TestFunctions(self.W)
118 # velocity mass matrix
119 a_M = df.inner(self.u, self.v) * df.dx
120 self.M = df.assemble(a_M)
122 # define the time-dependent inflow profile as an Expression
123 Uin = '4.0*1.5*sin(pi*t/8)*x[1]*(0.41 - x[1]) / pow(0.41, 2)'
124 self.u_in = df.Expression((Uin, '0'), pi=np.pi, t=t0, degree=self.order)
126 # define boundaries
127 inflow = 'near(x[0], 0)'
128 outflow = 'near(x[0], 2.2)'
129 walls = 'near(x[1], 0) || near(x[1], 0.41)'
130 cylinder = 'on_boundary && x[0]>0.1 && x[0]<0.3 && x[1]>0.1 && x[1]<0.3'
132 # define boundary conditions
133 bc_in = df.DirichletBC(self.W.sub(0), self.u_in, inflow)
134 bc_out = df.DirichletBC(self.W.sub(1), 0, outflow)
135 bc_walls = df.DirichletBC(self.W.sub(0), (0, 0), walls)
136 bc_cylinder = df.DirichletBC(self.W.sub(0), (0, 0), cylinder)
137 self.bc = [bc_cylinder, bc_walls, bc_out, bc_in]
139 # homogeneous boundary conditions for fixing the residual
140 bc_hom_u = df.DirichletBC(self.W.sub(0), df.Constant((0, 0)), 'on_boundary')
141 bc_hom_p = df.DirichletBC(self.W.sub(1), df.Constant(0), 'on_boundary')
142 self.bc_hom = [bc_hom_u, bc_hom_p]
143 self.fix_bc_for_residual = True
145 # define measure for drag and lift computation
146 Cylinder = df.CompiledSubDomain(cylinder)
147 CylinderBoundary = df.MeshFunction("size_t", mesh, mesh.topology().dim() - 1, 0)
148 Cylinder.mark(CylinderBoundary, 1)
149 self.dsc = df.Measure("ds", domain=mesh, subdomain_data=CylinderBoundary, subdomain_id=1)
151 # set forcing term as expression
152 self.g = df.Expression(('0', '0'), a=np.pi, b=self.nu, t=self.t0, degree=self.order)
154 # initialize XDMF files for velocity and pressure if needed
155 self.xdmffile_p = None
156 self.xdmffile_u = None
158 # interpolated forcing term referenced by the residual form
159 self.g_h = df.Function(self.V)
161 # residual form for a single node-to-node step, assembled once; the step size is a
162 # Constant and the boundary and forcing data carry the time dependence
163 self.factor = df.Constant(0.0)
164 self.w = df.Function(self.W)
165 u, p = df.split(self.w)
167 F = df.dot(u, self.v) * df.dx
168 F += self.factor * df.dot(df.dot(u, df.nabla_grad(u)), self.v) * df.dx
169 F += self.factor * self.nu * df.inner(df.nabla_grad(u), df.nabla_grad(self.v)) * df.dx
170 F -= self.factor * df.dot(p, df.div(self.v)) * df.dx
171 F -= self.factor * df.dot(self.g_h, self.v) * df.dx
172 F -= self.factor * df.dot(df.div(u), self.q) * df.dx
174 self.step = NewtonStep(F, df.derivative(F, self.w))
175 self.newton = df.NewtonSolver()
176 self.newton.parameters['absolute_tolerance'] = Sol_tol
178 def use_restricted_boundary_values(self, u, t):
179 """
180 Refuse multilevel runs: the solves impose the time-dependent boundary data, which at the coarse nodes is not
181 the restricted fine boundary values, so MLSDC and PFASST would not converge to the collocation solution. See
182 :meth:`pySDC.core.problem.Problem.use_restricted_boundary_values`.
183 """
184 raise NotImplementedError(f'{type(self).__name__} cannot impose restricted boundary values on coarse levels')
186 def solve_system(self, rhs, factor, u0, t):
187 r"""
188 Dolfin's nonlinear solver for :
189 .. math::
190 (u,v) + factor * (u \cdot \nabla u, v) + factor * \nu (\nabla u, \nabla v) - factor * (p, \nabla \cdot v) - factor * (g, v) - factor * (div(u), q) = (rhs_u, v) + (rhs_p, q)
192 Parameters
193 ----------
194 rhs : dtype_f
195 Right-hand side for the nonlinear system.
196 factor : float
197 Abbrev. for the node-to-node stepsize (or any other factor required).
198 u0 : dtype_u
199 Initial guess for the iterative solver (not used here so far).
200 t : float
201 Current time.
203 Returns
204 -------
205 w : dtype_u
206 Solution.
207 """
208 # update time in boundary conditions and in the forcing term
209 self.u_in.t = t
210 self.g.t = t
211 self.g_h.interpolate(self.g)
212 self.factor.assign(factor)
214 # the SDC right-hand side enters the residual as a vector, no mass matrix involved
215 self.w.vector()[:] = u0.values.vector()[:]
216 self.step.rhs = rhs.values.vector()
217 self.step.bcs = self.bc
219 # solve the nonlinear system using Newton's method
220 self.newton.solve(self.step, self.w.vector())
222 w = self.dtype_u(self.W)
223 w.values.vector()[:] = self.w.vector()[:]
225 return w
227 def eval_f(self, w, t):
228 """
229 Routine to evaluate both parts of the right-hand side of the problem.
231 Parameters
232 ----------
233 w : dtype_u
234 Current values of the numerical solution.
235 t : float
236 Current time at which the numerical solution is computed.
238 Returns
239 -------
240 f : dtype_f
241 The right-hand side.
242 """
244 f = self.dtype_f(self.W)
246 u, p = df.split(w.values)
248 # get the forcing term
249 self.g.t = t
250 g = self.dtype_f(df.interpolate(self.g, self.V))
252 F = -1.0 * df.dot(df.dot(u, df.nabla_grad(u)), self.v) * df.dx
253 F -= self.nu * df.inner(df.nabla_grad(u), df.nabla_grad(self.v)) * df.dx
254 F += df.dot(p, df.div(self.v)) * df.dx
255 F += df.dot(g.values, self.v) * df.dx
256 F += df.dot(df.div(u), self.q) * df.dx
258 df.assemble(F, tensor=f.values.vector())
260 return f
262 def apply_mass_matrix(self, w):
263 r"""
264 Routine to apply velocity mass matrix.
266 Parameters
267 ----------
268 w : dtype_u
269 Current values of the numerical solution.
271 Returns
272 -------
273 me : dtype_u
274 The product :math:`M \vec{w}`.
275 """
277 me = self.dtype_u(self.W)
278 self.M.mult(w.values.vector(), me.values.vector())
280 return me
282 def u_exact(self, t):
283 r"""
284 Routine to compute the exact solution at time :math:`t`.
286 Parameters
287 ----------
288 t : float
289 Time of the exact solution.
291 Returns
292 -------
293 me : dtype_u
294 Exact solution.
295 """
296 # define the exact solution
297 w = df.Function(self.W)
299 # assign the exact solution as an Expression
300 df.assign(w.sub(0), df.interpolate(df.Expression(('0.0', '0.0'), degree=self.order), self.V))
301 df.assign(w.sub(1), df.interpolate(df.Expression('0.0', degree=self.order - 1), self.Q))
303 # update time in Boundary conditions
304 self.u_in.t = t
305 [bc.apply(w.vector()) for bc in self.bc]
307 me = self.dtype_u(w, val=self.W)
309 return me
311 def fix_residual(self, res):
312 """
313 Applies homogeneous Dirichlet boundary conditions to the residual
315 Parameters
316 ----------
317 res : dtype_u
318 Residual
319 """
320 [bc.apply(res.values.vector()) for bc in self.bc_hom]
322 return None