Petrov-Gelerkin formulations and Fenicsx

I want to solve a Petrov-Galerkin Variational formulation using Fenicsx, i.e. a formulation where trial and test space are not equal, typically also including different boundary conditions. Small example:

I implemented that with f(x) = pi^2 sin(pi*x), b = 0, c = 1, i.e. the exact solution reads u(x) = sin(pi*x) with the following code:

import numpy as np
from mpi4py import MPI
from dolfinx import mesh, fem
from dolfinx.fem.petsc import LinearProblem
import ufl

np.set_printoptions(edgeitems=30, linewidth=100000, precision=2)

# define mesh
msh = mesh.create_unit_interval(MPI.COMM_WORLD, 5)
msh.topology.create_connectivity(msh.topology.dim-1, msh.topology.dim)
tdim = msh.topology.dim-1

# define problem
x = ufl.SpatialCoordinate(msh)
n  = ufl.FacetNormal(msh)

u_exact = lambda x: np.sin(np.pi * x)
b = u_exact(0)
c = np.pi * np.cos(np.pi*0)
f  = ufl.pi**2 * ufl.sin(ufl.pi * x[0])

# get the two boundary parts {0} and {1}
gamma0_facets = mesh.locate_entities_boundary(msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 0.0))  # Gamma_0 = {0}
gamma1_facets = mesh.locate_entities_boundary(msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 1.0))  # Gamma_1 = {1}

# construct the boundary measure over gamma_0
gamma0 = mesh.meshtags(msh, tdim, gamma0_facets, np.full_like(gamma0_facets, 0))
ds = ufl.Measure("ds", domain=msh, subdomain_data=gamma0)
ds_gamma0 = ds(0)

# define the two (!) function spaces -> Petrov-Galerkin
U = fem.functionspace(msh, ("Lagrange", 1))
V = fem.functionspace(msh, ("Lagrange", 1))

# setup the different (!) Dirichlet BCs for each space
gamma0_dofs_U = fem.locate_dofs_topological(U, tdim, gamma0_facets)
gamma1_dofs_V = fem.locate_dofs_topological(V, tdim, gamma1_facets)
bcs = [fem.dirichletbc(b,   gamma0_dofs_U, U),
       fem.dirichletbc(0.0, gamma1_dofs_V, V)]

# define variational problem
u = ufl.TrialFunction(U)
v = ufl.TestFunction(V)

A = ufl.dot(ufl.grad(u),ufl.grad(v)) * ufl.dx
l = f*v*ufl.dx - c*v*ds_gamma0

# print discrete system
print("A:\n" + str(fem.assemble_matrix(fem.form(A), bcs=bcs).to_scipy().todense()))
print("l:\n" + str(fem.assemble_vector(fem.form(l)).array))

# try to solve the problem -> failure
problem = LinearProblem(A,l, bcs=bcs,
    petsc_options={"ksp_type": "preonly", "pc_type": "lu", "ksp_error_if_not_converged" : True},
    petsc_options_prefix="petrov_galerkin_"
)
uh = problem.solve()

But Fenics is not able to solve that problem, so the solving throws an error, and when we look at the linear system, it is clear why:

A:
[[ 0. -5.  0.  0.  0.  0.]
 [ 0. 10. -5.  0.  0.  0.]
 [ 0. -5. 10. -5.  0.  0.]
 [ 0.  0. -5. 10. -5.  0.]
 [ 0.  0.  0. -5. 10. -5.]
 [ 0.  0.  0.  0.  0.  0.]]
l:
[-2.94  1.12  1.82  1.82  1.12  0.2 ]

The boundary conditions are not handled properly, resulting an all zero row and column. If i am not mistaking, Fenicsx handles Dirichlet BCs by 1) setting the corresponding rows and columns to zero and 2) placing a 1.0 on the diagonal, instead of 1) deleting these rows and columns completely and 2) modifying the right hand side. Of course, using this approach, the solver can not be able to solve a Petrov-Galerkin type problem, as it can no longer place a 1.0 at the diagonal (the fundamental assumption for that is that trial and test space dofs have a 1one1 relation, which is no longer guaranteed in the Petrov-Galerkin setting and is in fact not the case for the above example). Fenicsx / PETSc seems to realize that there is a problem, as it doesn’t place a 1.0 at the diagonal, but on the same time still sets the rows and columns to zero, thus leaving the system in an unfinished state (where the BCs are not handled properly and the resulting matrix is singular)

I can of course fix this on my own using numpy with the following code

A = fem.assemble_matrix(fem.form(A)).to_scipy().todense()
l = fem.assemble_vector(fem.form(l)).array

# identify free dofs (by removing dirichlet dofs)
dofs_U = np.ones(A.shape[1], dtype=bool)
dofs_U[dirichlet_dofs_U] = False
dofs_V = np.ones(A.shape[0], dtype=bool)
dofs_V[dirichlet_dofs_V] = False

# delete rows and columns corresponding to dirichlet dofs and modify right hand side
A_ = A[dofs_V,:][:,dofs_U]
l_ = l[dofs_V] + A[dofs_V, dirichlet_dofs_U] * b
# solve the reduced system and insert dirichlet values into the solution
u = np.zeros((A.shape[1],1))
u[dofs_U] = np.linalg.solve(A_,l_.T)
u[dirichlet_dofs_U] = b

import matplotlib.pyplot as plt
plt.plot(u)
plt.show()

This then gives a perfect sin function and hence reproduces the exact solution.

Now, finally to my question:
Is it possible to solve this problem whitin Fenicsx / PETSc, without having to manually extract the matricies to SciPy or NumPy and handle the dirichlet BCs manually?

I’m not answering you question here (I’ll leave that open for someone else to pitch in), but rather questioning what you’re doing is actually what you intent to do.

The page you’re citing is namely not an example of a Petrov Galerkin method. There is just one step missing to give us back a Bubnov Galerkin method; you have to “lift” the Dirichlet boundary condition to the right hand side. I believe I explain that here: https://www.youtube.com/watch?v=5MRoqVB2_qw&list=PLMHTjE57oyvpkTPG8ON1w6BChBoapsZTA&index=11

You’ll end up with a test and trial function both in H^1_0.

In practice, though, you really don’t have to bother with most of that. Take a look at this Poisson demo Solving the Poisson equation — FEniCSx tutorial and compare that to your case (it is exactly the same).

1 Like

The native assembler and BCs handling could be improved to allow what you describe above. On the other hand, you can handle Dirichlet BCs yourself on the algebraic level. Below is a change to the code you posted on the discourse.

There are few changes:

  1. bcs = [] as far as assembler is concerned,
  2. zeroing rows and columns in the matrix with petsc4py (and setting 1 on the diagonal). There is no zeroColumns in PETSc, so one has to trick it with rows on the transpose.

However, be careful with one of the spaces U or V not having homogeneous Dirichlet BCs. Then you need to lift the contribution and put into the weak form, so it will affect the RHS, as Stein says.

import dolfinx.fem.petsc
import numpy as np
import ufl
from dolfinx import fem, mesh
from mpi4py import MPI
from petsc4py import PETSc

msh = mesh.create_unit_interval(MPI.COMM_WORLD, 10)
msh.topology.create_connectivity(msh.topology.dim - 1, msh.topology.dim)
tdim = msh.topology.dim - 1

x = ufl.SpatialCoordinate(msh)
n = ufl.FacetNormal(msh)

u_exact = lambda x: np.sin(np.pi * x)
b = u_exact(0)
c = np.pi * np.cos(np.pi * 0)
f = ufl.pi**2 * ufl.sin(ufl.pi * x[0])

# get the two boundary parts {0} and {1}
gamma0_facets = mesh.locate_entities_boundary(
    msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 0.0)
)  # Gamma_0 = {0}
gamma1_facets = mesh.locate_entities_boundary(
    msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 1.0)
)  # Gamma_1 = {1}

# construct the boundary measure over gamma_0
gamma0 = mesh.meshtags(msh, tdim, gamma0_facets, np.full_like(gamma0_facets, 0))
ds = ufl.Measure("ds", domain=msh, subdomain_data=gamma0)
ds_gamma0 = ds(0)

# define the two (!) function spaces -> Petrov-Galerkin
U = fem.functionspace(msh, ("Lagrange", 1))
V = fem.functionspace(msh, ("Lagrange", 1))

gamma0_dofs_U = fem.locate_dofs_topological(U, tdim, gamma0_facets)
gamma1_dofs_V = fem.locate_dofs_topological(V, tdim, gamma1_facets)
bcs = []

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

A = ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx
l = f * v * ufl.dx - c * v * ds_gamma0

mat_A = dolfinx.fem.petsc.assemble_matrix(fem.form(A), bcs=bcs)
mat_A.assemble()

gamma0_dofs_U_global = np.int32(U.dofmap.index_map.local_to_global(gamma0_dofs_U))
gamma1_dofs_V_global = np.int32(V.dofmap.index_map.local_to_global(gamma1_dofs_V))

# Apply Dirichlet BCs on Gamma_1 for test functions
mat_A.zeroRows(gamma1_dofs_V_global, diag=1.0)

# Apply Dirichlet BCs on Gamma_0 for trial functions
AT = mat_A.transpose()
AT.assemble()
AT.zeroRows(gamma0_dofs_U_global, diag=1.0)
AT.transpose(out=mat_A)

vec_l = fem.assemble_vector(fem.form(l))
vec_l.petsc_vec.ghostUpdate(
    addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE
)
vec_l.array[gamma1_dofs_V] = 0.0

solver = PETSc.KSP().create(MPI.COMM_WORLD)
solver.setOperators(mat_A)
solver.setType("preonly")
pc = solver.getPC()
pc.setType("lu")
solver.setFromOptions()

solution = fem.Function(U)
solver.solve(vec_l.petsc_vec, solution.x.petsc_vec)
solution.x.petsc_vec.view()

1 Like

Thank you for your answer, it did indeed solve the problem i considered, but i have two follow up question:

  1. Out of curiosity, what is the deal with
    gamma0_dofs_U_global = np.int32(U.dofmap.index_map.local_to_global(gamma0_dofs_U))
    Why (or in which cases) is that important?

  2. I have extended my code for inhomogeneous Dirichlet conditions (setting b=1) and (just to confirm that there is no Dirichlet condition at x=1, i changed the test problem to something where the solution does not satisfy u(1)=0. Hence, consider f(x) = 1/4*pi^2 sin(pi/2*x), b=1, c=pi/2, i.e. the exact solution reads u(x) = sin(pi/2*x) + 1. Is there a way to solve that problem (with inhomogeneous Dirichlet conditions) as well with PETSc, as you and Stein said, one hast to lift it to the right hand side, like i did in the NumPy code. But how could that work with PETSc?
    For reference, here is my code. The first approach using NumPy recovers the exact solution using the lifting, the second using PETSc does not in its current sate

import numpy as np
from mpi4py import MPI
from dolfinx import mesh, fem
from dolfinx.fem.petsc import LinearProblem
import ufl

np.set_printoptions(edgeitems=30, linewidth=100000, precision=2)

# define mesh
msh = mesh.create_unit_interval(MPI.COMM_WORLD, 5)
msh.topology.create_connectivity(msh.topology.dim-1, msh.topology.dim)
tdim = msh.topology.dim-1

# define problem
x = ufl.SpatialCoordinate(msh)

u_exact = lambda x: np.sin(np.pi/2 * x) + 1
b = u_exact(0)
c = np.pi/2 * np.cos(np.pi/2*0)
f  = ufl.pi**2/4 * ufl.sin(ufl.pi/2 * x[0])

# get the two boundary parts {0} and {1}
gamma0_facets = mesh.locate_entities_boundary(msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 0.0))  # Gamma_0 = {0}
gamma1_facets = mesh.locate_entities_boundary(msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 1.0))  # Gamma_1 = {1}

# construct the boundary measure over gamma_0
gamma0 = mesh.meshtags(msh, tdim, gamma0_facets, np.full_like(gamma0_facets, 0))
ds = ufl.Measure("ds", domain=msh, subdomain_data=gamma0)
ds_gamma0 = ds(0)

# define the two (!) function spaces -> Petrov-Galerkin
U = fem.functionspace(msh, ("Lagrange", 1))
V = fem.functionspace(msh, ("Lagrange", 1))

# setup the different (!) Dirichlet BCs for each space
dirichlet_dofs_U = fem.locate_dofs_topological(U, tdim, gamma0_facets)
dirichlet_dofs_V = fem.locate_dofs_topological(V, tdim, gamma1_facets)
bcs = [fem.dirichletbc(b,   dirichlet_dofs_U, U),
       fem.dirichletbc(0.0, dirichlet_dofs_V, V)]

# define variational problem
u = ufl.TrialFunction(U)
v = ufl.TestFunction(V)

A = ufl.dot(ufl.grad(u),ufl.grad(v)) * ufl.dx
l = f*v*ufl.dx - c*v*ds_gamma0

# print discrete system
print("A:\n" + str(fem.assemble_matrix(fem.form(A), bcs=bcs).to_scipy().todense()))
print("l:\n" + str(fem.assemble_vector(fem.form(l)).array))

##########################################
# SOLVE 1: manual assembly and solve using NumPy
##########################################
# Manually assemble and solve the linear system by exporting to numpy
A_mat = fem.assemble_matrix(fem.form(A)).to_scipy().todense()
l_vec = fem.assemble_vector(fem.form(l)).array

# identify free dofs (by removing dirichlet dofs)
dofs_U = np.ones(A_mat.shape[1], dtype=bool)
dofs_U[dirichlet_dofs_U] = False
dofs_V = np.ones(A_mat.shape[0], dtype=bool)
dofs_V[dirichlet_dofs_V] = False

# delete rows and columns corresponding to dirichlet dofs and modify right hand side
A_ = A_mat[dofs_V,:][:,dofs_U]
l_ = l_vec[dofs_V] - A_mat[dofs_V, dirichlet_dofs_U] * b
# solve the reduced system and insert dirichlet values
u = np.zeros((A_mat.shape[1],1))
u[dofs_U] = np.linalg.solve(A_,l_.T)
u[dirichlet_dofs_U] = b

import matplotlib.pyplot as plt
plt.plot(u)
plt.show()


##########################################
# SOLVE 2: using PETSc
##########################################
from petsc4py import PETSc
import dolfinx.fem.petsc
mat_A = dolfinx.fem.petsc.assemble_matrix(fem.form(A), bcs=bcs)
mat_A.assemble()

dirichlet_dofs_U_global = np.int32(U.dofmap.index_map.local_to_global(dirichlet_dofs_U))
dirichlet_dofs_V_global = np.int32(V.dofmap.index_map.local_to_global(dirichlet_dofs_V))

# Apply Dirichlet BCs on Gamma_1 for test functions
mat_A.zeroRows(dirichlet_dofs_V_global, diag=1.0)

# Apply Dirichlet BCs on Gamma_0 for trial functions
AT = mat_A.transpose()
AT.assemble()
AT.zeroRows(dirichlet_dofs_U_global, diag=1.0)
AT.transpose(out=mat_A)

vec_l = fem.assemble_vector(fem.form(l))
vec_l.petsc_vec.ghostUpdate(
    addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE
)
vec_l.array[dirichlet_dofs_V] = 0.0

solver = PETSc.KSP().create(MPI.COMM_WORLD)
solver.setOperators(mat_A)
solver.setType("preonly")
pc = solver.getPC()
pc.setType("lu")
solver.setFromOptions()

solution = fem.Function(U)
solver.solve(vec_l.petsc_vec, solution.x.petsc_vec)
solution.x.petsc_vec.view()

print("A (PETSc):\n" + str(mat_A.getValues(range(0, mat_A.getSize()[0]), range(0,  mat_A.getSize()[1]))))

plt.figure()
plt.plot(solution.x.petsc_vec.array)
plt.show()

Thank you for your answer, that is just an example, and was probably not the best as this problem can quite easiely reduced to a Bubnov-Galerkin problem, you are right. I was just looking for an easy example where i can state a problem with different boundary conditions in Trial and Test space and find a way to solve that.

The lifting is in fact what i did in my solution Code using NumPy (l_ = l[dofs_V] - A[dofs_V, dirichlet_dofs_U] * b). The actual problems i am interested in are in particular space-time formulations (e.g. for the wave equation) with different regularity of the solution (ranging from L^2 to H^2). If you are interested may have a look at examples 2.2 to 2.7 in arXiv:2508.05407 to get an idea.

1 Like

Third question i forgot to ask in the first reply:

  1. Does this approach still work for Least-Squares formulations, where the Test space has more dofs then the Trial space, thus leading to a non quadratic matrix? Example: use U = fem.functionspace(msh, ("Lagrange", 2)) and V = fem.functionspace(msh, ("Lagrange", 3)), together with changing the NumPy solver of the reference solution to np.linalg.lstsq(A_,l_.T)[0] and the PETSc solver to pc.setType("qr"). In the current state of the PETSc approach, this does not work due to the non-quadratic shape, since mat_A.zeroRows(dirichlet_dofs_V_global, diag=1.0) can not set the corresponding diagonal entry (the row simply does not exist). Can we fix this approach (setting rows/columns to zero). How can we solve such a problem?

If someone finds this, searching for Petrov-Galerkin support on FEniCSx, i wrote my own add-on for FEniCSx, pgfenicsx, to handle the assembly of Petrov-Galerkin formulations. You can find it here:

and its documentation

It supports SciPy and PETSc for assembling / solving the system. This module just provides two functions, assemble_matrix and assemble_vector as a drop-in replacement of the corresponding FEniCSx / Dolfinx functions.

With that, the final code reads

import numpy as np
import scipy.sparse.linalg
from mpi4py import MPI
from dolfinx import mesh, fem
import ufl
from petsc4py import PETSc
import pgfenicsx

# define mesh
msh = mesh.create_unit_interval(MPI.COMM_WORLD, 5)
msh.topology.create_connectivity(msh.topology.dim-1, msh.topology.dim)
tdim = msh.topology.dim-1

# define problem
x = ufl.SpatialCoordinate(msh)
n  = ufl.FacetNormal(msh)

u_exact = lambda x: np.sin(np.pi * x[0])
b = u_exact([0])
c = np.pi * np.cos(np.pi*0)
f  = ufl.pi**2 * ufl.sin(ufl.pi * x[0])

# get the two boundary parts {0} and {1}
gamma0_facets = mesh.locate_entities_boundary(msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 0.0))  # Gamma_0 = {0}
gamma1_facets = mesh.locate_entities_boundary(msh, dim=tdim, marker=lambda tx: np.isclose(tx[0], 1.0))  # Gamma_1 = {1}

# construct the boundary measure over gamma_0
gamma0 = mesh.meshtags(msh, tdim, gamma0_facets, np.full_like(gamma0_facets, 0))
ds = ufl.Measure("ds", domain=msh, subdomain_data=gamma0)
ds_gamma0 = ds(0)

# define the two (!) function spaces -> Petrov-Galerkin
U = fem.functionspace(msh, ("Lagrange", 1))
V = fem.functionspace(msh, ("Lagrange", 1))

# setup the different (!) Dirichlet BCs for each space
gamma0_dofs_U = fem.locate_dofs_topological(U, tdim, gamma0_facets)
gamma1_dofs_V = fem.locate_dofs_topological(V, tdim, gamma1_facets)
bcs = [fem.dirichletbc(b,   gamma0_dofs_U, U),
       fem.dirichletbc(0.0, gamma1_dofs_V, V)]

# define variational problem
u = ufl.TrialFunction(U)
v = ufl.TestFunction(V)

A = ufl.dot(ufl.grad(u),ufl.grad(v)) * ufl.dx
l = f*v*ufl.dx - c*v*ds_gamma0

A = fem.form(A)
l = fem.form(l)

# solve using pgfenicsx assembly routines (SciPy)
A_scipy = pgfenicsx.assemble_matrix(A, bcs)
l_scipy = pgfenicsx.assemble_vector(l, U, bcs)

u_scipy = fem.Function(U)
u_scipy.x.array[:] = scipy.sparse.linalg.spsolve(A_scipy,l_scipy)

# solve using pgfenicsx assembly routines (PETSc)
A_petsc = pgfenicsx.assemble_matrix(A, bcs, petsc=True)
l_petsc = pgfenicsx.assemble_vector(l, U, bcs, petsc=True)

solver = PETSc.KSP().create(MPI.COMM_WORLD)
solver.setOperators(A_petsc)
solver.setType("preonly")
solver.getPC().setType("qr")
solver.setFromOptions()

u_petsc = fem.Function(U)
u_petsc.x.petsc_vec.ghostUpdate(
    addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE
)
solver.solve(l_petsc, u_petsc.x.petsc_vec)

# compute error
u_exact_ = fem.Function(U)
u_exact_.interpolate(u_exact)
error_scipy = np.linalg.norm(u_scipy.x.array - u_exact_.x.array, ord=np.inf)
error_petsc = np.linalg.norm(u_petsc.x.array - u_exact_.x.array, ord=np.inf)
print(f"Error (SciPy): {error_scipy}")
print(f"Error (PETSc): {error_petsc}")
1 Like

I just realized, i think you missed one point here, the Boundary conditions on U and V are not just different in value, but they differ with respect to the boundary part where they are applied, namely U enforces u(0)=..., while V enforces v(1)=.... Thus, the lifting does not give a Galerkin formulation. Of course, this could still be fixed easily by reversing v, i.e. find u in U such that a(u,Tv) = l(Tv) for all v in U, with Tv(x) = v(1-x). Then we would have a Galerkin formulation. But again, that was just an example.

1 Like