Unchanging residual for nonlinear fluid dynamics problem

Hello,

I am trying to run a multiphase flow problem in Dolfinx using the momentum, gas phase transport, and continuity equations. When solving the transient problem, the residual seems to change after the first iteration, but then does not change and ultimately does not converge.

Are there any steps I can take to fix the static residual problem? I changed the solver settings a few times, which changed the static residual from nan to a finite value, but still does not update.

Here is my code:

from mpi4py import MPI
from petsc4py import PETSc
import numpy as np
import ufl
import dolfinx
from dolfinx.fem import Constant, Function, functionspace, assemble_scalar, dirichletbc, form, locate_dofs_geometrical, locate_dofs_topological, set_bc
from dolfinx import default_scalar_type
from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, create_vector, set_bc
from dolfinx import mesh
from basix.ufl import element, mixed_element
from ufl import (FacetNormal, Identity, TestFunction, TrialFunction,
                 div, dot, ds, dx, inner, lhs, nabla_grad, rhs, sym)
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from dolfinx.io import XDMFFile
from dolfinx.mesh import GhostMode, locate_entities_boundary


# Define domain size
width, height = 0.5, 1.0
inlet_width = 0.2
nx, ny = 100, 100

# Create mesh
domain = mesh.create_rectangle(MPI.COMM_WORLD, [[0, 0], [width, height]], [nx, ny], mesh.CellType.triangle)

# Inlet: middle portion of bottom boundary
def inlet_boundary(x):
    return np.logical_and(
        np.isclose(x[1], 0.0),  # bottom edge
        np.logical_and(
            x[0] > (width / 2 - inlet_width),
            x[0] < (width / 2 + inlet_width)
        )
    )

# Walls: left and right edges
def wall_boundary(x):
    return np.logical_or(
        np.isclose(x[0], 0.0),  # left
        np.isclose(x[0], width)  # right
    )

# No-slip top boundary
def top_noslip_boundary(x):
    return np.isclose(x[1], height)  # top

with XDMFFile(domain.comm, "domain.xdmf", "w") as xdmf:
    xdmf.write_mesh(domain)

t = 0                                       # Initial time
final_t = 2                                 # Final time
dt = Constant(domain, 1/1000)
D_gc = 0.01  # Drift diffusion coeff
u_slip = Constant(domain, np.array((0.0, 1e-2), dtype=np.float64))
p_ref = 1e5
M_hydrogen = 2.02e-3  # kg/mol
T = 500
R = 8.31
rho_l = 2729.3 - 0.73 * T
mg_l = Constant(domain, 1e-5)
mu_l = Constant(domain, 4E-4 * np.exp(4170 / T))
g = Constant(domain,[0.0, -9.81])  # Example for 2D gravity vector

# Define the elements
V_ele = element("CG", domain.topology.cell_name(), 2, shape=(domain.geometry.dim, ))
Q_ele = element("CG", domain.basix_cell(), 1)  # Continuous Lagrange, degree 1

# Create a mixed function space
W = functionspace(domain, mixed_element([V_ele, Q_ele, Q_ele]))

upphi = Function(W)
upphi_n = Function(W)

vu, vp, vphi = ufl.TestFunctions(W)

u_l, p, phi_g = ufl.split(upphi)
u_l_n, p_n, phi_g_n = ufl.split(upphi_n)

phi_l = 1 - phi_g
phi_l_n = 1 - phi_g_n

# Definign sub spaces
V_sub, _ = W.sub(0).collapse()
Q1_sub, _ = W.sub(1).collapse()
Q2_sub, _ = W.sub(2).collapse()

# Inlet velocity conditions
inlet_facets = locate_entities_boundary(domain, 1, inlet_boundary)

# y-component = 1.0
u_inlety = Constant(domain, 1.0)
bcu_inlet1 = dirichletbc(u_inlety,
                         locate_dofs_topological(V_sub, 1, inlet_facets),
                         W.sub(0).sub(1))

# x-component = 0.0
u_inletx = Constant(domain, 0.0)
bcu_inlet2 = dirichletbc(u_inletx,
                         locate_dofs_topological(V_sub, 1, inlet_facets),
                         W.sub(0).sub(0))


# No-slip boundary condition for velocity
noslip = np.zeros(domain.geometry.dim, dtype=PETSc.ScalarType)
wall_facets = locate_entities_boundary(domain, 1, wall_boundary)

bc_wall = dirichletbc(noslip,
                      locate_dofs_topological(V_sub, 1, wall_facets),
                      V_sub)

top_facets = locate_entities_boundary(domain, 1, top_noslip_boundary)

bc_top = dirichletbc(noslip,
                     locate_dofs_topological(V_sub, 1, top_facets),
                     V_sub)

phi_val = Constant(domain, 0.1)

# Locate inlet dofs for phi_g
inlet_facets = locate_entities_boundary(domain, 1, inlet_boundary)
inlet_dofs_phi = locate_dofs_topological(Q2_sub, 1, inlet_facets)

# Apply DirichletBC
bc_phi_inlet = dirichletbc(phi_val, inlet_dofs_phi, Q2_sub)

# Combine all velocity BCs
bcs = [bcu_inlet1, bcu_inlet2, bc_wall, bc_top, bc_phi_inlet]

u_drift = -D_gc * ufl.grad(phi_g) / (phi_g + 1e-8)
u_g = u_l
rho_g = (p_ref + p) * M_hydrogen / (R * T)

continuity = (
    rho_l * (phi_l - phi_l_n) + rho_g * (phi_g - phi_g_n)
) / dt * vp * ufl.dx + ufl.inner(
    ufl.div(rho_l * phi_l * u_l + rho_g * phi_g * u_g), vp
) * ufl.dx

gas_phase_transport = (
    (rho_g * (phi_g - phi_g_n)) / dt * vphi * ufl.dx
    + ufl.div(rho_g * phi_g * u_l) * vphi * ufl.dx
    + ufl.inner(D_gc * rho_g * ufl.grad(phi_g), ufl.grad(vphi)) * ufl.dx
    + mg_l * vphi * ufl.dx
)

momentum = (
    phi_l * rho_l * ufl.inner((u_l - u_l_n) / dt, vu) * ufl.dx
    + phi_l * mu_l * ufl.inner(ufl.grad(u_l), ufl.grad(vu)) * ufl.dx
    + phi_l * rho_l * ufl.inner(ufl.grad(u_l), ufl.grad(vu)) * ufl.dx  # Fixed
    + p * ufl.div(vu) * ufl.dx
    - phi_l * rho_l * ufl.inner(g, vu) * ufl.dx
)

F = momentum + continuity + gas_phase_transport

problem = NonlinearProblem(F, upphi, bcs=bcs)
solver = NewtonSolver(MPI.COMM_WORLD, problem)
solver.convergence_criterion = "residual"
solver.rtol = 1e-8
solver.report = True

# PETSc solver options
ksp = solver.krylov_solver
opts = PETSc.Options()
option_prefix = ksp.getOptionsPrefix()
opts[f"{option_prefix}ksp_type"] = "gmres"
opts[f"{option_prefix}ksp_rtol"] = 1.0e-10
opts[f"{option_prefix}pc_type"] = "hypre"
opts[f"{option_prefix}pc_hypre_type"] = "boomeramg"
opts[f"{option_prefix}pc_hypre_boomeramg_max_iter"] = 1
opts[f"{option_prefix}pc_hypre_boomeramg_cycle_type"] = "v"
ksp.setFromOptions()
dolfinx.log.set_log_level(dolfinx.log.LogLevel.INFO)

import os
from dolfinx.io import VTXWriter

# Save directory
save_dir = "results/"
os.makedirs(save_dir, exist_ok=True)

# Initialize VTX writers 
vtx_u = VTXWriter(MPI.COMM_WORLD, f"{save_dir}u.bp", [Function(V_sub)])
vtx_p = VTXWriter(MPI.COMM_WORLD, f"{save_dir}p.bp", [Function(Q1_sub)])
vtx_phi = VTXWriter(MPI.COMM_WORLD, f"{save_dir}phi_g.bp", [Function(Q2_sub)])

vtx_u.write(t)
vtx_p.write(t)
vtx_phi.write(t)

# Time loop
while t < float(final_t):
    n, converged = solver.solve(upphi)
    assert converged

    u, p, phi_g = upphi.split()
    print("phi_g min/max:", phi_g.x.array.min(), phi_g.x.array.max())

    vtx_u.write(t)
    vtx_p.write(t)
    vtx_phi.write(t)

    print(f"Time = {t:.2f}, iterations = {n}, u[0] = {u.x.array[0]:.3e}")

    upphi_n.x.array[:] = upphi.x.array

    t += float(dt)

vtx_u.close()
vtx_p.close()
vtx_phi.close()

and the unchanging output until it reaches the max number of iterations:

[2025-04-24 14:19:30.102] [info] Newton iteration 0: r (abs) = 7142.13717233418 (tol = 1e-10), r (rel) = inf (tol = 1e-08)
[2025-04-24 14:19:30.331] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 14:19:31.013] [info] Newton iteration 1: r (abs) = 41517.80332674378 (tol = 1e-10), r (rel) = 614.353097327223 (tol = 1e-08)
[2025-04-24 14:19:31.234] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 14:19:31.445] [info] Newton iteration 2: r (abs) = 41517.80332674378 (tol = 1e-10), r (rel) = 614.353097327223 (tol = 1e-08)
[2025-04-24 14:19:31.663] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 14:19:31.865] [info] Newton iteration 3: r (abs) = 41517.80332674378 (tol = 1e-10), r (rel) = 614.353097327223 (tol = 1e-08)
[2025-04-24 14:19:32.081] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 14:19:32.283] [info] Newton iteration 4: r (abs) = 41517.80332674378 (tol = 1e-10), r (rel) = 614.353097327223 (tol = 1e-08)

What happens if you use a direct solver rather than gmres on the whole system? I’m expecting that the lack of convergence between Newton iterations that you’re seeing is actually due to a lack of convergence of your linear solver.

Hi Nate,

Thanks for your reply. I assume the direct solver is the default, so if I comment out:

opts[f"{option_prefix}ksp_type"] = "gmres"

I now get this:

[2025-04-24 16:22:50.604] [info] Newton iteration 0: r (abs) = 7142.13717233418 (tol = 1e-10), r (rel) = inf (tol = 1e-08)
[2025-04-24 16:22:50.843] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:51.206] [info] Newton iteration 1: r (abs) = 39183.7096065598 (tol = 1e-10), r (rel) = 581.4645299766764 (tol = 1e-08)
[2025-04-24 16:22:51.431] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:51.653] [info] Newton iteration 2: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:51.886] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:52.386] [info] Newton iteration 3: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:52.608] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:53.092] [info] Newton iteration 4: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:53.311] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:53.800] [info] Newton iteration 5: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:54.021] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:54.539] [info] Newton iteration 6: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:54.901] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:55.561] [info] Newton iteration 7: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:55.887] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:56.373] [info] Newton iteration 8: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)
[2025-04-24 16:22:56.595] [info] PETSc Krylov solver starting to solve system.
[2025-04-24 16:22:57.075] [info] Newton iteration 9: r (abs) = -nan (tol = 1e-10), r (rel) = -nan (tol = 1e-08)

It looks like it updates the 1st iteration residual, and then stays at -nan. Any suggestions where to go from here?

A direct solver typically is not the default. Try a ksp_type of preonly and pc_type of lu. See also PCFactorSetMatSolverType — PETSc 3.23.0 documentation to select a package/type which can support your null block.

Hello,
I tried changing ksp_type to preonly, and then tried changing pc_type to lu and cholesky - both jumped to -nan on the second iteration and fails to converge.

Not sure if this is relevant, but I have a similar working model in FEniCS. Is there any way to use the same solver settings here in my Dolfinx code? If not, are there any other suggested steps?

Fenics code:

import fenics as f
import numpy as np
import matplotlib.pyplot as plt

width = 0.5
height = 1
mesh = f.RectangleMesh(f.Point(0, 0), f.Point(width, height), 100, 100)
# plt.figure()
# plt.plot(mesh)

V_ele = f.VectorElement("CG", mesh.ufl_cell(), 2)
Q_ele = f.FiniteElement("CG", mesh.ufl_cell(), 1)
W = f.FunctionSpace(mesh, f.MixedElement([V_ele, Q_ele, Q_ele]))

upphi = f.Function(W)
upphi_n = f.Function(W)

vu, vp, vphi = f.TestFunctions(W)

u_l, p, phi_g = f.split(upphi)
u_l_n, p_n, phi_g_n = f.split(upphi_n)

phi_l = 1 - phi_g
phi_l_n = 1 - phi_g_n

D_gc = 0.01  # drift diffusion coeff

u_slip = f.Constant((0.0, 1e-2))
u_drift = -D_gc * f.grad(phi_g) / (phi_g + 1e-16)
u_g = u_l + u_drift + u_slip

p_ref = 1e5 # atmosphere
density_pressure = p_ref + p
M_hydogen = 2.02e-3 # kg/mol
T = 500 # room temp
R = 8.31 # J/molK
rho_l = 2729.3 - 0.73*T
rho_g = density_pressure*M_hydogen / (R*T)  # TODO see COMSOL guide for actual expression
mg_l = f.Constant(1e-5)  # m/s
mu_l = 4E-4 *np.exp(4170/T)  # dynamic visosity

g = f.Constant((0.0, -9.81))
F = f.Constant((0.0, 0.0))

dt = f.Constant(1)

continuity = (
    rho_l * (phi_l - phi_l_n) + rho_g * (phi_g - phi_g_n)
) / dt * vp * f.dx + f.inner(
    f.div(rho_l * phi_l * u_l + rho_g * phi_g * u_g), vp
) * f.dx

gas_phase_transport = (
    (rho_g * (phi_g - phi_g_n)) / dt * vphi * f.dx
    + f.div(rho_g * phi_g * u_g) * vphi * f.dx  # Use u_g instead of u_l + u_slip
    + f.inner(D_gc * rho_g * f.grad(phi_g), f.grad(vphi)) * f.dx
    + mg_l * vphi * f.dx
)


momentum = (
    phi_l * rho_l * f.inner((u_l - u_l_n) / dt, vu) * f.dx
    + phi_l * mu_l * f.inner(f.grad(u_l), f.grad(vu)) * f.dx
    + phi_l * rho_l * f.dot(f.dot(f.grad(u_l), u_l), vu) * f.dx
    + p * f.div(vu) * f.dx
    - phi_l * rho_l * f.inner(g, vu) * f.dx
    - f.inner(F, vu) * f.dx
)

F = momentum + continuity + gas_phase_transport

# Construct facet markers
bndry = f.MeshFunction("size_t", mesh, mesh.topology().dim() - 1)
for facet in f.facets(mesh):
    mp = facet.midpoint()
    if f.near(mp[1], 0.0):  # bottom wall
        bndry[facet] = 1
    if f.near(mp[1], 0.0) and width * 0.46 < mp[0] < width * 0.48:  # bottom inlet
        bndry[facet] = 2
    if f.near(mp[1], 0.0) and width * 0.52 < mp[0] < width * 0.54:  # bottom inlet
        bndry[facet] = 2
    elif f.near(mp[1], height):  # top
        bndry[facet] = 3
    elif f.near(mp[0], 0.0) or f.near(mp[0], width):  # walls
        bndry[facet] = 1


f.XDMFFile("facets.xdmf").write(bndry)


# boundary conditions

non_slip = f.DirichletBC(W.sub(0), f.Constant((0.0, 0.0)), bndry, 1)
# pressure_top = f.DirichletBC(W.sub(1), f.Constant(1), bndry, 3)
n = f.FacetNormal(mesh)
slip_bc = f.DirichletBC(W.sub(0).sub(1), f.Constant(0.0), bndry, 3)  # Enforces no vertical velocity at top

inlet_gas = f.DirichletBC(W.sub(2), f.Constant(0.1), bndry, 2)
# bcs = [non_slip, pressure_top, inlet_gas]
bcs = [non_slip, slip_bc, inlet_gas]

file_velocity = f.XDMFFile("velocity.xdmf")
file_pressure = f.XDMFFile("pressure.xdmf")
flie_phi = f.XDMFFile("gas_fraction.xdmf")

t = 0
while t < 60:
    f.solve(F==0, upphi, bcs)
    u, p, phi_g = upphi.split()
    file_velocity.write(u, t)
    file_pressure.write(p, t)
    flie_phi.write(phi_g, t)
    upphi_n.assign(upphi)
    print(f"Time = {t}")
    t += float(dt)

Cholesky factorisation expects an SPD matrix. I wouldn’t expect that to work for your system.

It may help to carefully go through your script, simplifying it step-by-step until you can produce a meaningful solution. Then build it back up making sure you can produce the expected result.

Okay, I will try that. And just to verify I am updating my variables correctly, am I missing any key steps in the time loop?

# Initialize VTX writers 
vtx_u = VTXWriter(MPI.COMM_WORLD, f"{save_dir}u.bp", [Function(V_sub)])
vtx_p = VTXWriter(MPI.COMM_WORLD, f"{save_dir}p.bp", [Function(Q1_sub)])
vtx_phi = VTXWriter(MPI.COMM_WORLD, f"{save_dir}phi_g.bp", [Function(Q2_sub)])

vtx_u.write(t)
vtx_p.write(t)
vtx_phi.write(t)


# Time loop
while t < float(final_t):
    n, converged = solver.solve(upphi)
    assert converged

    u, p, phi_g = upphi.split()

    vtx_u.write(t)
    vtx_p.write(t)
    vtx_phi.write(t)

    upphi_n.x.array[:] = upphi.x.array

    t += float(dt)

vtx_u.close()
vtx_p.close()
vtx_phi.close()