Hey all,
I’ve imported a curved 3D microchannel in FEniCS and I am attempting to model secondary flow/Dean drag forces. I followed guidance from a COMSOL Demo using the initial FEniCS Navier Stokes IPCS FEniCS demo as a basis. The only differences are XDMF mesh import, custom code to extract a boundary faces’ length/width and co-ordinates as well as using ‘bicgstab’ & ‘hypre_amg’ for the solver and pre-conditioner in IPCS (see code below). I visualise the secondary flows in Paraview but the vectors are always biased towards the positive X axis (if Y-Z plane cut) or negative Y axis (if X-Z plane cut) and I am unsure why. Flow otherwise looks appropriate for the simulation.
I visualise secondary flows as follows:
- Slice through 3D model
- Apply “Surface Vector” filter. I ensure that vectors are parallel to the surface and I am using the velocity vector field
- Apply glyph field onto the “Surface Vector” slice.
(This is based on Secondary Flow Visualisation in Paraview.)
I’ve attempted different solvers/preconditioners with no change in the secondary flows. I have increased the density of the mesh as much as possible with the workstation RAM being the limitation. I am looking into whether this is an error with Paraview but if anyone has any thoughts or further questions, let me know!
Many thanks,
Matthew
"""
FEniCS tutorial demo program: Incompressible Navier-Stokes equations
for channel flow (Poisseuille) on the unit square using the
Incremental Pressure Correction Scheme (IPCS).
u' + u . nabla(u)) - div(sigma(u, p)) = f
div(u) = 0
"""
from __future__ import print_function
from fenics import *
# from dolfin import *
import numpy as np
# from mpi4py import MPI
T = 3 # final time
num_steps = 3 #50 # number of time steps
dt = 0.000625 # time step size
mu = 0.001 # dynamic viscosity mPa/s
rho = 1000 # density
FlowVelocity = 0.016 # Flow velocity m/s
FlowVelocity = FlowVelocity * 1.6 # To average to the correcvt inflow
# Load mesh from file (NETGEN mesh as .grid to .xml using DOLFIN)
mesh = Mesh()
with XDMFFile(MPI.comm_world,"mesh_SpiralCrop.xdmf") as infile:
infile.read(mesh)
# # Scale mesh from mm to m
scaling_factor = 0.001
x = mesh.coordinates()
x[:, :] *= scaling_factor
# Move mesh so co-ords always positive
xymin = x.min(axis=0)
x[:, :] = x[:, :] - xymin
# Apply to mesh
mesh.bounding_box_tree().build(mesh)
# Use nonzero guesses - essential for CG with non-symmetric BC
parameters['krylov_solver']['nonzero_initial_guess'] = True
# Define function spaces
V = VectorFunctionSpace(mesh, "CG", 2, dim=3)#'P', 3)
Q = FunctionSpace(mesh, "CG", 1)#'P', 1)
# Define boundary conditions
dim = mesh.topology().dim()
if MPI.comm_world.Get_rank() == 0:
print('Dim:',dim)
# Import face domains from XML (Created in NETGEN/GMSH, conv MESHIO)
mvc = MeshValueCollection("size_t", mesh, dim-1)
with XDMFFile(MPI.comm_world,"mf_SpiralCrop.xdmf") as infile:
infile.read(mvc, "name_to_read")
facet_domains = cpp.mesh.MeshFunctionSizet(mesh, mvc)
## Calculate limits so inflow parabolic can work on co-ordinates not at 0
# Create empty variable space
X = []
Y = []
Z = []
xInflowRange = 0
yInflowRange = 0
zInflowRange = 0
xInflowMin = 0
yInflowMin = 0
zInflowMin = 0
# if (facet_domains.array()[facet_domains.array() == 6] != []):
# print("WARNING: Inlet face not detected, please re-assign")
# break
# Retrive all co-ords as element for desired face
It_facet = SubsetIterator(facet_domains,2)
# for mvc in It_facet:
# i = i + 1
# X1.append(mvc.midpoint().x())
# Y1.append(mvc.midpoint().y())
# Z1.append(mvc.midpoint().z())
#https://fenicsproject.org/qa/13995/print-coordinate-of-boundary-seperately-i-e-left-boundary/
#It_mesh = vertices([facet_domains.array() == 26])
# Collected all co-ords for desired face
for facet_domains in It_facet:
for v in vertices(facet_domains):
X.append(v.point().x())
Y.append(v.point().y())
Z.append(v.point().z())
# Ensure all processes collect co-ords for desired face
MPI.barrier(MPI.comm_world)
# Gather all co-ords to calc length/min
X = MPI.comm_world.gather(X, root=0)
Y = MPI.comm_world.gather(Y, root=0)
Z = MPI.comm_world.gather(Z, root=0)
# Sync all parallel processes for length/min calc
MPI.barrier(MPI.comm_world)
if MPI.comm_world.Get_rank() == 0:
# Remove empty and combine all arrays
X = np.concatenate(X)
Y = np.concatenate(Y)
Z = np.concatenate(Z)
# Calculate length and min values
xInflowRange = np.ptp(X,axis=0)
yInflowRange = np.ptp(Y,axis=0)
zInflowRange = np.ptp(Z,axis=0)
xInflowMin = np.amin(X)
yInflowMin = np.amin(Y)
zInflowMin = np.amin(Z)
# END: Sync all parallel processes for length/min calc
MPI.barrier(MPI.comm_world)
# Broadcast all length/min calc to all nodes used
xInflowRange = MPI.comm_world.bcast(xInflowRange, 0)
yInflowRange = MPI.comm_world.bcast(yInflowRange, 0)
zInflowRange = MPI.comm_world.bcast(zInflowRange, 0)
xInflowMin = MPI.comm_world.bcast(xInflowMin, 0)
yInflowMin = MPI.comm_world.bcast(yInflowMin, 0)
zInflowMin = MPI.comm_world.bcast(zInflowMin, 0)
# Print result of length/min from one process
if MPI.comm_world.Get_rank() == 0:
print("Point X range: ", xInflowRange)
print("Point Y range: ", yInflowRange)
print("Point Z range: ", zInflowRange)
print("Point X min: ", xInflowMin)
print("Point Y min: ", yInflowMin)
print("Point Z min: ", zInflowMin)
# Sync all processes
MPI.barrier(MPI.comm_world)
# xInflowRange = np.amax(comm.gather(xInflowRange))
# yInflowRange = np.amax(comm.gather(yInflowRange))
# zInflowRange = np.amax(comm.gather(zInflowRange))
# xInflowMin = np.amin(comm.gather(xInflowMin))
# yInflowMin = np.amin(comm.gather(yInflowMin))
# zInflowMin = np.amin(comm.gather(zInflowMin))
# print("Gathered Point X range: ", xInflowRange)
# print("Gathered Point Y range: ", yInflowRange)
# print("Gathered Point Z range: ", zInflowRange)
# print("Gathered Point X min: ", xInflowMin)
# print("Gathered Point Y min: ", yInflowMin)
# print("Gathered Point Z min: ", zInflowMin)
# Where x[0] = X, x[1] = Y and x[2] = Z
inflow = Expression(("0.0","((((A*4.0*(x[1] - yMin)*(yRange - (x[1] - yMin))) / pow(yRange,2)) + ((A*4.0*(x[0] - xMin)*(xRange - (x[0] - xMin))) / pow(xRange, 2)))/2)","0.0"),
A=Constant(FlowVelocity), xRange=Constant(xInflowRange), yRange=Constant(zInflowRange), yMin=Constant(zInflowMin), xMin=Constant(xInflowMin), degree=dim)
facet_domains = cpp.mesh.MeshFunctionSizet(mesh, mvc)
#facet_domains = MeshFunction("size_t", mesh, dim-1, mesh.domains()) #Faces of mesh
print('facet domains:',facet_domains.array())
print('facet domain 2:',facet_domains.array()[facet_domains.array() == 2])
# We know 5 and 6 are the inlet and outlet boundaries from NETGEN
#facet_domains.array()[facet_domains.array() == 30] = 2
#facet_domains.array()[facet_domains.array() == 7] = 0 #Set all boundaries above to 0 #2 and 22
#facet_domains.array()[facet_domains.array() == 6] = 0 #Set all boundaries above to 0 #2 and 22
#facet_domains.array()[facet_domains.array() == 3] = 0
# Define boundary conditions
bcu_noslip1 = DirichletBC(V, Constant((0.0, 0.0, 0.0)), facet_domains, 3)
bcu_noslip3 = DirichletBC(V, Constant((0.0, 0.0, 0.0)), facet_domains, 4)
bcu_noslip5 = DirichletBC(V, Constant((0.0, 0.0, 0.0)), facet_domains, 5)
bcu_noslip6 = DirichletBC(V, Constant((0.0, 0.0, 0.0)), facet_domains, 6)
bcu_inflow = DirichletBC(V, inflow, facet_domains, 2) # Constant((0,0,4.2371e-7))
bcp_outflow = DirichletBC(Q, Constant(0.0), facet_domains, 7)
bcu = [bcu_inflow, bcu_noslip1,bcu_noslip3,bcu_noslip5,bcu_noslip6]
bcp = [bcp_outflow]#bcp_inflow,
# Define trial and test functions
u = TrialFunction(V)
v = TestFunction(V)
p = TrialFunction(Q)
q = TestFunction(Q)
# Define functions for solutions at previous and current time steps
u_n = Function(V)
u_ = Function(V)
p_n = Function(Q)
p_ = Function(Q)
# Define expressions used in variational forms
U = 0.5*(u_n + u)
n = FacetNormal(mesh)
f = Constant((0, 0, 0))
k = Constant(dt)
mu = Constant(mu)
rho = Constant(rho)
# 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(len(u))
# Define variational problem for step 1
F1 = rho*dot((u - u_n) / k, v)*dx + \
rho*dot(dot(u_n, nabla_grad(u_n)), v)*dx \
+ inner(sigma(U, p_n), epsilon(v))*dx \
+ dot(p_n*n, v)*ds - dot(mu*nabla_grad(U)*n, v)*ds \
- dot(f, v)*dx
a1 = lhs(F1)
L1 = rhs(F1)
# Define variational problem for step 2
a2 = dot(nabla_grad(p), nabla_grad(q))*dx
L2 = dot(nabla_grad(p_n), nabla_grad(q))*dx - (1/k)*div(u_)*q*dx
# Define variational problem for step 3
a3 = dot(u, v)*dx
L3 = dot(u_, v)*dx - k*dot(nabla_grad(p_ - p_n), v)*dx
# Assemble matrices
A1 = assemble(a1)
A2 = assemble(a2)
A3 = assemble(a3)
# Use amg preconditioner if available
#prec = "amg" if has_krylov_solver_preconditioner("amg") else "default"
#solver_parameters={'linear_solver':'mumps'}
#solver_parameters={'krylov_solver':'nonzero_inital_guess':True}# = True
#solver = KrylovSolver("gmres", "hypre_amg")
# Apply boundary conditions to matrices
[bc.apply(A1) for bc in bcu]
[bc.apply(A2) for bc in bcp]
# Create XDMF files for visualization output
xdmffile_u = XDMFFile('navier_stokes_con/velocity.xdmf')
xdmffile_p = XDMFFile('navier_stokes_con/pressure.xdmf')
# Create time series (for use in reaction_system.py)
#timeseries_u = TimeSeries('navier_stokes_con/velocity_series')
#timeseries_p = TimeSeries('navier_stokes_con/pressure_series')
# Time-stepping
t = 0
i = 0
for n in range(num_steps):
# Update current time
t += dt
i += 1
# Step 1: Tentative velocity step
b1 = assemble(L1)
[bc.apply(b1) for bc in bcu]
solve(A1, u_.vector(), b1,'bicgstab', 'hypre_amg')#,"bicgstab","default")#"gmres", "ilu") #,"bicgstab","default")#
# Step 2: Pressure correction step
b2 = assemble(L2)
[bc.apply(b2) for bc in bcp]
solve(A2, p_.vector(), b2,'bicgstab', 'hypre_amg')#,"bicgstab","prec")#,"cg", prec) 'cg','petsc_amg')
# Step 3: Velocity correction step
b3 = assemble(L3)
solve(A3, u_.vector(), b3,'cg', 'sor')#,"bicgstab","default")#'gmres','ilu')#"cg", "hypre_amg")
# Plot solution
#plot(u_)
# Compute error
u_e = Expression(('4*x[1]*(1.0 - x[1])', '0', '0'), degree=3)
u_e = interpolate(u_e, V)
error = np.abs(u_e.vector() - u_.vector()).max()
print('t = %.3g: error = %.3g' % (t, error))
print('max u:', u_.vector().max())
print("Worst possible Courant number=",(dt*(u_.vector().max()))/mesh.hmin())
# Update previous solution
u_n.assign(u_)
p_n.assign(p_)
# Save solution to file (XDMF/HDF5)
# if (i == 1):
xdmffile_u.write_checkpoint(u_, "velocity",t, XDMFFile.Encoding.HDF5, True)
xdmffile_p.write_checkpoint(p_, "pressure",t, XDMFFile.Encoding.HDF5, True)
# Save nodal values to file
#timeseries_u.store(u_.vector(), t)
#timeseries_p.store(p_.vector(), t)
# Hold plot
#interactive()