Dear community,
I tried to adapt the 2D channel flow from Jørgen Dokken to 3D with custom boundary conditions. By custom I mean that I want to have an arbitrary number and location of inflows and outflows on the mesh. I currently implement this with topological DOFs and a distance check around my target points (see inflow() and outflow()). The pressure on the output images (using pyvista) are looking reasonable (see Fig. 3, Fig. 4) but the velocities seem to explode at the inflow and outflow points. At the beginning of the simulation the direction of those velocity vectors looks random. Later on they are redirecting in the expected direction (see Fig. 2) but their values are still exploding compared to the velocities inside the mesh (see colorbar on velocity figures). In addition the problem seems to get more extreme with a more fine-grained mesh (see Fig. 5).
Is this physically correct CFD behavior? Is there a bug in the code? Am I misunderstanding something?
Any help or comments are appreciated!
Figures:
Fig. 1
Fig. 2
Fig. 3
Fig. 4
Fig. 5
Minimal code to reproduce (using Python 3.10.8 and fenics-dolfinx 0.5.2):
from mpi4py import MPI
from petsc4py import PETSc
import numpy as np
from scipy.spatial import distance
import pyvista
from dolfinx.fem import Constant, Function, FunctionSpace, assemble_scalar, dirichletbc, form, locate_dofs_topological
from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, create_vector, set_bc
from dolfinx.mesh import locate_entities_boundary, create_unit_cube
from dolfinx.plot import create_vtk_mesh
from ufl import (FacetNormal, FiniteElement, Identity, TestFunction, TrialFunction, VectorElement,
div, dot, ds, dx, inner, lhs, nabla_grad, rhs, sym)
import os
mesh = create_unit_cube(MPI.COMM_WORLD, 10, 10, 10)
t = 0
T = 0.2
num_steps = 20
dt = T / num_steps
v_cg2 = VectorElement("CG", mesh.ufl_cell(), 2)
s_cg1 = FiniteElement("CG", mesh.ufl_cell(), 1)
V = FunctionSpace(mesh, v_cg2)
Q = FunctionSpace(mesh, s_cg1)
u = TrialFunction(V)
v = TestFunction(V)
p = TrialFunction(Q)
q = TestFunction(Q)
def inflow(x):
accept_dist = 0.15
return distance.cdist([[0, 0.5, 0.5]], x.T, 'euclidean') < accept_dist
def outflow(x):
accept_dist = 0.15
return distance.cdist([[1, 0.5, 0.5]], x.T, 'euclidean') < accept_dist
def boundary(x):
return np.logical_not(np.logical_or(inflow(x), outflow(x)))
facetdim = mesh.topology.dim - 1
bndry_facets = locate_entities_boundary(mesh, facetdim, boundary)
wall_dofs = locate_dofs_topological(V, facetdim, bndry_facets)
u_noslip = np.array((0,) * mesh.topology.dim, dtype=PETSc.ScalarType)
bc_noslip = dirichletbc(u_noslip, wall_dofs, V)
inflow_facets = locate_entities_boundary(mesh, facetdim, inflow)
inflow_dofs = locate_dofs_topological(Q, facetdim, inflow_facets)
bc_inflow = dirichletbc(PETSc.ScalarType(5), inflow_dofs, Q)
outflow_facets = locate_entities_boundary(mesh, facetdim, outflow)
outflow_dofs = locate_dofs_topological(Q, facetdim, outflow_facets)
bc_outflow = dirichletbc(PETSc.ScalarType(-5), outflow_dofs, Q)
bcu = [bc_noslip]
bcp = [bc_inflow, bc_outflow]
u_n = Function(V)
u_n.name = "u_n"
U = 0.5 * (u_n + u)
n = FacetNormal(mesh)
f = Constant(mesh, PETSc.ScalarType((0, 0, 0))) # CHANGED (0, 0) to (0, 0, 0) to adapt to 3D
k = Constant(mesh, PETSc.ScalarType(dt))
mu = Constant(mesh, PETSc.ScalarType(1))
rho = Constant(mesh, PETSc.ScalarType(1))
# Define strain-rate tensor
def epsilon(u):
return sym(nabla_grad(u))
# Define stress tensor
def sigma(u, p):
return 2 * mu * epsilon(u) - p * Identity(u.geometric_dimension())
# Define the variational problem for the first step
p_n = Function(Q)
p_n.name = "p_n"
F1 = rho * dot((u - u_n) / k, v) * dx
F1 += rho * dot(dot(u_n, nabla_grad(u_n)), v) * dx
F1 += inner(sigma(U, p_n), epsilon(v)) * dx
F1 += dot(p_n * n, v) * ds - dot(mu * nabla_grad(U) * n, v) * ds
F1 -= dot(f, v) * dx
a1 = form(lhs(F1))
L1 = form(rhs(F1))
A1 = assemble_matrix(a1, bcs=bcu)
A1.assemble()
b1 = create_vector(L1)
# Define variational problem for step 2
u_ = Function(V)
a2 = form(dot(nabla_grad(p), nabla_grad(q)) * dx)
L2 = form(dot(nabla_grad(p_n), nabla_grad(q)) * dx - (1 / k) * div(u_) * q * dx)
A2 = assemble_matrix(a2, bcs=bcp)
A2.assemble()
b2 = create_vector(L2)
# Define variational problem for step 3
p_ = Function(Q)
a3 = form(dot(u, v) * dx)
L3 = form(dot(u_, v) * dx - k * dot(nabla_grad(p_ - p_n), v) * dx)
A3 = assemble_matrix(a3)
A3.assemble()
b3 = create_vector(L3)
# Solver for step 1
solver1 = PETSc.KSP().create(mesh.comm)
solver1.setOperators(A1)
solver1.setType(PETSc.KSP.Type.BCGS)
pc1 = solver1.getPC()
pc1.setType(PETSc.PC.Type.HYPRE)
pc1.setHYPREType("boomeramg")
# Solver for step 2
solver2 = PETSc.KSP().create(mesh.comm)
solver2.setOperators(A2)
solver2.setType(PETSc.KSP.Type.BCGS)
pc2 = solver2.getPC()
pc2.setType(PETSc.PC.Type.HYPRE)
pc2.setHYPREType("boomeramg")
# Solver for step 3
solver3 = PETSc.KSP().create(mesh.comm)
solver3.setOperators(A3)
solver3.setType(PETSc.KSP.Type.CG)
pc3 = solver3.getPC()
pc3.setType(PETSc.PC.Type.SOR)
def u_exact(x):
values = np.zeros((3, x.shape[1]), dtype=PETSc.ScalarType) # CHANGED 2 to 3 to adapt to 3D
values[0] = 4 * x[1] * (1.0 - x[1])
return values
u_ex = Function(V)
u_ex.interpolate(u_exact)
L2_error = form(dot(u_ - u_ex, u_ - u_ex) * dx)
out_dir = 'velocity-3D'
p_out_dir = 'pressure-3D'
if not os.path.exists(out_dir):
os.makedirs(out_dir)
if not os.path.exists(p_out_dir):
os.makedirs(p_out_dir)
topology, cell_types, geometry = create_vtk_mesh(V)
# Create a point cloud of glyphs
function_grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
p_topology, p_cell_types, p_geometry = create_vtk_mesh(Q)
p_grid = pyvista.UnstructuredGrid(p_topology, p_cell_types, p_geometry)
# Create a pyvista-grid for the mesh
grid = pyvista.UnstructuredGrid(*create_vtk_mesh(mesh, mesh.topology.dim))
for i in range(num_steps):
# Update current time step
t += dt
# Step 1: Tentative velocity step
with b1.localForm() as loc_1:
loc_1.set(0)
assemble_vector(b1, L1)
apply_lifting(b1, [a1], [bcu])
b1.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
set_bc(b1, bcu)
solver1.solve(b1, u_.vector)
u_.x.scatter_forward()
# Step 2: Pressure correction step
with b2.localForm() as loc_2:
loc_2.set(0)
assemble_vector(b2, L2)
apply_lifting(b2, [a2], [bcp])
b2.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
set_bc(b2, bcp)
solver2.solve(b2, p_.vector)
p_.x.scatter_forward()
# Step 3: Velocity correction step
with b3.localForm() as loc_3:
loc_3.set(0)
assemble_vector(b3, L3)
b3.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
solver3.solve(b3, u_.vector)
u_.x.scatter_forward()
# Update variable with solution form this time step
u_n.x.array[:] = u_.x.array[:]
p_n.x.array[:] = p_.x.array[:]
# Write solutions to file
# xdmf.write_function(u_n, t)
# xdmf.write_function(p_n, t)
# Create plotter
plotter = pyvista.Plotter(off_screen=True)
p_plotter = pyvista.Plotter(off_screen=True)
p_plotter.add_title("Frame {}".format(i))
p_plotter.add_mesh(grid, style="wireframe", color="k")
p_grid["p"] = p_n.x.array.real
p_plotter.add_mesh(p_grid, cmap='viridis')
values = np.zeros((geometry.shape[0], 3), dtype=np.float64)
values[:, :len(u_n)] = u_n.x.array.real.reshape((geometry.shape[0], len(u_n)))
function_grid["u"] = values
glyphs = function_grid.glyph(orient="u", scale=True, factor=1)
plotter.add_title("Frame {}".format(i))
plotter.add_mesh(grid, style="wireframe", color="k")
plotter.add_mesh(glyphs, cmap='jet')
plotter.view_vector([0.5, -0.5, 0.5])
p_plotter.view_vector([0.5, -0.5, 0.5])
plotter.screenshot("{}/{}_glyphs.png".format(out_dir, i))
p_plotter.screenshot("{}/{}_pressure.png".format(p_out_dir, i))
# Compute error at current time-step
error_L2 = np.sqrt(mesh.comm.allreduce(assemble_scalar(L2_error), op=MPI.SUM))
error_max = mesh.comm.allreduce(np.max(u_.vector.array - u_ex.vector.array), op=MPI.MAX)
# Print error only every 20th step and at the last step
if (i % 20 == 0) or (i == num_steps - 1):
print(f"Time {t:.2f}, L2-error {error_L2:.2e}, Max error {error_max:.2e}")