Question about an issue with assemble() and project() for equations defined as UFL Forms

hello.:wave: It’s been a while since I’ve posted.

I recently posted to address a question I had while exploring legacy DOLFIN a bit more.

I’m having an issue when I’m trying to get the strain energy in the linear elasticity example provided by this link, and when I add the equation defined by UFL Form to the entire mesh domain with both the result obtained by assemble() and the vector element values obtained by project(), the values are different, and I’m wondering why the difference occurs and how I can reduce the difference.

In the FEA process, I only changed the parameter values in the example code as shown below.
(3D to 2D, number of mesh elements and f adjustment)

from fenics import *

# Scaled variables
L = 1; W = 0.2
mu = 1
rho = 1
delta = W/L
gamma = 0.4*delta**2
beta = 1.25
lambda_ = beta
g = gamma

# Create mesh and define function space
mesh = RectangleMesh(Point(0, 0), Point(L, W), 2, 2)
V = VectorFunctionSpace(mesh, 'P', 1)

# Define boundary condition
def clamped_boundary(x, on_boundary):
    return on_boundary and x[0] < 1e-14

bc = DirichletBC(V, Constant((0, 0)), clamped_boundary)

# Define strain and stress
def epsilon(u):
    return 0.5*(grad(u) + nabla_grad(u).T)
    #return sym(nabla_grad(u))

def sigma(u):
    return lambda_*div(u)*Identity(d) + 2*mu*epsilon(u)

# Define variational problem
u = TrialFunction(V)
d = u.geometric_dimension()  # space dimension
v = TestFunction(V)
f = Constant((0, -100))  # f = Constant((0, -rho*g))
T = Constant((0, 0))
a = inner(sigma(u), epsilon(v))*dx
L = dot(f, v)*dx + dot(T, v)*ds

# Compute solution
u = Function(V)
solve(a == L, u, bc)

And the strain energy function expression was defined in UFL format as follows.
Strain energy equation: \psi(\mathbf{u})=\frac{\lambda}{2}\{\textrm{tr}(\epsilon(\mathbf{u}))\}^{2}\ + \mu\textrm{tr}(\epsilon(\mathbf{u})^{2})
(\epsilon(\mathbf{u})=\frac{1}{2}(\nabla\mathbf{u}\ + (\nabla\mathbf{u})^{T})=\rm{sym}(\nabla\mathbf{u}))

def strain_energy(u):
    return 0.5*lambda_*(tr(epsilon(u))**2) + mu*tr(epsilon(u)*epsilon(u))

When trying to find the total strain energy value for a given mesh domain, the result obtained with assemble() and "dx", (type: ufl.algebra.Sum) was different from the result obtained when sum() of the vector results project() in DG0 space as shown below.
(I’d actually like to see the UFL Form data, but it’s hidden, so I can’t…)

print(f"type of strain energy: {type(strain_energy(u))}")

# same as print(f"strain energy using assemble() quad: {assemble(strain_energy(u)*Measure("dx", mesh, metadata={"quadrature_degree": 0}))}\n")
print(f"strain energy using assemble(): {assemble(strain_energy(u)*dx)}\n")
print(f"strain energy using project() to DG0: {sum(project(strain_energy(u), FunctionSpace(mesh, "DG", 0)).vector()[:])}")

type of strain energy: <class 'ufl.algebra.Sum'>
strain energy using assemble(): -0.008689827046779201

strain energy using project() to DG0: -0.3475930818711679

(In the case of dx used in assemble(), there is a difference even after adjusting the "quadrature_degree" of metadata, so my guess is that it’s an interpolation error when project() with DG0.)

My questions are
A) What mesh points(or functionspace?!) is that output obtained with assemble() using "dx" based on? I would like to know the underlying reason for the difference between these two values. Am I correct in my guess above as to why the error occurs?

B) If my guess is correct, I believe that the result obtained with assemble() and "dx" is more reliable value. If so, I would like to get the result obtained using project(): (value: -0.3475930818711679) to be the same value as the result using assemble(): (value: -0.008689827046779201), how can I do that?
(In fact, I wanted to get the strain energy vector array term corresponding to each element).

Any opinions are welcome. I can’t resolve my question. :joy: :pray:

If you use assemble(strain_energy(u)*dx)) you compute the integral of the strain energy over the whole domain. This will evaluate the strain energy at quadrature points of each cell and accumulate the results across all cells of the domain.

With your second command, you compute the strain-energy of u as piecewise constant function using a projection. When you sum these up, you are not taking the cell area into account, and therefore you get the wrong result.

i.e. if you use:

DG_strain = project(strain_energy(u), FunctionSpace(mesh, "DG", 0))
cell_sizes = project(CellVolume(mesh), FunctionSpace(mesh, "DG", 0))
# same as print(f"strain energy using assemble() quad: {assemble(strain_energy(u)*Measure("dx", mesh, metadata={"quadrature_degree": 0}))}\n")
import numpy as np

print(f"strain energy using assemble(): {assemble(strain_energy(u)*dx)}\n")
print(
    f"strain energy using project() to DG0: {np.dot(cell_sizes.vector().get_local(), DG_strain.vector().get_local())}\n"
)

you get:

strain energy using assemble(): -0.008689827046779201

strain energy using project() to DG0: -0.0086898270467792
1 Like

Aha!! :joy:
I hadn’t realised that using project(), it would divide the cell volume.

Thank you, @dokken! that was very helpful! :raised_hands: