Solution diverges for mesh refinement for higher order basis functions

I am currently simulating 3D beams (topologically 1D, geometrically 3D) using FEniCSx. I can attach the working code if it is helpful, but I believe the question may be more general than the specific problem that I am working on. When I use linear Lagrange basis functions

Ue = basix.ufl.element("P", domain.basix_cell(), 1, shape=(gdim,)) 

I am able to perform a fairly normal convergence analysis, and the solution converges to my real-world calibration data. But this required a large number of elements. So I tried to utilize higher-order elements (both quadratic and cubic), and I am getting strange results. For the cubic elements

Ue = basix.ufl.element("P", domain.basix_cell(), 3, shape=(gdim,)) 

I get the correct solution (according to the calibration data) for 10, 20, and 40 elements, but when I increase to 80 elements, I suddenly get NaNs in the Newton solver. For quadratic elements, I can solve for 10 and 20 elements, but at 40, I get NaNs.

My formulation is set up similarly to the hyperelasticity example, in which the bilinear form is obtained by taking the derivative of a strain energy function in the direction of the test function. In the cases where I get NaNs in the solver, I have confirmed that the total strain energy is not a NaN and is 0 (up to computer resolution) at the reference configuration.

Is this behavior expected for higher-order elements without special precautions (e.g., due to poor conditioning)? Are there strategies to avoid this issue?

Any help would be greatly appreciated!

At least personally, it would really help with an example code.

Here is the relevant code. Under the header “Construct Beam Mesh,” the two relevant parameters are degree and N which control the degree of the basis function and the number of elements, respectively.

import numpy as np
import gmsh
import ufl
import basix
from mpi4py import MPI
from dolfinx import mesh, fem, io
import dolfinx
import dolfinx.fem.petsc as petsc
from dolfinx.nls.petsc import NewtonSolver


#################################################################
#                    CONSTRUCT BEAM MESH                        #
#################################################################

# Initialize gmsh
gmsh.initialize()

# Parameters

degree = 3          # polynomial order of the functions
N = 10*(2**2)       # number of elements in the beam
L = 300e-3
char_len = L/(N-1)
d = 1

# Creating points and lines
p0 = gmsh.model.occ.addPoint(0, 0, 0, d)
p1 = gmsh.model.occ.addPoint(L, 0, 0, d)
line_ = gmsh.model.occ.addLine(p0, p1)
gmsh.model.occ.synchronize()

lines = gmsh.model.get_entities(1)
gmsh.model.add_physical_group(1, [l[1] for l in lines], 1)

gmsh.option.setNumber("Mesh.CharacteristicLengthMax",char_len)

gmsh.model.mesh.generate(dim=1)
domain, _, _ = io.gmshio.model_to_mesh(gmsh.model, MPI.COMM_WORLD, 0)

gmsh.finalize()

gdim = domain.geometry.dim
tdim = domain.topology.dim
print(f"Geometrical dimension = {gdim}")
print(f"Topological dimension = {tdim}")


#################################################################
#               CONSTRUCT REFERENCE CONFIGURATION               #
#################################################################

dx_dX = ufl.Jacobian(domain)[:, 0]
t = dx_dX 
print(type(t))
t /= ufl.sqrt(ufl.dot(t,t)) # uncomment to normalize tangent vector
ez = ufl.as_vector([0, 0, 1])
a1 = ufl.cross(ez, t)
a1 /= ufl.sqrt(ufl.dot(a1, a1))
a2 = ufl.cross(t, a1)
a2 /= ufl.sqrt(ufl.dot(a2, a2))

def dds(u):
    # use this to get derivatives along the rest configuration of the beam (d/ds)
    return ufl.dot(ufl.grad(u), t)


#################################################################
#               DEFINE PARAMETERS OF THE BEAM                   #
#################################################################

thick = fem.Constant(domain, 0.78e-3)
width = fem.Constant(domain, 30.4e-3)
E = fem.Constant(domain, 200e9) 
nu = fem.Constant(domain, 0.3)
G = E/2/(1+nu)
rho = fem.Constant(domain, 2.7e-3)*1.
g = fem.Constant(domain, 9.81)
S = thick*width
ES = E*S
EI1 = E*width*thick**3/12
EI2 = E*width**3*thick/12
GJ = G*0.26*thick*width**3
kappa = fem.Constant(domain, 5./6.)
GS1 = kappa*G*S
GS2 = kappa*G*S


#################################################################
#               SET UP ELEMENTS AND FUNCTION SPACES             #
#################################################################
Ue = basix.ufl.element("P", domain.basix_cell(), degree, shape=(gdim,)) # here "P" means Lagrange polynomial elements (here cubic)
Us = basix.ufl.element("P", domain.basix_cell(), 1)
W = fem.functionspace(domain, basix.ufl.mixed_element([Ue, Ue]))

dstate = ufl.TestFunction(W)
dr, dd = ufl.split(dstate)

state = fem.Function(W)
r,d1 = ufl.split(state)     # r: position vector, d: normal director

d2 = ufl.cross(dds(r),d1) # third director of the beam (bi-normal)

# setting up initial condition
def r0_func(x):
    return x
def d0_func(x):
    values = np.zeros((3,x.shape[1]))
    values[1,:] = 1.0
    return values

state.sub(0).interpolate(r0_func)
state.sub(1).interpolate(d0_func)
state.x.scatter_forward()

#################################################################
#      SET BOUNDARY CONDITIONS AND INTEGRATION MEASURES         #
#################################################################
def left(x):
    return np.isclose(x[0], 0)

def right(x):
    return np.isclose(x[0], L)

fdim = domain.topology.dim -1 
right_facets = mesh.locate_entities_boundary(domain,fdim,right)

marked_facets = np.hstack([right_facets])
marked_values = np.hstack([np.full(len(right_facets),1,dtype=np.int32)])
sorted_facets = np.argsort(marked_facets)
facet_tag = mesh.meshtags(domain,fdim,marked_facets[sorted_facets],marked_values[sorted_facets])

metadata={"quadrature_scheme":"default", "quadrature_degree": 0}
ds = ufl.Measure('ds',domain=domain,subdomain_data=facet_tag,metadata=metadata)

Vu, _ = W.sub(0).collapse()
Vt, _ = W.sub(1).collapse()
Veps = fem.functionspace(domain,Us)
u_dofs = fem.locate_dofs_geometrical((W.sub(0), Vu), left)
theta_dofs = fem.locate_dofs_geometrical((W.sub(1), Vt), left)

r0 = fem.Function(Vu)
r0.interpolate(r0_func)

d0 = fem.Function(Vt)
d0.interpolate(d0_func)

# Boundary conditions: no displacement, no rotation
bcs = [fem.dirichletbc(r0, u_dofs, W.sub(0)), fem.dirichletbc(d0, theta_dofs, W.sub(1))]


#################################################################
#                  DEFINING THE STRAIN METRICS                  #
#################################################################
def norm_stretch(r,d1,d2):
    eps = ufl.dot(dds(r), dds(r))
    return eps

def curvature(r,d1,d2):
    kap1 = ufl.dot(dds(r), dds(d1))
    kap2 = ufl.dot(dds(r), dds(d2))
    return kap1,kap2

def shear_strain(r,d1,d2):
    gam1 = ufl.dot(dds(r), d1)
    gam2 = ufl.dot(dds(r), d2)
    return gam1,gam2

def shear_couples(r,d1,d2):
    delt1 = ufl.dot(dds(d1),d1)
    delt2 = ufl.dot(dds(d2),d2)
    return delt1, delt2

def thickness_stretch(r,d1,d2):
    lam1 = ufl.dot(d1,d1)
    lam2 = ufl.dot(d2,d2)
    return lam1, lam2





#################################################################
#                  DEFINING THE STRAIN ENERGY                   #
#################################################################
def psi(r,d1,d2):

    psi = 0

    eps = norm_stretch(r,d1,d2)
    kap1,kap2 = curvature(r,d1,d2)
    gam1,gam2 = shear_strain(r,d1,d2)
    delt1,delt2 = shear_couples(r,d1,d2)
    lam1,lam2 = thickness_stretch(r,d1,d2)

    # axial stress contributions
    psi += (ES / (2*(1-nu**2)))*( (nu/4)*(eps - 1)**2 )

    # shear stress constributions
    psi += (GS1/2)*gam1**2 + (GS2/2)*gam2**2

    # primary bending moments contributions
    psi += (EI1/2)*kap1**2 + (EI2/2)*kap2**2

    # shear couple contributions
    psi += 0 

    # CSA change contributions
    psi += (ES/2)*( (lam1-1)**2 + (lam2-1)**2 )

    return psi

# total potential energy
PSI = psi(r,d1,d2) * ufl.dx

# defining the work
T = fem.Constant(domain, dolfinx.default_scalar_type((0, 0, 0)))
M = fem.Constant(domain, dolfinx.default_scalar_type((0, 0, 0)))
grav = fem.Constant(domain, dolfinx.default_scalar_type((0, 1.54, 0)))
Work = ufl.dot(T,dr)*ds(1) + ufl.dot(M,dd)*ds(1) + ufl.dot(grav,dr)*ufl.dx

# Final weak form
residual = ufl.derivative(PSI,state,dstate) + Work



#################################################################
#             CALCULATING VALUES FOR DEBUGGING                  #
#################################################################
drds = fem.Function(Vu)
drds.interpolate(fem.Expression(dds(r),Vu.element.interpolation_points()))

a = fem.Function(Veps)
a.interpolate(fem.Expression(norm_stretch(r,d1,d2) ,Veps.element.interpolation_points()))

psi_ = fem.assemble_scalar(fem.form(PSI))


print("Debugging: Are there any NaNs?")
print("tangent vector:",drds.x.array)
print("normal stretch:",a.x.array)
print("Strain Energy:",psi_)



#################################################################
#             SETTING UP THE SOLVER AND ITERATING               #
#################################################################
to_solve = 1
compare_to_data = 0 # this requires a data file that I can provide if it is helpful

if to_solve:
    problem = petsc.NonlinearProblem(residual,state,bcs)
    solver = NewtonSolver(domain.comm, problem)

    # Set Newton solver options
    solver.atol = 1e-8
    solver.rtol = 1e-8
    solver.convergence_criterion = "incremental"

    dolfinx.log.set_log_level(dolfinx.log.LogLevel.INFO)


    steps = 10
    # incrementally apply a vertical load
    for i in range(steps):
        # update the value of T
        T.value[1] = 3.94*i/(steps-1)
        solver.solve(state)

        state.x.scatter_forward()


    r_sol = state.sub(0).collapse()
    d_sol = state.sub(1).collapse()

    # getting displacement so we can use wrapped by vector
    u = fem.Function(Vu)
    u.interpolate(fem.Expression(r_sol-r0,Vu.element.interpolation_points()))

    import pyvista
    from dolfinx import plot

    plotter = pyvista.Plotter()
    topology, cell_types, geometry = plot.vtk_mesh(Vu)
    grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
    grid.point_data["Deflection"] = u.x.array.reshape(-1, 3)
    warped = grid.warp_by_vector("Deflection", factor=1.0)
    plotter.add_mesh(grid, show_edges=True, color="k", line_width=1, opacity=0.5)
    plotter.add_mesh(warped, show_edges=True, line_width=5)
    plotter.show()

    from dolfinx import geometry
    bb_tree = geometry.bb_tree(domain, domain.topology.dim)

    points = np.zeros((3,N))
    points[0] = np.linspace(0,L,N)

    cells = []
    points_on_proc = []
    # Find cells whose bounding-box collide with the the points
    cell_candidates = geometry.compute_collisions_points(bb_tree, points.T)
    # Choose one of the cells that contains the point
    colliding_cells = geometry.compute_colliding_cells(domain, cell_candidates, points.T)
    for i, point in enumerate(points.T):
        if len(colliding_cells.links(i)) > 0:
            points_on_proc.append(point)
            cells.append(colliding_cells.links(i)[0])

    points_on_proc = np.array(points_on_proc, dtype=np.float64)
    r_values = r_sol.eval(points_on_proc, cells)

    import matplotlib.pyplot as plt

    fig = plt.figure()
    plt.plot(points[0,:],points[1,:],'--',color=(0.8,0.4,0.4))
    plt.plot(r_values[:,0],r_values[:,1],'-r')
    if compare_to_data:
        cal_data = np.genfromtxt('CalibrationData.csv', delimiter=',')
        xData = 10*cal_data[:,0]
        yData = 10*cal_data[:,1]

        plt.plot(xData,yData,'.k')
    plt.axis("equal")
    plt.show()

Consider the following simplification of your code:

import numpy as np
import gmsh
import ufl
import basix
from mpi4py import MPI
from dolfinx import mesh, fem, io
import dolfinx
import dolfinx.fem.petsc as petsc
from dolfinx.nls.petsc import NewtonSolver


#################################################################
#                    CONSTRUCT BEAM MESH                        #
#################################################################

# Initialize gmsh
gmsh.initialize()

# Parameters

degree = 3          # polynomial order of the functions
N = 20*(2**2)       # number of elements in the beam
L = 300e-3
char_len = L/(N-1)
d = 1
# Creating points and lines
p0 = gmsh.model.occ.addPoint(0, 0, 0, d)
p1 = gmsh.model.occ.addPoint(L, 0, 0, d)
line_ = gmsh.model.occ.addLine(p0, p1)
gmsh.model.occ.synchronize()

lines = gmsh.model.get_entities(1)
gmsh.model.add_physical_group(1, [l[1] for l in lines], 1)

gmsh.option.setNumber("Mesh.CharacteristicLengthMax",char_len)

gmsh.model.mesh.generate(dim=1)
mesh_data = io.gmshio.model_to_mesh(gmsh.model, MPI.COMM_WORLD, 0)
try:
    domain, _, _ = mesh_data
except ValueError:
    domain = mesh_data.mesh
gmsh.finalize()

gdim = domain.geometry.dim
tdim = domain.topology.dim
print(f"Geometrical dimension = {gdim}")
print(f"Topological dimension = {tdim}")


#################################################################
#               CONSTRUCT REFERENCE CONFIGURATION               #
#################################################################

dx_dX = ufl.Jacobian(domain)[:, 0]
t = dx_dX 
print(type(t))
t /= ufl.sqrt(ufl.dot(t,t)) # uncomment to normalize tangent vector
ez = ufl.as_vector([0, 0, 1])
a1 = ufl.cross(ez, t)
a1 /= ufl.sqrt(ufl.dot(a1, a1))
a2 = ufl.cross(t, a1)
a2 /= ufl.sqrt(ufl.dot(a2, a2))

def dds(u):
    # use this to get derivatives along the rest configuration of the beam (d/ds)
    return ufl.dot(ufl.grad(u), t)


#################################################################
#               DEFINE PARAMETERS OF THE BEAM                   #
#################################################################

thick = fem.Constant(domain, 0.78e-3)
width = fem.Constant(domain, 30.4e-3)
E = fem.Constant(domain, 200e9) 
nu = fem.Constant(domain, 0.3)
G = E/2/(1+nu)
rho = fem.Constant(domain, 2.7e-3)*1.
g = fem.Constant(domain, 9.81)
S = thick*width
ES = E*S
EI1 = E*width*thick**3/12
EI2 = E*width**3*thick/12
GJ = G*0.26*thick*width**3
kappa = fem.Constant(domain, 5./6.)
GS1 = kappa*G*S
GS2 = kappa*G*S


#################################################################
#               SET UP ELEMENTS AND FUNCTION SPACES             #
#################################################################
Ue = basix.ufl.element("P", domain.basix_cell(), degree, shape=(gdim,)) # here "P" means Lagrange polynomial elements (here cubic)
Us = basix.ufl.element("P", domain.basix_cell(), 1)
W = fem.functionspace(domain, basix.ufl.mixed_element([Ue, Ue]))

dstate = ufl.TestFunction(W)
dr, dd = ufl.split(dstate)

state = fem.Function(W)
r,d1 = ufl.split(state)     # r: position vector, d: normal director

d2 = ufl.cross(dds(r),d1) # third director of the beam (bi-normal)

# setting up initial condition
def r0_func(x):
    return x
def d0_func(x):
    values = np.zeros((3,x.shape[1]))
    values[1,:] = 1.0
    return values

state.sub(0).interpolate(r0_func)
state.sub(1).interpolate(d0_func)
state.x.scatter_forward()

#################################################################
#      SET BOUNDARY CONDITIONS AND INTEGRATION MEASURES         #
#################################################################
def left(x):
    return np.isclose(x[0], 0)

def right(x):
    return np.isclose(x[0], L)

fdim = domain.topology.dim -1 
right_facets = mesh.locate_entities_boundary(domain,fdim,right)

marked_facets = np.hstack([right_facets])
marked_values = np.hstack([np.full(len(right_facets),1,dtype=np.int32)])
sorted_facets = np.argsort(marked_facets)
facet_tag = mesh.meshtags(domain,fdim,marked_facets[sorted_facets],marked_values[sorted_facets])

metadata={"quadrature_scheme":"default", "quadrature_degree": 0}
ds = ufl.Measure('ds',domain=domain,subdomain_data=facet_tag,metadata=metadata)

Vu, Vu_to_W = W.sub(0).collapse()
Vt, Vt_to_W = W.sub(1).collapse()
Veps = fem.functionspace(domain,Us)
u_dofs = fem.locate_dofs_geometrical((W.sub(0), Vu), left)
theta_dofs = fem.locate_dofs_geometrical((W.sub(1), Vt), left)

r0 = fem.Function(Vu)
r0.interpolate(r0_func)

d0 = fem.Function(Vt)
d0.interpolate(d0_func)

# Boundary conditions: no displacement, no rotation
bcs = [fem.dirichletbc(r0, u_dofs, W.sub(0)), fem.dirichletbc(d0, theta_dofs, W.sub(1))]


#################################################################
#                  DEFINING THE STRAIN METRICS                  #
#################################################################
def norm_stretch(r,d1,d2):
    eps = ufl.dot(dds(r), dds(r))
    return eps

def curvature(r,d1,d2):
    kap1 = ufl.dot(dds(r), dds(d1))
    kap2 = ufl.dot(dds(r), dds(d2))
    return kap1,kap2

def shear_strain(r,d1,d2):
    gam1 = ufl.dot(dds(r), d1)
    gam2 = ufl.dot(dds(r), d2)
    return gam1,gam2

def shear_couples(r,d1,d2):
    delt1 = ufl.dot(dds(d1),d1)
    delt2 = ufl.dot(dds(d2),d2)
    return delt1, delt2

def thickness_stretch(r,d1,d2):
    lam1 = ufl.dot(d1,d1)
    lam2 = ufl.dot(d2,d2)
    return lam1, lam2





#################################################################
#                  DEFINING THE STRAIN ENERGY                   #
#################################################################
def psi(r,d1,d2):

    psi = 0

    eps = norm_stretch(r,d1,d2)
    kap1,kap2 = curvature(r,d1,d2)
    gam1,gam2 = shear_strain(r,d1,d2)
    delt1,delt2 = shear_couples(r,d1,d2)
    lam1,lam2 = thickness_stretch(r,d1,d2)

    # axial stress contributions
    psi += (ES / (2*(1-nu**2)))*( (nu/4)*(eps - 1)**2 )

    # shear stress constributions
    psi += (GS1/2)*gam1**2 + (GS2/2)*gam2**2

    # primary bending moments contributions
    psi += (EI1/2)*kap1**2 + (EI2/2)*kap2**2

    # shear couple contributions
    psi += 0 

    # CSA change contributions
    psi += (ES/2)*( (lam1-1)**2 + (lam2-1)**2 )

    return psi

# total potential energy
PSI = psi(r,d1,d2) * ufl.dx

# defining the work
T = fem.Constant(domain, dolfinx.default_scalar_type((0, 0, 0)))
M = fem.Constant(domain, dolfinx.default_scalar_type((0, 0, 0)))
grav = fem.Constant(domain, dolfinx.default_scalar_type((0, 1.54, 0)))
Work = ufl.dot(T,dr)*ds(1) + ufl.dot(M,dd)*ds(1) + ufl.dot(grav,dr)*ufl.dx

# Final weak form
residual = ufl.derivative(PSI,state,dstate) + Work



#################################################################
#             CALCULATING VALUES FOR DEBUGGING                  #
#################################################################
drds = fem.Function(Vu)
try:
    Vup = Vu.element.interpolation_points()
except TypeError:
    Vup = Vu.element.interpolation_points
drds.interpolate(fem.Expression(dds(r),Vup))

a = fem.Function(Veps)
try:
    Vepsp = Veps.element.interpolation_points()
except TypeError:
    Vepsp = Veps.element.interpolation_points
a.interpolate(fem.Expression(norm_stretch(r,d1,d2) ,Vepsp))

psi_ = fem.assemble_scalar(fem.form(PSI))


print("Debugging: Are there any NaNs?")
print("tangent vector:",drds.x.array)
print("normal stretch:",a.x.array)
print("Strain Energy:",psi_)



#################################################################
#             SETTING UP THE SOLVER AND ITERATING               #
problem = petsc.NonlinearProblem(residual,state,bcs)
solver = NewtonSolver(domain.comm, problem)

# Set Newton solver options
solver.atol = 1e-8
solver.rtol = 1e-8
solver.convergence_criterion = "incremental"

ksp = solver.krylov_solver
ksp.setType("preonly")
ksp.setErrorIfNotConverged(True)
ksp.getPC().setType("lu")
ksp.getPC().setFactorSolverType("mumps")

dolfinx.log.set_log_level(dolfinx.log.LogLevel.INFO)




r_sol = state.sub(0).collapse()
d_sol = state.sub(1).collapse()

vtx = dolfinx.io.VTXWriter(domain.comm, "output.bp", [r_sol])


steps = 10
# incrementally apply a vertical load
for i in range(steps):
    # update the value of T
    T.value[1] = 3.94*i/(steps-1)
    solver.solve(state)

    state.x.scatter_forward()
    r_sol.x.array[:] = state.x.array[Vu_to_W]
    vtx.write(i)
vtx.close()

Especially, running with:


ksp = solver.krylov_solver
ksp.setType("preonly")
ksp.setErrorIfNotConverged(True)
ksp.getPC().setType("lu")
ksp.getPC().setFactorSolverType("mumps")

is what ensures that the code runs.
If you just run with

ksp = solver.krylov_solver
ksp.setErrorIfNotConverged(True)

you get


[2025-05-08 18:10:06.882] [info] PETSc Krylov solver starting to solve system.
[2025-05-08 18:10:06.893] [info] PETSc error in '/root/shared/dolfinx/cpp/dolfinx/la/petsc.cpp', 'KSPSolve'
[2025-05-08 18:10:06.893] [info] PETSc error code '71' 'Zero pivot in LU factorization: https://petsc.org/release/faq/#zeropivot'
Traceback (most recent call last):
  File "/root/shared/debug/mwe_i.py", line 288, in <module>
    solver.solve(state)
  File "/root/shared/dolfinx/python/dolfinx/nls/petsc.py", line 69, in solve
    n, converged = super().solve(u.x.petsc_vec)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Failed to successfully call PETSc function 'KSPSolve'. PETSc error code is: 71, Zero pivot in LU factorization: https://petsc.org/release/faq/#zeropivot

Secondly, your choice:

is very strange, as you are forcing under-integration here by setting the degree to 0.
I would at least increase this degree (even if that is not sufficient to resolve the issue about using the wrong solver type.

Thank you for your response! Running the modified code that you provided (and making the same changes in my original code), I can run similar mesh refinements as before, but it is still failing for any further refinement. I am now getting an error with the KSP solver:

Failed to successfully call PETSc function 'KSPSolve'. PETSc error code is: 76, Error in external library

This should be reproducable by changing the code you sent, such that:

N = 20*(2**4)

From other discourse posts, I see that this sometimes points to an out-of-memory issue. I do no believe this is the case because a) for linear basis functions, I can successful run a convergence analysis up to about ~82k elements (quadratic gets the error at about 40 elements) and b) when I run with quadratic or cubic elements, the amount of memory used by VScode is only ever at most 1GB of the 16 I have available.

Other posts have suggested looking at the quadrature degree. I played around with increasing the quadrature (up to 4 times the degree of the elements) and using the default values. This might allow me to increase the mesh density a bit, but not to convergence, and I will eventually get the same error. Regarding your comment about the degree 0 integration – this was based on the beam example presented in the Numerical Tours of Computational Mechanics book (for others reading this thread, I highly recommend this book in addition to the main tutorial, especially if you are interested in computational mechanics). I have also changed this to use the same quadrature as the full domain integral to no avail.

Are there other causes of this error that I should investigate? Do you have suggestions on other aspects of the solver that I should play with?

I was able to generate more information about the issue (and this may be a useful step for others who get the dreaded Error Code 76) by implementing the custom Newton solver from the FEniCSx tutorial but with the KSP setting suggested above.

NewtonSolverCustom.py:

## setting up Custom Newton's solvers in FEniCSx
import numpy as np
import gmsh
import ufl
import basix
from mpi4py import MPI
from dolfinx import mesh, fem, io
import dolfinx
import dolfinx.fem.petsc as petsc
from dolfinx.nls.petsc import NewtonSolver
from petsc4py import PETSc



class NewtonSolverCustom():
    def __init__(self, domain, F, u, bcs, params = {}):
        # set up the class and create all of the things you need to do the solving
        
        self.domain = domain
        self.F = F
        self.u = u
        self.bcs = bcs

        self.V = self.u.function_space

        self.residual = dolfinx.fem.form(self.F)    # construct the residual form for the problem
        
        self.J = ufl.derivative(self.F, self.u)     # calculate the jacobian of the problem
        self.jacobian = dolfinx.fem.form(self.J)

        # set up elements of the linear equations to solve
        self.A = dolfinx.fem.petsc.create_matrix(self.jacobian)
        self.L = dolfinx.fem.petsc.create_vector(self.residual)

        # create the linear algebra backend to solve the system
        self.solver = PETSc.KSP().create(self.domain.comm)
        self.solver.setOperators(self.A)
        self.solver.setType("preonly")
        self.solver.setErrorIfNotConverged(True)
        self.solver.getPC().setType("lu")
        self.solver.getPC().setFactorSolverType("mumps")



        self.du = dolfinx.fem.Function(self.V) # step to take in the Newton Solver

        # parameters of the solver
        self.max_iterations = 1000
        self.atol = 1e-8
        self.verbose = False

    def solve(self,u):
        i = 0
        while i < self.max_iterations:
            # Assemble Jacobian and residual
            with self.L.localForm() as loc_L:
                loc_L.set(0)
            self.A.zeroEntries()
            dolfinx.fem.petsc.assemble_matrix(self.A, self.jacobian, bcs=self.bcs)
            self.A.assemble()

            dolfinx.fem.petsc.assemble_vector(self.L, self.residual)
            self.L.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
            self.L.scale(-1)
        
            # Compute b - J(u_D-u_(i-1))
            dolfinx.fem.petsc.apply_lifting(self.L, [self.jacobian], [self.bcs], x0=[self.u.x.petsc_vec], alpha=1)
            # Set du|_bc = u_{i-1}-u_D
            dolfinx.fem.petsc.set_bc(self.L, self.bcs, self.u.x.petsc_vec, 1.0)

            self.L.ghostUpdate(addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD)

            # Solve linear problem
            self.solver.solve(self.L, self.du.x.petsc_vec)
            self.du.x.scatter_forward()
            # Update u_{i+1} = u_i + delta u_i
            u.x.array[:] += self.du.x.array
            i += 1

            # Compute norm of update
            correction_norm = self.du.x.petsc_vec.norm(0)
            if self.verbose:
                print(f"Iteration {i}: Correction norm {correction_norm}")
            if correction_norm < self.atol:
                converged = 1
                break
        if i>=self.max_iterations:
            converged = 0
            print("Max iterations hit. System did not converge.")
        return converged

With this, the code looks like the following:

import numpy as np
import gmsh
import ufl
import basix
from mpi4py import MPI
from dolfinx import mesh, fem, io
import dolfinx
import dolfinx.fem.petsc as petsc
from dolfinx.nls.petsc import NewtonSolver
from NewtonSolverCustom import *

#################################################################
#                    CONSTRUCT BEAM MESH                        #
#################################################################

# Initialize gmsh
gmsh.initialize()

# Parameters

degree = 3          # polynomial order of the functions
N = 20*(2**2)       # number of elements in the beam
L = 300e-3
char_len = L/(N-1)
d = 1
# Creating points and lines
p0 = gmsh.model.occ.addPoint(0, 0, 0, d)
p1 = gmsh.model.occ.addPoint(L, 0, 0, d)
line_ = gmsh.model.occ.addLine(p0, p1)
gmsh.model.occ.synchronize()

lines = gmsh.model.get_entities(1)
gmsh.model.add_physical_group(1, [l[1] for l in lines], 1)

gmsh.option.setNumber("Mesh.CharacteristicLengthMax",char_len)

gmsh.model.mesh.generate(dim=1)
mesh_data = io.gmshio.model_to_mesh(gmsh.model, MPI.COMM_WORLD, 0)
try:
    domain, _, _ = mesh_data
except ValueError:
    domain = mesh_data.mesh
gmsh.finalize()

gdim = domain.geometry.dim
tdim = domain.topology.dim
print(f"Geometrical dimension = {gdim}")
print(f"Topological dimension = {tdim}")


#################################################################
#               CONSTRUCT REFERENCE CONFIGURATION               #
#################################################################

dx_dX = ufl.Jacobian(domain)[:, 0]
t = dx_dX 
print(type(t))
t /= ufl.sqrt(ufl.dot(t,t)) # uncomment to normalize tangent vector
ez = ufl.as_vector([0, 0, 1])
a1 = ufl.cross(ez, t)
a1 /= ufl.sqrt(ufl.dot(a1, a1))
a2 = ufl.cross(t, a1)
a2 /= ufl.sqrt(ufl.dot(a2, a2))

def dds(u):
    # use this to get derivatives along the rest configuration of the beam (d/ds)
    return ufl.dot(ufl.grad(u), t)


#################################################################
#               DEFINE PARAMETERS OF THE BEAM                   #
#################################################################

thick = fem.Constant(domain, 0.78e-3)
width = fem.Constant(domain, 30.4e-3)
E = fem.Constant(domain, 200e9) 
nu = fem.Constant(domain, 0.3)
G = E/2/(1+nu)
rho = fem.Constant(domain, 2.7e-3)*1.
g = fem.Constant(domain, 9.81)
S = thick*width
ES = E*S
EI1 = E*width*thick**3/12
EI2 = E*width**3*thick/12
GJ = G*0.26*thick*width**3
kappa = fem.Constant(domain, 5./6.)
GS1 = kappa*G*S
GS2 = kappa*G*S


#################################################################
#               SET UP ELEMENTS AND FUNCTION SPACES             #
#################################################################
Ue = basix.ufl.element("P", domain.basix_cell(), degree, shape=(gdim,)) # here "P" means Lagrange polynomial elements (here cubic)
Us = basix.ufl.element("P", domain.basix_cell(), 1)
W = fem.functionspace(domain, basix.ufl.mixed_element([Ue, Ue]))

dstate = ufl.TestFunction(W)
dr, dd = ufl.split(dstate)

state = fem.Function(W)
r,d1 = ufl.split(state)     # r: position vector, d: normal director

d2 = ufl.cross(dds(r),d1) # third director of the beam (bi-normal)

# setting up initial condition
def r0_func(x):
    return x
def d0_func(x):
    values = np.zeros((3,x.shape[1]))
    values[1,:] = 1.0
    return values

state.sub(0).interpolate(r0_func)
state.sub(1).interpolate(d0_func)
state.x.scatter_forward()

#################################################################
#      SET BOUNDARY CONDITIONS AND INTEGRATION MEASURES         #
#################################################################
def left(x):
    return np.isclose(x[0], 0)

def right(x):
    return np.isclose(x[0], L)

fdim = domain.topology.dim -1 
right_facets = mesh.locate_entities_boundary(domain,fdim,right)

marked_facets = np.hstack([right_facets])
marked_values = np.hstack([np.full(len(right_facets),1,dtype=np.int32)])
sorted_facets = np.argsort(marked_facets)
facet_tag = mesh.meshtags(domain,fdim,marked_facets[sorted_facets],marked_values[sorted_facets])

metadata={"quadrature_scheme":"default", "quadrature_degree": 4*degree}
ds = ufl.Measure('ds',domain=domain,subdomain_data=facet_tag,metadata=metadata)
dx = ufl.Measure('dx',domain=domain,subdomain_data=facet_tag,metadata=metadata)

Vu, Vu_to_W = W.sub(0).collapse()
Vt, Vt_to_W = W.sub(1).collapse()
Veps = fem.functionspace(domain,Us)
u_dofs = fem.locate_dofs_geometrical((W.sub(0), Vu), left)
theta_dofs = fem.locate_dofs_geometrical((W.sub(1), Vt), left)

r0 = fem.Function(Vu)
r0.interpolate(r0_func)

d0 = fem.Function(Vt)
d0.interpolate(d0_func)

# Boundary conditions: no displacement, no rotation
bcs = [fem.dirichletbc(r0, u_dofs, W.sub(0)), fem.dirichletbc(d0, theta_dofs, W.sub(1))]


#################################################################
#                  DEFINING THE STRAIN METRICS                  #
#################################################################
def norm_stretch(r,d1,d2):
    eps = ufl.dot(dds(r), dds(r))
    return eps

def curvature(r,d1,d2):
    kap1 = ufl.dot(dds(r), dds(d1))
    kap2 = ufl.dot(dds(r), dds(d2))
    return kap1,kap2

def shear_strain(r,d1,d2):
    gam1 = ufl.dot(dds(r), d1)
    gam2 = ufl.dot(dds(r), d2)
    return gam1,gam2

def shear_couples(r,d1,d2):
    delt1 = ufl.dot(dds(d1),d1)
    delt2 = ufl.dot(dds(d2),d2)
    return delt1, delt2

def thickness_stretch(r,d1,d2):
    lam1 = ufl.dot(d1,d1)
    lam2 = ufl.dot(d2,d2)
    return lam1, lam2





#################################################################
#                  DEFINING THE STRAIN ENERGY                   #
#################################################################
def psi(r,d1,d2):

    psi = 0

    eps = norm_stretch(r,d1,d2)
    kap1,kap2 = curvature(r,d1,d2)
    gam1,gam2 = shear_strain(r,d1,d2)
    delt1,delt2 = shear_couples(r,d1,d2)
    lam1,lam2 = thickness_stretch(r,d1,d2)

    # axial stress contributions
    psi += (ES / (2*(1-nu**2)))*( (nu/4)*(eps - 1)**2 )

    # shear stress constributions
    psi += (GS1/2)*gam1**2 + (GS2/2)*gam2**2

    # primary bending moments contributions
    psi += (EI1/2)*kap1**2 + (EI2/2)*kap2**2

    # shear couple contributions
    psi += 0 

    # CSA change contributions
    psi += (ES/2)*( (lam1-1)**2 + (lam2-1)**2 )

    return psi

# total potential energy
PSI = psi(r,d1,d2) * ufl.dx

# defining the work
T = fem.Constant(domain, dolfinx.default_scalar_type((0, 0, 0)))
M = fem.Constant(domain, dolfinx.default_scalar_type((0, 0, 0)))
grav = fem.Constant(domain, dolfinx.default_scalar_type((0, 1.54, 0)))
Work = ufl.dot(T,dr)*ds(1) + ufl.dot(M,dd)*ds(1) + ufl.dot(grav,dr)*ufl.dx

# Final weak form
residual = ufl.derivative(PSI,state,dstate) + Work



#################################################################
#             CALCULATING VALUES FOR DEBUGGING                  #
#################################################################
drds = fem.Function(Vu)
try:
    Vup = Vu.element.interpolation_points()
except TypeError:
    Vup = Vu.element.interpolation_points
drds.interpolate(fem.Expression(dds(r),Vup))

a = fem.Function(Veps)
try:
    Vepsp = Veps.element.interpolation_points()
except TypeError:
    Vepsp = Veps.element.interpolation_points
a.interpolate(fem.Expression(norm_stretch(r,d1,d2) ,Vepsp))

psi_ = fem.assemble_scalar(fem.form(PSI))


print("Debugging: Are there any NaNs?")
print("tangent vector:",drds.x.array)
print("normal stretch:",a.x.array)
print("Strain Energy:",psi_)



#################################################################
#             SETTING UP THE SOLVER AND ITERATING               #
use_custom_solver = 1

if use_custom_solver:
    solver = NewtonSolverCustom(domain, residual, state, bcs)
else:
    problem = petsc.NonlinearProblem(residual,state,bcs)
    solver = NewtonSolver(domain.comm, problem)

    # Set Newton solver options
    solver.atol = 1e-8
    solver.rtol = 1e-8
    solver.convergence_criterion = "incremental"

    ksp = solver.krylov_solver
    ksp.setType("preonly")
    ksp.setErrorIfNotConverged(True)
    ksp.getPC().setType("lu")
    ksp.getPC().setFactorSolverType("mumps")

dolfinx.log.set_log_level(dolfinx.log.LogLevel.INFO)




r_sol = state.sub(0).collapse()
d_sol = state.sub(1).collapse()

vtx = dolfinx.io.VTXWriter(domain.comm, "output.bp", [r_sol])


steps = 10
# incrementally apply a vertical load
for i in range(steps):
    # update the value of T
    T.value[1] = 3.94*i/(steps-1)
    solver.solve(state)

    state.x.scatter_forward()
    r_sol.x.array[:] = state.x.array[Vu_to_W]
    vtx.write(i)
vtx.close()


plot_pretty_picture = 1

if plot_pretty_picture:
    from dolfinx import geometry
    bb_tree = geometry.bb_tree(domain, domain.topology.dim)

    points = np.zeros((3,N))
    points[0] = np.linspace(0,L,N)

    cells = []
    points_on_proc = []
    # Find cells whose bounding-box collide with the the points
    cell_candidates = geometry.compute_collisions_points(bb_tree, points.T)
    # Choose one of the cells that contains the point
    colliding_cells = geometry.compute_colliding_cells(domain, cell_candidates, points.T)
    for i, point in enumerate(points.T):
        if len(colliding_cells.links(i)) > 0:
            points_on_proc.append(point)
            cells.append(colliding_cells.links(i)[0])

    points_on_proc = np.array(points_on_proc, dtype=np.float64)
    r_values = r_sol.eval(points_on_proc, cells)

    import matplotlib.pyplot as plt

    plt.figure(facecolor='black')
    plt.rcParams['font.size'] = 10  # Sets the default font size to 12

    ax = plt.gca()
    ax.set(facecolor='black')

    # set various colors
    ax.spines['bottom'].set_color('white')
    ax.spines['top'].set_color('white') 
    ax.spines['right'].set_color('white')
    ax.spines['left'].set_color('white')
    ax.xaxis.label.set_color('white')
    ax.yaxis.label.set_color('white')
    ax.tick_params(colors='white', which='both')  # 'both' refers to minor and major axes

    plt.plot(points[0,:],points[1,:],'--',color=(0.8,0.4,0.8))
    plt.plot(r_values[:,0],r_values[:,1],'-m')

    plt.axis("equal")
    plt.title("N="+str(N),color='white')
    plt.show()

I have confirmed that this solver works correctly and provides the same linear element convergence as the built-in solver.

When I try to run the denser meshes for quadratic or cubic functions, instead of just getting

Failed to successfully call PETSc function 'KSPSolve'. PETSc error code is: 76, Error in external library

I get more specific error from MUMPS. Specifically, I either get

MUMPS error in numerical factorization: INFOG(1)=-9, INFO(2)=404 

(reproduced by setting N=20*(2**4)) or

MUMPS error in numerical factorization: INFOG(1)=-10, INFO(2)=11506 

(reproduced by setting N=20*(2**4)). According to the MUMPS docs, the first (INFOG(1)=-9) corresponds to “The main internal real/complex workarray S is too small.” The second (INFOG(1)=-10) corresponds to “Numerically singular matrix, or zero pivot encountered.”

Do you have suggestions for how to sniff out the singularity (if it is that) or the zero pivot? Given that the linear elements and less dense cubic elements provide the correct answer, I do not believe this is a weak form issue, but likely arising from the elements side of things. Any suggestions would be greatly appreciated! (I am also working on the plate extension of this model, and for the sake of convergence rates, I would really like to use higher order elements, but I would first like to confirm that I can demonstrate proper convergence first.)

You can adjust the working memory by tweaking the parameter icntl 14 to a bigger number, either with:
ksp.getPC().getFactorMatrix().setMumpsIcntl(icntl =14 , ival = 120)
or as done through

If the matrix is singular, you have a real problem. If it is a zero pivot you can make mumps detect it (see icntl 24): MATSOLVERMUMPS — PETSc v3.23.2-145-ga91af5e1d55c documentation

2 Likes

Thank you so much! Those adjustments to the solver fixed things (I made both changes simultaneously, so I’m not sure if it was more related to one solution than the other). I can now successfully converge all of the different element degrees for this model. I really appreciate the help!