Hi everyone,
I implemented a FEniCS code for Boussinesq flow using mixed elements ([P2, P1, P1]), and it worked well. However, after transitioning to FEniCSx, the nonlinear Newton solver fails to converge, with the residual going to -nan
.
The problem is the differentially heated square cavity for natural convection. The boundary conditions are:
Velocity: No-slip at all walls
Temperature: High temperature on the left wall, lower temperature on the right wall
Pressure: A constraint at the bottom right corner for better convergence
I have carefully verified my weak forms, which worked correctly in FEniCS. I suspect the issue may be related to how boundary conditions are applied in FEniCSx.
Below is my FEniCSx implementation:
import numpy as np
from mpi4py import MPI
from petsc4py import PETSc
from dolfinx import mesh, fem, plot
from dolfinx.plot import vtk_mesh
from dolfinx.io import XDMFFile
from dolfinx.mesh import create_unit_square, CellType, locate_entities_boundary
import basix
from basix.ufl import element, mixed_element
from ufl import split, grad, inner, dx, div, sym, dot, Constant, TestFunctions, TrialFunctions, as_vector, derivative
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from dolfinx.log import set_log_level, LogLevel
import pyvista as pv
import pyvistaqt as pvqt
from pyvista.utilities import xvfb
set_log_level(LogLevel.INFO) # Enables logging to monitor solver progress
def plot_vector_field(field, function_space, mesh, field_name, output_filename, scale_factor=0.2):
pv.OFF_SCREEN = True
pv.start_xvfb()
field_collapsed = field.collapse()
topology, cell_types, geometry = plot.vtk_mesh(function_space)
values = np.zeros((geometry.shape[0], 3), dtype=np.float64)
values[:, :len(field_collapsed)] = field_collapsed.x.array.real.reshape((geometry.shape[0], len(field_collapsed)))
function_grid = pv.UnstructuredGrid(topology, cell_types, geometry)
function_grid[field_name] = values
glyphs = function_grid.glyph(orient=field_name, factor=scale_factor)
mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim)
grid = pv.UnstructuredGrid(*plot.vtk_mesh(mesh, mesh.topology.dim))
plotter = pv.Plotter(off_screen=True)
plotter.add_mesh(grid, style="wireframe", color="k")
plotter.add_mesh(glyphs, show_scalar_bar=True)
plotter.view_xy()
plotter.screenshot(output_filename)
print(f"{field_name} visualization saved as {output_filename}")
def plot_scalar_field(field, function_space, field_name, output_filename):
pv.OFF_SCREEN = True
pv.start_xvfb()
u_n = field.collapse()
grid = pv.UnstructuredGrid(*plot.vtk_mesh(function_space))
grid.point_data[field_name] = u_n.x.array.real
grid.set_active_scalars(field_name)
plotter = pv.Plotter(off_screen=True)
plotter.add_mesh(grid, show_edges=True, show_scalar_bar=True)
plotter.view_xy()
plotter.screenshot(output_filename)
print(f"{field_name} field visualization saved as {output_filename}")
# Create a mesh
nx, ny = 32, 32
domain = create_unit_square(MPI.COMM_WORLD, nx, ny)
# Define vector and scalar elements using basix.element
P2 = element("P", domain.basix_cell(), degree=2, shape=(domain.geometry.dim,)) # Velocity element
P1 = element("P", domain.basix_cell(), degree=1) # Pressure and temperature elements
# Create a mixed function space
W = fem.functionspace(domain, mixed_element([P2, P1, P1]))
# Define trial and test functions
w = fem.Function(W)
w0 = fem.Function(W) # Previous solution
(v, p, T) = split(w) # Trial functions
(v_test, p_test, T_test) = TestFunctions(W) # Test functions
w0.x.array[:] = 0.0
# Collapse the subspaces
V_vel, _ = W.sub(0).collapse()
T_space, _ = W.sub(2).collapse()
p_space, _ = W.sub(1).collapse()
# Define initialization functions
def T_init(x):
values = 1.0 - x[0] # Linearly decreasing with respect to x-axis
return values.reshape(1, -1)
def V_init(x):
return np.zeros((domain.geometry.dim, x.shape[1]))
def P_init(x):
return np.zeros((1, x.shape[1]))
# Assign the interpolated values back to the mixed function space
w0.sub(0).interpolate(V_init)
w0.sub(1).interpolate(P_init)
w0.sub(2).interpolate(T_init)
# w.x.scatter_forward()
# w.sub(2).interpolate(T_init) # Small perturbation
print(w0.sub(0).x.array[:5])
print(w0.sub(1).x.array[:5])
print(w0.sub(2).x.array[:5])
print("Initial velocity max:", np.max(w0.sub(0).x.array))
print("Initial pressure max:", np.max(w0.sub(1).x.array))
print("Initial temperature max:", np.max(w0.sub(2).x.array))
(v0, p0, T0) = split(w0) # Previous solution
# Time-stepping parameters
dt = 0.005
T_end = 0.5
t = 0.0
num_steps = int(T_end / dt)
# Define material properties
rho_0 = 1.0 # Density
mu = 1.0 # Dynamic viscosity
K = 1.0 # Thermal conductivity
C_p = 1.0 # Heat capacity
alpha = K / (rho_0 * C_p) # Thermal diffusivity
Ra = 1.e3 # Rayleigh number
# # Define buoyancy force
# def f_expr(x):
# """Expression for the applied buoyancy force"""
# force = Ra * T * C_p * mu / K
# return np.vstack((np.zeros_like(x[0]), force)) # Force applied in y-direction
# f_B = fem.Function(V_vel)
# f_B.interpolate(f_expr)
f_B = as_vector([0, Ra * T * C_p * mu / K])
v_t = (v - v0) / dt
T_t = (T - T0) / dt
# Define variational forms
dx = dx(metadata={"quadrature_degree": 3 * 2})
F_mass = -div(v) * p_test * dx
F_momentum = (rho_0 * dot(v_t, v_test) * dx
+ rho_0 * dot(dot(grad(v), v), v_test) * dx
+ 2 * mu * inner(sym(grad(v)), sym(grad(v_test))) * dx
- p * div(v_test) * dx
- rho_0 * dot(f_B, v_test) * dx)
F_energy = (dot(T_t, T_test) * dx
- dot(v * T, grad(T_test)) * dx
+ alpha * dot(grad(T), grad(T_test)) * dx)
# Combine all forms
F = F_mass + F_momentum + F_energy
# Define boundary markers
def walls(x):
return np.logical_or.reduce((
np.isclose(x[0], 0.0), # Left wall
np.isclose(x[0], 1.0), # Right wall
np.isclose(x[1], 0.0), # Bottom wall
np.isclose(x[1], 1.0) # Top wall
))
def hot_wall(x):
return np.isclose(x[0], 0.0)
def cold_wall(x):
return np.isclose(x[0], 1.0)
def corner_point(x):
return np.logical_and(np.isclose(x[0], 1.0), np.isclose(x[1], 0.0))
boundary_facets = locate_entities_boundary(domain, domain.topology.dim - 1, walls)
hot_wall_facets = locate_entities_boundary(domain, domain.topology.dim - 1, hot_wall)
cold_wall_facets = locate_entities_boundary(domain, domain.topology.dim - 1, cold_wall)
corner_facets = locate_entities_boundary(domain, domain.topology.dim - 1, corner_point)
# Collapse the subspaces
V_vel, _ = W.sub(0).collapse()
T_space, _ = W.sub(2).collapse()
p_space, _ = W.sub(1).collapse()
# Define boundary function values
u_noslip_func = fem.Function(V_vel)
u_noslip_func.interpolate(lambda x: np.zeros((domain.geometry.dim, x.shape[1])))
T_hot_func = fem.Function(T_space)
T_hot_func.interpolate(lambda x: np.ones((1, x.shape[1])))
T_cold_func = fem.Function(T_space)
T_cold_func.interpolate(lambda x: np.zeros((1, x.shape[1])))
p_corner_func = fem.Function(p_space)
p_corner_func.interpolate(lambda x: np.zeros((1, x.shape[1])))
# Locate boundary DOFs using topology
boundary_dofs_v = fem.locate_dofs_topological(W.sub(0), domain.topology.dim - 1, boundary_facets)
boundary_dofs_T_hot = fem.locate_dofs_topological(W.sub(2), domain.topology.dim - 1, hot_wall_facets)
boundary_dofs_T_cold = fem.locate_dofs_topological(W.sub(2), domain.topology.dim - 1, cold_wall_facets)
corner_dofs = fem.locate_dofs_geometrical(p_space, corner_point) #pointwise
# Define boundary conditions for velocity (no-slip condition)
u_noslip = np.array((0,) * domain.geometry.dim, dtype=PETSc.ScalarType)
bc_v = fem.dirichletbc(u_noslip_func, boundary_dofs_v)#, W.sub(0))
# Define boundary conditions for temperature
bc_T_hot = fem.dirichletbc(T_hot_func, boundary_dofs_T_hot) #, W.sub(2))
bc_T_cold = fem.dirichletbc(T_cold_func, boundary_dofs_T_cold) #, W.sub(2))
# Define the Dirichlet condition for pressure at the corner
bc_p_corner = fem.dirichletbc(p_corner_func, corner_dofs) #, W.sub(1))
# Apply boundary conditions
bcs = [bc_v, bc_T_hot, bc_T_cold, bc_p_corner]
if np.any(np.isnan(w.x.array)):
raise RuntimeError(f"NaN detected in solution at step {step}, time {t:.3f}")
# Create nonlinear problem and solver
problem = NonlinearProblem(F, w, bcs)
solver = NewtonSolver(domain.comm, problem)
solver.rtol = 1e-6 # tolerance
solver.atol = 1e-6 # absolute tolerance
solver.max_it = 50 # max iterations (default is 10)
solver.convergence_criterion = "residual"
solver.relaxation_parameter = 0.7 # Add relaxation
solver.report = True
# Configure PETSc linear solver options
ksp = solver.krylov_solver
opts = PETSc.Options()
option_prefix = ksp.getOptionsPrefix()
# opts[f"{option_prefix}ksp_type"] = "gmres"
# opts[f"{option_prefix}pc_type"] = "ilu"
opts[f"{option_prefix}ksp_type"] = "preonly" # Use direct solver
opts[f"{option_prefix}pc_type"] = "lu" # Use LU factorization
opts[f"{option_prefix}pc_factor_mat_solver_type"] = "mumps" # Set MUMPS as the solver
ksp.setFromOptions()
# Prepare output file
output_file = XDMFFile(MPI.COMM_WORLD, "output.xdmf", "w")
output_file.write_mesh(domain)
# Visualization setup
xvfb.start_xvfb()
topology, cell_types, x = plot.vtk_mesh(T_space)
grid = pv.UnstructuredGrid(topology, cell_types, x)
grid.point_data["T"] = w.sub(2).collapse().x.array
plotter = pv.Plotter()
plotter.open_gif("temperature_evolution.gif", fps=5)
plotter.add_mesh(grid, scalars="T", show_edges=True)
plotter.add_text(f"time: {t:.3f}", font_size=12, name="timelabel")
# Time-stepping loop
for step in range(num_steps):
print("Max velocity:", np.max(w.sub(0).x.array))
print("Max pressure:", np.max(w.sub(1).x.array))
print("Max temperature:", np.max(w.sub(2).x.array))
if np.any(np.isnan(w.x.array)):
raise RuntimeError(f"NaN detected in solution at step {step}, time {t:.3f}")
t += dt
num_iterations, converged = solver.solve(w)
if not converged:
raise RuntimeError(f"Solver failed to converge at step {step}, time {t:.3f}")
w0.x.array[:] = w.x.array # Update previous solution
print(f"Time: {t:.3f}, Newton iterations: {num_iterations}")
# Save results
output_file.write_function(w.sub(2), t)
# Update visualization
grid.point_data["T"][:] = w.sub(2).collapse().x.array
plotter.update_scalars(grid.point_data["T"])
plotter.write_frame()
# Finalize visualization
plotter.close()
output_file.close()
print("Simulation complete!")
# Plot the final results for velocity and pressure
plot_vector_field(w.sub(0), V_vel, domain, "Velocity", "velocity_final.png", scale_factor=0.5)
plot_scalar_field(w.sub(2), T_space, "Temperature", "temperature_final.png")
plot_scalar_field(w.sub(1), p_space, "Pressure", "pressure_final.png")
print("Simulation complete!")
Has anyone encountered similar issues when moving from FEniCS to FEniCSx? Any insights or suggestions would be greatly appreciated.