You can also try the third medium contact method, which is for instance covered in: First order finite element formulations for third medium contact | Computational Mechanics | Springer Nature Link
Here is an example implementation:
"""
Author: Jørgen S. Dokken
SPDX-License-Identifier: MIT
Third medium contact example from DOI: 10.1007/s00466-025-02628-y
"""
from mpi4py import MPI
import dolfinx
import dolfinx.fem.petsc
import ufl
import numpy as np
tol = 1.0e-6
def thirdmedium(x):
return (x[0] >= 0.1 - tol) & (x[2] >= 0.1 - tol) & (x[2] <= 0.6 + tol)
def top(x):
return np.isclose(x[2], 0.7)
def left(x):
return np.isclose(x[0], 0.0)
BODY_marker = 1
AIR_marker = 2
mesh = dolfinx.mesh.create_box(
MPI.COMM_WORLD,
[[0, 0, 0], [2, 0.7, 0.7]],
[40, 14, 14],
ghost_mode=dolfinx.mesh.GhostMode.shared_facet,
)
num_cells_local = (
mesh.topology.index_map(mesh.topology.dim).size_local
+ mesh.topology.index_map(mesh.topology.dim).num_ghosts
)
markers = np.full(num_cells_local, BODY_marker, dtype=np.int32)
markers[dolfinx.mesh.locate_entities(mesh, mesh.topology.dim, thirdmedium)] = AIR_marker
ct = dolfinx.mesh.meshtags(mesh, mesh.topology.dim, np.arange(num_cells_local), markers)
LEFT_marker = 2
NO_TRACTION_marker = 3
POTENTIAL_CONTACT_marker = 4
mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim)
num_facets_local = (
mesh.topology.index_map(mesh.topology.dim - 1).size_local
+ mesh.topology.index_map(mesh.topology.dim - 1).num_ghosts
)
all_body_facets = dolfinx.mesh.compute_incident_entities(
mesh.topology, ct.find(BODY_marker), mesh.topology.dim, mesh.topology.dim - 1
)
all_air_facets = dolfinx.mesh.compute_incident_entities(
mesh.topology, ct.find(AIR_marker), mesh.topology.dim, mesh.topology.dim - 1
)
interface_facets = np.intersect1d(all_body_facets, all_air_facets)
all_exterior_facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
facet_markers = np.zeros(num_facets_local, dtype=np.int32)
facet_markers[np.intersect1d(all_exterior_facets, all_body_facets)] = NO_TRACTION_marker
facet_markers[dolfinx.mesh.locate_entities(mesh, mesh.topology.dim - 1, left)] = (
LEFT_marker
)
facet_markers[interface_facets] = POTENTIAL_CONTACT_marker
f_to_c = mesh.topology.connectivity(mesh.topology.dim - 1, mesh.topology.dim)
ft_indices = np.flatnonzero(facet_markers)
ft = dolfinx.mesh.meshtags(
mesh, mesh.topology.dim - 1, ft_indices, facet_markers[ft_indices]
)
ft.name = "facet_markers"
with dolfinx.io.XDMFFile(MPI.COMM_WORLD, "markers.xdmf", "w") as xdmf:
xdmf.write_mesh(mesh)
xdmf.write_meshtags(ct, mesh.geometry)
xdmf.write_meshtags(ft, mesh.geometry)
V = dolfinx.fem.functionspace(mesh, ("Lagrange", 1, (mesh.geometry.dim,)))
d = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(mesh.geometry.dim))
third_medium_mesh, medium_map = dolfinx.mesh.create_submesh(
mesh, mesh.topology.dim, ct.find(AIR_marker)
)[0:2]
P = dolfinx.fem.functionspace(third_medium_mesh, ("Lagrange", 1, (3,)))
W = ufl.MixedFunctionSpace(V, P)
p = dolfinx.fem.Function(P)
u = dolfinx.fem.Function(V)
I = ufl.Identity(len(u))
x = ufl.SpatialCoordinate(mesh)
phi = x + u
F = I + ufl.grad(u)
J = ufl.det(F)
C = F.T * F
# 3. Separate Material Properties
E = 5
nu = 0.3
K = E / (3 * (1 - 2 * nu))
mu = E / (2 * (1 + nu))
K_body = dolfinx.fem.Constant(mesh, K)
mu_body = dolfinx.fem.Constant(mesh, mu)
Psi_body = K_body / 2 * ufl.ln(J) ** 2 + mu_body / 2 * (J ** (-2 / 3) * ufl.tr(C) - 3)
dx = ufl.Measure("dx", domain=mesh, subdomain_data=ct)
dxVol = dx(BODY_marker)
dxThird = dx(AIR_marker)
ds = ufl.Measure("ds", domain=mesh, subdomain_data=ft)
b = dolfinx.fem.Constant(
mesh, np.zeros(mesh.geometry.dim, dtype=dolfinx.default_scalar_type)
)
t = dolfinx.fem.Constant(
mesh, np.zeros(mesh.geometry.dim, dtype=dolfinx.default_scalar_type)
)
Pi = (
Psi_body * dxVol
- ufl.inner(b, phi) * dxVol
- ufl.inner(t, phi) * ds((NO_TRACTION_marker))
)
gamma = dolfinx.fem.Constant(mesh, 4.0e-6)
Pi_third = gamma * Psi_body * dxThird
beta_1 = dolfinx.fem.Constant(mesh, 1.0)*gamma
beta_2 = dolfinx.fem.Constant(mesh, 100.0)*gamma
f0_skew = 0.5 * (F[0, 1] - F[1, 0])
f1_skew = 0.5 * (F[0, 2] - F[2, 0])
f2_skew = 0.5 * (F[1, 2] - F[2, 1])
fi_skew = ufl.as_vector((f0_skew, f1_skew, f2_skew))
L_i = np.zeros(3)
for dim in range(mesh.geometry.dim):
x_i_max = mesh.comm.allreduce(mesh.geometry.x[:, dim].max(), op=MPI.MAX)
x_i_min = mesh.comm.allreduce(mesh.geometry.x[:, dim].min(), op=MPI.MIN)
L_i[dim] = x_i_max - x_i_min
d = dolfinx.fem.Constant(mesh, np.max(L_i))
skew_term = fi_skew - 1 / d * p
Pi_fi = (
beta_1 / 2 * ufl.dot(skew_term, skew_term) + beta_2 / 2 * ufl.inner(ufl.grad(p), ufl.grad(p))
) * dxThird
left_dofs = dolfinx.fem.locate_dofs_topological(
V, mesh.topology.dim - 1, ft.find(LEFT_marker)
)
bc_left = dolfinx.fem.dirichletbc(
np.zeros(mesh.geometry.dim, dtype=dolfinx.default_scalar_type), left_dofs, V
)
mesh.topology.create_connectivity(0, mesh.topology.dim)
node = dolfinx.mesh.locate_entities(mesh, 0, lambda x: np.isclose(x[0], 2.0) & np.isclose(x[1], 0.0) & np.isclose(x[2], 0.7))
dofs_point_z = dolfinx.fem.locate_dofs_topological(V.sub(2), 0, node)
dofs_point_y = dolfinx.fem.locate_dofs_topological(V.sub(1), 0, node)
applied_z = dolfinx.fem.Constant(mesh, 0.0)
applied_y = dolfinx.fem.Constant(mesh, 0.0)
bc_point_z = dolfinx.fem.dirichletbc(applied_z, dofs_point_z, V.sub(2))
bc_point_y = dolfinx.fem.dirichletbc(applied_y, dofs_point_y, V.sub(1))
bcs = [bc_left, bc_point_z, bc_point_y]
dup = ufl.TestFunctions(W)
residual = ufl.derivative(Pi + Pi_third + Pi_fi, [u, p], dup)
problem = dolfinx.fem.petsc.NonlinearProblem(
ufl.extract_blocks(residual),
[u, p],
bcs=bcs,
entity_maps=[medium_map],
petsc_options_prefix="snes",
petsc_options={
"snes_type": "newtonls",
"snes_monitor": None,
"snes_converged_reason": None,
"snes_error_if_not_converged": True,
"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps",
"snes_linesearch_type": "bt",
"snes_atol": 5e-5,
"snes_rtol": 5e-5,
"snes_max_it": 100,
},
)
# Do partial loading
full_disp = np.array([0.0, 0.5, -2])
tm = dolfinx.fem.functionspace(mesh, ("DG", 0))
tm_func = dolfinx.fem.Function(tm, name="medium")
tm_func.x.array[:] = ct.values
with dolfinx.io.VTXWriter(mesh.comm, "solution.bp", [u, tm_func]) as bp:
loading_steps = 10
loading = np.linspace(0, 1, loading_steps + 1)[1:]
for i,load in enumerate(loading):
applied_z.value = full_disp[2] * load
applied_y.value = full_disp[1] * load
problem.solve()
bp.write(i)
Depending on what kind of elastic model you want for the body, it might be easier to get convergence that it was from the test in Wriggers paper, which I haven’t found a satisfactory linear loading method for (probably need a adaptive loading method).