Consistency between displacement-controlled and load-controlled FEAs

Dear community/developers,

I am using FEniCSx 0.9 with Docker on Microsoft Windows.

In the MWE posted below, I perform a 2-D displacement-controlled FEA and compute the displacement U_disp_ctrl and stress sigma_disp_ctrl. What I am interested in is: imposing tractions obtained by t_N = ufl.dot(sigma_disp_ctrl, N) with N = ufl.FacetNormal(mesh) in a subsequent load-controlled FEA, and having a displacement field that is consistent with the one from the displacement-controlled FEA:

import dolfinx
from dolfinx import fem
import ufl
import dolfinx.fem.petsc
import dolfinx.nls.petsc
from mpi4py import MPI
import numpy as np

l_x = 10.0
l_y = 20.0
n_x = 10
n_y = 20

gamma_max = 2.0 # Prescribed extension in mm

mesh = dolfinx.mesh.create_rectangle(MPI.COMM_WORLD, np.array([[0, 0], [l_x, l_y]]), [n_x, n_y], cell_type=dolfinx.mesh.CellType.quadrilateral)

rank = mesh.comm.Get_rank()  # Get process rank (ID)
num_process = mesh.comm.Get_size()

tdim = mesh.topology.dim # Topological dimension
fdim = tdim - 1          # Facet dimension
gdim = mesh.geometry.dim


# BOUNDARY CONDITIONS
def boundary_bottom(x):
    return np.isclose(x[1], 0.0)
def boundary_top(x):
    return np.isclose(x[1], l_y)
def boundary_left(x):
    return np.isclose(x[0], 0.0)
def boundary_right(x):
    return np.isclose(x[0], l_x)

facets = []
for boundary_locator in [boundary_bottom, boundary_top, boundary_left, boundary_right]:
    facet = dolfinx.mesh.locate_entities_boundary(mesh, fdim, boundary_locator)
    facets.append(facet)

facets_indexes_dict = {
    "bottom": 1,
    "top": 2,
    "left": 3,
    "right": 4
}

num_facets = mesh.topology.index_map(fdim).size_local
markers = np.zeros(num_facets, dtype=np.int32)
for facet, facet_index in zip(facets, facets_indexes_dict.values()):
    markers[facet] = facet_index

facet_tags = dolfinx.mesh.meshtags(mesh, fdim, np.arange(num_facets, dtype=np.int32), markers)

V = fem.functionspace(mesh, ("Lagrange", 1, (gdim,)))

zero = fem.Function(V)
zero.x.array[:] = 0.0

gamma = fem.Constant(mesh, gamma_max)

u_ext = fem.Function(V)
u_ext_expression = fem.Expression(ufl.as_vector([0.0, gamma]), V.element.interpolation_points()) # u_ext on the top boundary

dofs_bottom_x = fem.locate_dofs_topological(V.sub(0), fdim, facet_tags.find(facets_indexes_dict["bottom"]))
dofs_bottom_y = fem.locate_dofs_topological(V.sub(1), fdim, facet_tags.find(facets_indexes_dict["bottom"]))
dofs_top_x = fem.locate_dofs_topological(V.sub(0), fdim, facet_tags.find(facets_indexes_dict["top"]))
dofs_top_y = fem.locate_dofs_topological(V.sub(1), fdim, facet_tags.find(facets_indexes_dict["top"]))

bc_bottom_x = fem.dirichletbc(zero.sub(0), dofs_bottom_x)
bc_bottom_y = fem.dirichletbc(zero.sub(1), dofs_bottom_y)
bc_top_x = fem.dirichletbc(zero.sub(0), dofs_top_x)
bc_top_y = fem.dirichletbc(u_ext.sub(1), dofs_top_y)

bc_all_disp_ctrl_list = [bc_bottom_y, bc_top_y, bc_bottom_x, bc_top_x]
bc_all_load_ctrl_list = [bc_bottom_y, bc_bottom_x, bc_top_x]
dofsets_known_reaction_list = [dofs_top_y]

# MATERIAL MODEL
U_disp_ctrl = fem.Function(V, name="U_disp_ctrl")
U_load_ctrl = fem.Function(V, name="U_load_ctrl")

E = dolfinx.default_scalar_type(1.0e4)
nu = dolfinx.default_scalar_type(0.3)
mu = fem.Constant(mesh, E / (2 * (1 + nu)))
lmbda = fem.Constant(mesh, E * nu / ((1 + nu) * (1 - 2 * nu)))
I = ufl.Identity(gdim)

sigma_disp_ctrl = 2.0 * mu * ufl.sym(ufl.grad(U_disp_ctrl)) + lmbda * ufl.tr(ufl.sym(ufl.grad(U_disp_ctrl))) * I
sigma_load_ctrl = 2.0 * mu * ufl.sym(ufl.grad(U_load_ctrl)) + lmbda * ufl.tr(ufl.sym(ufl.grad(U_load_ctrl))) * I

# RESIDUALS OF THE FINITE ELEMENT PROBLEMS
dU = ufl.TrialFunction(V)
deltaU = ufl.TestFunction(V)
dS = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tags)
dX = ufl.Measure("dx", domain=mesh)

F_int_disp_ctrl = ufl.inner(sigma_disp_ctrl, ufl.grad(deltaU)) * dX
F_int_load_ctrl = ufl.inner(sigma_load_ctrl, ufl.grad(deltaU)) * dX

resFE_disp_ctrl = F_int_disp_ctrl

N = ufl.FacetNormal(mesh)
t_N = ufl.dot(sigma_disp_ctrl, N)

F_ext_load_ctrl = ufl.inner(t_N, deltaU) * dS(facets_indexes_dict["top"])
# F_ext_load_ctrl =  resFE_disp_ctrl
resFE_load_ctrl = F_int_load_ctrl - F_ext_load_ctrl

# JACOBIANS OF THE FINITE ELEMENT RESIDUALS
DresFE_disp_ctrl_DU_disp_ctrl = ufl.derivative(resFE_disp_ctrl, U_disp_ctrl, dU)
DresFE_load_ctrl_DU = ufl.derivative(resFE_load_ctrl, U_load_ctrl, dU)

# DISPLACEMENT-CONTROLLED PROBLEM
problem_disp_ctrl = dolfinx.fem.petsc.NonlinearProblem(F=resFE_disp_ctrl, u=U_disp_ctrl, bcs=bc_all_disp_ctrl_list, J=DresFE_disp_ctrl_DU_disp_ctrl)
solver_disp_ctrl = dolfinx.nls.petsc.NewtonSolver(comm=mesh.comm, problem=problem_disp_ctrl)

# LOAD-CONTROLLED PROBLEM
problem_load_ctrl = dolfinx.fem.petsc.NonlinearProblem(F=resFE_load_ctrl, u=U_load_ctrl, bcs=bc_all_load_ctrl_list, J=DresFE_load_ctrl_DU)
solver_load_ctrl = dolfinx.nls.petsc.NewtonSolver(comm=mesh.comm, problem=problem_load_ctrl)

# SOLVE DISPLACEMENT-CONTROLLED PROBLEM
u_ext.interpolate(u_ext_expression)
solver_disp_ctrl.solve(U_disp_ctrl)

# SOLVE LOAD-CONTROLLED PROBLEM
solver_load_ctrl.solve(U_load_ctrl)

# L-2 NORM OF DIFFERENCE BETWEEN DISPLACEMENT FIELDS
U_diff = U_load_ctrl - U_disp_ctrl
U_diff_L2_norm_form = fem.form(ufl.inner(U_diff, U_diff) * dX)
l2_norm_U_diff = np.sqrt(mesh.comm.allreduce(fem.assemble_scalar(U_diff_L2_norm_form), op=MPI.SUM))
if rank == 0:
    print(f"\nL-2 norm of difference between displacement fields: {l2_norm_U_diff:.6e}")

Unfortunately, using the traction vector t_N I am not able to reach consistency:

L-2 norm of difference between displacement fields: 9.492333e-01

However, if I uncomment the line

# F_ext_load_ctrl =  resFE_disp_ctrl

and directly impose the residual of the displacement-controlled problem as the external force in the load-controlled FEA, this inconsistency is resolved:

L-2 norm of difference between displacement fields: 8.206510e-13

Any ideas and solutions to solve this problem are very much appreciated.

P.S.: I am aware of using a non-linear solver for a linear elasticity problem; it is just because of the original problem that I need to use the non-linear solver.

Ah, if I understand your question well, then you’ve run into one of the more elegant but challenging bits of math in finite element analysis: flux extraction.

Essentially, one should not evaluate the fluxes (i.e., traction in the case of elasticity/linear momentum equations) as simply the stress operator acting on the discrete solution. While this operator is “permitted” in volumes, regularity issues do not “permit” this operation on surfaces. While it may seem like the absolutely logical thing to do, in fact, it is not the flux that is balanced in the finite element scheme.

Which fluxes do balance in FE is not straightforward to see. It relates to the question in fluid mechanics “is FEM conservative?”, which has been debated heavily (and misconceptions still persist). The answer is yes; but only w.r.t. the appropriate definition of the flux (see Redirecting)

What you should not do

Evaluate the traction as \boldsymbol{\sigma}(\boldsymbol{u}^h)\cdot \boldsymbol{n}

What you should do

Post process the solution \boldsymbol{u}^h to extract the corresponding flux:

Define a new variational form, find t^h \in V^h s.t. \forall \, v \in V^h :

\int_{\partial\Omega} t^h\cdot v \, ds = L(v)-B(u^h,v)

Where V^h is your discrete space, without enforcement of the Dirichlet conditions. Once solved for t (which nodal valued should only be nonzero for nodes on the boundary), you can use this field in your subsequent computation in place of your t_N

I quite like this paper on this concept: https://doi.org/10.1115/1.4005187

p.s.to lighten the above computation, you can also constrain all nodes that do not live on the boundary with a dirichletbc

1 Like

Dear Stein,

Thank you very much for your clear explanations and for directly pointing out the problem!
As per your suggestion, I tried to find t_N out of a surface projection problem, for which the code is attached below:

import dolfinx
from dolfinx import fem
import ufl
import dolfinx.fem.petsc
import dolfinx.nls.petsc
from mpi4py import MPI
import numpy as np
from petsc4py import PETSc

l_x = 10.0
l_y = 20.0
n_x = 10
n_y = 20

gamma_max = 2.0 # Prescribed extension in mm

mesh = dolfinx.mesh.create_rectangle(MPI.COMM_WORLD, np.array([[0, 0], [l_x, l_y]]), [n_x, n_y], cell_type=dolfinx.mesh.CellType.quadrilateral)

rank = mesh.comm.Get_rank()  # Get process rank (ID)
num_process = mesh.comm.Get_size()

tdim = mesh.topology.dim # Topological dimension
fdim = tdim - 1          # Facet dimension
gdim = mesh.geometry.dim


# BOUNDARY CONDITIONS
def boundary_bottom(x):
    return np.isclose(x[1], 0.0)
def boundary_top(x):
    return np.isclose(x[1], l_y)
def boundary_left(x):
    return np.isclose(x[0], 0.0)
def boundary_right(x):
    return np.isclose(x[0], l_x)

facets = []
for boundary_locator in [boundary_bottom, boundary_top, boundary_left, boundary_right]:
    facet = dolfinx.mesh.locate_entities_boundary(mesh, fdim, boundary_locator)
    facets.append(facet)

facets_indexes_dict = {
    "bottom": 1,
    "top": 2,
    "left": 3,
    "right": 4
}

num_facets = mesh.topology.index_map(fdim).size_local
markers = np.zeros(num_facets, dtype=np.int32)
for facet, facet_index in zip(facets, facets_indexes_dict.values()):
    markers[facet] = facet_index

facet_tags = dolfinx.mesh.meshtags(mesh, fdim, np.arange(num_facets, dtype=np.int32), markers)

V = fem.functionspace(mesh, ("Lagrange", 1, (gdim,)))

zero = fem.Function(V)
zero.x.array[:] = 0.0

gamma = fem.Constant(mesh, gamma_max)

u_ext = fem.Function(V)
u_ext_expression = fem.Expression(ufl.as_vector([0.0, gamma]), V.element.interpolation_points()) # u_ext on the top boundary

dofs_bottom_x = fem.locate_dofs_topological(V.sub(0), fdim, facet_tags.find(facets_indexes_dict["bottom"]))
dofs_bottom_y = fem.locate_dofs_topological(V.sub(1), fdim, facet_tags.find(facets_indexes_dict["bottom"]))
dofs_top_x = fem.locate_dofs_topological(V.sub(0), fdim, facet_tags.find(facets_indexes_dict["top"]))
dofs_top_y = fem.locate_dofs_topological(V.sub(1), fdim, facet_tags.find(facets_indexes_dict["top"]))

bc_bottom_x = fem.dirichletbc(zero.sub(0), dofs_bottom_x)
bc_bottom_y = fem.dirichletbc(zero.sub(1), dofs_bottom_y)
bc_top_x = fem.dirichletbc(zero.sub(0), dofs_top_x)
bc_top_y = fem.dirichletbc(u_ext.sub(1), dofs_top_y)

bc_all_disp_ctrl_list = [bc_bottom_y, bc_top_y, bc_bottom_x, bc_top_x]
bc_all_load_ctrl_list = [bc_bottom_y, bc_bottom_x, bc_top_x]
dofsets_known_reaction_list = [dofs_top_y]

# MATERIAL MODEL
U_disp_ctrl = fem.Function(V, name="U_disp_ctrl")
U_load_ctrl = fem.Function(V, name="U_load_ctrl")

E = dolfinx.default_scalar_type(1.0e4)
nu = dolfinx.default_scalar_type(0.3)
mu = fem.Constant(mesh, E / (2 * (1 + nu)))
lmbda = fem.Constant(mesh, E * nu / ((1 + nu) * (1 - 2 * nu)))
I = ufl.Identity(gdim)

sigma_disp_ctrl = 2.0 * mu * ufl.sym(ufl.grad(U_disp_ctrl)) + lmbda * ufl.tr(ufl.sym(ufl.grad(U_disp_ctrl))) * I
sigma_load_ctrl = 2.0 * mu * ufl.sym(ufl.grad(U_load_ctrl)) + lmbda * ufl.tr(ufl.sym(ufl.grad(U_load_ctrl))) * I

# RESIDUALS OF THE FINITE ELEMENT PROBLEMS
dU = ufl.TrialFunction(V)
deltaU = ufl.TestFunction(V)
dS = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tags)
dX = ufl.Measure("dx", domain=mesh)

F_int_disp_ctrl = ufl.inner(sigma_disp_ctrl, ufl.grad(deltaU)) * dX
F_int_load_ctrl = ufl.inner(sigma_load_ctrl, ufl.grad(deltaU)) * dX

resFE_disp_ctrl = F_int_disp_ctrl

t_N = fem.Function(V, name="t_N")
F_ext_load_ctrl = ufl.inner(t_N, deltaU) * dS(facets_indexes_dict["top"])
# F_ext_load_ctrl =  resFE_disp_ctrl
resFE_load_ctrl = F_int_load_ctrl - F_ext_load_ctrl

# JACOBIANS OF THE FINITE ELEMENT RESIDUALS
DresFE_disp_ctrl_DU_disp_ctrl = ufl.derivative(resFE_disp_ctrl, U_disp_ctrl, dU)
DresFE_load_ctrl_DU = ufl.derivative(resFE_load_ctrl, U_load_ctrl, dU)

# DISPLACEMENT-CONTROLLED PROBLEM
problem_disp_ctrl = dolfinx.fem.petsc.NonlinearProblem(F=resFE_disp_ctrl, u=U_disp_ctrl, bcs=bc_all_disp_ctrl_list, J=DresFE_disp_ctrl_DU_disp_ctrl)
solver_disp_ctrl = dolfinx.nls.petsc.NewtonSolver(comm=mesh.comm, problem=problem_disp_ctrl)

# LOAD-CONTROLLED PROBLEM
problem_load_ctrl = dolfinx.fem.petsc.NonlinearProblem(F=resFE_load_ctrl, u=U_load_ctrl, bcs=bc_all_load_ctrl_list, J=DresFE_load_ctrl_DU)
solver_load_ctrl = dolfinx.nls.petsc.NewtonSolver(comm=mesh.comm, problem=problem_load_ctrl)

# COMPUTE TRACTION BY L2-PROJECTION OF THE RESIDUAL ACTION ON THE TOP BOUNDARY
b_proj_form = fem.form(ufl.action(resFE_disp_ctrl, deltaU))
b_proj = dolfinx.fem.Function(V)

a_proj_form = fem.form(ufl.inner(dU, deltaU) * dS(facets_indexes_dict["top"]))
A = dolfinx.fem.petsc.create_matrix(a_proj_form)
dolfinx.fem.petsc.assemble_matrix(A, a_proj_form)
A.assemble()

# MAKE A INVERTIBLE: SET DIAGONAL = 1 ON ROWS WHOSE DIAGONAL IS ZERO
diag = A.getDiagonal()
diag_arr = diag.getArray()
zero_rows = np.where(np.isclose(diag_arr, 0.0))[0]

A.setOption(PETSc.Mat.Option.NEW_NONZERO_ALLOCATION_ERR, False)
for i in zero_rows:
    A.setValues([int(i)], [int(i)], [1.0], addv=False)
A.assemble()
A.setOption(PETSc.Mat.Option.NEW_NONZERO_ALLOCATION_ERR, True)

ksp = PETSc.KSP().create()
ksp.setOperators(A)
ksp.setType("preonly")
ksp.getPC().setType("lu")
ksp.setFromOptions()

# SOLVE DISPLACEMENT-CONTROLLED PROBLEM
u_ext.interpolate(u_ext_expression)
solver_disp_ctrl.solve(U_disp_ctrl)

# SOLVE LOAD-CONTROLLED PROBLEM
b_proj.x.array[:] = 0.0
dolfinx.fem.petsc.assemble_vector(b_proj.x.petsc_vec, b_proj_form)
b_proj.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
b_proj.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD)
ksp.solve(b_proj.x.petsc_vec, t_N.x.petsc_vec)
t_N.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD)
solver_load_ctrl.solve(U_load_ctrl)

# L-2 NORM OF DIFFERENCE BETWEEN DISPLACEMENT FIELDS
U_diff = U_load_ctrl - U_disp_ctrl
U_diff_L2_norm_form = fem.form(ufl.inner(U_diff, U_diff) * dX)
l2_norm_U_diff = np.sqrt(mesh.comm.allreduce(fem.assemble_scalar(U_diff_L2_norm_form), op=MPI.SUM))
if rank == 0:
    print(f"\nL-2 norm of difference between displacement fields: {l2_norm_U_diff:.6e}")

When I run this with a single processor, thankfully, I achieve the expected consistency:
L-2 norm of difference between displacement fields: 1.628373e-13

However, I have the following problems:

  1. I could not solve the projection problem by defining dirichletbc. Instead, I make the matrix A invertible by setting the zero diagonals to 1.0.
  2. In parallel processing, the Newton solver for the load-controlled problem does not converge, which I think is related to the first point, i.e., the way A is modified.

Again, I appreciate any help with this problem; many thanks in advance!

Have you considered: Modify matrix diagonal -- dolfinx version for A.ident_zeros() - #3 by dokken

1 Like

Dear Dokken,

Thank you very much for your prompt and very helpful reply!
I had to adapt the source you suggested a bit to match my problem and my dolfinx version, but the overall approach worked very well.
Now the code runs in parallel too, and I achieve consistency.

For the reference of other people, I insert the working code below:

import dolfinx
from dolfinx import fem
import ufl
import dolfinx.fem.petsc
import dolfinx.nls.petsc
from mpi4py import MPI
import numpy as np
from petsc4py import PETSc

l_x = 10.0
l_y = 20.0
n_x = 10
n_y = 20

gamma_max = 2.0 # Prescribed extension in mm

mesh = dolfinx.mesh.create_rectangle(MPI.COMM_WORLD, np.array([[0, 0], [l_x, l_y]]), [n_x, n_y], cell_type=dolfinx.mesh.CellType.quadrilateral)

rank = mesh.comm.Get_rank()  # Get process rank (ID)
num_process = mesh.comm.Get_size()

tdim = mesh.topology.dim # Topological dimension
fdim = tdim - 1          # Facet dimension
gdim = mesh.geometry.dim


# BOUNDARY CONDITIONS
def boundary_bottom(x):
    return np.isclose(x[1], 0.0)
def boundary_top(x):
    return np.isclose(x[1], l_y)
def boundary_left(x):
    return np.isclose(x[0], 0.0)
def boundary_right(x):
    return np.isclose(x[0], l_x)

facets = []
for boundary_locator in [boundary_bottom, boundary_top, boundary_left, boundary_right]:
    facet = dolfinx.mesh.locate_entities_boundary(mesh, fdim, boundary_locator)
    facets.append(facet)

facets_indexes_dict = {
    "bottom": 1,
    "top": 2,
    "left": 3,
    "right": 4
}

num_facets = mesh.topology.index_map(fdim).size_local
markers = np.zeros(num_facets, dtype=np.int32)
for facet, facet_index in zip(facets, facets_indexes_dict.values()):
    markers[facet] = facet_index

facet_tags = dolfinx.mesh.meshtags(mesh, fdim, np.arange(num_facets, dtype=np.int32), markers)

V = fem.functionspace(mesh, ("Lagrange", 1, (gdim,)))

zero = fem.Function(V)
zero.x.array[:] = 0.0

gamma = fem.Constant(mesh, gamma_max)

u_ext = fem.Function(V)
u_ext_expression = fem.Expression(ufl.as_vector([0.0, gamma]), V.element.interpolation_points()) # u_ext on the top boundary

dofs_bottom_x = fem.locate_dofs_topological(V.sub(0), fdim, facet_tags.find(facets_indexes_dict["bottom"]))
dofs_bottom_y = fem.locate_dofs_topological(V.sub(1), fdim, facet_tags.find(facets_indexes_dict["bottom"]))
dofs_top_x = fem.locate_dofs_topological(V.sub(0), fdim, facet_tags.find(facets_indexes_dict["top"]))
dofs_top_y = fem.locate_dofs_topological(V.sub(1), fdim, facet_tags.find(facets_indexes_dict["top"]))

bc_bottom_x = fem.dirichletbc(zero.sub(0), dofs_bottom_x)
bc_bottom_y = fem.dirichletbc(zero.sub(1), dofs_bottom_y)
bc_top_x = fem.dirichletbc(zero.sub(0), dofs_top_x)
bc_top_y = fem.dirichletbc(u_ext.sub(1), dofs_top_y)

bc_all_disp_ctrl_list = [bc_bottom_y, bc_top_y, bc_bottom_x, bc_top_x]
bc_all_load_ctrl_list = [bc_bottom_y, bc_bottom_x, bc_top_x]
dofsets_known_reaction_list = [dofs_top_y]

# MATERIAL MODEL
U_disp_ctrl = fem.Function(V, name="U_disp_ctrl")
U_load_ctrl = fem.Function(V, name="U_load_ctrl")

E = dolfinx.default_scalar_type(1.0e4)
nu = dolfinx.default_scalar_type(0.3)
mu = fem.Constant(mesh, E / (2 * (1 + nu)))
lmbda = fem.Constant(mesh, E * nu / ((1 + nu) * (1 - 2 * nu)))
I = ufl.Identity(gdim)

sigma_disp_ctrl = 2.0 * mu * ufl.sym(ufl.grad(U_disp_ctrl)) + lmbda * ufl.tr(ufl.sym(ufl.grad(U_disp_ctrl))) * I
sigma_load_ctrl = 2.0 * mu * ufl.sym(ufl.grad(U_load_ctrl)) + lmbda * ufl.tr(ufl.sym(ufl.grad(U_load_ctrl))) * I

# RESIDUALS OF THE FINITE ELEMENT PROBLEMS
dU = ufl.TrialFunction(V)
deltaU = ufl.TestFunction(V)
dS = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tags)
dX = ufl.Measure("dx", domain=mesh)

F_int_disp_ctrl = ufl.inner(sigma_disp_ctrl, ufl.grad(deltaU)) * dX
F_int_load_ctrl = ufl.inner(sigma_load_ctrl, ufl.grad(deltaU)) * dX

resFE_disp_ctrl = F_int_disp_ctrl

t_N = fem.Function(V, name="t_N")
F_ext_load_ctrl = ufl.inner(t_N, deltaU) * dS(facets_indexes_dict["top"])
# F_ext_load_ctrl =  resFE_disp_ctrl
resFE_load_ctrl = F_int_load_ctrl - F_ext_load_ctrl

# JACOBIANS OF THE FINITE ELEMENT RESIDUALS
DresFE_disp_ctrl_DU_disp_ctrl = ufl.derivative(resFE_disp_ctrl, U_disp_ctrl, dU)
DresFE_load_ctrl_DU = ufl.derivative(resFE_load_ctrl, U_load_ctrl, dU)

# DISPLACEMENT-CONTROLLED PROBLEM
problem_disp_ctrl = dolfinx.fem.petsc.NonlinearProblem(F=resFE_disp_ctrl, u=U_disp_ctrl, bcs=bc_all_disp_ctrl_list, J=DresFE_disp_ctrl_DU_disp_ctrl)
solver_disp_ctrl = dolfinx.nls.petsc.NewtonSolver(comm=mesh.comm, problem=problem_disp_ctrl)

# LOAD-CONTROLLED PROBLEM
problem_load_ctrl = dolfinx.fem.petsc.NonlinearProblem(F=resFE_load_ctrl, u=U_load_ctrl, bcs=bc_all_load_ctrl_list, J=DresFE_load_ctrl_DU)
solver_load_ctrl = dolfinx.nls.petsc.NewtonSolver(comm=mesh.comm, problem=problem_load_ctrl)

# COMPUTE TRACTION BY L2-PROJECTION OF THE RESIDUAL ACTION ON THE TOP BOUNDARY
b_proj_form = fem.form(ufl.action(resFE_disp_ctrl, deltaU))
b_proj = dolfinx.fem.Function(V)

a_proj_form = fem.form(ufl.inner(dU, deltaU) * dS(facets_indexes_dict["top"]))

# DEFINE SPARSITY PATTERN FOR EVERYTHING NOT ASSOCIATED WITH THE FACETS WE ARE INTEGRATING OVER
boundary_blocks = fem.locate_dofs_topological(V, fdim, facet_tags.find(facets_indexes_dict["top"]))
imap = V.dofmap.index_map
all_blocks = np.arange(imap.size_local + imap.num_ghosts, dtype=np.int32)
inner_blocks = np.delete(all_blocks, boundary_blocks)

sp = fem.create_sparsity_pattern(a_proj_form)
sp.insert_diagonal(inner_blocks)
sp.finalize()

A = dolfinx.cpp.la.petsc.create_matrix(mesh.comm, sp)
A.setOption(A.Option.NEW_NONZERO_ALLOCATION_ERR, 1)
fem.petsc.assemble_matrix(A, a_proj_form)
A.assemble(A.AssemblyType.FLUSH)

one = fem.Function(V)
one.x.array[:] = 1.0

def set_bc_diag(A_):
    bc = fem.dirichletbc(one, inner_blocks)
    dolfinx.cpp.fem.petsc.insert_diagonal(A_, V._cpp_object, [bc._cpp_object], 1.)

set_bc_diag(A)
A.assemble()

ksp = PETSc.KSP().create()
ksp.setOperators(A)
ksp.setType("preonly")
ksp.getPC().setType("lu")
ksp.setFromOptions()

# SOLVE DISPLACEMENT-CONTROLLED PROBLEM
u_ext.interpolate(u_ext_expression)
solver_disp_ctrl.solve(U_disp_ctrl)

# PROJECT TRACTION
b_proj.x.array[:] = 0.0
dolfinx.fem.petsc.assemble_vector(b_proj.x.petsc_vec, b_proj_form)
b_proj.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
b_proj.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD)
ksp.solve(b_proj.x.petsc_vec, t_N.x.petsc_vec)
t_N.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD)

# SOLVE LOAD-CONTROLLED PROBLEM
solver_load_ctrl.solve(U_load_ctrl)

# L-2 NORM OF DIFFERENCE BETWEEN DISPLACEMENT FIELDS
U_diff = U_load_ctrl - U_disp_ctrl
U_diff_L2_norm_form = fem.form(ufl.inner(U_diff, U_diff) * dX)
l2_norm_U_diff = np.sqrt(mesh.comm.allreduce(fem.assemble_scalar(U_diff_L2_norm_form), op=MPI.SUM))
if rank == 0:
    print(f"\nL-2 norm of difference between displacement fields: {l2_norm_U_diff:.6e}")

which in a single-process run gives:

L-2 norm of difference between displacement fields: 1.628373e-13

and in parallel with 4 processes:

L-2 norm of difference between displacement fields: 4.190577e-14

Many thanks for the great help!

Sorry for bothering you again!
With this approach of setting the diagonals to one, do we still incur a cost of solving a big system of equations for all the dofs, or, is it only the cost of solving a small system for the specific boundary dofs?
In case it is the big system, could you please explain how one can only solve for the specific boundary dofs?

The matrix will still be big. However, as the diagonal is an identity, the cost of solving the system with these are very low.

1 Like