Rotational IPC keeps diverging

Hi,

I have this attached geometry consisted of one inlet and two outlets, but it keeps diverging whatever I tried to do (enhancing the mesh resolution, decreasing the time step, decreasing the inlet flow rate, using different preconditioner and stabilization) one thing I noticed as well is that I’m applying the parabolic inlet profile but the region after the inlet suddenly doesn’t follow that (kinda discontnuity) and after a few steps the velocity explode and backflow happen.

Geometry:

The inlet profile discontinuity:

My script

#!/usr/bin/env python3

# coding: utf-8

“”"

Optimized IPCS (Oasis/DFG-style) scheme in classic FEniCS (dolfin):

- BDF2 in time: (3 u^{n+1} - 4 u^n + u^{n-1}) / (2 dt)

- Convection in A1 with explicit convecting velocity: u_conv = 2 u^n - u^{n-1}

  i.e. (u_conv · ∇ u^{n+1}, v)

- Diffusion implicit: mu (∇u^{n+1}, ∇v)

- Pressure term in step 1 uses current pressure p^n (held in p_)

- Pressure correction Poisson:

    (∇phi, ∇q) = -(rho * 3/(2 dt)) (div u_s, q)

- Rotational pressure update:

    p^{n+1} = p^n + phi - mu * div(u_s)

  (div(u_s) is projected into Q via scalar mass solve)

- Velocity correction:

    rho (u^{n+1}, v) = rho (u_s, v) - (2 dt/3) (∇phi, v)




Performance-minded:

- Preassemble constant matrices (vector mass Mv, stiffness K, scalar mass Ms, A2, A3)

- Reuse convection matrix storage (assemble into tensor=C each step)

- Reuse RHS vectors and temporary vectors

- Print AND save every single step (as you requested)




Mesh input:

    Mesh/Volume_mesh.xml

    Mesh/BCnodeFacets.xml

Boundary tags:

    1 inlet, 2 wall, 3/4 outlets




"""




from __future__ import print_function

import os

import time

import numpy as np

from fenics import *




# ==============================================================================

# MPI / LOGGING

# ==============================================================================

comm = MPI.comm_world

rank = MPI.rank(comm)




def log(msg):

if rank == 0:

print(msg, flush=True)




def banner(msg):

if rank == 0:

print("\n" + "=" * 80)

print(msg)

print("=" * 80 + "\n", flush=True)




# ==============================================================================

# FEniCS SETTINGS

# ==============================================================================

parameters["ghost_mode"] = "shared_facet"

parameters["form_compiler"]["optimize"] = True

parameters["form_compiler"]["quadrature_degree"] = 2

set_log_level(LogLevel.PROGRESS)




# ==============================================================================

# USER PARAMETERS

# ==============================================================================

banner("USER PARAMETERS")




MU  = 0.04

RHO = 1.06




INLET_ID   = 1

WALL_ID    = 2

OUTLET_IDS = [3, 4]




DT = 1e-5

T_MAX = 1.0

MAX_STEPS = int(np.ceil(T_MAX / DT))




INLET_U_TARGET = 57.0

N_RAMP = 1000




OUT_DIR = "results_bdf2_rotational_ipcs"

os.makedirs(OUT_DIR, exist_ok=True)




U_XDMF = os.path.join(OUT_DIR, "velocity.xdmf")

P_XDMF = os.path.join(OUT_DIR, "pressure.xdmf")




log(f"MU={MU}, RHO={RHO}")

log(f"DT={DT}, T_MAX={T_MAX}, MAX_STEPS={MAX_STEPS}")

log(f"INLET_U_TARGET={INLET_U_TARGET}, N_RAMP={N_RAMP}")

log(f"OUT_DIR={OUT_DIR}")




# ==============================================================================

# LOAD MESH + FACET TAGS

# ==============================================================================

banner("LOADING MESH")




mesh = Mesh("Mesh_1k/Volume_mesh.xml")

facets = MeshFunction("size_t", mesh, "Mesh_1k/BCnodeFacets.xml")




if rank == 0:

log(f"[MESH] Cells    : {mesh.num_cells()}")

log(f"[MESH] Vertices : {mesh.num_vertices()}")

log(f"[MESH] Dim      : {mesh.geometry().dim()}")

log(f"[FACETS] Unique IDs: {np.unique(facets.array())}")




# ==============================================================================

# FUNCTION SPACES

# ==============================================================================

banner("FUNCTION SPACES")




V = VectorFunctionSpace(mesh, "CG", 1)

Q = FunctionSpace(mesh, "CG", 1)




u = TrialFunction(V)

v = TestFunction(V)




p = TrialFunction(Q)

q = TestFunction(Q)




# velocity states

u_nm1 = Function(V, name="u_nm1")  # u^{n-1}

u_n   = Function(V, name="u_n")    # u^{n}

u_s   = Function(V, name="u_star") # tentative

u_new = Function(V, name="u")      # u^{n+1}




# pressure states

p_    = Function(Q, name="p_n")    # p^{n}

p_new = Function(Q, name="p")      # p^{n+1}

phi_  = Function(Q, name="phi")    # pressure correction




# convecting velocity for step 1

u_conv = Function(V, name="u_conv")




log(f"[DoF] V.dim = {V.dim()} | Q.dim = {Q.dim()}")




# ==============================================================================

# INLET PROFILE (your PCA normal version from earlier, kept compatible)

# ==============================================================================

banner("INLET PROFILE")




# If you want your PCA-normal parabolic inlet, keep it.

# Here we reuse your earlier idea: infer inlet patch vertices and compute PCA normal + radius.




inlet_facet_ids = np.where(facets.array() == INLET_ID)[0]

if inlet_facet_ids.size == 0:

raise RuntimeError("No inlet facets found. Check INLET_ID and facet tags.")




vertex_ids = []

for fid in inlet_facet_ids:

vertex_ids.extend(Facet(mesh, int(fid)).entities(0))

vertex_ids = np.unique(np.array(vertex_ids, dtype=np.int64))




coords = mesh.coordinates()[vertex_ids]

inlet_center = coords.mean(axis=0)




X = coords - inlet_center

Ccov = np.dot(X.T, X) / max(X.shape[0], 1)

eigvals, eigvecs = np.linalg.eigh(Ccov)

normal = eigvecs[:, np.argmin(eigvals)]

normal = normal / np.linalg.norm(normal)




domain_center = mesh.coordinates().mean(axis=0)

if np.dot(normal, domain_center - inlet_center) < 0:

normal *= -1




radius = float(np.max(np.linalg.norm(coords - inlet_center, axis=1)))




if rank == 0:

log(f"[INLET] center = {inlet_center}")

log(f"[INLET] radius = {radius}")

log(f"[INLET] normal = {normal}")




class ParabolicInlet(UserExpression):

def __init__(self, center, radius, normal, Umax, **kwargs):

self.c = np.array(center, dtype=np.float64)

self.R = float(radius)

self.n = np.array(normal, dtype=np.float64)

self.Umax = float(Umax)

super().__init__(**kwargs)




def value_shape(self):

return (mesh.geometry().dim(),)




def eval(self, values, x):

d = np.array(x, dtype=np.float64) - self.c

rvec = d - np.dot(d, self.n) * self.n

r = np.linalg.norm(rvec)

if r <= self.R:

values[:] = self.Umax * (1.0 - (r / self.R) ** 2) * self.n

else:

values[:] = 0.0




inlet_expr = ParabolicInlet(

center=inlet_center,

radius=radius,

normal=normal,

Umax=0.0,

degree=2

)




# ==============================================================================

# BOUNDARY CONDITIONS

# ==============================================================================

banner("BOUNDARY CONDITIONS")




bcu = [

    DirichletBC(V, inlet_expr, facets, INLET_ID),

    DirichletBC(V, Constant((0.0, 0.0, 0.0)), facets, WALL_ID),

]




# Pressure Dirichlet on outlets (same style as your earlier scripts)press

bcp = [DirichletBC(Q, Constant(0.0), facets, i) for i in OUTLET_IDS]




log(f"[BC] inlet={INLET_ID}, wall={WALL_ID}, outlets={OUTLET_IDS}")




# ==============================================================================

# FORMS (BDF2 + explicit convecting vel, rotational correction)

# ==============================================================================

banner("FORMS")




dt  = Constant(DT)

rho = Constant(RHO)

mu  = Constant(MU)




nrm = FacetNormal(mesh)




# Constant matrices / patterns

mass_v_form   = inner(u, v) * dx

stiff_v_form  = inner(grad(u), grad(v)) * dx

mass_s_form   = inner(p, q) * dx

poisson_form  = inner(grad(p), grad(q)) * dx

mass_rho_form = rho * inner(u, v) * dx




# Convection bilinear form with explicit convecting velocity u_conv:

#   (u_conv · ∇ u^{n+1}, v)

conv_form = inner(dot(u_conv, nabla_grad(u)), v) * dx




# Pressure term in step 1 uses p^n:

#   - (p^n, div(v)) dx + <p^n n, v> ds




ds = Measure("ds", domain=mesh, subdomain_data=facets)




press_rhs_form = (- p_ * div(v) * dx) + dot(p_ * nrm, v) * (ds(3) + ds(4))




# press_rhs_form = (- p_ * div(v) * dx) + dot(p_ * nrm, v) * ds




# Step 2 RHS:

#   (∇phi, ∇q) = -(rho * 3/(2dt)) (div u_s, q)

rhs_phi_form = -(rho * (3.0 / (2.0 * dt))) * div(u_s) * q * dx




# Velocity correction RHS:

#   rho (u^{n+1}, v) = rho (u_s, v) - (2dt/3) (∇phi, v)

velcorr_rhs_form = rho * inner(u_s, v) * dx - (2.0 * dt / 3.0) * inner(grad(phi_), v) * dx




# For rotational pressure update: project div(u_s) into Q:

# Solve: (d, q) = (div(u_s), q)

div_rhs_form = div(u_s) * q * dx




# ==============================================================================

# PREASSEMBLE CONSTANT MATRICES

# ==============================================================================

banner("ASSEMBLING CONSTANT MATRICES")




Mv = assemble(mass_v_form)        # vector mass

K  = assemble(stiff_v_form)       # vector stiffness

Ms = assemble(mass_s_form)        # scalar mass

A2 = assemble(poisson_form)       # Poisson matrix

A3 = assemble(mass_rho_form)      # rho * mass




# Apply pressure Dirichlet to Poisson matrix once

for bc in bcp:

bc.apply(A2)




# Assemble and lock

Mv.ident_zeros()

K.ident_zeros()

Ms.ident_zeros()

A2.ident_zeros()

A3.ident_zeros()




# Convection matrix placeholder (same sparsity as Mv typically for CG1 velocity)

C = PETScMatrix()

assemble(conv_form, tensor=C)  # allocate pattern once

C.zero()




# Tentative velocity matrix placeholder

A1 = PETScMatrix()

assemble(mass_v_form, tensor=A1)  # allocate once

A1.zero()




# Reusable vectors

b1 = Vector(); Mv.init_vector(b1, 0)

tmp = Vector(); Mv.init_vector(tmp, 0)

tmp2 = Vector(); Mv.init_vector(tmp2, 0)




b2 = Vector(); A2.init_vector(b2, 0)

b3 = Vector(); A3.init_vector(b3, 0)




bdiv = Vector(); Ms.init_vector(bdiv, 0)




# ==============================================================================

# SOLVERS

# ==============================================================================

banner("SOLVERS")




# Step 1: tentative velocity

solver_u = KrylovSolver("gmres", "ilu")

solver_u.parameters["relative_tolerance"] = 1e-8

solver_u.parameters["absolute_tolerance"] = 1e-10

solver_u.parameters["maximum_iterations"] = 1000

solver_u.parameters["monitor_convergence"] = False




# Step 2: pressure correction

solver_phi = KrylovSolver("cg", "hypre_amg")

solver_phi.parameters["relative_tolerance"] = 1e-8

solver_phi.parameters["absolute_tolerance"] = 1e-10

solver_phi.parameters["maximum_iterations"] = 2000

solver_phi.parameters["monitor_convergence"] = False




# Step 3: velocity correction (rho mass)

solver_corr = KrylovSolver("cg", "jacobi")

solver_corr.parameters["relative_tolerance"] = 1e-8

solver_corr.parameters["absolute_tolerance"] = 1e-10

solver_corr.parameters["maximum_iterations"] = 500

solver_corr.parameters["monitor_convergence"] = False




# Rotational projection: scalar mass solve for div(u_s)

solver_div = KrylovSolver("cg", "jacobi")

solver_div.parameters["relative_tolerance"] = 1e-8

solver_div.parameters["absolute_tolerance"] = 1e-10

solver_div.parameters["maximum_iterations"] = 500

solver_div.parameters["monitor_convergence"] = False




# ==============================================================================

# OUTPUT (SAVE EVERY STEP)

# ==============================================================================

banner("OUTPUT")




xdmf_u = XDMFFile(comm, U_XDMF)

xdmf_p = XDMFFile(comm, P_XDMF)




xdmf_u.parameters["flush_output"] = True

xdmf_u.parameters["rewrite_function_mesh"] = False

xdmf_p.parameters["flush_output"] = True

xdmf_p.parameters["rewrite_function_mesh"] = False




# ==============================================================================

# INITIALIZATION

# ==============================================================================

banner("INITIALIZATION")




u_nm1.vector().zero()

u_n.vector().zero()

p_.vector().zero()




t = 0.0

start_time = time.time()




# Write initial condition

u_new.assign(u_n)

p_new.assign(p_)

xdmf_u.write(u_new, t)

xdmf_p.write(p_new, t)




# ==============================================================================

# TIME LOOP (PRINT & SAVE EVERY STEP)

# ==============================================================================

banner("TIME LOOP (BDF2 + ROTATIONAL IPCS)")




for step in range(MAX_STEPS):




# Update time and inlet ramp

t = (step + 1) * DT

inlet_expr.Umax = INLET_U_TARGET * (step + 1) / N_RAMP if step < N_RAMP else INLET_U_TARGET




# Build convecting velocity:

# u_conv = 2 u^n - u^{n-1}  (BDF2 explicit)

u_conv.vector().zero()

u_conv.vector().axpy(2.0, u_n.vector())

u_conv.vector().axpy(-1.0, u_nm1.vector())




# Assemble convection matrix into preallocated tensor

C.zero()

    assemble(conv_form, tensor=C)




# -----------------------------

# STEP 1: Tentative velocity

#

# For step==0, do BDF1 startup:

#   (rho/dt)(u - u^n) + (u^n · ∇u) - div(mu ∇u) + ∇p^n = 0

# For step>=1, BDF2:

#   (rho/(2dt))(3u - 4u^n + u^{n-1}) + (u_conv · ∇u) - div(mu ∇u) + ∇p^n = 0

# -----------------------------

A1.zero()

b1.zero()




if step == 0:

# Matrix: (rho/dt) M + mu K + C

A1.axpy(float(RHO / DT), Mv, True)

A1.axpy(float(MU), K, True)

A1.axpy(1.0, C, True)




# RHS: (rho/dt) M u^n  + pressure RHS

Mv.mult(u_n.vector(), b1)

b1 *= float(RHO / DT)




else:

# Matrix: (3 rho / (2 dt)) M + mu K + C

A1.axpy(float(3.0 * RHO / (2.0 * DT)), Mv, True)

A1.axpy(float(MU), K, True)

A1.axpy(1.0, C, True)




# RHS: (rho/(2dt)) M (4 u^n - u^{n-1}) + pressure RHS

# tmp = 4 u^n - u^{n-1}

tmp2.zero()

tmp2.axpy(4.0, u_n.vector())

tmp2.axpy(-1.0, u_nm1.vector())




Mv.mult(tmp2, b1)

b1 *= float(RHO / (2.0 * DT))




# Add pressure RHS (assembled vector)

b_press = assemble(press_rhs_form)

b1.axpy(1.0, b_press)




# Apply velocity Dirichlet BCs

for bc in bcu:

bc.apply(A1, b1)




# Solve for tentative velocity

solver_u.solve(A1, u_s.vector(), b1)




# -----------------------------

# STEP 2: Pressure correction

#   A2 phi = -(rho * 3/(2dt)) div(u_s)

# -----------------------------

b2.zero()

    assemble(rhs_phi_form, tensor=b2)




# Apply pressure Dirichlet BCs to RHS

for bc in bcp:

bc.apply(b2)




solver_phi.solve(A2, phi_.vector(), b2)




# Pressure update: p = p + phi

p_new.vector().zero()

p_new.vector().axpy(1.0, p_.vector())

p_new.vector().axpy(1.0, phi_.vector())




# Rotational correction: p = p - mu * div(u_s)

# Compute d in Q such that (d, q) = (div(u_s), q)

bdiv.zero()

    assemble(div_rhs_form, tensor=bdiv)




d = Function(Q, name="div_u_s_proj")

for bc in bcp:

bc.apply(Ms, bdiv)  # safe: keeps pressure dirichlet consistent at outlets if you want

solver_div.solve(Ms, d.vector(), bdiv)




p_new.vector().axpy(-float(MU), d.vector())




# Enforce outlet pressure strongly (keeps consistent)

for bc in bcp:

bc.apply(p_new.vector())




# -----------------------------

# STEP 3: Velocity correction

#   A3 u = rho M u = rho M u_s - (2dt/3) (∇phi, v)

# -----------------------------

b3.zero()

    assemble(velcorr_rhs_form, tensor=b3)




# Apply velocity BCs to corrected velocity solve

for bc in bcu:

bc.apply(A3, b3)




solver_corr.solve(A3, u_new.vector(), b3)




n = FacetNormal(mesh)




ds_in = Measure("ds", domain=mesh, subdomain_data=facets, subdomain_id=INLET_ID)

ds_o3 = Measure("ds", domain=mesh, subdomain_data=facets, subdomain_id=3)

ds_o4 = Measure("ds", domain=mesh, subdomain_data=facets, subdomain_id=4)




Q_in = assemble(dot(u_new, n) * ds_in)

Q_o3 = assemble(dot(u_new, n) * ds_o3)

Q_o4 = assemble(dot(u_new, n) * ds_o4)




if rank == 0:

print(f"           flux: Qin={Q_in:+.6e} | Qo3={Q_o3:+.6e} | Qo4={Q_o4:+.6e} | sum={Q_in+Q_o3+Q_o4:+.3e}")




div_l2 = np.sqrt(assemble((div(u_new)**2) * dx))

if rank == 0:

print(f"           div_L2={div_l2:.3e}", flush=True)




# -----------------------------

# PRINT EVERY STEP

# -----------------------------

# relative change measure (against u_n)

du = u_new.vector().copy()

du.axpy(-1.0, u_n.vector())

rel = du.norm("l2") / max(u_new.vector().norm("l2"), 1e-30)




# max velocity magnitude (approx, using l_inf of vector dofs)

u_max = u_new.vector().norm("linf")




if rank == 0:

print(

f"[STEP {step:7d}] t={t:.6e} | Uin={inlet_expr.Umax:9.3f} | rel={rel:.3e} | u_max={u_max:.3e}",

flush=True

        )




# -----------------------------

# SAVE EVERY STEP

# -----------------------------

xdmf_u.write(u_new, t)

xdmf_p.write(p_new, t)




# Roll time levels: u^{n-1} <- u^n, u^n <- u^{n+1}, p^n <- p^{n+1}

u_nm1.assign(u_n)

u_n.assign(u_new)

p_.assign(p_new)




elapsed = time.time() - start_time

banner("DONE")

log(f"Total wall time = {elapsed:.2f} s")


and this is my mesh

Your code is not really properly formatted.
Furthermore, starting with a zero fluid flow (unm1=0), and without a ramping in time for the inlet:

you will get a pressure shock in the first time steps.
I would suggest that you ramp up the inflow gradually (see: Nonzero pressure Navier Stokes - #2 by dokken)