Poisson problem with Neumann boundary condition

Good evening,

I am solving the singular Poisson problem, I saw that the best way to remove the singularity is to add a null space to the matrix, the theory I found behind the method for an iterative solver can be founded in the article “On the singular Neumann problem in linear elasticity”. Regarding the implementation the result I got are without any sense. I took inspiration from the post Issues Solving Pure Neumann Problems in dolfinx - #2 by dokken
Can someone tell me what I am missing?

The code is the following:

from mpi4py import MPI
from dolfinx import mesh, fem

import numpy as np
import pyvista

from dolfinx.fem import (Constant,  Function, FunctionSpace, assemble_scalar, 
                         dirichletbc, form, locate_dofs_topological)
from dolfinx.fem.petsc import LinearProblem
from dolfinx.mesh import create_unit_square, locate_entities, meshtags
from mpi4py import MPI
from petsc4py.PETSc import ScalarType
from ufl import (FacetNormal, Measure, SpatialCoordinate, TestFunction, TrialFunction, 
                 div, dot, dx, grad, inner, lhs, rhs)
from dolfinx.io import XDMFFile
from dolfinx.plot import create_vtk_mesh
N = 160
domain = mesh.create_unit_square(MPI.COMM_WORLD, N, N, mesh.CellType.quadrilateral)
#domain = mesh.create_box(comm=MPI.COMM_WORLD,
#                            points=((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), n=(N, N, N))

from dolfinx.fem import (Constant, Function, FunctionSpace, dirichletbc, Expression,
                         extract_function_spaces, form, assemble_scalar,
                         locate_dofs_geometrical, locate_dofs_topological)
import numpy as np
from petsc4py import PETSc
import basix, basix.ufl_wrapper
V = FunctionSpace(domain, ("CG", 1))

def u_exact(x):
    return np.cos(2*np.pi*x[0])

import ufl
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)

from petsc4py.PETSc import ScalarType
x = ufl.SpatialCoordinate(domain)
f = 4*ufl.pi*ufl.pi * ufl.cos(2*ufl.pi*x[0])
g = -2*ufl.pi* ufl.sin(2*ufl.pi*x[0])

a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx
L = ufl.inner(f,v)* ufl.dx

boundaries = [(1, lambda x: np.isclose(x[0], 0)),
              (2, lambda x: np.isclose(x[0], 1)),
              (3, lambda x: np.isclose(x[1], 0)),
              (4, lambda x: np.isclose(x[1], 1))]

facet_indices, facet_markers = [], []
fdim = domain.topology.dim - 1
for (marker, locator) in boundaries:
    facets = locate_entities(domain, fdim, locator)
    facet_indices.append(facets)
    facet_markers.append(np.full_like(facets, marker))
facet_indices = np.hstack(facet_indices).astype(np.int32)
facet_markers = np.hstack(facet_markers).astype(np.int32)
sorted_facets = np.argsort(facet_indices)
facet_tag = meshtags(domain, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets])

domain.topology.create_connectivity(domain.topology.dim-1, domain.topology.dim)
with XDMFFile(domain.comm, "facet_tags.xdmf", "w") as xdmf:
    xdmf.write_mesh(domain)
    xdmf.write_meshtags(facet_tag)

ds = Measure("ds", domain=domain, subdomain_data=facet_tag)

class BoundaryCondition():
    def __init__(self, type, marker, values):
        self._type = type
        if type == "Dirichlet":
            u_D = Function(V)
            u_D.interpolate(values)
            facets = facet_tag.find(marker)
            dofs = locate_dofs_topological(V, fdim, facets)
            self._bc = dirichletbc(u_D, dofs)
        elif type == "Neumann":
            self._bc = values*v* ds(marker)
        else:
            raise TypeError("Unknown boundary condition: {0:s}".format(type))
    @property
    def bc(self):
        return self._bc

    @property
    def type(self):
        return self._type

boundary_conditions = [BoundaryCondition("Neumann", 1, -g),
                       BoundaryCondition("Neumann", 2, g),
                       BoundaryCondition("Neumann", 3, 0),
                       BoundaryCondition("Neumann", 4, 0)]

bcs = []
for condition in boundary_conditions:
    if condition.type == "Dirichlet":
        bcs.append(condition.bc)
    else:
        L += condition.bc

'''def create_nullspace(rhs_form):
    null_vec = fem.petsc.create_vector(rhs_form)
    null_vec.set(1.0)
    null_vec.normalize()
    nsp = PETSc.NullSpace().create(null_vec)
    return nsp

vec = fem.form(L)
nsp = create_nullspace(vec)
'''
######### ASSEMBLE THE NESTED SYSTEM
def assemble_system(lhs_form, rhs_form, bcs):
    A = fem.petsc.assemble_matrix(lhs_form, bcs=bcs)
    A.assemble()

    b = fem.petsc.create_vector(rhs_form)

    return A, b

A, b = assemble_system(fem.form(a), fem.form(L), bcs)
'''assert nsp.test(A)
A.setNullSpace(nsp)'''

# Create vector that spans the null space
nullspace = PETSc.NullSpace().create(constant=True,comm=MPI.COMM_WORLD)
assert nullspace.test(A)
A.setNullSpace(nullspace)

# orthogonalize b with respect to the nullspace ensures that 
# b does not contain any component in the nullspace
nullspace.remove(b)

######### KRILOV SOLVER
ksp = PETSc.KSP().create(MPI.COMM_WORLD)
ksp.setOperators(A)
ksp.setType("cg")
ksp.getPC().setType("asm")
ksp.setTolerances(rtol=1e-10)
# Monitor the convergence of the KSP
ksp.setFromOptions()

uh = fem.Function(V)

# Not needed in this case since pure neumann
'''with b.localForm() as loc:
        loc.set(0)
fem.petsc.assemble_vector(b, fem.form(L))
fem.petsc.apply_lifting(b, [fem.form(a)], [bcs])
b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
fem.petsc.set_bc(b, bcs)'''

ksp.solve(b, uh.vector)

'''problem = fem.petsc.LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "cg", "pc_type": "asm", "ksp_monitor": None, "ksp_view":None, "ksp_rtol":1e-6, "ksp_atol":1e-10, "ksp_max_it": 1000})
uh = problem.solve()'''

V2 = fem.FunctionSpace(domain, ("CG", 3))
uex = fem.Function(V2)
uex.interpolate(u_exact)

L2_error = fem.form(ufl.inner(uh - uex, uh - uex) * ufl.dx)
error_local = fem.assemble_scalar(L2_error)
error_L2 = np.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))

if domain.comm.rank == 0:
    print(f"Error_L2 : {error_L2:.2e}")

eh = uh - uex
error_H10 = fem.form(ufl.inner(ufl.grad(eh), ufl.grad(eh)) * ufl.dx)
E_H10 = np.sqrt(domain.comm.allreduce(fem.assemble_scalar(error_H10), op=MPI.SUM))
if domain.comm.rank == 0:
    print(f"H01-error: {E_H10:.2e}")

from dolfinx.io import XDMFFile
with XDMFFile(domain.comm, "stokes_true/stoke.xdmf", "w") as file:
    file.write_mesh(domain)
    file.write_function(uh)

You are using an iterative solver

Then you cannot remove the nullspace directly.

Im not sure if asm is suitable to solve singular problems efficiently. Why not use “boomeramg”
https://petsc.org/release/manual/ksp/#algebraic-multigrid-amg-preconditioners
Or

Thanks for the answer. Then, which are the right commands to do this? I also took inspiration from the demo of the Stoke problem Stokes equations with Taylor-Hood elements — DOLFINx 0.5.1 documentation .
But, still I am struggling on understating the correct functioning.

Boomeramg is for instance used in: https://github.com/jorgensd/dolfinx_mpc/blob/360166711fff96c8ecdd3c1c0f0033ca5af9a28d/python/demos/demo_periodic_geometrical.py#L95
and setnearnullspace is used in: https://github.com/search?q=repo%3Ajorgensd%2Fdolfinx_mpc%20setnearnullspace&type=code

Thanks for the example you have suggested. I modify the code accordingly but still doesn’t work

# Create PETSc nullspace
nullspace = PETSc.NullSpace().create(constant=True)
PETSc.Mat.setNearNullSpace(A, nullspace)

######### KRILOV SOLVER
ksp = PETSc.KSP().create(MPI.COMM_WORLD)
ksp.setOperators(A)

ksp.setType("cg")

ksp.getPC().setType(PETSc.PC.Type.HYPRE)
ksp.getPC().setHYPREType("boomeramg")
ksp.setTolerances(rtol=1e-10)
# Monitor the convergence of the KSP
ksp.setFromOptions()

uh = fem.Function(V)

# Not needed in this case since pure neumann
'''with b.localForm() as loc:
        loc.set(0)
fem.petsc.assemble_vector(b, fem.form(L))
fem.petsc.apply_lifting(b, [fem.form(a)], [bcs])
b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
fem.petsc.set_bc(b, bcs)'''

ksp.solve(b, uh.vector)

You should take a step back and make sure that the code does what you expect.
Consider the following example: Add singular Poisson · Issue #139 · jorgensd/dolfinx-tutorial · GitHub

# Copyright (C) 2023 Jørgen S. Dokken
#
# Solve a singular Poisson problem with a null-space
#
# SPDX-License-Identifier:   MIT


from dolfinx import mesh, fem, io
from pathlib import Path
import ufl
import numpy as np
from petsc4py import PETSc
from mpi4py import MPI
import argparse
from enum import Enum


class SolverMode(Enum):
    direct = 1
    iterative = 2


parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--N",
                    dest="N", type=int, default=160,
                    help="Number of elements in each direction")
parser.add_argument("--iterative", action="store_true",
                    dest="iterative", default=False,
                    help="Direct solver if not set, else iterative")
args = parser.parse_args()

N = args.N
iterative_solver = args.iterative
    

domain = mesh.create_unit_square(MPI.COMM_WORLD, N, N, mesh.CellType.quadrilateral)


V = fem.FunctionSpace(domain, ("Lagrange", 1))

def u_exact(x):
    return np.cos(2*np.pi*x[0])


u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)


x = ufl.SpatialCoordinate(domain)
u_ex = ufl.cos(2*ufl.pi*x[0])
f = -ufl.div(ufl.grad(u_ex))
n = ufl.FacetNormal(domain)
g = -ufl.dot(ufl.grad(u_ex), n)

a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx
L = ufl.inner(f,v)* ufl.dx + ufl.inner(g, v) * ufl.ds

A = fem.petsc.assemble_matrix(fem.form(a))
A.assemble()
A.setOption(PETSc.Mat.Option.SYMMETRIC, True)
A.setOption(PETSc.Mat.Option.SYMMETRY_ETERNAL, True)

b = fem.petsc.assemble_vector(fem.form(L))
b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)

# Create vector that spans the null space
nullspace = PETSc.NullSpace().create(constant=True,comm=MPI.COMM_WORLD)
assert nullspace.test(A)
if args.iterative:
    A.setNearNullSpace(nullspace)
else:
    A.setNullSpace(nullspace)
    nullspace.remove(b)

ksp = PETSc.KSP().create(domain.comm)
ksp.setOperators(A)

opts = PETSc.Options()
if args.iterative:
    opts["ksp_type"] = "gmres"
    opts["ksp_rtol"] = 1.0e-10
    opts["pc_type"] = "hypre"
    opts['pc_hypre_type'] = 'boomeramg'
    opts["pc_hypre_boomeramg_max_iter"] = 1
    opts["pc_hypre_boomeramg_cycle_type"] = "v"
    opts["mg_levels_ksp_type"] = "cg"
    opts["mg_levels_pc_type"] = "jacobi"
    opts["mg_levels_esteig_ksp_type"] = "cg"

else:
    opts["ksp_type"] = "preonly"
    opts["pc_type"] = "lu"
    opts["pc_factor_solver_type"] = "mumps"
    opts["mat_mumps_icntl_24"] = 1  # Option to support solving a singular matrix
    opts["mat_mumps_icntl_25"] = 0  # Option to support solving a singular matrix

ksp.setFromOptions()

uh = fem.Function(V)



ksp.solve(b, uh.vector)
uh.x.scatter_forward()
iterations = ksp.getIterationNumber()
# See: https://petsc.org/release/manualpages/KSP/KSPConvergedReason/
converged_reason = ksp.getConvergedReason()
if converged_reason > 0:
    print(f"PETSc solver converged in {iterations=}")
else:
    raise RuntimeError(f"PETSc solver did not converge with {converged_reason=}")

ex_mean = domain.comm.allreduce(fem.assemble_scalar(fem.form(u_ex*ufl.dx)), op=MPI.SUM)
approx_mean = domain.comm.allreduce(fem.assemble_scalar(fem.form(uh*ufl.dx)), op=MPI.SUM)
uh.x.array[:] -= approx_mean - ex_mean



L2_error = fem.form(ufl.inner(uh - u_ex, uh - u_ex) * ufl.dx)
error_local = fem.assemble_scalar(L2_error)
error_L2 = np.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))

if domain.comm.rank == 0:
    print(f"Error_L2 : {error_L2:.2e}")

eh = uh - u_ex
error_H10 = fem.form(ufl.inner(ufl.grad(eh), ufl.grad(eh)) * ufl.dx)
E_H10 = np.sqrt(domain.comm.allreduce(fem.assemble_scalar(error_H10), op=MPI.SUM))
if domain.comm.rank == 0:
    print(f"H01-error: {E_H10:.2e}")

results = Path("results")
results.mkdir(exist_ok=True)
with io.XDMFFile(domain.comm, results / "solution.xdmf", "w") as file:
    file.write_mesh(domain)
    file.write_function(uh)

out_ex = fem.Expression(u_ex, V.element.interpolation_points())
exact = fem.Function(V)
exact.interpolate(out_ex)
with io.XDMFFile(domain.comm, results / "exact.xdmf", "w") as file:
    file.write_mesh(domain)
    file.write_function(exact)

which gives you the correct solution for both iterative and direct solvers (direct solver on very coarse grids might fail).

Thanks a lot is really helpful, for what I saw the problem is my code was related to how I deal with the right hand side, true? Also, why this formulation is not working for the cg method?

I suggest you spend some time comparing the resulting vectors on the RHS, Where I use automatic differentiation for the RHS source term an Neumann condition.

You should also visualize the solution and the exact solution in Paraview, and see if the problem is determined up to a constant.

I’ve had some issues with CG on coarse grids for this problem.