Mechanical equilibrium of a single finite element

Dear community. I would like to know if there is way to compute the nodal forces on the nodes of one finite element that mechanically balance the mechanical deformation of such an element, i.e., such that the virtual power of such nodal forces equals the strain energy of the element. In other words, I wonder how I could implement the following integral,

\mathbf{f}^{n}=\int_{\mathcal{B}e}\sigma^t \cdot\nabla N^n dV

where \nabla N^n dV Is the gradient of the shape function of the node n; and \mathcal{B}e the volume domain of the single element.

To this end, I guess that I need a way to locate an individual finite element within a larger mesh, to obtain the derivative of the nodal shape functions, and to compute the integral on that element. Then, do the same for the rest of nodes of the element.

Many thanks.

Assuming uh is the deformation vector

def sigma(u):
    return lmbda * ufl.nabla_div(u) * Identity(len(u)) + 2*mu*epsilon(u)

v = TestFunction(V)
f = assemble(inner(sigma(uh), grad(v))*dx)

Thank you, dokken.
How can I get the displacement vector uh?
I guess it must contain only the solved nodal degrees of freedom. I have tried u.vector().array() using FEniCSx, but I get the error “‘petsc4py.PETSc.Vec’ object is not callable”.
Also, which Function from dolfinx.fem.assemble should I use?

I would assume that you are solving a linear elasticity problem to get your deformation field uh, otherwise how would you define \sigma?

Im not sure what you mean by this.

You did not state what version of DOLFIN you were running, so I was assuming the legacy version. The equivalent for DOLFINx would be

def sigma(u):
    return lmbda * ufl.nabla_div(u) * ufl.Identity(len(u)) + 2*mu*epsilon(u)

v = ufl.TestFunction(V)
f = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(ufl.inner(sigma(uh), ufl.grad(v))*ufl.dx))
print(f.get_local())

No, I work with finite strains and the deformation gradient (\mathbf{F}). More precisely, it is not the Cauchy stress tensor the one I want to use, but the Eshelby one, but this does not affect the implementation. I define u as a Function from a Function Space, and then, \mathbf{F} = \mathbf{I} + \text{ufl.grad}(\mathbf{u}). With this implementation, I aim to compute the material (configurational) nodal forces.
Hence, is this u solution function what I should use in the assemble you proposed?

Since you wrote uh, I am not sure if that is simply my Function \mathbf{u} already solved, or just the nodal d.o.f.s ordered in a different manner.

That for the assemble works, thanks. However, with print(f.get_local()) I get the following error: AttributeError: 'petsc4py.PETSc.Vec' object has no attribute 'get_local'

Sorry, mixing legacy and dolfinx, replace

With f.array()

As you didn’t post a u in your initial code, I used uh to illustrate the numerical displacement function (stemming from one process or another). uh should therefore be replaced with your u

When I do this:

i.e.,
f = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(ufl.inner(eshelby_material_stress(F), ufl.grad(v_u))*dx))
print(f.array())

I still get this error: TypeError: 'numpy.ndarray' object is not callable

print(f.array) then.

This error states that f.array is a numpy array, and does not have a call function, i.e. f.array().

This seems to work. Thank you for the explanations.

Then, the only thing I still need to wrap up this implementation would be to get a vector with the coordinates of the nodes for which the nodal forces in the vector f.array correspond to. I would appreciate your help again here.

V.tabulate_dof_coordinates(), see

Thanks. This works.

And for the coordinates of the d.o.f.s in the deformed configuration?

You can just sum the coordinate of the unperturbed state with the deformation (as you can get the displacement at every node in the exact same way).

Now everything seems to be working. Many thanks.

One additional concern related to the nodal forces:
How could I write the forces on the nodes, i.e., the vector \mathbf{f}, to my .xdmf file to visualize them on Paraview?

Rewrite this to:

def sigma(u):
    return lmbda * ufl.nabla_div(u) * ufl.Identity(len(u)) + 2*mu*epsilon(u)

v = ufl.TestFunction(V)
f = dolfinx.fem.Function(V)
dolfinx.fem.petsc.assemble_vector(f.vector, dolfinx.fem.form(ufl.inner(sigma(uh), ufl.grad(v))*ufl.dx))

and then save f to file as you would usually do with DOLFINx

Hello everyone,

I greatly appreciate the assistance provided in this discussion—it has been quite helpful. Thank you for that! However, I am still encountering a related issue that I am unable to resolve on my own.

My objective is to compute the volume integral of a tensor-valued function within a mixed function space. I am considering three approaches, all of which, in my opinion, should yield similar results:

  1. Utilizing the divergence theorem and performing a surface integral over the boundary of the domain.
  2. Conducting a volume integral and initially applying the ufl.div operation to the tensor.
  3. Calculating the nodal forces as previously discussed and summing them up.

My preference lies with the third option, but I am unable to achieve the anticipated results (which I obtain for options 1 and 2). Additionally, I am struggling to properly configure option 3 so that the results are at least unaffected by the number of processes.

I have also prepared a minimal working example for which it is feasible to compute the correct result. The setup involves a 1x1x1 cube with relatively straightforward fields, from which I derive the tensor-valued function and subsequently attempt to compute the integral using all three options.

My primary concern pertains to how to correctly establish option 3. Any insights you could provide would be greatly appreciated.

Best regards, Alex

import dolfinx as dlfx
from mpi4py import MPI
import ufl
import numpy as np
import os
from  dolfinx.cpp.la import InsertMode

script_path = os.path.dirname(__file__)
comm = MPI.COMM_WORLD

N = 16

domain = dlfx.mesh.create_unit_cube(comm,N,N,N,cell_type=dlfx.mesh.CellType.hexahedron)


Ve = ufl.VectorElement("Lagrange", domain.ufl_cell(), 1) 
Te = ufl.FiniteElement("Lagrange", domain.ufl_cell(), 1) 
W = dlfx.fem.FunctionSpace(domain, ufl.MixedElement([Ve, Te]))

w =  dlfx.fem.Function(W)

w.sub(0).sub(0).interpolate(lambda x: x[0])
w.sub(0).sub(1).interpolate(lambda x: x[1])
w.sub(0).sub(2).interpolate(lambda x: x[2])
w.sub(1).interpolate(lambda x: x[0])

u,s = w.split()


def tensor_function_of_u_s(u,s):
    return ufl.as_tensor([[u[0]*s, 0, 0],
                          [u[1], 0, 0],
                          [u[2], 0, 0]
                          ])

tensor = tensor_function_of_u_s(u,s)

##1. from surface integral ###############################
# returns the expected value 1 and is independent on number of processes  
def surface_integral_of_tensor(tensor, n: ufl.FacetNormal, ds: ufl.Measure = ufl.ds):
    Jx = (tensor[0,0]*n[0]+tensor[0,1]*n[1]+tensor[0,2]*n[2])*ds
    Jxa = dlfx.fem.assemble_scalar(dlfx.fem.form(Jx))
    Jy = (tensor[1,0]*n[0]+tensor[1,1]*n[1]+tensor[1,2]*n[2])*ds
    Jya = dlfx.fem.assemble_scalar(dlfx.fem.form(Jy))
    Jz = (tensor[2,0]*n[0]+tensor[2,1]*n[1]+tensor[2,2]*n[2])*ds
    Jza = dlfx.fem.assemble_scalar(dlfx.fem.form(Jz))
    return Jxa, Jya, Jza

n = ufl.FacetNormal(domain)
Jx_s, Jy_s, Jz_s = surface_integral_of_tensor(tensor,n)

##2. from volume integral ###############################
# returns the expected value 1 and is independent on number of processes 
def volume_integral_of_tensor(tensor, dx: ufl.Measure = ufl.dx):
    Jxa = dlfx.fem.assemble_scalar(dlfx.fem.form( ( ( ufl.div(tensor)[0] ) * dx ) ))
    Jya = dlfx.fem.assemble_scalar(dlfx.fem.form( ( ( ufl.div(tensor)[1] ) * dx ) ))
    Jza = dlfx.fem.assemble_scalar(dlfx.fem.form( ( ( ufl.div(tensor)[2] ) * dx )))
    return Jxa, Jya, Jza
Jx_v, Jy_v, Jz_v = volume_integral_of_tensor(tensor)


##3. from nodal values ###############################
# fails to return the expected value 1 and is dependent on number of processes 
dw = ufl.TestFunction(W)
du, dv = ufl.split(dw)
J_nodal_vector : dlfx.la.Vector = dlfx.fem.assemble_vector(dlfx.fem.form( ufl.inner(-tensor, ufl.grad(du))*ufl.dx ))
# J_nodal_vector.scatter_reverse(InsertMode.add)

# cast into function
temp = dlfx.fem.Function(W)
temp.sub(0).x.array[:] = J_nodal_vector.array
J_nodal_function = temp.sub(0).collapse()
Jx_from_sum_of_nodal_vectors = np.sum(J_nodal_function.sub(0).x.array)

###########################################################
print('Jx from surface integral:  {0:.4e}\nJx from volume integral: {1:.4e}\nJx from nodal vectors: {2:.4e}'.format(Jx_s, Jx_v, Jx_from_sum_of_nodal_vectors))

Are you sure that the integration and divergence theorem makes sense for a discrete function space, as the following initial nodal force calculation works:

import dolfinx as dlfx
from mpi4py import MPI
import ufl
import numpy as np
import os
from dolfinx.cpp.la import InsertMode

script_path = os.path.dirname(__file__)
comm = MPI.COMM_WORLD

N = 16

domain = dlfx.mesh.create_unit_cube(
    comm, N, N, N, cell_type=dlfx.mesh.CellType.hexahedron
)


Ve = ufl.VectorElement("Lagrange", domain.ufl_cell(), 1)
Te = ufl.FiniteElement("Lagrange", domain.ufl_cell(), 1)
W = dlfx.fem.FunctionSpace(domain, ufl.MixedElement([Ve, Te]))

w = dlfx.fem.Function(W)

w.sub(0).sub(0).interpolate(lambda x: x[0])
w.sub(0).sub(1).interpolate(lambda x: x[1])
w.sub(0).sub(2).interpolate(lambda x: x[2])
w.sub(1).interpolate(lambda x: x[0])

u, s = w.split()


def tensor_function_of_u_s(u, s):
    return ufl.as_tensor([[u[0] * s, 0, 0], [u[1], 0, 0], [u[2], 0, 0]])


tensor = tensor_function_of_u_s(u, s)


##1. from surface integral ###############################
# returns the expected value 1 and is independent on number of processes
def surface_integral_of_tensor(tensor, n: ufl.FacetNormal, ds: ufl.Measure = ufl.ds):
    Jx = (tensor[0, 0] * n[0] + tensor[0, 1] * n[1] + tensor[0, 2] * n[2]) * ds
    Jxa = dlfx.fem.assemble_scalar(dlfx.fem.form(Jx))
    Jy = (tensor[1, 0] * n[0] + tensor[1, 1] * n[1] + tensor[1, 2] * n[2]) * ds
    Jya = dlfx.fem.assemble_scalar(dlfx.fem.form(Jy))
    Jz = (tensor[2, 0] * n[0] + tensor[2, 1] * n[1] + tensor[2, 2] * n[2]) * ds
    Jza = dlfx.fem.assemble_scalar(dlfx.fem.form(Jz))
    return (
        MPI.COMM_WORLD.allreduce(Jxa, op=MPI.SUM),
        MPI.COMM_WORLD.allreduce(Jya, op=MPI.SUM),
        MPI.COMM_WORLD.allreduce(Jza, op=MPI.SUM),
    )


n = ufl.FacetNormal(domain)
Jx_s, Jy_s, Jz_s = surface_integral_of_tensor(tensor, n)


##2. from volume integral ###############################
# returns the expected value 1 and is independent on number of processes
def volume_integral_of_tensor(tensor, dx: ufl.Measure = ufl.dx):
    Jxa = dlfx.fem.assemble_scalar(dlfx.fem.form(((ufl.div(tensor)[0]) * dx)))
    Jya = dlfx.fem.assemble_scalar(dlfx.fem.form(((ufl.div(tensor)[1]) * dx)))
    Jza = dlfx.fem.assemble_scalar(dlfx.fem.form(((ufl.div(tensor)[2]) * dx)))
    return (
        MPI.COMM_WORLD.allreduce(Jxa, op=MPI.SUM),
        MPI.COMM_WORLD.allreduce(Jya, op=MPI.SUM),
        MPI.COMM_WORLD.allreduce(Jza, op=MPI.SUM),
    )


Jx_v, Jy_v, Jz_v = volume_integral_of_tensor(tensor)


##3. from nodal values ###############################
# fails to return the expected value 1 and is dependent on number of processes
V, _ = W.sub(0).collapse()
du = ufl.TestFunction(V)
J_nodal_vector = dlfx.fem.Function(V)

boundary_facets = dlfx.mesh.exterior_facet_indices(domain.topology)
bc_dofs = dlfx.fem.locate_dofs_topological(V, domain.topology.dim - 1, boundary_facets)
bc_zero = dlfx.fem.Function(V)
bc = dlfx.fem.dirichletbc(bc_zero, bc_dofs)
form_0 = dlfx.fem.form(ufl.inner(-tensor, ufl.grad(du)) * ufl.dx)
form_0 = dlfx.fem.form(ufl.dot(ufl.div(tensor), du) * ufl.dx)
dlfx.fem.assemble_vector(J_nodal_vector.x.array, form_0)
J_nodal_vector.x.scatter_reverse(InsertMode.add)
J_nodal_vector.x.scatter_forward()


num_dofs_local = V.dofmap.index_map.size_local * V.dofmap.index_map_bs
J_nodal = np.zeros(3, dtype=np.float64)
for i in range(3):
    J_sub = J_nodal_vector.sub(i).collapse()
    sub_imap = J_sub.function_space.dofmap
    num_dofs_local = sub_imap.index_map.size_local * sub_imap.index_map_bs
    J_nodal[i] = MPI.COMM_WORLD.allreduce(
        np.sum(J_sub.x.array[:num_dofs_local]), op=MPI.SUM
    )
print(J_nodal)

###########################################################
print(
    "Jx from surface integral:  {0:.4e}\nJx from volume integral: {1:.4e}\nJx from nodal vectors: {2:.4e}".format(
        Jx_s, Jx_v, J_nodal[0]
    )
)

Thank you for your prompt reply. The level of support you and your colleagues provide is amazing.

The implementation you have provided is very helpful and works perfectly for the MWE. I also found the reduction to global values quite interesting.

Regarding your question: I can accept that the divergence theorem does not necessarily hold perfectly on a discrete function space. However, it is common to compute the nodal integral values as I outlined. See for example the following resource where the tensor function is acutally the Eshelby stress tensor Sigma (as in the original post as I believe):

In my particular situation (not the MWE) the tensor quantity displays steep gradients, which I feel makes partial integration particularly necessary in order to avoid derivatives of the tensor quantity for better accuracy.

In fact, I only obtain reasonable results for the surface integral variant. The solution proposed by you still relies on computing the div of the tensor quantity. So - although it now agrees with the second variant I propose - it is still off when compared to the surface integral. I hope to remedy this by variant 3 if it is at all possible in dolfinx