Stokes problem with nullspace to ensure unicity

I’m trying to solve a Stokes problem using the axisymmetrical hypothesis.
Following the solution provided in Stokes problem with only Dirichlet boundary condition - #2 by dokken I tried to apply the same nullspace to ensure unicity of the solution (since I impose only Dirichlet BC), however I have an error that the nullspace is not correctly imposed.

Here the example code with a testcase already in place:

import numpy as np
from matplotlib import pyplot as plt
import ufl
import dolfinx.fem.petsc

from dolfinx import cpp as _cpp
from dolfinx import fem, plot
from dolfinx.fem import (Constant, Function, FunctionSpace, dirichletbc,
                         extract_function_spaces, form,
                         locate_dofs_geometrical, locate_dofs_topological)
from dolfinx.io import XDMFFile
from dolfinx.mesh import (CellType, GhostMode, create_rectangle,
                          locate_entities_boundary)
from ufl import as_tensor, div, dx, grad, inner, ds, sym, dot

from mpi4py import MPI
from petsc4py import PETSc


def error_L2(uh, u_ex, degree_raise=3):
    # Create higher order function space
    ufl_el = uh.function_space.ufl_element()
    new_el = ufl_el.reconstruct(degree=degree_raise+ufl_el.degree())
    mesh = uh.function_space.mesh
    W = fem.FunctionSpace(mesh, new_el)
    # Interpolate approximate solution
    u_W = fem.Function(W)
    u_W.interpolate(uh)

    # Interpolate exact solution, special handling if exact solution
    # is a ufl expression or a python lambda function
    u_ex_W = fem.Function(W)
    if isinstance(u_ex, ufl.core.expr.Expr):
        u_expr = fem.Expression(u_ex, W.element.interpolation_points())
        u_ex_W.interpolate(u_expr)
    else:
        u_ex_W.interpolate(u_ex)

    # Compute the error in the higher order function space
    e_W = fem.Function(W)
    e_W.x.array[:] = u_W.x.array - u_ex_W.x.array

    # Integrate the error
    error = fem.form(ufl.inner(e_W, e_W) * ufl.dx)
    error_local = fem.assemble_scalar(error)
    error_global = mesh.comm.allreduce(error_local, op=MPI.SUM)
    return np.sqrt(error_global)


mu = 1         # dynamic viscosity

nx = 16
ny = 4*nx
# Create mesh
msh = create_rectangle(MPI.COMM_WORLD,
                       [np.array([0, 0]), np.array([0.5, 2])],
                       [nx, ny],
                       CellType.triangle)

x = ufl.SpatialCoordinate(msh)

# Cylindrical coordinates: x[0] --> r, x[1] --> z

# Function spaces
P2 = ufl.VectorElement("Lagrange", msh.ufl_cell(), 2)
P1 = ufl.FiniteElement("Lagrange", msh.ufl_cell(), 1)
TH = P2 * P1
W = FunctionSpace(msh, TH)
V, V_to_W = W.sub(0).collapse()
Q, Q_to_W = W.sub(1).collapse()

# Functions to geometrically locate subsets
def wall1_boundary(x):
    return np.isclose(x[0], 0.5)

def wall2_boundary(x):
    return np.isclose(x[0], 0)

def inlet_boundary(x):
    return np.isclose(x[1], 0.0)

def outlet_boundary(x):
    return np.isclose(x[1], 2.0)

# Inflow velocity
def in_velocity_expression(x):
    return np.vstack((np.zeros(x.shape[1]), -2*(np.ones(x.shape[1])-x[0]**2)))


# Walls velocity
def wall1_velocity_expression(x):
    return np.vstack((np.zeros(x.shape[1]), -(3/2)*np.ones(x[1].shape[0])))


def wall2_velocity_expression(x):
    return np.vstack((np.zeros(x.shape[1]),-2*np.ones(x[1].shape[0])))


# Pressure out
def pressure_out_expression(x):
    return 14*np.ones(x[1].shape[0])



# Boundary conditions
# Wall1 velocity 
wall1_velocity = Function(V)
wall1_velocity.interpolate(wall1_velocity_expression)
facets = locate_entities_boundary(msh, 1, wall1_boundary)
dofs = locate_dofs_topological((W.sub(0), V), 1, facets)
bc0 = dirichletbc(wall1_velocity, dofs, W.sub(0))

# Out pressure 
out_pressure = Function(Q)
out_pressure.interpolate(pressure_out_expression)
facets = locate_entities_boundary(msh, 1, outlet_boundary)
dofs = locate_dofs_topological((W.sub(1), Q), 1, facets)
bc1 = dirichletbc(out_pressure, dofs, W.sub(1))

# Inflow velocity
in_velocity = Function(V)
in_velocity.interpolate(in_velocity_expression)
facets = locate_entities_boundary(msh, 1, inlet_boundary)
dofs = locate_dofs_topological((W.sub(0), V), 1, facets)
bc2 = dirichletbc(in_velocity, dofs, W.sub(0))

# Wall2 velocity
wall2_velocity = Function(V)
wall2_velocity.interpolate(wall2_velocity_expression)
facets = locate_entities_boundary(msh, 1, wall2_boundary)
dofs = locate_dofs_topological((W.sub(0), V), 1, facets)
bc3 = dirichletbc(wall2_velocity, dofs, W.sub(0))

# Collect Dirichlet boundary conditions
bcs = [bc0, bc1, bc2, bc3]

# Cylindrical coordinates
def div_cyl(v):
    return (1/x[0])*(x[0]*v[0]).dx(0) + v[1].dx(1)

def epsilon(v):
    return sym(grad(v))


# Define variational problem
(u, p) = ufl.TrialFunctions(W)
(v, q) = ufl.TestFunctions(W)

f = Function(V)
f = ufl.as_vector((0, -1))

n  = ufl.FacetNormal(msh)
a = form(mu * inner(epsilon(u), grad(v)) * x[0] * dx - inner(div_cyl(u), q) * x[0] * dx - inner(p, div_cyl(v)) * x[0] * dx)
L = form(inner(f, v) * x[0] * dx)

# Assemble LHS matrix and RHS vector
A = fem.petsc.assemble_matrix(a, bcs=bcs)
A.assemble()
b = fem.petsc.assemble_vector(L)

fem.petsc.apply_lifting(b, [a], bcs=[bcs])
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)

# assemble A and apply BCs
ns_vec = dolfinx.fem.Function(W)
ns_vec.x.array[Q_to_W] = 1
dolfinx.la.orthonormalize([ns_vec.vector])
nullspace = PETSc.NullSpace().create(vectors=[ns_vec.vector])
assert nullspace.test(A)
A.setNearNullSpace(nullspace)

# Set Dirichlet boundary condition values in the RHS
fem.petsc.set_bc(b, bcs)

# Create and configure solver
ksp = PETSc.KSP().create(msh.comm)
ksp.setOperators(A)
ksp.setType("preonly")
ksp.getPC().setType("lu")
ksp.getPC().setFactorSolverType("superlu_dist")

# Compute the solution
U = Function(W)
ksp.solve(b, U.vector)

# Split the mixed solution and collapse
uh = U.sub(0).collapse()
ph = U.sub(1).collapse()


# Compute error L2
# Velocity
def u_e(x):
    return np.vstack((np.zeros(x.shape[1]), -2*(np.ones(x[1].shape[0])-x[0]**2)))

print('error_L2_velocity  =', error_L2(uh, u_e))

# Pressure
def p_e(x):
    return  7*x[1]

print('error_L2_pressure  =', error_L2(ph, p_e))

Are you sure you have applied boundary conditions on all facets?
I will illustrate this on a massively simplified problem:


import numpy as np
from matplotlib import pyplot as plt
import ufl
import dolfinx.fem.petsc

from dolfinx import cpp as _cpp
from dolfinx import fem, plot
from dolfinx.fem import (Constant, Function, FunctionSpace, dirichletbc,
                         extract_function_spaces, form,
                         locate_dofs_geometrical, locate_dofs_topological)
from dolfinx.io import XDMFFile
from dolfinx.mesh import (CellType, GhostMode, create_rectangle,
                          locate_entities_boundary)
from ufl import as_tensor, div, dx, grad, inner, ds, sym, dot

from mpi4py import MPI
from petsc4py import PETSc


mu = 1         # dynamic viscosity
nx = 16
ny = 4*nx
# Create mesh
msh = create_rectangle(MPI.COMM_WORLD,
                       [np.array([0, 0]), np.array([0.5, 2])],
                       [nx, ny],
                       CellType.triangle)

x = ufl.SpatialCoordinate(msh)


# Function spaces
P2 = ufl.VectorElement("Lagrange", msh.ufl_cell(), 2)
P1 = ufl.FiniteElement("Lagrange", msh.ufl_cell(), 1)
TH = P2 * P1
W = FunctionSpace(msh, TH)
V, V_to_W = W.sub(0).collapse()
Q, Q_to_W = W.sub(1).collapse()


def div_cyl(v):
    return (1/x[0])*(x[0]*v[0]).dx(0) + v[1].dx(1)


def epsilon(v):
    return sym(grad(v))


# Define variational problem
(u, p) = ufl.TrialFunctions(W)
(v, q) = ufl.TestFunctions(W)

uD = dolfinx.fem.Function(V)
tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)
boundary_facets = dolfinx.mesh.exterior_facet_indices(msh.topology)
boundary_dofs = dolfinx.fem.locate_dofs_topological(
    (W.sub(0), V), fdim, boundary_facets)


bcs = [dolfinx.fem.dirichletbc(uD, boundary_dofs, W.sub(0))]  # works
bcs = []  # Nullspace test fails

# Normal stokes             
a = fem.form(ufl.inner(ufl.grad(u), ufl.grad(v)) * dx +
             ufl.div(v)*p*dx + q*ufl.div(u)*dx)
# Cylindrical stokes
a = form(mu * inner(epsilon(u), grad(v)) *
         x[0] * dx - inner(div_cyl(u), q) * x[0] * dx - inner(p, div_cyl(v)) * x[0] * dx)


# Assemble LHS matrix and RHS vector
A = fem.petsc.assemble_matrix(a, bcs=bcs)
A.assemble()

# assemble A and apply BCs
ns_vec = dolfinx.fem.Function(W)
ns_vec.x.array[Q_to_W] = 1
dolfinx.la.orthonormalize([ns_vec.vector])
nullspace = PETSc.NullSpace().create(vectors=[ns_vec.vector])
assert nullspace.test(A)

with empty bcs either of the nullspace tests, fails.
However, if you add in bcs, either formulation has the given nullspace.

For further posts, please think carefully about how you can remove code from your problem to illustrate the problem.

I guess your issue is that you actually set values on the pressure, so you do not have a null space issue?

2 Likes

I have a very similar problem. The only difference is that I am using a mixed formulation between a vector function space and a tensor function space. I am unable to set up the Null space correctly.
this a short MWE:

from mpi4py import MPI
from petsc4py import PETSc
import numpy as np
from dolfinx.fem import (Function, functionspace, dirichletbc, form, locate_dofs_topological)
from dolfinx.fem.petsc import assemble_matrix
from dolfinx.mesh import locate_entities_boundary, create_box, CellType
from ufl import (Identity, TestFunctions, TrialFunctions, dx, indices, as_tensor)
from basix.ufl import element, mixed_element
import dolfinx
Fl = 1e-8
Fs = 1e-5
Fp = 1e-4
# ....................
C_star = Fl ** 2 / Fs
h_star = 1 / Fl
F_star = Fs
# ........................................ Parameters
E = 1 * 152e9 * C_star
nu = 0.33
la = E * nu / ((1 + nu) * (1 - 2 * nu))
mu = E / (2 * (1 + nu))
# ......................................... Reading mesh file
h = 0.76e-3 / Fl
H = h
mesh = create_box(MPI.COMM_WORLD, [np.array([0, 0, 0]), np.array([h, h, h])],
                  [10, 10, 10], cell_type=CellType.hexahedron)
# ......................................... Define Elements
v = element("Lagrange", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,))  # Displacement Element
s = element("Lagrange", mesh.basix_cell(), 1, shape=(3, 3))  # Displacement gradient Element
lm = element("Lagrange", mesh.basix_cell(), 1, shape=(3, 3))  # Lagrange multipliers Element
# ......................................... Define mixed element
MIXEL = mixed_element([v, s, lm])
Space = functionspace(mesh, MIXEL)
# ......................................... Define Trail and Test Functions
(u, gradu, lamu) = TrialFunctions(Space)
(del_u, del_gradu, del_lamu) = TestFunctions(Space)
# ....................................... Mark boundary regions
tol = 1e-16
def Bottom(x):
    return np.isclose(x[2], 0, atol=tol)
# ....................................... BCs
S0, S0_to_Space = Space.sub(0).collapse()
clamp = Function(S0)
clamp.x.array[:] = 0
clamp_facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, Bottom)
# .........Fixed BC
clamp_dofs = locate_dofs_topological((Space.sub(0), S0), mesh.topology.dim - 1, clamp_facets)
bc0 = dirichletbc(clamp, clamp_dofs, Space.sub(0))
bc = [bc0]
# ......................................... Define tensor
delta = Identity(len(u))
i, j, k = indices(3)
# Strain Tensor
def StrainT(u):
    return as_tensor((1. / 2. * (u[i].dx(j) + u[j].dx(i))), [i, j])

def StressT(u):
    return as_tensor((la * StrainT(u)[i, i] * delta[j, k] + 2 * mu * StrainT(u)[j, k]), [j, k])
def Constraint(u, gradu):               # CONSTRAINT
    return as_tensor(gradu[j, k] - u[k].dx(j), (j, k))
# ......................................... Define Variational Formulation
a = StressT(u)[j, k] * StrainT(del_u)[j, k] * dx
a += Constraint(u, gradu)[j, k] * del_lamu[j, k] * dx
a += Constraint(del_u, del_gradu)[j, k] * lamu[j, k] * dx
#.................................................Assembling
bilinear_form = form(a)
A = assemble_matrix(bilinear_form, bcs=bc)
A.assemble()
################################### Apply Nullspace
DG0, DG0_to_Space = Space.sub(1).collapse()
LA0, LA0_to_Space = Space.sub(2).collapse()
ns_vec = dolfinx.fem.Function(Space)
ns_vec.x.array[DG0_to_Space] = 1
ns_vec.x.array[LA0_to_Space] = 1
dolfinx.la.orthonormalize([ns_vec.vector])
nullspace = PETSc.NullSpace().create(vectors = [ns_vec.vector])
assert nullspace.test(A)
A.setNearNullSpace(nullspace)

I am getting the AssertionError.

A you haven’t expressed what the variational form is (in mathematics) or expressed what the null space of the discrete problem is, it is rather hard to give you guidance. To me it seems like you assume that the nullspace is whenever both variables are constant, is this true?

Thanks for the reply. here is the variational formulation:


The delta (δ) symbols represent the variation of the variable. and the stress and strain are as follow:
image
The displacement is denoted by 𝑢, and we used Lagrange multipliers to enforce the following constraints within the domain
image

Thanks

So you are solving an elasticity problem, not a Stokes problem. Thus you have a different null-space, ie. Rigid body motions.

I think The null space of my variational problem consists of

  • functions u that represent rigid body motions (translations and rotations),
  • ψ that matches the gradient of these rigid body motions,
  • and α being zero
    Here is my question:
    1- I am specifying zero displacement on one face. Do I need to consider rigid body motion?
    2- How should I address the null space in terms of ψ and α in the code. Becuase I am getting error right now.

Thanks

Rigid body motions has been described in:

Please note that your question is not related to the original topic of this post, is the Stokes equation.

As long as you have enough boundary conditions enforced to eliminate the null space, you do not need null space elimination when using a direct solver. However, it would be required for certain iterative solvers, such as gamg.