Problem of time-dependent Dirichlet BC for elastoplastic model

Dear Community,

I am currently constructing an elastoplastic model using FEniCSx, referencing the content in Elasto-plastic analysis of a 2D von Mises material. In this example, Dirichlet BCs are applied to the left and bottom edges of the quarter hollow cylinder model, with a time-varying loading applied to the inner wall of the hollow cylinder.

I now wish to modify the Dirichlet BCs such that the lower edge has fixed displacement of 0 in both the x and y directions, while the left edge gets a displacement increasing over time in the positive x direction up to 0.05mm (as a time-dependent Dirichlet BC). Additionally, I wish to remove the load applied to the inner wall of the hollow cylinder. So I changed the following part of the code (the rest remains same):

Residual = ufl.inner(eps(u_), as_3D_tensor(sig)) * dx #- ufl.inner(-loading * n, u_) * ds(3)
tangent_form = ufl.inner(eps(v), sigma_tang(eps(u_))) * dx

u_bottom = fem.Constant(domain, ScalarType((0.000, 0.000)))
bottom_dofs = fem.locate_dofs_topological(V, gdim - 1, facets.find(1))
bc_bottom = fem.dirichletbc(u_bottom, bottom_dofs, V)

u_left = fem.Constant(domain, ScalarType((0.000)))
left_dofs_x = fem.locate_dofs_topological(V.sub(0), gdim - 1, facets.find(2))
bc_left_x = fem.dirichletbc(u_left, left_dofs_x, V.sub(0))

bcs = [bc_left_x, bc_bottom]

Nitermax, tol = 200, 1e-6  # parameters of the Newton-Raphson procedure
Nincr = 20
load_steps = np.linspace(0, 2, Nincr + 1) # [1:] ** 0.5
results = np.zeros((Nincr + 1, 3))

# we set all functions to zero before entering the loop in case we would like to reexecute this code cell
sig.vector.set(0.0)
sig_old.vector.set(0.0)
p.vector.set(0.0)
u.vector.set(0.0)
n_elas.vector.set(0.0)
beta.vector.set(0.0)

Δε = eps(Du)
sig_, n_elas_, beta_, dp_ = constitutive_update(Δε, sig_old, p)

Tensor_element = element("Lagrange", domain.basix_cell(), 1, shape=(2,))
V_Tensor = fem.functionspace(domain, Tensor_element)
Result_u = fem.Function(V_Tensor)

v_load = 0.05e-3

with io.XDMFFile(domain.comm, foldername + "/u.xdmf", "w") as xdmf_u:
    xdmf_u.write_mesh(domain)
    for i, t in enumerate(load_steps):
        # loading.value = t * q_lim
        u_left.value = ScalarType((v_load * t))
        bc_left_x = fem.dirichletbc(u_left, left_dofs_x, V.sub(0))
        bcs=[bc_left_x, bc_bottom]
        # compute the residual norm at the beginning of the load step
        tangent_problem.assemble_rhs()
        nRes0 = tangent_problem._b.norm()
        nRes = nRes0
        Du.x.array[:] = 0

        niter = 0
        while nRes / nRes0 > tol and niter < Nitermax:
            # solve for the displacement correction
            tangent_problem.assemble_lhs()
            tangent_problem.solve_system()

            # update the displacement increment with the current correction
            Du.vector.axpy(1, du.vector)  # Du = Du + 1*du
            Du.x.scatter_forward()

            # interpolate the new stresses and internal state variables
            interpolate_quadrature(sig_, sig)
            interpolate_quadrature(n_elas_, n_elas)
            interpolate_quadrature(beta_, beta)

            # compute the new residual
            tangent_problem.assemble_rhs()
            nRes = tangent_problem._b.norm()

            niter += 1

        # Update the displacement with the converged increment
        u.vector.axpy(1, Du.vector)  # u = u + 1*Du
        u.x.scatter_forward()

        # Update the previous plastic strain
        interpolate_quadrature(dp_, dp)
        p.vector.axpy(1, dp.vector)

        # Update the previous stress
        sig_old.x.array[:] = sig.x.array[:]

        if len(bottom_inside_dof) > 0:  # test if proc has dof
            results[i + 1, :] = (u.x.array[bottom_inside_dof[0]], t, niter)
    # -
        Result_u.name = "u"
        Result_u.interpolate(u)
        xdmf_u.write_function(Result_u, t)

However, I have encountered the following error:

Traceback (most recent call last):
  File "/root/shared/Code/2026_02_20/plasticity_original_example.py", line 271, in <module>
    while nRes / nRes0 > tol and niter < Nitermax:
ZeroDivisionError: float division by zero

I believe I need to modify the CustomLinearProblem function to adjust the BCs, which is:

class CustomLinearProblem(fem.petsc.LinearProblem):
    def assemble_rhs(self, u=None):
        """Assemble right-hand side and lift Dirichlet bcs.
        Parameters
        ----------
        u : dolfinx.fem.Function, optional
            For non-zero Dirichlet bcs u_D, use this function to assemble rhs with the value u_D - u_{bc}
            where u_{bc} is the value of the given u at the corresponding. Typically used for custom Newton methods
            with non-zero Dirichlet bcs.
        """
        # Assemble rhs
        with self._b.localForm() as b_loc:
            b_loc.set(0)
        fem.petsc.assemble_vector(self._b, self._L)

        # Apply boundary conditions to the rhs
        x0 = [] if u is None else [u.vector]
        fem.petsc.apply_lifting(self._b, [self._a], bcs=[self.bcs], x0=x0, scale=1.0)
        self._b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
        x0 = None if u is None else u.vector
        fem.petsc.set_bc(self._b, self.bcs, x0, scale=1.0)

    def assemble_lhs(self):
        self._A.zeroEntries()
        fem.petsc.assemble_matrix_mat(self._A, self._a, bcs=self.bcs)
        self._A.assemble()

    def solve_system(self):
        # Solve linear system and update ghost values in the solution
        self._solver.solve(self._b, self._x)
        self.u.x.scatter_forward()


tangent_problem = CustomLinearProblem(
    tangent_form,
    -Residual,
    u=du,
    bcs=bcs,
    petsc_options={
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver_type": "mumps",
    },
)

But after searching through the FEniCSx tutorials, I remain unable to find a solution. Could someone kindly provide me some suggestions? If anyone needs, I can also provide the full code. Many thanks in advance!

As your code is not reproducible, it’s hard to pinpoint exactly what is wrong.

Indicates that nRes0 is 0. Thus,

it means that the boundary conditions you are prescribing is not adding anything to the rhs.

Thank you for the reply, in the following is the complete code that I use:

import numpy as np
import matplotlib.pyplot as plt

import gmsh
from mpi4py import MPI
import ufl
import basix
from dolfinx import mesh, fem, io
import dolfinx.fem.petsc
from petsc4py import PETSc
from petsc4py.PETSc import ScalarType
from basix.ufl import element, mixed_element
from dolfinx import log, default_scalar_type
hsize = 0.2

Re = 1.3
Ri = 1.0

gmsh.initialize()
gdim = 2
model_rank = 0
if MPI.COMM_WORLD.rank == 0:
    gmsh.option.setNumber("General.Terminal", 0)  # to disable meshing info
    gmsh.model.add("Model")

    geom = gmsh.model.geo
    center = geom.add_point(0, 0, 0)
    p1 = geom.add_point(Ri, 0, 0)
    p2 = geom.add_point(Re, 0, 0)
    p3 = geom.add_point(0, Re, 0)
    p4 = geom.add_point(0, Ri, 0)

    x_radius = geom.add_line(p1, p2)
    outer_circ = geom.add_circle_arc(p2, center, p3)
    y_radius = geom.add_line(p3, p4)
    inner_circ = geom.add_circle_arc(p4, center, p1)

    boundary = geom.add_curve_loop([x_radius, outer_circ, y_radius, inner_circ])
    surf = geom.add_plane_surface([boundary])

    geom.synchronize()

    gmsh.option.setNumber("Mesh.CharacteristicLengthMin", hsize)
    gmsh.option.setNumber("Mesh.CharacteristicLengthMax", hsize)

    gmsh.model.addPhysicalGroup(gdim, [surf], 1)
    gmsh.model.addPhysicalGroup(gdim - 1, [x_radius], 1, name="bottom")
    gmsh.model.addPhysicalGroup(gdim - 1, [y_radius], 2, name="left")
    gmsh.model.addPhysicalGroup(gdim - 1, [inner_circ], 3, name="inner")

    gmsh.model.mesh.generate(gdim)

out = io.gmshio.model_to_mesh(
    gmsh.model, MPI.COMM_WORLD, model_rank, gdim=gdim
)
domain = out[0]
cell_markers = out[1]
facets = out[2]
gmsh.finalize()
# -
foldername = "2026_02_21_test_plasticity_10"

E = fem.Constant(domain, 70e3)  # in MPa
nu = fem.Constant(domain, 0.3)
lmbda = E * nu / (1 + nu) / (1 - 2 * nu)
mu = E / 2.0 / (1 + nu)
sig0 = fem.Constant(domain, 250.0)  # yield strength in MPa
Et = E / 100.0  # tangent modulus
H = E * Et / (E - Et)  # hardening modulus

deg_u = 2
shape = (gdim,)
V = fem.functionspace(domain, ("P", deg_u, shape))

domain.topology.create_connectivity(gdim - 1, domain.topology.dim)

u_bottom = fem.Constant(domain, ScalarType((0.000, 0.000)))
bottom_dofs = fem.locate_dofs_topological(V, gdim - 1, facets.find(1))
bc_bottom = fem.dirichletbc(u_bottom, bottom_dofs, V)

u_top = fem.Constant(domain, ScalarType((0.000)))
top_dofs_x = fem.locate_dofs_topological(V.sub(0), gdim - 1, facets.find(2))
bc_top_x = fem.dirichletbc(u_top, top_dofs_x, V.sub(0))

bcs = [bc_top_x, bc_bottom]

n = ufl.FacetNormal(domain)
q_lim = float(2 / np.sqrt(3) * np.log(Re / Ri) * sig0)

loading = fem.Constant(domain, 0.0)

deg_quad = 2  # quadrature degree for internal state variable representation
W0e = basix.ufl.quadrature_element(
    domain.basix_cell(), value_shape=(), scheme="default", degree=deg_quad
)
We = basix.ufl.quadrature_element(
    domain.basix_cell(), value_shape=(4,), scheme="default", degree=deg_quad
)
W = fem.functionspace(domain, We)
W0 = fem.functionspace(domain, W0e)

sig = fem.Function(W)
sig_old = fem.Function(W)
n_elas = fem.Function(W)
beta = fem.Function(W0)
p = fem.Function(W0, name="Cumulative_plastic_strain")
dp = fem.Function(W0)
u = fem.Function(V, name="Total_displacement")
du = fem.Function(V, name="Iteration_correction")
Du = fem.Function(V, name="Current_increment")
v = ufl.TrialFunction(V)
u_ = ufl.TestFunction(V)

P0 = fem.functionspace(domain, ("DG", 0))
p_avg = fem.Function(P0, name="Plastic_strain")

def eps(v):
    e = ufl.sym(ufl.grad(v))
    return ufl.as_tensor([[e[0, 0], e[0, 1], 0], [e[0, 1], e[1, 1], 0], [0, 0, 0]])

def elastic_behavior(eps_el):
    return lmbda * ufl.tr(eps_el) * ufl.Identity(3) + 2 * mu * eps_el

def as_3D_tensor(X):
    return ufl.as_tensor([[X[0], X[3], 0], [X[3], X[1], 0], [0, 0, X[2]]])

def to_vect(X):
    return ufl.as_vector([X[0, 0], X[1, 1], X[2, 2], X[0, 1]])

ppos = lambda x: ufl.max_value(x, 0)

def constitutive_update(Δε, old_sig, old_p):
    sig_n = as_3D_tensor(old_sig)
    sig_elas = sig_n + elastic_behavior(Δε)
    s = ufl.dev(sig_elas)
    sig_eq = ufl.sqrt(3 / 2.0 * ufl.inner(s, s))
    f_elas = sig_eq - sig0 - H * old_p
    dp = ppos(f_elas) / (3 * mu + H)
    n_elas = s / sig_eq * ppos(f_elas) / f_elas
    beta = 3 * mu * dp / sig_eq
    new_sig = sig_elas - beta * s
    return to_vect(new_sig), to_vect(n_elas), beta, dp

def sigma_tang(eps):
    N_elas = as_3D_tensor(n_elas)
    return (
        elastic_behavior(eps)
        - 3 * mu * (3 * mu / (3 * mu + H) - beta) * ufl.inner(N_elas, eps) * N_elas
        - 2 * mu * beta * ufl.dev(eps)
    )

ds = ufl.Measure("ds", domain=domain, subdomain_data=facets)
dx = ufl.Measure(
    "dx",
    domain=domain,
    metadata={"quadrature_degree": deg_quad, "quadrature_scheme": "default"},
)
Residual = ufl.inner(eps(u_), as_3D_tensor(sig)) * dx #- ufl.inner(-loading * n, u_) * ds(3)
tangent_form = ufl.inner(eps(v), sigma_tang(eps(u_))) * dx

basix_celltype = getattr(basix.CellType, domain.topology.cell_type.name)
quadrature_points, weights = basix.make_quadrature(basix_celltype, deg_quad)

map_c = domain.topology.index_map(domain.topology.dim)
num_cells = map_c.size_local + map_c.num_ghosts
cells = np.arange(0, num_cells, dtype=np.int32)

def interpolate_quadrature(ufl_expr, function):
    expr_expr = fem.Expression(ufl_expr, quadrature_points)
    expr_eval = expr_expr.eval(domain, cells)
    function.x.array[:] = expr_eval.flatten()[:]

class CustomLinearProblem(fem.petsc.LinearProblem):
    def assemble_rhs(self, u=None):
        """Assemble right-hand side and lift Dirichlet bcs.

        Parameters
        ----------
        u : dolfinx.fem.Function, optional
            For non-zero Dirichlet bcs u_D, use this function to assemble rhs with the value u_D - u_{bc}
            where u_{bc} is the value of the given u at the corresponding. Typically used for custom Newton methods
            with non-zero Dirichlet bcs.
        """

        # Assemble rhs
        with self._b.localForm() as b_loc:
            b_loc.set(0)
        fem.petsc.assemble_vector(self._b, self._L)

        # Apply boundary conditions to the rhs
        x0 = [] if u is None else [u.vector]
        fem.petsc.apply_lifting(self._b, [self._a], bcs=[self.bcs], x0=x0, scale=1.0)
        self._b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
        x0 = None if u is None else u.vector
        fem.petsc.set_bc(self._b, self.bcs, x0, scale=1.0)

    def assemble_lhs(self):
        self._A.zeroEntries()
        fem.petsc.assemble_matrix_mat(self._A, self._a, bcs=self.bcs)
        self._A.assemble()

    def solve_system(self):
        # Solve linear system and update ghost values in the solution
        self._solver.solve(self._b, self._x)
        self.u.x.scatter_forward()

tangent_problem = CustomLinearProblem(
    tangent_form,
    -Residual,
    u=du,
    bcs=bcs,
    petsc_options={
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver_type": "mumps",
    },
)

Nitermax, tol = 200, 1e-6  # parameters of the Newton-Raphson procedure
Nincr = 20
load_steps = np.linspace(0, 1.1, Nincr + 1) #[1:] ** 0.5
results = np.zeros((Nincr + 1, 3))

# we set all functions to zero before entering the loop in case we would like to reexecute this code cell
sig.vector.set(0.0)
sig_old.vector.set(0.0)
p.vector.set(0.0)
u.vector.set(0.0)
n_elas.vector.set(0.0)
beta.vector.set(0.0)

Δε = eps(Du)
sig_, n_elas_, beta_, dp_ = constitutive_update(Δε, sig_old, p)

Tensor_element = element("Lagrange", domain.basix_cell(), 1, shape=(2,))
V_Tensor = fem.functionspace(domain, Tensor_element)
Result_u = fem.Function(V_Tensor)

v_load = 0.05e-3

with io.XDMFFile(domain.comm, foldername + "/u.xdmf", "w") as xdmf_u:
    xdmf_u.write_mesh(domain)
    for i, t in enumerate(load_steps):
        # loading.value = t * q_lim
        u_top.value = ScalarType((v_load * t))
        bc_top = fem.dirichletbc(u_top, top_dofs_x, V.sub(0))
        bcs=[bc_bottom, bc_top]
        # compute the residual norm at the beginning of the load step
        tangent_problem.assemble_rhs()
        nRes0 = tangent_problem._b.norm()
        nRes = nRes0
        Du.x.array[:] = 0

        niter = 0
        while nRes / nRes0 > tol and niter < Nitermax:
            # solve for the displacement correction
            tangent_problem.assemble_lhs()
            tangent_problem.solve_system()

            # update the displacement increment with the current correction
            Du.vector.axpy(1, du.vector)  # Du = Du + 1*du
            Du.x.scatter_forward()

            # interpolate the new stresses and internal state variables
            interpolate_quadrature(sig_, sig)
            interpolate_quadrature(n_elas_, n_elas)
            interpolate_quadrature(beta_, beta)

            # compute the new residual
            tangent_problem.assemble_rhs()
            nRes = tangent_problem._b.norm()

            niter += 1

        # Update the displacement with the converged increment
        u.vector.axpy(1, Du.vector)  # u = u + 1*Du
        u.x.scatter_forward()

        # Update the previous plastic strain
        interpolate_quadrature(dp_, dp)
        p.vector.axpy(1, dp.vector)

        # Update the previous stress
        sig_old.x.array[:] = sig.x.array[:]

    # -
        Result_u.name = "u"
        Result_u.interpolate(u)
        xdmf_u.write_function(Result_u, t)

I see that the BCs I prescribed doesn’t work in this code. Could you give any suggestions on how to correctly apply BCs to the rhs in this situation? I am also a bit confused with the CustomLinearProblem function, namely the custom Newton solver.

Many thanks in advance!

Let’s look at your residual (which is the RHS).

For the first solve, sig=0, thus, residual is 0 (as you have turned of the loading term).

Furthermore, your redefinition of bc_top and bcs is not propagated into the tangent_problem.
i.e.

thus just remove the lines

Furthermore, at time=0, all your boundary conditions are zero, thus the solution to the problem is 0, and your residual is zero.
Therefore you should modify your condition at

to something like while (t>0) and (nRes/nRes0) > tol and niter < NiterMax:

Thank you for the reply, now the code can run.

But I got another problem with the residual. When I run the code, the residual nRes is quite large at beginning (already 7244721 in the first solve) and getting larger and larger after each loop, also nRes / nRes0 is still not less than 0.5 after 200 iterations (which should fulfill the condition nRes / nRes0 < tol).

I tried to refine the mesh, set smaller time steps and change the element function, but nothing helps. Could you please perhaps identify the problem here? Thank you in advance. Here is the part of the code which I changed to apply a time dependent displacement BC:

bottom_dofs_xy = fem.locate_dofs_topological(V, gdim - 1, ft.find(9))
u_D = np.array([0.000, 0.000], dtype=default_scalar_type)
bc_bottom_xy = fem.dirichletbc(u_D, bottom_dofs_xy, V)

u_top = fem.Constant(domain, ScalarType((0.000)))
top_dofs_u = fem.locate_dofs_topological(V.sub(1), gdim - 1, ft.find(8))
bc_top = fem.dirichletbc(u_top, top_dofs_u, V.sub(1))
bcs = [bc_bottom_xy, bc_top] 

tangent_form = ufl.inner(eps(v), sigma_tang(eps(u_))) * dx
Residual = ufl.inner(eps(u_), as_3D_tensor(sig)) * dx
t = 0.0
dt = 0.05
v_load = 0.05e-3   # mm/s

with io.XDMFFile(domain.comm, foldername + "/u.xdmf", "w") as xdmf_u:
    xdmf_u.write_mesh(domain)
    while t <= 1.0: 
        t += dt
        if t >= 0.7:
            dt= 0.01
        u_top.value = ScalarType(v_load * t)
        # compute the residual norm at the beginning of the load step
        tangent_problem.assemble_rhs()
        nRes0 = tangent_problem._b.norm()
        nRes = nRes0
        Du.x.array[:] = 0
        print ('nRes:', nRes)
        niter = 0
        while nRes / nRes0  > tol and niter < Nitermax:   #
            # solve for the displacement correction
            print ('Tolerance:', nRes / nRes0, ', Iteration', niter)
            tangent_problem.assemble_lhs()
            tangent_problem.solve_system()

            # update the displacement increment with the current correction
            Du.vector.axpy(1, du.vector)  # Du = Du + 1*du
            Du.x.scatter_forward()

            # interpolate the new stresses and internal state variables
            interpolate_quadrature(sig_, sig)
            interpolate_quadrature(n_elas_, n_elas)
            interpolate_quadrature(beta_, beta)

            # compute the new residual
            tangent_problem.assemble_rhs()
            nRes = tangent_problem._b.norm()

            niter += 1

        # Update the displacement with the converged increment
        u.vector.axpy(1, Du.vector)  # u = u + 1*Du
        u.x.scatter_forward()

        # Update the previous plastic strain
        interpolate_quadrature(dp_, dp)
        p.vector.axpy(1, dp.vector)

        # Update the previous stress
        sig_old.x.array[:] = sig.x.array[:]

        Result_u.name = "u"
        Result_u.interpolate(u)
        xdmf_u.write_function(Result_u, t)