If I want to package my often repeated UFL objects in functions I notice the system becomes singular, which I guess is because UFL isn’t getting the objects together properly. Does anyone know a ‘correct way’ to do that, or is it ill-advised?
As an MWE I can take the hyperelasticity demo:
from dolfinx import log, default_scalar_type
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
#import pyvista
import numpy as np
import ufl
from petsc4py import PETSc
from mpi4py import MPI
from dolfinx import fem, mesh, plot
L = 20.0
domain = mesh.create_box(MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, 1, 1]], [20, 5, 5], mesh.CellType.hexahedron)
V = fem.functionspace(domain, ("Lagrange", 2, (domain.geometry.dim, )))
def left(x):
return np.isclose(x[0], 0)
def right(x):
return np.isclose(x[0], L)
fdim = domain.topology.dim - 1
left_facets = mesh.locate_entities_boundary(domain, fdim, left)
right_facets = mesh.locate_entities_boundary(domain, fdim, right)
# Concatenate and sort the arrays based on facet indices. Left facets marked with 1, right facets with two
marked_facets = np.hstack([left_facets, right_facets])
marked_values = np.hstack([np.full_like(left_facets, 1), np.full_like(right_facets, 2)])
sorted_facets = np.argsort(marked_facets)
facet_tag = mesh.meshtags(domain, fdim, marked_facets[sorted_facets], marked_values[sorted_facets])
u_bc = np.array((0,) * domain.geometry.dim, dtype=default_scalar_type)
left_dofs = fem.locate_dofs_topological(V, facet_tag.dim, facet_tag.find(1))
bcs = [fem.dirichletbc(u_bc, left_dofs, V)]
B = fem.Constant(domain, default_scalar_type((0, 0, 0)))
T = fem.Constant(domain, default_scalar_type((0, 0, 0)))
v = ufl.TestFunction(V)
u = fem.Function(V)
# Elasticity parameters
E = default_scalar_type(1.0e4)
nu = default_scalar_type(0.3)
mu = fem.Constant(domain, E / (2 * (1 + nu)))
lmbda = fem.Constant(domain, E * nu / ((1 + nu) * (1 - 2 * nu)))
metadata = {"quadrature_degree": 4}
ds = ufl.Measure('ds', domain=domain, subdomain_data=facet_tag, metadata=metadata)
dx = ufl.Measure("dx", domain=domain, metadata=metadata)
this represents the construction of the problem parameters.
Now if the following is left as-is it solves the original problem. But if I change False to True it solves the problem with variables generated by functions:
metadata = {"quadrature_degree": 4}
ds = ufl.Measure('ds', domain=domain, subdomain_data=facet_tag, metadata=metadata)
dx = ufl.Measure("dx", domain=domain, metadata=metadata)
if False:
def F(u):
# Spatial dimension
d = len(u)
# Identity tensor
I = ufl.variable(ufl.Identity(d))
# Deformation gradient
return ufl.variable(I + ufl.grad(u))
def psi(u):
dudx = F(u)
# Invariants of deformation tensors
Ic = ufl.variable(ufl.tr(dudx.T*dudx))
J = ufl.variable(ufl.det(dudx))
return (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J))**2
def PK1(u):
return ufl.diff(psi(u), F(u))
# Define form F (we want to find u such that F(u) = 0)
Form = ufl.inner(ufl.grad(v), PK1(u)) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2)
else:
# Spatial dimension
d = len(u)
# Identity tensor
I = ufl.variable(ufl.Identity(d))
# Deformation gradient
F = ufl.variable(I + ufl.grad(u))
# Right Cauchy-Green tensor
C = ufl.variable(F.T * F)
# Invariants of deformation tensors
Ic = ufl.variable(ufl.tr(C))
J = ufl.variable(ufl.det(F))
# Stored strain energy density (compressible neo-Hookean model)
psi = (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J))**2
# Stress
# Hyper-elasticity
P = ufl.diff(psi, F)
Form = ufl.inner(ufl.grad(v), P) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2)
and the resulting matrix is singular.
problem = NonlinearProblem(Form, u, bcs,)
solver = NewtonSolver(domain.comm, problem)
ksp = solver.krylov_solver
opts = PETSc.Options() # type: ignore
option_prefix = ksp.getOptionsPrefix()
sys = PETSc.Sys() # type: ignore
opts[f"{option_prefix}ksp_type"] = "preonly"
opts[f"{option_prefix}pc_type"] = "lu"
if sys.hasExternalPackage("mumps"):
opts[f"{option_prefix}pc_factor_mat_solver_type"] = "mumps"
ksp.setFromOptions()
# Set Newton solver options
solver.atol = 1e-8
solver.rtol = 1e-8
solver.convergence_criterion = "incremental"
log.set_log_level(log.LogLevel.INFO)
tval0 = -1.5
for n in range(1, 10):
T.value[2] = n * tval0
num_its, converged = solver.solve(u)
assert (converged)
u.x.scatter_forward()
[2025-09-29 14:09:35.688] [info] Requesting connectivity (2, 0) - (3, 0)
[2025-09-29 14:09:35.688] [info] Requesting connectivity (3, 0) - (2, 0)
[2025-09-29 14:09:35.688] [info] Requesting connectivity (2, 0) - (3, 0)
[2025-09-29 14:09:35.688] [info] Requesting connectivity (3, 0) - (2, 0)
[2025-09-29 14:09:35.692] [info] Requesting connectivity (2, 0) - (3, 0)
[2025-09-29 14:09:35.692] [info] Requesting connectivity (3, 0) - (2, 0)
[2025-09-29 14:09:35.692] [info] Column ghost size increased from 0 to 0
[2025-09-29 14:09:35.694] [info] PETSc Krylov solver starting to solve system.
[2025-09-29 14:09:35.694] [info] PETSc Krylov solver starting to solve system.
[2025-09-29 14:09:35.695] [info] Newton iteration 2: r (abs) = inf (tol = 1e-08), r (rel) = -nan (tol = 1e-08)