Discontinuity of pressure in Navier Stokes solution

I have a question regarding the case when solving Navier-Stokes and on some boundaries, both Neumann and Dirichlet boundary conditions are applied, on neighboring segments.

For example I considered the analytical solution:
\mathbf{u} = \begin{pmatrix}-\sin (2\pi y) \\ \sin(2\pi x) \end{pmatrix} \exp(-4\nu\pi^2 t)
p = -\cos(2\pi x) \cos(2\pi y) \exp (-8\nu \pi^2 t)

taken from https://arxiv.org/abs/1706.09252

The domain is square and on each side, half the length is an inflow and the other half an outflow. Where inflow occurs, Dirichlet boundary conditions are assigned, while Neumann boundary conditions are given on the outflow segments. I define the domains accordingly:

and solve Navier Stokes with the standard steps of the IPCS solution algorithm:

  • Momentum step \langle\frac{ 3/2 \mathbf{u} - 2 \mathbf{u}_0 + 1/2 \mathbf{u}_{00}}{\Delta t}, v \rangle_{\Omega} + \langle 2(\mathbf{u} _0 \cdot \nabla) \mathbf{u}_0 - (\mathbf{u}_{00}\cdot \nabla) \mathbf{u}_{00}, v\rangle_{\Omega} + \langle \sigma (\mathbf{u} , p_0), \epsilon(v)\rangle_{\Omega} + \\ \langle p_0, v\rangle_{\partial \Omega} - \langle \nu \nabla \mathbf{u} \cdot \mathbf{n}, v\rangle_{\partial \Omega} = 0. A Neumann boundary condition also appears, while the old pressure value p_0 is used. BDF2 time stepping.

  • Pressure step \langle \nabla p - p_0 , \nabla q \rangle_{\Omega} = -3/2 \frac{1}{\Delta t} \langle \text{div} \mathbf{u} , q\rangle_{\Omega} with Dirichlet boundary condition p = p^{n+1} on the Neumann boundaries.

  • Correction step \langle \mathbf{u}^{n+1},v \rangle = \langle \mathbf{u} + \frac{2}{3} \Delta t \nabla (p-p_0), v \rangle_{\Omega}

Simulations are run with standard P2/P1 finite elements until time T=1, with various time steps. The problem is that the pressure shows an irregular behaviour at the corners and at x=0, y=0 on the boundary, that is the points where Neumann and Dirichlet segments touch. Here is the divergence field after the beginning of the simulation:

which clearly shows at which points trouble is starting. The resulting pressure at T=1:

Can you help me figure out what is going wrong here? Things work if I assign Dirichlet over all of the boundary, but not in this case.

My code, based on the FEniCS tutorial

import matplotlib.pyplot as plt
from dolfin import *
import ufl

# Print log messages only from the root process in parallel
parameters["std_out_all_processes"] = False;

Nc = 32
#mesh = UnitSquareMesh(MPI.comm_world,Nc,Nc)
mesh = RectangleMesh(Point(-0.5, -0.5), Point(0.5, 0.5), Nc, Nc)
x,y = SpatialCoordinate(mesh)
n = FacetNormal(mesh)

# Define function spaces (P2-P1)
V = VectorFunctionSpace(mesh, "Lagrange", 2)
Q = FunctionSpace(mesh, "Lagrange", 1)

# Define trial and test functions
u = TrialFunction(V)
p = TrialFunction(Q)
v = TestFunction(V)
q = TestFunction(Q)

# Set parameter values
dt = 1e-2
T = 1.0
nu = Constant(0.025)
h = Constant(1.0)

# Define domains and boundary measure
class DirichletBoundary(SubDomain):
    def inside(self, x, on_boundary):
        return (near(x[0], -0.5, 1e-4) and x[1] > -DOLFIN_EPS) or (near(x[1], 0.5, 1e-4) and x[0] > -DOLFIN_EPS ) or (near(x[0], 0.5, 1e-4) and x[1] < DOLFIN_EPS ) or (near(x[1], -0.5, 1e-4) and x[0] < DOLFIN_EPS)
class NeumannBoundary(SubDomain):
    def inside(self, x, on_boundary):
        return (near(x[0], -0.5, 1e-4) and x[1] <  DOLFIN_EPS) or (near(x[1], 0.5, 1e-4) and x[0] <  DOLFIN_EPS ) or (near(x[0], 0.5, 1e-4) and x[1] > - DOLFIN_EPS ) or (near(x[1], -0.5, 1e-4) and x[0] > - DOLFIN_EPS)

domains = MeshFunction("size_t", mesh, 1)
domains.set_all(0)
DirichletBoundary().mark(domains, 1)
NeumannBoundary().mark(domains, 2)
ds = Measure("ds", domain=mesh, subdomain_data=domains)

with XDMFFile(MPI.comm_world, 'domains_WALL.xdmf') as f:
	f.write(domains)
# Define time-dependent pressure and velocity boundary conditions
up_ex_str = ['-sin(2.0*pi*x[1])*exp(-4.0*nu*pi*pi*t)', 'sin(2.0*pi*x[0])*exp(-4.0*nu*pi*pi*t)', '-cos(2.0*pi*x[0])*cos(2.0*pi*x[1])*exp(-8.0*nu*pi*pi*t)']
u_ex = Expression((up_ex_str[0],up_ex_str[1]),element=V.ufl_element(),domain=mesh,t=0.0,nu=nu)
p_ex = Expression(up_ex_str[2],element=Q.ufl_element(),domain=mesh,t=0.0,nu=nu)
# UFL form for the source term

# Define boundary conditions
bcu = [DirichletBC(V, u_ex, domains, 2)]#, DirichletBC(V, u_ex, domains, 2)]
bcp = [DirichletBC(Q, p_ex, domains, 1)]

# Create functions
u0 = Function(V)
u00 = Function(V)
u1 = Function(V)
p1 = Function(Q)
p0 = Function(Q)
div1 = Function(Q)
fi,VV = [[u0,p0], [V,Q]]
# Initialize the solutions for old time steps
for i,ui in enumerate([('-sin(2.0*pi*x[1])*exp(-4.0*nu*pi*pi*t)', 'sin(2.0*pi*x[0])*exp(-4.0*nu*pi*pi*t)'), '-cos(2.0*pi*x[0])*cos(2.0*pi*x[1])*exp(-8.0*nu*pi*pi*t)']):
    deltat = dt / 2. if i==1 else 0.
    vv = interpolate(Expression(ui, element=VV[i].ufl_element(),t=0.0 + deltat, nu=nu), VV[i])
    fi[i].vector()[:] = vv.vector()[:]
    if not i == 1:
        deltat = -dt
        vv = interpolate(Expression(ui, element=VV[i].ufl_element(), t=0.0 + deltat, nu=nu), VV[i])
        u00.vector()[:] = vv.vector()[:]

# Define coefficients
U  = 0.5*(u0 + u)
n  = FacetNormal(mesh)
k = Constant(dt)
f = Constant((0, 0))

# Define symmetric gradient
def epsilon(u):
    return sym(nabla_grad(u))

# Define stress tensor
def sigma(u, p):
    return 2*nu*epsilon(u) - p*Identity(len(u))

# Define variational problem for step 1
F1 = dot(( 1.5*u - 2.0*u0 +0.5*u00) / k, v)*dx \
   + dot(dot( 2.0*u0, nabla_grad(u0)), v)*dx - dot(dot( u00, nabla_grad(u00)), v)*dx\
   + inner(sigma(u, p0), epsilon(v))*dx \
   + dot(p0*n, v)*ds - dot(nu*dot(nabla_grad(u_ex),n), v)*ds\
   - dot(f, v)*dx
   # + dot(dot(u0, nabla_grad(u0)), v)*dx\
   # + dot(grad(p0), v)*dx - dot(nu*nabla_grad(U)*n, v)*ds \
a1 = lhs(F1)
L1 = rhs(F1)


# Define variational problem for step 2
a2 = dot(nabla_grad(p), nabla_grad(q))*dx
L2 = dot(nabla_grad(p0), nabla_grad(q))*dx - 1.5*(1/k)*div(u1)*q*dx

# Define variational problem for step 3
a3 = dot(u, v)*dx
L3 = dot(u1, v)*dx - 2./3.*k*dot(nabla_grad(p1 - p0), v)*dx

# Assemble matrices
A1 = assemble(a1)
A2 = assemble(a2)
A3 = assemble(a3)

# Attach pressure nullspace if needed
if bcp == []:
    null_vec = Vector(p1.vector())
    Q.dofmap().set(null_vec, 1.0)
    null_vec *= 1.0 / null_vec.norm('l2')
    Aa = as_backend_type(A2)
    null_space = VectorSpaceBasis([null_vec])
    Aa.set_nullspace(null_space)
    Aa.null_space = null_space

# Use amg preconditioner if available -- define pressure solver
prec = "amg" if has_krylov_solver_preconditioner("amg") else "default"
# Use nonzero guesses - essential for CG with non-symmetric BC
parameters['krylov_solver']['nonzero_initial_guess'] = True

# Create files for storing solution
ufile = File("results/velocity.pvd")
pfile = File("results/pressure.pvd")
divfile = File("results/divergence.pvd")
# Time-stepping
t = dt
while t < T + DOLFIN_EPS:

    # Update pressure boundary condition
    u_ex.t = t
    p_ex.t = t

    # Compute tentative velocity step
    b1 = assemble(L1)
    t_ = Constant(t)
    um,pm = u_man(x,y,t_), p_man(x,y,t_)
    [bc.apply(A1, b1) for bc in bcu]
    solve(A1, u1.vector(), b1, "bicgstab", "default")

    # Pressure correction
    b2 = assemble(L2)
    [bc.apply(A2, b2) for bc in bcp]
    [bc.apply(p1.vector()) for bc in bcp]
    if bcp == []:
        null_space.orthogonalize(b2)
    solve(A2, p1.vector(), b2, "bicgstab", prec)
    if bcp == []:
            normalize(p1.vector())

    # Velocity correction
    b3 = assemble(L3)
    [bc.apply(A3, b3) for bc in bcu]
    solve(A3, u1.vector(), b3, "bicgstab", "default")

    div1.assign( project(div(u1),Q) )
    # Save to file
    ufile << u1
    pfile << p1
    divfile << div1
    # Move to next time step
    u00.assign(u0)
    u0.assign(u1)
    p0.assign(p1)
    t += dt

    #compute errors
    err = {}
    SS = [u1,p1]
    for i, ui in enumerate([u_ex,p_ex]):
        ue = interpolate(ui, VV[i])
        uen = norm(ue.vector())
        ue.vector().axpy(-1, SS[i].vector())
        error = norm(ue.vector()) / uen
        err[i] = "{0:2.6e}".format(norm(ue.vector()) / uen)
    if MPI.rank(MPI.comm_world) == 0:
        print("Error is ", err, " at time = ", t)

# Plot solution
plt.figure()
plot(p1, title="Pressure")

plt.figure()
plot(u1, title="Velocity")
try:
    plt.show()
except:
    pass

Perhaps I’m misunderstanding the IPCS scheme used, but I believe the terms in F1 to weakly enforce the Neumann condition corresponding to the exact solution and the integration-by-parts used on the Cauchy stress term should be

- dot(dot(sigma(u_ex,p_ex),n), v)*ds

instead of

+ dot(p0*n, v)*ds - dot(nu*dot(nabla_grad(u_ex),n), v)*ds

Hello @kamensky, your suggestion greatly improves things: if I substitute the term in F1 with the one you suggest the solution looks smooth again, both for velocity and pressure, and the errors are reduced in magnitude. Pressure no longer shows strange behavior at corners or at the border of Neumann and Dirichlet boundaries:

Actually, I’ve noticed that if instead I use

- dot(dot(sigma(u_ex,p0),n), v)*ds

with p0 in place of p_ex, the error is even lower. Was the original term wrong because it’s missing part of the symmetric gradient, the transpose of nabla_grad?

Unfortunately however, even if the errors are of magnitude of 1e^{-4} for velocity and 1e^{-3} for pressure, they still don’t converge with the expected law when decreasing the time step. Actually, they simply don’t improve at all. I suspect this is due to the fact that the divergence still is irregular at the common points between Dirichlet and Neumann edges, even though pressure is less affected now:


there is still a singularity at the corners. The article where this test is proposed actually concerns ( high-order) Discontinuous Galerkin mehods, I guess the interface between Dirichlet and Neumann edges in that case is less of a problem, but still since this solution is smooth, P2/P1 elements should still work right? Could something like grad-div stabilization improve things here?

Updated code:


F1 = dot(( 1.5*u - 2.0*u0 +0.5*u00) / k, v)*dx \
   + dot(dot( 2.0*u0, nabla_grad(u0)), v)*dx - dot(dot( u00, nabla_grad(u00)), v)*dx\
   + inner(sigma(u, p0), epsilon(v))*dx \
   - dot(dot(sigma(u_ex,p0),n), v)*ds\
   - dot(f, v)*dx

a1 = lhs(F1)
L1 = rhs(F1)

a2 = dot(nabla_grad(p), nabla_grad(q))*dx
L2 = dot(nabla_grad(p0), nabla_grad(q))*dx - 1.5*(1/k)*div(u1)*q*dx

a3 = dot(u, v)*dx
L3 = dot(u1, v)*dx - 2./3.*k*dot(nabla_grad(p1 - p0), v)*dx

# Assemble matrices
A1 = assemble(a1)
A2 = assemble(a2)
A3 = assemble(a3)

So, you should keep in mind that there are two common representations of the divergence of the viscous stress for incompressible flow:

-\nu\Delta \mathbf{u} = -\nabla\cdot(\nu\nabla\mathbf{u})

and

-\nabla\cdot(2\nu\pmb{\varepsilon}(\mathbf{u}))

which happen to be equivalent when \nabla\cdot\mathbf{u} = 0. However, you’ve included the interior term corresponding to the second option, but the boundary term corresponding to the first option, which is inconsistent. In general, I prefer the second option, since then the Neumann boundary data has a clear physical interpretation, as the traction \pmb{\sigma}\cdot\mathbf{n}.