Slow code (function approximation in hyperelasticity)

Hello
I am solving a problem in hyperelasticity where I have a new term in strain energy function which is defined as:

image
In the above equation:

image

Where Ei represents the integral exponential function and C are the components of the right Cauchy stress tensor.
As the Ei function does not exist in the UFL, so I used function approximation based on this formula: image. Gamma is a constant and Ein represents entire function that is defined as: image . (The whole approximation could be found here. I implemented (and tested) this function approximation into my code. I also used trapezoidal rule for solving the first integral (G(\theta)). I am using 10 terms in order to approximate the Ei function and also 10 intervals for the integral calculation but this code is unbelievably slow although there is only 1 element!
Here is the full implementation:

from dolfin import *
import numpy as np

mesh = UnitCubeMesh.create(1,1,1,CellType.Type.hexahedron)

# Define boundary
class Left(SubDomain):
    def inside(self, x, on_boundary):
        return abs(x[0] - 0.0) < DOLFIN_EPS and on_boundary

class Right(SubDomain):
    def inside(self, x, on_boundary):
        return abs(x[0] - 1.0) < DOLFIN_EPS and on_boundary

Element1 = FiniteElement("CG", mesh.ufl_cell(), 1)
Element2 = VectorElement("CG", mesh.ufl_cell(), 2)

W_elem = MixedElement([Element1, Element2])

W = FunctionSpace(mesh, W_elem)
left = Left()
right = Right()

boundaries = MeshFunction('size_t', mesh, mesh.topology().dim()-1)
subdomains = MeshFunction('size_t', mesh, mesh.topology().dim())

boundaries.set_all(0)
left.mark(boundaries, 1)
right.mark(boundaries, 2)

dx = Measure('dx', domain=mesh, subdomain_data=subdomains, metadata={'quadrature_degree': 10})
ds = Measure('ds', domain=mesh, subdomain_data=boundaries, metadata={'quadrature_degree': 10})

w = Function(W)
p,u = split(w)    # p corresponds to hydrostatic pressure

d = u.geometric_dimension()
I = Identity(d)
F = I + grad(u)
C = F.T*F

Ic = tr(C)
J  = det(F)

def trapezoidal(f, a, b, n):

    h = float(b - a) / n
    s = 0.0
    s += f(a)/2.0
    for i in range(1, n):
        s += f(a + i*h)
    s += f(b)/2.0
    return s * h

c_1 = 10.
gamma = 0.5772156649

def Ei(X, k, b):

    LN = ln(X)

    for s in range(1, k + 1):

        fact = 1

        for i in range(1, s + 1):
            fact = fact * i

        b += (pow(-1., s + 1) * pow(-X, s) / (s * fact))

    return  gamma + LN - b

def G(theta):

    LMDA = ((pow(sin(theta), 2) * C[0, 0] + pow(cos(theta), 2) * C[1, 1] )) ** (0.5)

    return Ei(LMDA, 10, 0)

R = trapezoidal(G, -np.pi / 2., np.pi / 2., 10)

# Total potential energy
psi = (c_1 * (Ic - 3.) + R + p * (J - 1)) * dx

F1 = derivative(psi, w, TestFunction(W))
Jac = derivative(F1, w, TrialFunction(W))

bc_left = DirichletBC(W.sub(1), Constant((0., 0.0, 0.0)), boundaries, 1)
bc_right = DirichletBC(W.sub(1), Constant((0.5, 0.0, 0.0)), boundaries, 2)
bc_all = [bc_left,bc_right]

problem = NonlinearVariationalProblem(F1, w, bc_all, Jac)
solver = NonlinearVariationalSolver(problem)
solver.solve()
(p, u) = w.split(True)

Is there any way to speed up this code?
Thanks in advance for your help.

First, I doubt there is much benefit from using 10-th degree quadrature in space (i.e., the degree of dx and ds, not the integrals over \theta at each stress point) with P2–P1 Taylor–Hood elements, even if the constitutive model is quite complicated. You can get some savings by reducing to degree 4 quadrature in space.

However, the much bigger cost saving is from switching to the experimental TSFC form representation. TSFC is not installed by default, but you can install Jan Blechta’s FEniCS port of it like

pip3 install git+https://github.com/blechta/tsfc.git@2018.1.0
pip3 install git+https://github.com/blechta/COFFEE.git@2018.1.0
pip3 install git+https://github.com/blechta/FInAT.git@2018.1.0
pip3 install singledispatch networkx pulp

in my experience, this is usually much faster and more robust for quad/hex elements, and also gets around some bugs in the default UFLACS representation related to the use of quadrature function spaces. Once it’s installed you can invoke TSFC in your script with

parameters["form_compiler"]["representation"] = "tsfc"

right after importing DOLFIN. I also find that, for some complicated forms, I need to manually increase Python’s recursion limit when using TSFC. You can do that with

import sys
sys.setrecursionlimit(10000)
2 Likes

Thank you for the solution. After installing TSFC the speed of code increased significantly. However changing the quadrature degree from 10 to 4 affects the results a little bit.

I have 1 more question if you do not mind me asking. I changed the above code a little bit by adding 1 more term to the strain energy:

K = 1000000
psi = (c_1 * (Ic - 3.) + R + p * (J - 1)) * dx+ K * pow(ln(J), 2) * dx

and also I defined the boundary conditions in a pointwise manner. I want to fix the nodes on the left side in the x-direction and apply displacement in x-direction on the nodes on the right side. So instead of:

bc_left = DirichletBC(W.sub(1), Constant((0., 0.0, 0.0)), boundaries, 1)
bc_right = DirichletBC(W.sub(1), Constant((0.6, 0.0, 0.0)), boundaries, 2)
bc_all = [bc_left, bc_right]

I want to do this:

def point_left_1(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 0.0) < tol) and (abs(x[1] - 0) < tol) and (abs(x[2] - 0) < tol)

BC_POINT_left_1 = DirichletBC(W.sub(1), Constant((0., 0., 0.)), point_left_1, method="pointwise")

def point_left_2(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 0.0) < tol) and (abs(x[1] - 0) < tol) and (abs(x[2] - 1) < tol)

BC_POINT_left_2 = DirichletBC(W.sub(1), Constant((0., 0., 0.)), point_left_2, method="pointwise")

def point_left_3(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 0.0) < tol) and (abs(x[1] - 1) < tol) and (abs(x[2] - 0) < tol)

BC_POINT_left_3 = DirichletBC(W.sub(1), Constant((0., 0., 0.)), point_left_3, method="pointwise")


def point_left_4(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 0.0) < tol) and (abs(x[1] - 1) < tol) and (abs(x[2] - 1) < tol)

BC_POINT_left_4 = DirichletBC(W.sub(1), Constant((0., 0., 0.)), point_left_4, method="pointwise")

def point_right_1(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 1.0) < tol) and (abs(x[1] - 0) < tol) and (abs(x[2] - 0) < tol)

BC_POINT_right_1 = DirichletBC(W.sub(1), Constant((1.0, 0., 0.)), point_right_1, method="pointwise")

def point_right_2(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 1.0) < tol) and (abs(x[1] - 1.0) < tol) and (abs(x[2] - 0) < tol)

BC_POINT_right_2 = DirichletBC(W.sub(1), Constant((1., 0, 0.)), point_right_2, method="pointwise")

def point_right_3(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 1.0) < tol) and (abs(x[1] - 0) < tol) and (abs(x[2] - 1.) < tol)

BC_POINT_right_3 = DirichletBC(W.sub(1), Constant((1.0, 0., 0.)), point_right_3, method="pointwise")

def point_right_4(x, on_boundary):
    tol = DOLFIN_EPS
    return (abs(x[0] - 1.0) < tol) and (abs(x[1] - 1.) < tol) and (abs(x[2] - 1.) < tol)


BC_POINT_right_4 = DirichletBC(W.sub(1).sub(2), Constant((0.)), point_right_4, method="pointwise")

bc_all = [BC_POINT_left_1, BC_POINT_left_2, BC_POINT_left_3, BC_POINT_left_4 ,BC_POINT_right_1, BC_POINT_right_2, BC_POINT_right_3, BC_POINT_right_4]

But it diverges.I played with different parts of the code to figure it out. I found that If I use K = 100 or setting 'quadrature_degree': 2 it converges (However this is not what I want as the results are wrong). Is there any way to fix this convergence issue by any chance?
Thanks again!

I believe the K term in this formulation is misguided. Note that there is already a Lagrange multiplier, p, enforcing the constraint J=1. If that constraint is satisfied, \ln J = 0, so the K term has no influence on the exact solution to the PDE system. However, for large K, this term is trying to enforce J=1 pointwise in the discrete solution, whereas the Lagrange multiplier p is enforcing the constraint weighted by every test function in the \text{CG}_1 pressure space. Trying to enforce pointwise (near-)incompressibility in the discrete problem is typically unstable. In the linearized problem, this instability can be formalized in terms of the inf-sup/Babuška–Brezzi condition on the variational problem. For example, the Taylor–Hood element you are using is well-known to be inf-sup stable for the Stokes flow problem, which is formally identical to linearized incompressible isotropic elasticity. Typically, the pressure space with respect to which incompressibility is enforced needs to be lower-order than the displacement/velocity space. For more mathematical detail on inf-sup conditions, some thorough lecture notes can be found here.

A classic observation that you’ve re-discovered is that reduced quadrature seems to improve stability when penalizing compression pointwise. For some cases, it turns out that using reduced quadrature for just the dilational terms in a formulation (i.e., “selective reduced quadrature”) can be re-framed as discretizing pressure in a lower-order space (https://doi.org/10.1016/0045-7825(78)90005-1), which is the mathematical explanation for why it helps.

1 Like

Thank you for this clarification. Strain energy I want to use comes from here (section 4.1.2.17) and it is in this form: ss
I tried to simplify it in my question. It is absolutely right that in case of incompressibility, the ln(J) should be equal to zero. So this is also my question why they have it in the strain energy.
In another paper, they say : "To enforce incompressibiliy we took K = 100MPa"
Does it mean by keeping the last term in the strain energy function (Large K), we can remove the p * (J - 1)?

I believe that when you use a mixed formulation the term K/2 (\ln J)^2 or equivalently K/2(J-1)^2 shouldn’t be present. The constraint J=1 is taken care of by the term with the lagrange multiplier p(J-1). For more clarity, you can start with a “pure-displacement” strain energy (with the K term) and take a partial legendre transform of that to arrive at a mixed formulation.

Edit: You can keep the K/2(\ln J)^2 term and instead of using “Taylor-Hood” elements use higher order lagrange elements (p=2,3) to prevent volumetric locking and (may) use K=1000. Among other things, this might be a good check for your displacements coming from a mixed formulation

1 Like