Hi!
I want to plot the load-displacement curve for a somewhat complex problem which I load using dirichlet BCs, but can’t make it work. I found this nice tutorial from @bleyerj for computing the internal forces. I’ve used it to create an MWE for a simple beam under tension with dirichlet BCs, with a linear material.
Although the response seems correct, I was wondering if this is the proper way to do it, since both my load and this reaction force method overwrite the same boundary vector u_right
?
The code for this MWE:
import numpy as np
from mpi4py import MPI
import ufl
from dolfinx import fem, mesh, nls, io
import petsc4py.PETSc as PETSc
import dolfinx.fem.petsc
import dolfinx.nls.petsc
from dolfinx import nls
L = 5.0
H = 1.0
Nx = 20
Ny = 5
domain = mesh.create_rectangle(
MPI.COMM_WORLD,
[(0.0, -H / 2), (L, H / 2)],
(Nx, Ny),
diagonal=mesh.DiagonalType.crossed,
)
E = fem.Constant(domain, 1e5)
nu = fem.Constant(domain, 0.3)
mu = E / 2 / (1 + nu)
lmbda = E * nu / (1 + nu) / (1 - 2 * nu)
def eps(v):
return ufl.sym(ufl.grad(v))
def sigma(v):
return lmbda * ufl.tr(eps(v)) * ufl.Identity(2) + 2.0 * mu * eps(v)
V = fem.functionspace(domain, ("P", 1, (2,)))
u = fem.Function(V, name="Displacement")
u_ = ufl.TestFunction(V)
residual = ufl.inner(sigma(u), eps(u_)) * ufl.dx
# Dirichlet BC
def left(x):
return np.isclose(x[0], 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)
left_dofs = fem.locate_dofs_geometrical(V, left)
u_bc = fem.Function(V)
u_bc.x.array[:] = 0.0
# Right moving boundary
right_dofs = fem.locate_dofs_topological(V.sub(0), fdim, right_facets)
u_right = fem.Constant(domain, PETSc.ScalarType(1.))
bcs = [fem.dirichletbc(u_bc, left_dofs), fem.dirichletbc(u_right, right_dofs, V.sub(0))]
# Define problem
problem = fem.petsc.NonlinearProblem(residual, u, bcs)
# Newton solver
solver = nls.petsc.NewtonSolver(MPI.COMM_WORLD, problem)
solver.rtol = 1e-6
solver.atol = 1e-7 # I use an additional absolute tolerance to avoid convergence issues when having loadsteps with no change in loading: the GP numerical inaccuracies keep giving slight tangent changes.
solver.convergence_criterion = "incremental"
solver.report = True
# Prepare internal forces
reac = fem.Function(V)
virtual_work_form = fem.form(ufl.action(residual, reac))
# Load
U_x = np.linspace(0, 0.2 , 11)[1:]
# Plot
file_results = io.XDMFFile(
domain.comm,
f"out_MWE.xdmf",
"w",
)
file_results.write_mesh(domain)
for i in range(len(U_x)):
u_right.value = U_x[i]
print(U_x[i])
its, converged = solver.solve(u)
assert converged
u.x.scatter_forward()
file_results.write_function(u, i)
# Compute internal forces
u_right.value = 1.0
fem.set_bc(reac.vector, bcs)
print(f"Horizontal reaction Rx = {fem.assemble_scalar(virtual_work_form):.6f}")
However, when I apply it to my more complex problem (nonlinear material), the code runs but the reaction forces stay 0. What is the problem here?
I’ve provided the code below, but since it won’t run unless I provide a bunch more code. I hope that seeing the relevant parts of the code is sufficient to identify what my mistake is.
import numpy as np
from mpi4py import MPI
import ufl
from dolfinx import io, fem, mesh
from dolfinx.cpp.nls.petsc import NewtonSolver
from df_mat_utils.quadrature_map import QuadratureMap
from df_mat_utils.solvers import NonlinearMaterialProblem
from petsc4py.PETSc import ScalarType
import petsc4py.PETSc as PETSc
from constitutive_models import CustomHyperElastic_jax
"""
Mesh creation
"""
params = {
"mesh_element_size": 0.05,
"E": 3.13e3,
"nu": 0.37,
...
}
filename = f"dogbone_mesh.xdmf"
mesh_reader = io.XDMFFile(MPI.COMM_WORLD,filename,"r")
domain = mesh_reader.read_mesh(name="dogbone")
domain.topology.create_connectivity(domain.topology.dim - 1, domain.topology.dim)
print(f"Finished loading mesh...")
material = CustomHyperElastic_jax(mesh=domain, params=params)
order = 1
deg_quad = 2 * (order - 1)
shape = (2,)
V = fem.functionspace(domain, ("P", order, shape))
def strain(u):
return ufl.as_vector(
[
u[0].dx(0),
u[1].dx(1),
1 / np.sqrt(2) * (u[1].dx(0) + u[0].dx(1)),
]
)
du = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
u = fem.Function(V, name = "u") # total displacement
"""
Boundary conditions
"""
def bot_left_corner(x):
return np.logical_and(np.isclose(x[1], 0.0), np.isclose(x[0], 0.0))
def bot_right_corner(x):
return np.logical_and(np.isclose(x[1], 0.0), np.isclose(x[0], 1.0))
def left(x):
return np.isclose(x[0], 0)
def right(x):
return np.isclose(x[0], 1)
# Doing it like this, rather than locating geometrically directly, is necessary afaik to get only 1 dof per side.
fdim = domain.topology.dim-1
left_facets = mesh.locate_entities_boundary(domain, fdim, left)
right_facets = mesh.locate_entities_boundary(domain, fdim, right)
left_dofs = fem.locate_dofs_topological(V.sub(0), fdim, left_facets)
right_dofs = fem.locate_dofs_topological(V.sub(0), fdim, right_facets)
# bottom left:
bottom_value1 = fem.Function(V)
bottom_value1.x.array[:] = 0
left_corner_vertex = mesh.locate_entities_boundary(domain, 0, bot_left_corner)
dofs = fem.locate_dofs_topological((V, V), 0, left_corner_vertex)
# bottom right:
ME0y, _ = V.sub(1).collapse()
bottom_value2 = fem.Function(ME0y)
bottom_value2.x.array[:] = 0
right_corner_vertex = mesh.locate_entities_boundary(domain, 0, bot_right_corner)
right_dof = fem.locate_dofs_topological((V.sub(1), ME0y), 0, right_corner_vertex)
u_right = fem.Constant(domain,PETSc.ScalarType(1.))
# bottom left corner bottom right corner left x right x
bcs = [fem.dirichletbc(bottom_value1, dofs, V), fem.dirichletbc(bottom_value2, right_dof, V.sub(1)), fem.dirichletbc(0.0, left_dofs, V.sub(0)), fem.dirichletbc(u_right, right_dofs, V.sub(0))]
"""
Problem setup
"""
qmap = QuadratureMap(domain, deg_quad, material)
qmap.register_gradient(material.gradient_names[0], strain(u))
sig = qmap.fluxes["Stress"]
Res = ufl.dot(sig, strain(v)) * qmap.dx
Jac = qmap.derivative(Res, u, du)
problem = NonlinearMaterialProblem(qmap, Res, Jac, u, bcs)
newton = NewtonSolver(MPI.COMM_WORLD)
newton.rtol = 1e-6
newton.atol = 1e-7 # I use an additional absolute tolerance to avoid convergence issues when having loadsteps with no change in loading: the GP numerical inaccuracies keep giving slight tangent changes.
newton.convergence_criterion = "incremental"
newton.report = True
U_x = np.linspace(0, 0.2 , 11)[1:]
# Prep to obtain the load value
# Based on: https://bleyerj.github.io/comet-fenicsx/tips/computing_reactions/computing_reactions.html#
reac = fem.Function(V)
virtual_work_form = fem.form(ufl.action(Res, reac))
for i in range(len(U_x)):
u_right.value = U_x[i]
converged, nr_iters = problem.solve(newton)
# Get internal forces, to plot load-displacement curve
u_right.value = 1.0
fem.set_bc(reac.vector, bcs)
print(f"Horizontal reaction Rx = {fem.assemble_scalar(virtual_work_form):.6f}")
Thank you!