Hi!
I have been toying around with Jorgen’s IPCS schemes (from his tutorial) for the Navier-Stokes equation and was evaluating their performance on the lid-driven cavity flow benchmark (see https://doi.org/10.1115/1.4052149). While I am able to obtain qualitatively accurate results that look fine, the convergence seems quite suspect:
The solver code I have implemented is transient, so I tested the simulation time to check if I have actually reached steady state and this was not the source of the issue. I have also compared the simulation results to other benchmarks in the literature and observed similar behavior. I also used the 2D CFL number to set the dt for the simulation with a CFL of 0.5.
Would appreciate any insight that anyone may have!
"""
Lid‐driven cavity IPCS in DOLFINx, with center‐line sampling of u and p
moved inside the solver function. Uses Marchi et al, 2021 Tables 20 & 21
for reference.
"""
from mpi4py import MPI
from petsc4py import PETSc
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
import dolfinx
from dolfinx.fem import (Constant, Function, functionspace,
dirichletbc, locate_dofs_geometrical,
form, Expression)
from dolfinx.fem.petsc import (assemble_matrix, assemble_vector,
apply_lifting, create_vector, set_bc)
from dolfinx.mesh import create_unit_square
from basix.ufl import element
from ufl import (FacetNormal, Identity, TestFunction, TrialFunction,
div, dot, ds, dx, inner, lhs, nabla_grad, rhs, sym)
from dolfinx import geometry
#formatting
plt.rcParams["text.usetex"] = True
plt.rc('font', family='serif')
# ————— Reference data from Tables 9 & 10 —————
y_ref = np.array([0.0546875, 0.0625, 0.0703125, 0.1015625,0.125,0.171875,0.1875,
0.25,0.28125,0.3125,0.375,0.4375,0.453125,0.5,0.5625,0.6171875,
0.625,0.6875,0.734375,0.75,0.8125,0.8515625,0.875,
0.9375,0.953125,0.9609375,0.96875,0.9765625])
u_ref = np.array([-1.8125321e-1,-2.023291e-1,-2.229271e-1,-3.003688e-1,-3.478431e-1,-3.885676e-1,-3.844078e-1,
-3.1894525e-1,-2.8042785e-1,-2.4569306e-1,-1.8373161e-1,-1.2341016e-1, -1.0817529e-1,-6.205605e-2,5.6167e-4,
5.700436e-2,6.524839e-2,1.3357199e-1,1.8864358e-1,2.0791378e-1,2.884412e-1,3.371763e-1,3.625438e-1,
4.229307e-1,4.724491e-1,5.171830e-1,5.803655e-1,6.6397203e-1])
x_ref = np.array([
0.0625, 0.0703125, 0.078125, 0.09375,
0.125, 0.15625, 0.1875, 0.2265625,
0.234375, 0.25, 0.3125, 0.375,
0.4375, 0.5, 0.5625, 0.625,
0.6875, 0.75, 0.8046875, 0.8125,
0.859375, 0.875, 0.90625, 0.9375,
0.9453125,0.953125, 0.9609375, 0.96875
])
v_ref = np.array([
2.807042e-1, 2.962922e-1, 3.099493e-1, 3.329768e-1,
3.650400e-1, 3.769157e-1, 3.678512e-1, 3.340319e-1,
3.253866e-1, 3.0710340e-1, 2.3126780e-1, 1.6056381e-1,
0.9296911e-1, 2.5799473e-2, -4.184046e-2, -1.1079783e-1,
-1.8167907e-1, -2.533806e-1, -3.201954e-1, -3.31565806e-1,
-4.263892e-1, -4.677741e-1, -5.264155e-1, -4.561505e-1,
-4.102923e-1, -3.551312e-1, -2.933786e-1, -2.283407e-1
])
def sample_function_at_points(mesh, func, phys_points):
# build bounding‐box tree
bb = geometry.bb_tree(mesh, mesh.topology.dim)
# find candidate cells for each point (points as 3×N in tutorial, so transpose)
candidates = geometry.compute_collisions_points(bb, phys_points)
# filter to actual containing cells
colliding = geometry.compute_colliding_cells(mesh, candidates, phys_points)
pts_on_proc, cells, indices = [], [], []
for i, p in enumerate(phys_points):
links = colliding.links(i)
if len(links) > 0:
pts_on_proc.append(p)
cells.append(links[0])
indices.append(i)
if not pts_on_proc:
# no sample on this rank
return np.full(len(phys_points), np.nan)
pts_on_proc = np.array(pts_on_proc, dtype=np.float64)
cells = np.array(cells, dtype=np.int32)
# evaluate your FE function
vals = func.eval(pts_on_proc, cells)
# flatten scalar result
if vals.ndim == 2 and vals.shape[1] == 1:
vals = vals[:, 0]
# reassemble full‐length array
if vals.ndim == 1:
full = np.full(len(phys_points), np.nan)
for idx, v in zip(indices, vals):
full[idx] = v
else:
# vector case
dim = vals.shape[1]
full = np.full((len(phys_points), dim), np.nan)
for idx, v in zip(indices, vals):
full[idx, :] = v
return full
def solve_lid_cavity(Nx, T, Re):
#Set up time stepping
Re = Re
CFL = 0.5
T = T
stride = 50
nu_val = 1.0 / Re
Nx=Ny = Nx
#Set up mesh
domain = create_unit_square(MPI.COMM_WORLD, Nx, Ny)
v_cg2 = element("Lagrange", domain.topology.cell_name(), 2, shape=(domain.geometry.dim, ))
s_cg1 = element("Lagrange", domain.topology.cell_name(), 1)
V = functionspace(domain, v_cg2)
Q = functionspace(domain, s_cg1)
coords = domain.geometry.x
xs = np.unique(coords[:, 0]); ys = np.unique(coords[:, 1])
dxs = float(np.min(np.diff(np.sort(xs))))
dy = float(np.min(np.diff(np.sort(ys))))
dt_value = CFL * (dxs * dy) / (dxs + dy)
dt = PETSc.ScalarType(dt_value)
dt_const = Constant(domain, dt)
nu_const = Constant(domain, PETSc.ScalarType(nu_val))
u = TrialFunction(V)
v = TestFunction(V)
p = TrialFunction(Q)
q = TestFunction(Q)
def top(x): return np.isclose(x[1], 1.0)
def walls(x):
return np.logical_or(
np.isclose(x[1], 0.0),
np.logical_or(np.isclose(x[0], 0.0), np.isclose(x[0], 1.0))
)
wall_dofs = locate_dofs_geometrical(V, walls)
u_noslip = np.array((0.0,)*domain.geometry.dim, dtype=PETSc.ScalarType)
bc_walls = dirichletbc(u_noslip, wall_dofs, V)
lid_dofs = locate_dofs_geometrical(V, top)
u_lid = np.array((1.0, 0.0), dtype=PETSc.ScalarType)
bc_lid = dirichletbc(u_lid, lid_dofs, V)
bcs_u = [bc_lid, bc_walls]
p_dofs = locate_dofs_geometrical(
Q, lambda x: np.logical_and(np.isclose(x[0], 0.0),
np.isclose(x[1], 0.0))
)
bc_p = [dirichletbc(Constant(domain, 0.0),p_dofs, Q)]
u_n = Function(V)
u_ = Function(V)
p_n = Function(Q)
p_ = Function(Q)
n = FacetNormal(domain)
U = 0.5*(u_n + u)
f = Constant(domain, PETSc.ScalarType((0, 0)))
def epsilon(u):
return sym(nabla_grad(u))
def sigma(u, p):
return 2 * nu_const * epsilon(u) - p * Identity(len(u))
F1 = dot((u - u_n) / dt_const, v) * dx
F1 += dot(dot(u_n, nabla_grad(u_n)), v) * dx
F1 += inner(sigma(U, p_n), epsilon(v)) * dx
F1 -= dot(f, v) * dx
a1 = form(lhs(F1))
L1 = form(rhs(F1))
A1 = assemble_matrix(a1, bcs=bcs_u)
A1.assemble()
b1 = create_vector(L1)
F2 = (dot(nabla_grad(p - p_n), nabla_grad(q))*dx
+(1/dt_const)*div(u_)*q*dx
)
a2, L2 = form(lhs(F2)), form(rhs(F2))
A2 = assemble_matrix(a2, bcs=bc_p)
A2.assemble()
b2 = create_vector(L2)
F3 = dot(u, v)*dx + dt_const*dot(nabla_grad(p_ - p_n), v)*dx
F3 -= dot(u_, v)*dx
a3, L3 = form(lhs(F3)), form(rhs(F3))
A3 = assemble_matrix(a3)
A3.assemble()
b3 = create_vector(L3)
solver1 = PETSc.KSP().create(domain.comm)
solver1.setOperators(A1)
solver1.setType(PETSc.KSP.Type.BCGS)
pc1 = solver1.getPC()
pc1.setType(PETSc.PC.Type.HYPRE)
pc1.setHYPREType("boomeramg")
solver2 = PETSc.KSP().create(domain.comm)
solver2.setOperators(A2)
solver2.setType(PETSc.KSP.Type.BCGS)
pc2 = solver2.getPC()
pc2.setType(PETSc.PC.Type.HYPRE)
pc2.setHYPREType("boomeramg")
solver3 = PETSc.KSP().create(domain.comm)
solver3.setOperators(A3)
solver3.setType(PETSc.KSP.Type.CG)
pc3 = solver3.getPC()
pc3.setType(PETSc.PC.Type.SOR)
u_n.interpolate(lambda x: (np.zeros_like(x[0]), np.zeros_like(x[0])))
p_n.interpolate(lambda x: np.zeros_like(x[0]))
step = 0
t = 0.0
while t < T - 1e-6:
t += float(dt)
print(t)
with b1.localForm() as loc_1:
loc_1.set(0)
assemble_vector(b1, L1)
apply_lifting(b1, [a1], [bcs_u])
b1.ghostUpdate(addv=PETSc.InsertMode.ADD,
mode=PETSc.ScatterMode.REVERSE)
set_bc(b1, bcs_u)
solver1.solve(b1, u_.x.petsc_vec)
u_.x.scatter_forward()
with b2.localForm() as loc_2:
loc_2.set(0)
assemble_vector(b2, L2)
apply_lifting(b2, [a2], [bc_p])
b2.ghostUpdate(addv=PETSc.InsertMode.ADD,
mode=PETSc.ScatterMode.REVERSE)
set_bc(b2, bc_p)
solver2.solve(b2, p_.x.petsc_vec)
p_.x.scatter_forward()
with b3.localForm() as loc_3:
loc_3.set(0)
assemble_vector(b3, L3)
b3.ghostUpdate(addv=PETSc.InsertMode.ADD,
mode=PETSc.ScatterMode.REVERSE)
solver3.solve(b3, u_.x.petsc_vec)
u_.x.scatter_forward()
u_n.x.array[:] = u_.x.array[:]
p_n.x.array[:] = p_.x.array[:]
step += 1
b1.destroy()
b2.destroy()
b3.destroy()
solver1.destroy()
solver2.destroy()
solver3.destroy()
# ————— sample at final time —————
mesh = u_n.function_space.mesh
# coarse points for error
if MPI.COMM_WORLD.rank == 0:
phys_u = np.column_stack([0.5*np.ones_like(y_ref), y_ref, np.zeros_like(y_ref)])
phys_v = np.column_stack([x_ref, 0.5*np.ones_like(x_ref), np.zeros_like(x_ref)])
# fine points for plotting
y_plot = np.linspace(0.0, 1.0, 100)
x_plot = np.linspace(0.0, 1.0, 100)
phys_u_f = np.column_stack([0.5*np.ones_like(y_plot), y_plot, np.zeros_like(y_plot)])
phys_v_f = np.column_stack([x_plot, 0.5*np.ones_like(x_plot), np.zeros_like(x_plot)])
else:
phys_u = phys_v = phys_u_f = phys_v_f = np.empty((0,3), dtype=np.float64)
# coarse samples
u_vals = sample_function_at_points(mesh, u_n, phys_u)[:,0]
p_u_vals = sample_function_at_points(mesh, p_n, phys_u)
v_vals = sample_function_at_points(mesh, u_n, phys_v)[:,1]
p_v_vals = sample_function_at_points(mesh, p_n, phys_v)
# fine samples for smooth plotting
u_plot = sample_function_at_points(mesh, u_n, phys_u_f)[:,0]
v_plot = sample_function_at_points(mesh, u_n, phys_v_f)[:,1]
return (
u_vals, p_u_vals, # coarse (y_ref/x_ref) samples
v_vals, p_v_vals,
y_plot, x_plot, # the fine grids
u_plot, v_plot,dxs # smooth samples on the fine grids
)
if __name__ == "__main__":
comm = MPI.COMM_WORLD
rank = comm.rank
Nx_list = [20,40,50]
u_plot_list, v_plot_list = [], []
dx_list = []
du_list, dv_list = [], []
err_L2_u, err_inf_u = [], []
err_L2_v, err_inf_v = [], []
for Nx in Nx_list:
print(f"\nRunning Nx={Nx}")
(u_vals, pu_vals,
v_vals, pv_vals,
y_plot, x_plot,
u_plot, v_plot,dxs) = solve_lid_cavity(Nx, 40.0, 1000.0)
du = u_vals - u_ref
dv = v_vals - v_ref
u_plot_list.append(u_plot)
v_plot_list.append(v_plot)
du_list.append(du)
dv_list.append(dv)
dx_list.append(dxs)
err_L2_u.append(np.sqrt(np.mean(du**2)))
err_inf_u.append(np.max(np.abs(du)))
err_L2_v.append(np.sqrt(np.mean(dv**2)))
err_inf_v.append(np.max(np.abs(dv)))
if rank == 0:
fig, axs = plt.subplots(2, 2, figsize=(12, 8))
# Top‐left: Table 9 reference and simulations
axs[0, 0].scatter(y_ref, u_ref, marker='o', color='k', label='Reference')
for Nx, u_plot, dxs in zip(Nx_list, u_plot_list, dx_list):
axs[0, 0].plot(y_plot, u_plot, '-', label=rf'$\Delta x={dxs:.4f}$')
axs[0, 0].set_xlabel('y')
axs[0, 0].set_ylabel('u')
axs[0, 0].set_title(r'$u_{x}$ along x=0.5')
axs[0, 0].legend()
# Top‐right: Table 10 reference and simulations
axs[0, 1].scatter(x_ref, v_ref, marker='o', color='k', label='Reference')
for Nx, v_plot, dxs in zip(Nx_list, v_plot_list, dx_list):
axs[0, 1].plot(x_plot, v_plot, '-', label=rf'$\Delta x={dxs:.4f}$')
axs[0, 1].set_xlabel('x')
axs[0, 1].set_ylabel('v')
axs[0, 1].set_title(r'$u_{y}$ along y=0.5')
axs[0, 1].legend()
# Bottom‐left: Error metrics for u (RMSE & max abs vs Nx)
axs[1, 0].plot(dx_list, err_L2_u, 'o-', label='RMSE')
axs[1, 0].plot(dx_list, err_inf_u, 's--', label='MAE')
axs[1, 0].set_xlabel(r'$\Delta x$')
axs[1, 0].set_ylabel('Error')
# axs[1, 0].set_title('Error metrics: Horizontal velocity')
axs[1,0].set_yscale("log")
axs[1,0].set_xscale("log")
axs[1, 0].legend()
# Bottom‐right: Error metrics for v (RMSE & max abs vs Nx)
axs[1, 1].plot(dx_list, err_L2_v, 'o-', label='RMSE')
axs[1, 1].plot(dx_list, err_inf_v, 's--', label='MAE')
axs[1, 1].set_xlabel(r'$\Delta x$')
axs[1, 1].set_ylabel('Error')
axs[1,1].set_yscale("log")
axs[1,1].set_xscale("log")
# axs[1, 1].set_title('Error metrics: Vertical velocity')
axs[1, 1].legend()
plt.tight_layout()
plt.savefig("ipcs_a_liddriven.png",dpi=300)
plt.show()




