Implementing Nitsche's method for perfect-slip BCs with a nontrivial normal vector

Hello,
I am working with a 2d mesh, given by a square with a circular hole placed in the middle. Let me call \partial \Omega the boundary at the circle. I have a PDE for a vector field \vec{v}, and I want to impose the boundary conditions

[\hat{n}(\vec{v})] \cdot \vec{v} = 0 \textrm{ on } \partial \Omega.

Where \hat{n}(\vec{v}) is a vector field defined on \partial \Omega which depends on \vec{v}.

I looked up in the literature how to construct the Nitsche’s functional F_N to add to the PDE’s variational functional to implement the BC, in particular “On weakly imposed boundary conditions for second order problems”, J. Freund and R. Steinberg, Proceedings of the International Conference on Finite Elements in Fluidds (1995).

Following that reference, I set

F_N = \frac{\beta}{h} \int_{\partial \Omega} dS \{ [\hat{n}( \vec{v})] \cdot \vec{v}\} \{ [\hat{n}(\vec{v})] \cdot \vec{ \nu} },

where \vec{\nu} is the test function associated with \vec{v}, h is the cell mesh size and \beta Nitsche’s parameter. When I implement this in Fenics, this does not work: the BC is not satisfied.

May you please tell me what is the general procedure to construct F_N given the BC? Is the F_N above the correct one?

Thank you !

Not sure if I understand the question correctly.

How is it possible that the normal vector depends on the test function? It should depend only on the geometry, not on the test or trial function.

In that case, ufl has a ufl.FacetNormal that you may use.

If what you need is a controlled flux BC on a nontrival boundary, some solutions were suggested at

This is not using Nietsche’s method, it’s a different way to set the BC using FacetNormals.

Please forget the term ‘normal’ here, consider \hat{n} as a general vector, which depends on \vec{v}

That is not what I need, please read the original post.

You may want to clarify the original post then, considering that at least two people did not understand what you were asking.

It’s unlikely that the form you wrote there is correct, since it would result in a nonlinear dependence on the test function.

EDIT: well, nu is the test function and v the solution, so disregard my last sentence.

What is the explicit relation between n and v?
As you haven’t provided the code, of the ode you are solving it’s really hard to give advice, other than suggesting literature on Nitsche methods, such as AMS :: Mathematics of Computation

Here a minimal working example. \Omega is the unit disk, \partial \Omega its boundary, the vector field v^i is the unknown, the PDE is
\nabla^2 v^i = f^i \textrm{ in } \Omega
with BCs
\partial_i v^1 = g \textrm{ on } \partial \Omega
[\vec{n}(\vec{v})] \cdot \vec{v} = 0 \textrm{ on } \partial \Omega \;\; (1)

where n^i(v) \equiv A^{ij}(v)\, \hat{n}^j, A is the inverse of B and B=[ [ 1+(v^1)^2, v^1 v^2], [v^1 v^2, 1+(v^2)^2] ] . f^i is a known field.

Here is the code, I have not included the symmetrizing terms

from __future__ import print_function
from fenics import *
import ufl as ufl
from mshr import *

# Create mesh
disk = Circle( Point( 0, 0 ), 1 )
domain =  disk
mesh = generate_mesh(domain, 64)

#radius of the smallest cell in the mesh
r_mesh = mesh.hmin()

#nitsche's parameter
beta = 1e2

xdmffile_u = XDMFFile( "u.xdmf" )
xdmffile_n_dot_v = XDMFFile( "n_dot_u.xdmf" )

i, j = ufl.indices( 2 )

Q = FunctionSpace( mesh, 'P', 2 )
V = VectorFunctionSpace(mesh, 'P', 2)

facet_normal = FacetNormal( mesh )

class grad_u0_expression(UserExpression):
    def eval(self, values, x):
        values[0] = 2.0*x[0]
        values[1] = 4.0*x[1]
    def value_shape(self):
        return (4,)

class laplacian_u_expression(UserExpression):
    def eval(self, values, x):
        values[0] = 6.0
        values[1] = -2.0
    def value_shape(self):
        return (2,)

# Define variational problem
v = Function( V )
n_dot_v = Function( Q )
nu = TestFunction( V )
f = Function(V)
grad_v0 = Function( V )

f.interpolate(laplacian_u_expression(element=V.ufl_element()))
grad_v0.interpolate( grad_u0_expression( element=V.ufl_element() ) )

def calc_normal_cg2(mesh):
    n = FacetNormal(mesh)
    V = VectorFunctionSpace(mesh, "CG", 2)
    u = TrialFunction(V)
    v = TestFunction(V)
    a = inner(u, v) * ds
    l = inner(n, v) * ds
    A = assemble(a, keep_diagonal=True)
    L = assemble(l)

    A.ident_zeros()
    nh = Function(V)
    solve(A, nh.vector(), L)
    return nh


def facet_normal_smooth():
    u = calc_normal_cg2(mesh)
    return as_tensor(u[i], (i))

def B(v):
    return as_tensor( [[1 + (v[0]) ** 2, (v[0]) * (v[1])], [(v[0]) * (v[1]), 1 + (v[1]) ** 2]] )

def A(v):
    return ufl.inv( B( v ) )

def n(u):
    return as_tensor( A( u )[i, j] * facet_normal[j], (i) )

def n_smooth(u):
    return as_tensor( A( u )[i, j] * facet_normal_smooth()[j], (i) )

F_0 = (((v[i]).dx( j )) * ((nu[i]).dx( j )) + f[i] * nu[i]) * dx - ((facet_normal[i] * grad_v0[i]) * nu[0] + facet_normal[i] * ((v[1]).dx( i )) * nu[1]) * ds
F_N = beta / r_mesh * (n( v )[j] * v[j]) * (n( v )[i] * nu[i]) * ds
F = F_0 + F_N

solve( F == 0, v )

xdmffile_u.write( v, 0 )
n_dot_v.assign( project( n_smooth( v )[i] * v[i], Q ) )
xdmffile_n_dot_v.write( n_dot_v, 0 )

If I run and plot the result, the BC (1) is not satisfied, see the screenshot attached.

. Increasing beta does not change anything.

Any ideas ? :hushed:

I am not sure you can enforce that condition.
It doesn’t really fit the Nitsche framework, as presented in

or
https://arxiv.org/pdf/2203.02603

I’ve never seen a non-linear relation in the nitsche bc, except for contact mechanics

I would read: https://epubs.siam.org/doi/abs/10.1137/S0036142901384162
as is goes through nitsche and DG methods very thoroughly

Thank you! I will read through these references and let you know.

Meanwhile, I realize something strange. In the code which I posted I replaced B with the diagonal matrix, so now we are back to ordinary, linear constraint with respect to the facet normal (I also changed laplacian_u_expression, but this is not important).

The code is

from __future__ import print_function
from fenics import *
import ufl as ufl
from mshr import *

# Create mesh
disk = Circle( Point( 0, 0 ), 1 )
domain =  disk
mesh = generate_mesh(domain, 64)

#radius of the smallest cell in the mesh
r_mesh = mesh.hmin()

#nitsche's parameter
beta = 1e4

xdmffile_u = XDMFFile( "u.xdmf" )
xdmffile_n_dot_v = XDMFFile( "n_dot_u.xdmf" )

i, j = ufl.indices( 2 )

Q = FunctionSpace( mesh, 'P', 2 )
V = VectorFunctionSpace(mesh, 'P', 2)

facet_normal = FacetNormal( mesh )

class grad_u0_expression(UserExpression):
    def eval(self, values, x):
        values[0] = 2.0*x[0]
        values[1] = 4.0*x[1]
    def value_shape(self):
        return (4,)

class laplacian_u_expression(UserExpression):
    def eval(self, values, x):
        values[0] = x[1]
        values[1] = -x[0]
    def value_shape(self):
        return (2,)

# Define variational problem
v = Function( V )
n_dot_v = Function( Q )
nu = TestFunction( V )
f = Function(V)
grad_v0 = Function( V )

f.interpolate(laplacian_u_expression(element=V.ufl_element()))
grad_v0.interpolate( grad_u0_expression( element=V.ufl_element() ) )

def calc_normal_cg2(mesh):
    n = FacetNormal(mesh)
    V = VectorFunctionSpace(mesh, "CG", 2)
    u = TrialFunction(V)
    v = TestFunction(V)
    a = inner(u, v) * ds
    l = inner(n, v) * ds
    A = assemble(a, keep_diagonal=True)
    L = assemble(l)

    A.ident_zeros()
    nh = Function(V)
    solve(A, nh.vector(), L)
    return nh


def facet_normal_smooth():
    u = calc_normal_cg2(mesh)
    return as_tensor(u[i], (i))

def B(v):
    # return as_tensor( [[1 + (v[0]) ** 2, (v[0]) * (v[1])], [(v[0]) * (v[1]), 1 + (v[1]) ** 2]] )
    return as_tensor( [[1 , 0], [0, 1]] )

def A(v):
    return ufl.inv( B( v ) )

def n(u):
    return as_tensor( A( u )[i, j] * facet_normal[j], (i) )

def n_smooth(u):
    return as_tensor( A( u )[i, j] * facet_normal_smooth()[j], (i) )

F_0 = (((v[i]).dx( j )) * ((nu[i]).dx( j )) + f[i] * nu[i]) * dx - ((facet_normal[i] * grad_v0[i]) * nu[0] + facet_normal[i] * ((v[1]).dx( i )) * nu[1]) * ds
F_N = beta / r_mesh * (n( v )[j] * v[j]) * (n( v )[i] * nu[i]) * ds
F = F_0 + F_N

solve( F == 0, v )

xdmffile_u.write( v, 0 )
n_dot_v.assign( project( n_smooth( v )[i] * v[i], Q ) )
xdmffile_n_dot_v.write( n_dot_v, 0 )

which runs ok, and it gives a field v orthogonal to the facet normal indeed

However, if I plot v \cdot \hat{n}, I get a value ~ 1e-3 (not quite small)

Also, this value does not decrease if I increase the Nitsche’s parameter, which looks strange.

Do you know what may be going on here ?
Thanks

To respond to your last comment: are you sure that the v\cdot n value on the boundary also does not decrease with increasing penalty? This may be a bit tricky to visualize in paraview, and may be more appropriately tested by simply integrating the (absolute) quantity in fenics.

It seems that you’re observing some wiggles just a little away from the boundary. This is to be expected, and indeed does not decrease with increasing penalty. Simply put, the discrete function space does not have the flexibility to satisfy all BC’s exactly and something “has to give”. If you force the v\cdot n condition too much, this comes at the cost of a mismatch of the \nabla v condition and/or oscillations in the interior.

If you decrease the penalty parameter, the oscillations may even become a little smaller, or at least the solution will look a little smoother… Of course, there are mathematical conditions on the minimal value the penalty parameter is permitted to have.

Nonlinear problems
For nonlinear problems, Nitsche’s method is indeed a little sketchy. Usually, the consistency term is not an issue (as that simply follows from the usual integration by parts, without performing the substitution of the natural condition), but the symmstry term is; symmetrizing a nonlinear term would imply a nonlinearity on the test function, which is of course not possible.

It seems to me that you can approach this in one of two ways:

  • You don’t care too much about the symmetry term. This term exists to make the formulation adjoint consistent, which is required in the Aubin-Nitsche argument for proving optimal convergence in other norms than the natural norm. For non-linear problems, this whole story does not apply anyway. And, in fact, even anti-symmetric variants exist (“Baumann-Oden” in DG, or “non-symmetric Nitsche”).
  • You symmetrize in such a way to achieve something else. E.g., to get a symmetric tangent matrix in each Newton iteration, or to make the weak form satisfy some sort of energy condition. In this regard, I dipped my toes in the water in this work: Redirecting.
1 Like

Plotting v\cdot n is a challenge, as the normal vector is not well-defined at the vertices of the mesh (each side of the vertex will have a different normal). Thus during a projection, you will of course not fulfill v \cdot n at the degrees of freedom. I would rather compute: assemble(ufl.dot(v, n)**2*ufl.ds) and look at this quantity as a measure of the amount of flow in normal direction.

@Stein and @dokken thank you for your reply. While I look into the literature that you cited, I gave a try at computing assemble((dot(v, n(v))**2)*ds). The value is indeed very small and, most importantly, it decreases as beta increases, so this is working.

What is more striking is "Nitsche’s’’ method seems to work even with the nonlinear constraint: In order to make the facet_normal markedly differ from n(v), I changed the matrix B in the code. Then I impose n(v).v=0 with “Nitsche’s” method, and I get that the BC is being enforced correctly: the solution is perpendicular to n(v) but not to facet_normal as it should:

from __future__ import print_function
from fenics import *
import ufl as ufl
from mshr import *

# Create mesh
disk = Circle( Point( 0, 0 ), 1 )
domain =  disk
mesh = generate_mesh(domain, 64)

#radius of the smallest cell in the mesh
r_mesh = mesh.hmin()

#nitsche's parameter
beta = 1e2

xdmffile_v = XDMFFile( "v.xdmf" )
xdmffile_facet_normal = XDMFFile( "facet_normal.xdmf" )
xdmffile_n = XDMFFile( "n.xdmf" )
xdmffile_n_dot_v = XDMFFile( "n_dot_v.xdmf" )

i, j = ufl.indices( 2 )

Q = FunctionSpace( mesh, 'P', 2 )
V = VectorFunctionSpace(mesh, 'P', 2)

facet_normal = FacetNormal( mesh )

class grad_v0_expression( UserExpression ):
    def eval(self, values, x):
        values[0] = 2.0*x[0]
        values[1] = 4.0*x[1]
    def value_shape(self):
        return (4,)

class laplacian_u_expression(UserExpression):
    def eval(self, values, x):
        values[0] = 6.0
        values[1] = -2.0
    def value_shape(self):
        return (2,)

# Define variational problem
v = Function( V )
n_dot_v = Function( Q )
nu = TestFunction( V )
f = Function(V)
grad_v0 = Function( V )

f.interpolate(laplacian_u_expression(element=V.ufl_element()))
grad_v0.interpolate( grad_v0_expression( element=V.ufl_element() ) )

def calc_normal_cg2(mesh):
    n = FacetNormal(mesh)
    V = VectorFunctionSpace(mesh, "CG", 2)
    u = TrialFunction(V)
    v = TestFunction(V)
    a = inner(u, v) * ds
    l = inner(n, v) * ds
    A = assemble(a, keep_diagonal=True)
    L = assemble(l)

    A.ident_zeros()
    nh = Function(V)
    solve(A, nh.vector(), L)
    return nh


def facet_normal_smooth():
    u = calc_normal_cg2(mesh)
    return as_tensor(u[i], (i))

def B(v):
    return as_tensor( [[1 + (v[0]) ** 2, (v[0]) * (v[1])], [(v[0]) * (v[1]), 2 + (v[1]) ** 2]] )
    # return as_tensor( [[1 , 0], [0, 1]] )

def A(v):
    return ufl.inv( B( v ) )

def n(u):
    return as_tensor( A( u )[i, j] * facet_normal[j], (i) )

def n_smooth(u):
    return as_tensor( A( u )[i, j] * facet_normal_smooth()[j], (i) )

F_0 = (((v[i]).dx( j )) * ((nu[i]).dx( j )) + f[i] * nu[i]) * dx - ((facet_normal[i] * grad_v0[i]) * nu[0] + facet_normal[i] * ((v[1]).dx( i )) * nu[1]) * ds
F_N = beta / r_mesh * (n( v )[j] * v[j]) * (n( v )[i] * nu[i]) * ds
F = F_0 + F_N

solve( F == 0, v )

xdmffile_v.write( v, 0 )

n_dot_v.assign( project( n_smooth( v )[i] * v[i], Q ) )
xdmffile_n_dot_v.write( n_dot_v, 0 )

xdmffile_facet_normal.write( facet_normal_smooth(), 0 )
xdmffile_n.write( project(n_smooth(v), V), 0 )

print("\int_{\partial \Omega} [n(v).v]^2 dS = ", assemble((dot(v, n(v))**2)*ds))
print("\int_{\partial \Omega} [facet_normal.v]^2 dS = ", assemble((dot(v, facet_normal)**2)*ds))
$ python3 example.py 
No Jacobian form specified for nonlinear variational problem.
Differentiating residual form F to obtain Jacobian J = F'.
Solving nonlinear variational problem.
  Newton iteration 0: r (abs) = 7.263e-01 (tol = 1.000e-10) r (rel) = 1.000e+00 (tol = 1.000e-09)
  [...]
  Newton iteration 4: r (abs) = 3.661e-10 (tol = 1.000e-10) r (rel) = 5.041e-10 (tol = 1.000e-09)
  Newton solver finished in 4 iterations and 4 linear solver iterations.
\int_{\partial \Omega} [n(v).v]^2 dS =  2.2367125330045466e-05
\int_{\partial \Omega} [facet_normal.v]^2 dS =  0.17642189719899645

This is also clear visually if I plot v, facet_normal and n(v), I clearly see that v is orthogonal to n(v) but not to facet_normal.

So it looks like “Nitsche’s” method is working also with a non-linear constraint, while you said that it is conceived for linear constraints only. Do you know why is it working ?

Thanks

Well, even a penalty method typically just works. Nitsche’s method is a “consistent” penalty method. The consistency helps to improve:

  • General solution quality
  • Convergence (& convergence order)

In regards to the latter, Nitche’s method is optimally convergent, and convergences to the correct solution. Penalty methods converge to the wrong solution (namely one with a Robin condition instead of a Dirichlet condition) if the penalty parameter doesn’t scale, and converge suboptimally with penalty scaling (maximally at the order of the penalty scaling, which will require insanely high penalty factors really quickly).

So your observation that your boundary condition is properly enforced may be purely due to penalization, and the actual benefits that Nitsche brings may not be quite so pronounced.

For the nonlinear case, I would advice to:

  • add the penalty term
  • add the consistency term (that will help with convergence, limiting the necessary magnitude of the penalty factor)
  • be creative with how you define the symmetry term (see my earlier post). That’s where each nonlinear problem requires its own strategy, and the science becomes an art.
1 Like