Steady-State Navier Stokes (2D) Multi-Region Flow Issue

Hello! I’m new to DOLFINx and FEniCS, so I hope this isn’t an overly simple question.

I’m trying to write FEniCS code to solve a Conjugate Heat Transfer problem on a 2D domain composed of air, a PCB, and components.

Since I’m dealing with forced convection, I’m trying to decouple the energy equation, which I plan to solve after the fluid problem. However, I’m currently unable to solve the steady-state Navier-Stokes problem.

I’ve written the following code (see below), but when I open the results in ParaView, I see solutions where entire fields are zero.

The console output says it solved successfully:

Info    : Reading 'results/circuit_mesh_2d.msh'...
Info    : 68 entities
Info    : 13869 nodes
Info    : 14502 elements
Info    : Done reading 'results/circuit_mesh_2d.msh'
solved successfully
p wrote successfully
u wrote successfully

This is my full code:

import os
from pathlib import Path
from typing import Final, List

import dolfinx
import numpy as np
import ufl
from basix.ufl import element, mixed_element
from dolfinx import fem, io
from dolfinx.fem import Constant, Function, dirichletbc, locate_dofs_topological
from dolfinx.fem.petsc import LinearProblem
from mpi4py import MPI
from petsc4py import PETSc

COMM: Final[MPI.Intracomm] = MPI.COMM_WORLD

FLUID_TAG: Final[int] = 1
PCB_TAG: Final[int] = 2
COMPONENT_BASE_TAG: Final[int] = 3
COMPONENT_TAGS: Final[List[int]] = [3, 4, 5, 6, 7]

INLET_TAG: Final[int] = 11
OUTLET_TAG: Final[int] = 12
WALLS_TAG: Final[int] = 13
SOLID_TAG: Final[int] = 14  # Fluid-solid interface (no-slip)
NU: Final[float] = 1.48e-5  # Kinematic viscosity [m²/s]
VEL_INLET: Final[float] = 0.85  # Inlet velocity (x-direction) [m/s]
MESH_FILE: Final[str] = "circuit_mesh_2d.msh"
OUTPUT_FOLDER: Final[str] = "results"
k = 1  # Polynomial degree


def log(msg: str) -> None:
    """
    Print message only on MPI rank 0.

    Args:
        msg: Message to print.
    """
    if COMM.rank == 0:
        print(msg, flush=True)


def inlet_velocity(x: np.ndarray) -> np.ndarray:
    """
    Defines a constant velocity profile at an inlet boundary (x-direction only).

    Returns a velocity array of shape (num_components, num_points):
    [VEL_INLET, 0.0, ...]
    """
    ux = np.full_like(x[0], VEL_INLET)
    return np.array([ux, np.full_like(x[0], 0.0)])


def define_bcs(msh, ft, ct, W):
    msh.topology.create_connectivity(msh.topology.dim - 1, msh.topology.dim)
    wall_facets = ft.find(WALLS_TAG)
    inlet_facets = ft.find(INLET_TAG)
    outlet_facets = ft.find(OUTLET_TAG)
    solid_facets = ft.find(SOLID_TAG)
    combined_indices = np.concatenate(
        [ct.find(tag) for tag in COMPONENT_TAGS + [PCB_TAG]]
    )
    # DOFS
    dofs_wall = locate_dofs_topological(W.sub(0), msh.topology.dim - 1, wall_facets)
    dofs_inlet = locate_dofs_topological(W.sub(0), msh.topology.dim - 1, inlet_facets)
    dofs_solid = locate_dofs_topological(W.sub(0), msh.topology.dim - 1, solid_facets)
    dofs_solid_cell = locate_dofs_topological(
        W.sub(0), msh.topology.dim, combined_indices
    )
    dofs_outlet = locate_dofs_topological(W.sub(1), msh.topology.dim - 1, outlet_facets)
    # BC
    zero_vector_func = Function(W.sub(0).collapse()[0])
    zero_vector_func.x.array[:] = 0.0
    bc_noslip = dirichletbc(zero_vector_func, dofs_wall)
    bc_solid_u = dirichletbc(zero_vector_func, dofs_solid)
    u_inlet_func = Function(W.sub(0).collapse()[0])
    u_inlet_func.interpolate(inlet_velocity)
    bc_inlet_x = dirichletbc(u_inlet_func, dofs_inlet)
    bc_outlet_p = dirichletbc(
        Constant(msh, PETSc.ScalarType(0.0)), dofs_outlet, W.sub(1)
    )
    bc_solid_cell_u = dirichletbc(zero_vector_func, dofs_solid_cell)

    bcs = [bc_noslip, bc_inlet_x, bc_solid_u]  # , bc_solid_cell_u]
    return bcs, [bc_outlet_p]


folder = Path(OUTPUT_FOLDER)
folder.mkdir(exist_ok=True, parents=True)

mesh_file = os.path.join(folder, MESH_FILE)

msh = io.gmsh.read_from_msh(mesh_file, COMM, gdim=2)
msh, ct, ft = msh.mesh, msh.cell_tags, msh.facet_tags

# Constants and function spaces for the Stokes/Navier-Stokes problem
nu_const = fem.Constant(msh, dolfinx.default_scalar_type(NU))
gdim = msh.geometry.dim
h = ufl.CellDiameter(msh)
n = ufl.FacetNormal(msh)

# Function spaces for the velocity and for the pressure
v_cg2 = element("Lagrange", msh.basix_cell(), 2, shape=(msh.geometry.dim,))
s_cg1 = element("Lagrange", msh.basix_cell(), 1)
V = fem.functionspace(msh, v_cg2)
Q = fem.functionspace(msh, s_cg1)
w_mixed_element = mixed_element([v_cg2, s_cg1])
W = fem.functionspace(msh, w_mixed_element)

# BC
bcs = define_bcs(msh, ft, ct, W)
all_bcs = bcs[0] + bcs[1]

w = Function(W)

(u, p) = w.split()
(v, q) = ufl.TestFunctions(W)

F = (
    nu_const * ufl.inner(ufl.grad(u), ufl.grad(v))  # Viscous term
    - ufl.div(v) * p  # Pressure term (divergence of test function)
    + q * ufl.div(u)  # Continuity term
    + ufl.inner(ufl.grad(u) * u, v)  # Advection term (Non-linear)
) * ufl.dx()

petsc_options = {
    "ksp_type": "preonly",
    "pc_type": "lu",
    "pc_factor_mat_solver_type": "mumps",
    "snes_linesearch_type": "bt",
}
pde = fem.petsc.NonlinearProblem(
    F, w, bcs=all_bcs, petsc_options_prefix="nls", petsc_options=petsc_options
)
w_o = pde.solve()
assert pde.solver.getConvergedReason() > 0

print("solved successfully")

p_file = io.VTKFile(msh.comm, folder / "p.pvd", "w")
# p_file.write_mesh(msh)
p_collapsed_func = w_o.sub(1).collapse()
p_file.write_function(p_collapsed_func, 0.0)
p_file.close()

print("p wrote successfully")

u_file = io.VTKFile(msh.comm, folder / "u.pvd", "w")
# u_file.write_mesh(msh)
u_collapsed_func = w_o.sub(0).collapse()
element_v1 = element("Lagrange", msh.basix_cell(), 1, shape=(msh.geometry.dim,))
V1 = fem.functionspace(msh, element_v1)
u_save = fem.Function(V1)
u_save.interpolate(u_collapsed_func)
u_file.write_function(u_save, 0.0)
u_file.close()

print("u wrote successfully")

I’m not immediately seeing anything wrong. You’re code is not runnable though, as it requires a mesh file. If you could replace the mesh with a basic square mesh then it’d be easier to help.

The nonlinear solver should also produce output. The SNES solver should report the residuals of the steps it takes. Do you have that output? That should also be quite telling about what goes wrong.

Unrelated, and more technically, your line about bc_outlet_p is not formally correct. One should essentially never prescribe the pressure field. Just removing that line should also work. On your outflow boundary, you then have a natural condition that says that the flow traction is zero. For all intents and purpuses that is the same as saying p=0 except that it is mathematically sound.

1 Like

Thank you very much for your help! I didn’t know that the pressure condition was superfluous and have now removed it.

I’ll reply in two parts.

Boxed Mesh

I cannot determine if I have introduced a new error or if I am seeing symptoms of the same mistakes related to the full mesh. The code for the rectangular mesh now yields the following error when starting the solve():

start solving

free(): invalid next size (normal)
[localhost:73824] *** Process received signal ***

from this code:

from pathlib import Path
from typing import Final, List

import dolfinx.fem.petsc
import numpy as np
import ufl
from basix.ufl import element, mixed_element
from dolfinx import fem, io
from dolfinx.fem import Function, dirichletbc, locate_dofs_topological
from dolfinx.fem.petsc import LinearProblem
from mpi4py import MPI

COMM: Final[MPI.Intracomm] = MPI.COMM_WORLD

FLUID_TAG: Final[int] = 1
PCB_TAG: Final[int] = 2
COMPONENT_BASE_TAG: Final[int] = 3
COMPONENT_TAGS: Final[List[int]] = [3, 4, 5, 6, 7]

INLET_TAG: Final[int] = 11
OUTLET_TAG: Final[int] = 12
WALLS_TAG: Final[int] = 13
SOLID_TAG: Final[int] = 14  # Fluid-solid interface (no-slip)
NU: Final[float] = 1.48e-5  # Kinematic viscosity [m²/s]
VEL_INLET: Final[float] = 0.85  # Inlet velocity (x-direction) [m/s]
MESH_FILE: Final[str] = "circuit_mesh_2d.msh"
OUTPUT_FOLDER: Final[str] = "results"
k = 1  # Polynomial degree


###################
# Mesh Generation #
###################
L = 10.0  # Length of the domain
H = 1.0   # Height of the domain
nx = 100   # Number of elements in x-direction
ny = 10   # Number of elements in y-direction

msh = dolfinx.mesh.create_rectangle(
    comm=COMM,
    points=[(0.0, 0.0), (L, H)],
    n=[nx, ny],
    cell_type=dolfinx.mesh.CellType.triangle,
    ghost_mode=dolfinx.mesh.GhostMode.shared_facet,
)

# Tags
def mark_inlet(x):
    """Marks the left boundary (x=0) as the inlet."""
    return np.isclose(x[0], 0.0)

def mark_outlet(x):
    """Marks the right boundary (x=L) as the outlet."""
    return np.isclose(x[0], L)

def mark_walls(x):
    """Marks the top (y=H) and bottom (y=0) boundaries as walls (no-slip)."""
    # Check if x[1] is close to 0.0 OR H
    return np.logical_or(np.isclose(x[1], 0.0), np.isclose(x[1], H))

facet_dim = msh.topology.dim - 1
ft_indices, ft_markers = [], []
for marker, func in [(INLET_TAG, mark_inlet), (OUTLET_TAG, mark_outlet), (WALLS_TAG, mark_walls)]:
    facets = dolfinx.mesh.locate_entities_boundary(msh, facet_dim, func)
    ft_indices.extend(facets)
    ft_markers.extend(np.full_like(facets, marker))

sorted_indices = np.argsort(ft_indices)
ft = dolfinx.mesh.meshtags(
    msh=msh,
    dim=facet_dim,
    entities=np.asarray(ft_indices, np.int32)[sorted_indices],
    values=np.asarray(ft_markers, np.int32)[sorted_indices]
)

###############
# Formulation #
###############

gdim = msh.geometry.dim

# Function spaces for the velocity and for the pressure
v_cg2 = element("Lagrange", msh.basix_cell(), 2, shape=(msh.geometry.dim,))
s_cg1 = element("Lagrange", msh.basix_cell(), 1)
V = fem.functionspace(msh, v_cg2)
Q = fem.functionspace(msh, s_cg1)
w_mixed_element = mixed_element([v_cg2, s_cg1])
W = fem.functionspace(msh, w_mixed_element)
w = Function(W)

(u, p) = w.split()
(v, q) = ufl.TestFunctions(W)

nu_const = fem.Constant(msh, dolfinx.default_scalar_type(NU))
F = (
            nu_const * ufl.inner(ufl.grad(u), ufl.grad(v))  # Viscous term
            - ufl.div(v) * p                                # Pressure term (divergence of test function)
            + q * ufl.div(u)                                # Continuity term
            + ufl.inner(ufl.grad(u) * u, v)                 # Advection term (Non-linear)
    ) * ufl.dx()

############
###  BC  ###
############
def inlet_velocity(x: np.ndarray) -> np.ndarray:
    ux = np.full_like(x[0], VEL_INLET)
    return np.array([ux, np.full_like(x[0], 0.0)])

def define_bcs(msh, ft, W):
    V, map_v = W.sub(0).collapse()

    msh.topology.create_connectivity(msh.topology.dim - 1, msh.topology.dim)
    wall_facets = ft.find(WALLS_TAG)
    inlet_facets = ft.find(INLET_TAG)

    dofs_wall = locate_dofs_topological(W.sub(0), msh.topology.dim - 1, wall_facets)
    dofs_inlet = locate_dofs_topological(W.sub(0), msh.topology.dim - 1, inlet_facets)

    zero_vector_func = Function(V)
    zero_vector_func.x.array[:] = 0.0

    bc_noslip = dirichletbc(zero_vector_func, dofs_wall)
    u_inlet_func = Function(V)
    u_inlet_func.interpolate(inlet_velocity)
    bc_inlet_x = dirichletbc(u_inlet_func, dofs_inlet)
    bcs_u = [bc_noslip, bc_inlet_x]
    return bcs_u

bcs = define_bcs(msh, ft, W)


petsc_options = {
    "ksp_type": "preonly",
    "pc_type": "lu",
    "pc_factor_mat_solver_type": "mumps",
    "snes_linesearch_type": "bt",
    # 1. Residual Monitoring (Prints residual norm at each iteration)
    "snes_monitor": None,  # Non-linear solver (SNES) residual
    "ksp_monitor": None,  # Linear solver (KSP) residual
    # 2. Convergence/Termination Reason (Highly useful for debugging)
    "snes_converged_reason": None,  # Print SNES termination reason
    "ksp_converged_reason": None,  # Print KSP termination reason
    # 4. Added for NaN/Initial Guess Debugging (Prints solver configurations)
    "snes_view": None,  # Print the full SNES setup (type, tolerance, etc.)
    "ksp_view": None,  # Print the full KSP setup (type, preconditioner, etc.)
}
pde = fem.petsc.NonlinearProblem(
    F, w, bcs=bcs, petsc_options_prefix="nls", petsc_options=petsc_options
)
print("start solving\n")
w_o = pde.solve()
assert pde.solver.getConvergedReason() > 0

print("solved successfully")

Code related to the full mesh with more detailed log

I applied the same petsc_options of the code above to get more detailed information from the iterative solver for the full mesh case:

  0 SNES Function norm 0.000000000000e+00
  Nonlinear nls solve converged due to CONVERGED_FNORM_ABS iterations 0
SNES Object: (nls) 1 MPI process
  type: newtonls
  maximum iterations=50, maximum function evaluations=10000
  tolerances: relative=1e-08, absolute=1e-50, solution=1e-08
  total number of linear solver iterations=0
  total number of function evaluations=1
  norm schedule ALWAYS
  SNESLineSearch Object: (nls) 1 MPI process
    type: bt
      interpolation: cubic
      alpha=1.000000e-04
    maxlambda=1.000000e+00, minlambda=1.000000e-12
    tolerances: relative=1.000000e-08, absolute=1.000000e-15, lambda=1.000000e-08
    maximum iterations=40
  KSP Object: (nls) 1 MPI process
    type: preonly
    maximum iterations=10000, initial guess is zero
    tolerances: relative=1e-05, absolute=1e-50, divergence=10000.
    left preconditioning
    using DEFAULT norm type for convergence test
  PC Object: (nls) 1 MPI process
    type: lu
    PC has not been set up so information may be incomplete
      out-of-place factorization
      tolerance for zero pivot 2.22045e-14
    linear system matrix = precond matrix:
    Matrix has not been assembled yet
solved successfully
p wrote successfully
u wrote successfully

Process finished with exit code 139 (interrupted by signal 11:SIGSEGV)

Firstly, there is a small bug in your imposition of the Dirichlet BCs. For the mixed element, you have to pass FEniCS a little more information on the interrelation between W and V:

    dofs_wall = locate_dofs_topological((W.sub(0),V), msh.topology.dim - 1, wall_facets)
    dofs_inlet = locate_dofs_topological((W.sub(0),V), msh.topology.dim - 1, inlet_facets)

    bc_noslip = dirichletbc(zero_vector_func, dofs_wall, V)
    bc_inlet_x = dirichletbc(u_inlet_func, dofs_inlet, V)

Note the use of (W.sub(0),V) as the first argument to locate_dofs_topological and V as the last argument to dirichletbc (also see Stokes equations using Taylor-Hood elements — DOLFINx 0.10.0.0 documentation)

Now I am getting as output

0 SNES Function norm 3.895189340712e+00
    Residual norms for nls solve.
    0 KSP Residual norm 3.895189340712e+00
    1 KSP Residual norm 3.895189340712e+00
    Linear nls solve did not converge due to DIVERGED_PC_FAILED iterations 0
                   PC failed due to FACTOR_OTHER

Saying the mumps linear solver doesn’t manage to solve the system.

In part that appears to be due to the line (u, p) = w.split(), which should be (u, p) = ufl.split(w) ( I always have to check myself: Cahn-Hilliard equation — DOLFINx 0.10.0.0 documentation )

It it still cannot solve the system… Which I can’t immediately put my finger on. I’ll try to take a look later in the day. (I think your diffusivity is too low, which requires stabilization, but increasing it doesn’t improve the situation either).

I am extremely grateful for the help!

Your fixes are improving a lot the behavior of the code. The biggest issue was problem was really the splitting (from (u, p) = w.split() to (u, p) = ufl.split(w)).

Despite these fixes, as you are experiencing, the problem seems unsolvable. I have tested the code under extremely low inlet velocity and high viscosity. Even under these parameters, the solver consistently fails to converge. I would appreciate any further input because I’m completely lost :sweat_smile: