Issues with a viscoelastic problem

Hi,

I am solving a finite viscoelastic problem. The second PK stress tensor for the material is given as
image

where C1e and C1oe are material stiffness parameters, C is right Cauchy-Green deformation tensor. C can be decomposed into an elastic, Ce, and an inelastic part, Ci. So in the equation above Ci is the inelastic part of C. It follows a time-dependent equation as follows
image
where image is the viscosity of the material and (.)^D is the deviatoric part of the tensor.

For small strain, the solution of this model should match with that of the standard linear solid (SLS) model. Here is my code to solve this problem. When the loading rate is very high during an uniaxial loading in the x direction, i.e., stretching is done in T=0.1 sec, Fenics solution matches with the analytical solution of SLS model. However, for a slower loading rate, say stretching in T =1 sec, Fenics solution does not match with analytical solution.

I believe, there is something wrong in the way I update my variables. Could anyone please help me to identify the error.

Here is my code



from dolfin import *
import matplotlib.pyplot as plt
import os
import dolfin as dlf
import numpy as np
import math 


# Optimization options for the form compiler
parameters["form_compiler"]["cpp_optimize"] = True
parameters["form_compiler"]["representation"] = "uflacs"


def geometry_3d():
    #mesh
    mesh = UnitCubeMesh(6, 6, 6)
    boundary_parts = MeshFunction('size_t', mesh, mesh.topology().dim()-1)
    x0 = AutoSubDomain(lambda x: near(x[0], 0))
    x1 = AutoSubDomain(lambda x: near(x[0], 1))
    y0 = AutoSubDomain(lambda x: near(x[1], 0))
    z0 = AutoSubDomain(lambda x: near(x[2], 0))
    x0.mark(boundary_parts, 1)
    y0.mark(boundary_parts, 2)
    z0.mark(boundary_parts, 3)
    x1.mark(boundary_parts, 4)
    return boundary_parts


# Kinematics
def CalculatePKStress(u,pressure,Ci,ce,coe,eta):
    I = Identity(V.mesh().geometry().dim())  ### Identity tensor
    F = I + grad(u)          ### Deformation gradient
    J = det(F)
    C = F.T*F               ### Deformation tensor
    T = 2*ce*I+2*coe * inv(Ci)-pressure * inv(C) ### second PK stress tensor
    return T, (J-1)


# slope of Ci, inelastic strain tensor
def CalcGradCi(u_old,Ci_old,coe,eta):
    I = Identity(V.mesh().geometry().dim()) 
    F_old = I + grad(u_old)
    C_old = F_old.T*F_old
    Toe_old= 2*coe*inv(Ci_old)
    slope =(2/eta)*C_old*dev(Toe_old)*Ci_old
    return slope


# Create mesh and define function space ============================================
facet_function = geometry_3d()
mesh = facet_function.mesh()
gdim = mesh.geometry().dim()
dx = Measure("dx")
print('Number of nodes: ',mesh.num_vertices())
print('Number of cells: ',mesh.num_cells())

# Limit quadrature degree
dx = dx(degree=4)

# Time stepping parameters
Tend = 1 # simulation time
Nsteps = 10 # number of steps
time = np.linspace(0, Tend, Nsteps+1)
dt = time[1]-time[0]

# material parameters
ce, coe, eta = 2, 2, 13.34 #3D model parameters
E1, E2, eta_sls = 12, 12, 20 # SLS model parameters
tau_sigma, tau_eps = (eta_sls/E1)*(1+E1/E2), eta_sls/E2 # SLS model time constant
eps_dot = 0.01/Tend # applied strain rate to stretch the cube in x direction


# Create function space
element_v = VectorElement("P", mesh.ufl_cell(), 1) 
element_s = FiniteElement("P", mesh.ufl_cell(), 1) 
mixed_element = MixedElement([element_v, element_s]) 
V = FunctionSpace(mesh, mixed_element)


# Define test and trial functions
dup = TrialFunction(V)
_u, _p = TestFunctions(V)

_u_p = Function(V)
u, p = split(_u_p)

_u_p_old = Function(V)
u_old, p_old = split(_u_p_old)


# define Ci_old
WF = TensorFunctionSpace(mesh, "DG", degree=0)
I = Identity(WF.mesh().geometry().dim())
initial_value = project(I, WF)
Ci_old = interpolate(initial_value, WF) 


# Create tensor function spaces for stress and stretch output
defGrad = Function(WF, name='F')
PK_stress = Function(WF, name='PK1/2')


# Define Dirichlet boundary
bc0 = DirichletBC(V.sub(0).sub(0), Constant(0.), facet_function, 1)
bc1 = DirichletBC(V.sub(0).sub(1), Constant(0.), facet_function, 2)
bc2 = DirichletBC(V.sub(0).sub(2), Constant(0.), facet_function, 3)

tDirBC = Expression(('eps_d*time_'),eps_d = eps_dot, time_=0.0 , degree=0)
bc3 = DirichletBC(V.sub(0).sub(0), tDirBC, facet_function, 4) 
bcs = [bc0,bc1,bc2,bc3]


# # Total potential energy
I = Identity(V.mesh().geometry().dim())
pkstrs, hydpress =  CalculatePKStress(u,p,Ci_old,ce,coe,eta)
F_cur = I + grad(u)
F1 = inner(dot(F_cur,pkstrs), grad(_u))*dx  # weak form
F2 = hydpress*_p*dx                         # weak form for pressue term                     
F = F1+F2
J = derivative(F, _u_p,dup)

# Create nonlinear variational problem and solve
problem = NonlinearVariationalProblem(F, _u_p, bcs=bcs, J=J)
solver = NonlinearVariationalSolver(problem)
solver.parameters['newton_solver']['relative_tolerance'] = 1e-6
solver.parameters['newton_solver']['linear_solver'] = 'mumps'



# Save solution in VTK format
file_results = XDMFFile("./Results/TestUniaxialLoading/Uniaxial.xdmf")
file_results.parameters["flush_output"] = True
file_results.parameters["functions_share_mesh"] = True

stretch_vec = np.zeros((Nsteps+1,1))
stress_vec =  np.zeros((Nsteps+1,1))
stress_ana =  np.zeros((Nsteps+1,1))

for i in range(len(time)):
    tt = time[i]
    print(i, 'time: ', tt)

    # update time-dependent variable
    tDirBC.time_ = tt
    
    # Ci calculation
    slope = CalcGradCi(u_old,Ci_old,coe,eta)
    Ci = Ci_old+Constant(dt)*slope
    Ci_proj = project(Ci, WF)
    Ci_old.vector()[:] = Ci_proj.vector()
    
    # solve and save disp
    solver.solve()
    
    # update old variables
    _u_p_old.assign(_u_p)
    
    # Extract solution components
    u_print, p_print = _u_p.split()

    # get stretch at a point for plotting
    point = (0.5,0.5,0.5) # pick a point for plotting stretch and stress
    DF = I + grad(u_print)
    defGrad.assign(project(DF, WF))
    stretch_vec[i] = defGrad(point)[0]

    # get stress at a point for plotting
    PK_s,tempPress = CalculatePKStress(u_print,p_print,Ci,ce,coe,eta)
    PK_stress.assign(project(PK_s, WF))
    stress_vec[i] = PK_stress(point)[0]


    
# get analytical solution for SLS model for uniaxial stretching
for i in range(len(time)):
    stress_ana[i] = E1*eps_dot*(tau_sigma-tau_eps)*(1-np.exp(-time[i]/tau_eps))+E1*(stretch_vec[i]-1 ) 

# plot solutions to compare Fenics and analytical solution
f = plt.figure(figsize=(12,6))
plt.plot(stretch_vec, stress_vec,'r-')
plt.plot(stretch_vec, stress_ana,'k.')

This code produces results like this.

where the red line is the Fenics solution and the black dots are the analytical solution.

1 Like

Are you sure about this choice of function spaces for the displacement-pressure ? The lowest order Taylor-hood element is \mathbb{P}_k-P_{k-1}. The standard approach would be to run a hyperelastic problem, test convergence and then move to viscoelasticity

Yes, I have tried a hyperelastic problem. For a hyperelstic probelm, Fenics solution matches exactly with the analytical solution.

See this: Incompressible Humphrey hyperelastic material in Fencis

Could you format the code? I could take a look

Sorry, can not format it properly here. Here is also a drive link for the python script.

https://drive.google.com/file/d/16etclV96_BU0zqK9w8XONxIKqNj8e9X9/view?usp=sharing

Thank you so much

You can do so by enclosing the code inside triple ticks (followed by python). ```
This is way too long to be formatted, indented, executed and then debugged.

1 Like

I Fixed the indentation issue, But @swatisharmapanda has to remove all of the > preceeding his code.

Sorry I did not know that one. I have formatted the code above.

Is it okay now? Sorry for the trouble.

yes it looks fine now. Will post you once I have taken a look at it.
Thanks

There are several possible reasons for the solution to not match the analytical result that I could see just by looking at the code.

This seems to me a forward-euler (first order explicit) discretization of the ODE. This may not be the best choice and in-fact may not even be stable for larger more complicated problems. Try implementing backward euler for instance.

Also there is a single-pass where you solve for Ci and then update u. If you are solving two coupled equations in a non-monolithic fashion then you cannot do this. See for instance Jeremy Bleyer’s tutorial

This in addition to my previous comment on using an inf-sup stable mixed element, such as Taylor-Hood (\mathbb{P}_k-P_{k-1}) element should be the way to go.

Thank you so much for your help.

I found the issues. First of all, there is something wrong in the time-dependent equation of Ci. Thus, instead of second PK stress tensor and Ci, I used Cauchy stress and elastic part of left Cauchy-Green deformation tensor, Be. Now the constitutive equations are
image
where S is the Cauchy stress, B is Cauchy-Green deformation tensor, p is hydrostatic pressure, L is velocity gradient tensor.
image

I followed Jeremy Bleyer’s tutorial tutorial and treated Be as a function space.

With these modification, now Fencis solution matches exactly with the analytical solution.

Here is the complete code.

### 3D finite viscoelastic model for stress relaxation
from dolfin import *
import matplotlib.pyplot as plt
import os
import dolfin as dlf
import numpy as np
import xml.etree.ElementTree as ET
import math 

### Optimization options for the form compiler
parameters["form_compiler"]["cpp_optimize"] = True
parameters["form_compiler"]["representation"] = "uflacs"

### traction 
class Traction(UserExpression):
    def __init__(self):
        super().__init__(self)
        self.t = 0.0
    def eval(self, values, x):
        values[0] = 0*self.t
        values[1] = 0.0
        values[2] = 0.0
    def value_shape(self):
        return (3,)

### mesh generation
def geometry_3d():
    mesh = UnitCubeMesh(6, 6, 6)
    boundary_parts = MeshFunction('size_t', mesh, mesh.topology().dim()-1)
    x0 = AutoSubDomain(lambda x: near(x[0], 0))
    x1 = AutoSubDomain(lambda x: near(x[0], 1))
    y0 = AutoSubDomain(lambda x: near(x[1], 0))
    z0 = AutoSubDomain(lambda x: near(x[2], 0))
    x0.mark(boundary_parts, 1)
    y0.mark(boundary_parts, 2)
    z0.mark(boundary_parts, 3)
    x1.mark(boundary_parts, 4)
    return boundary_parts

### stress calculation
def CalculatePKStress(u,pressure,Be,ce,coe):
    I = Identity(V.mesh().geometry().dim())  # Identity tensor
    F = I + grad(u)          # Deformation gradient
    invF = inv(F)
    J = det(F)
    B = F*F.T
    S = 2*ce*B+2*coe*Be-pressure*I # Cauchy stress tensor
    T = J*invF*S*invF.T            # 2nd PK stress tensor
    return T, (J-1)


### Gradient of Be
def CalcGradBe(u,u_old,Be_old,dt,coe,eta):
    I = Identity(V.mesh().geometry().dim()) 
    F_old = I + grad(u_old)
    F = I + grad(u)
    L = ((F-F_old)/Constant(dt))*inv(F)
    slope =L*Be_old+Be_old*L.T-(2.0/eta)*Be_old*2*coe*dev(Be_old)
    return slope


### Create mesh and define function space 
facet_function = geometry_3d()
mesh = facet_function.mesh()
gdim = mesh.geometry().dim()
dx = Measure("dx")
ds = Measure("ds", subdomain_data=facet_function, subdomain_id=4)
print('Number of nodes: ',mesh.num_vertices())
print('Number of cells: ',mesh.num_cells())
dx = dx(degree=4)
ds = ds(degree=4)


### Time stepping parameters
Tload = 1
Tend = 5
dt = 0.1
Nsteps = int(Tend/dt)
time = np.linspace(0, Tend, Nsteps+1)
eps_max = 0.01 # max applied strain during stress relaxation
eps_dot = eps_max/Tload  # strain rate to be applied during the loading phase

### material parameters
ce, coe, eta = 2, 2, 13.34 # 3D viscoelastic model parameter
E1, E2, eta_sls = 12, 12, 20 # Equivalent SLS model parameters
tau_sigma, tau_eps = (eta_sls/E1)*(1+E1/E2), eta_sls/E2 # SLS model time constants


### Create function space
element_s = FiniteElement("CG", mesh.ufl_cell(), 1) 
element_v = VectorElement("CG", mesh.ufl_cell(), 1) 
element_t = TensorElement("DG", mesh.ufl_cell(), 0)
mixed_element = MixedElement([element_v, element_s,element_t])   
V = FunctionSpace(mesh, mixed_element)


### Define test and trial functions
dupbe = TrialFunction(V)
_u, _p, _Be = TestFunctions(V)

_u_p_be = Function(V)
u, p, Be = split(_u_p_be)
_u_p_be_old = Function(V)
u_old, p_old, Be_old = split(_u_p_be_old)


### initialize variables
# u_initial= interpolate(Constant((0.0,0.0,0.0)), V.sub(0).collapse())
# p_initial= interpolate(Constant(2*(ce+coe)), V.sub(1).collapse())
# Be_initial= interpolate(Constant(((1.0,0.0, 0.0),(0.0,1.0, 0.0),(0.0,0.0,1.0))),V.sub(2).collapse())
# assign(_u_p_be, [u_initial,p_initial,Be_initial])
# assign(_u_p_be_old, [u_initial,p_initial,Be_initial])

I = Identity(V.mesh().geometry().dim())
WF = TensorFunctionSpace(mesh, "DG", degree=0)
Be_initial = project(I, WF)
assign(_u_p_be.sub(2),Be_initial)
assign(_u_p_be_old.sub(2),Be_initial)

### Create tensor function spaces for stress and stretch for result plotting
defGrad = Function(WF, name='F')
PK_stress = Function(WF, name='PK1')

### initialize traction class
h = Traction() # Traction force on the boundary

### Define Dirichlet boundary
bc0 = DirichletBC(V.sub(0).sub(0), Constant(0.), facet_function, 1)
bc1 = DirichletBC(V.sub(0).sub(1), Constant(0.), facet_function, 2)
bc2 = DirichletBC(V.sub(0).sub(2), Constant(0.), facet_function, 3)
tDirBC = Expression(('eps_d*time_'),eps_d = eps_dot, time_=0.0 , degree=0)
bc3 = DirichletBC(V.sub(0).sub(0), tDirBC, facet_function, 4) 
bcs = [bc0,bc1,bc2,bc3]


### weak forms
I = Identity(V.mesh().geometry().dim())
F_cur = I + grad(u)
pkstrs, hydpress =  CalculatePKStress(u,p,Be,ce,coe)
F1 = inner(dot(F_cur,pkstrs), grad(_u))*dx - dot(h, _u)*ds  # PK2 weak form
F2 = hydpress*_p*dx                                         # hydrostatic pressure weak form
F3 = inner((Be - Be_old),_Be)*dx \
        - Constant(dt)*inner(CalcGradBe(u,u_old,Be_old,dt,coe,eta),_Be)*dx  # Evolution equation weak form
F = F1+F2+F3
J = derivative(F, _u_p_be,dupbe)


### Create nonlinear variational problem and solve
problem = NonlinearVariationalProblem(F, _u_p_be, bcs=bcs, J=J)
solver = NonlinearVariationalSolver(problem)
solver.parameters['newton_solver']['relative_tolerance'] = 1e-6
solver.parameters['newton_solver']['linear_solver'] = 'mumps'


### Save solution in VTK format
file_results = XDMFFile("./Results/TestUniaxialLoading/Uniaxial.xdmf")
file_results.parameters["flush_output"] = True
file_results.parameters["functions_share_mesh"] = True

stretch_vec = np.zeros((Nsteps+1,1))
stress_vec =  np.zeros((Nsteps+1,1))
stress_ana =  np.zeros((Nsteps+1,1))

for i in range(len(time)):
    tt = time[i]
    if (i%10)==0:
        print(i, 'time: ', tt)

    ### update time-dependent variables
    h.t = tt
    tDirBC.time_ = tt if tt<=Tload else Tload # stress relaxation
    
    ### solve 
    solver.solve()
    
    ### Extract solution components
    u_print, p_print, Be_print = _u_p_be.split()
    u_print.rename("u", "displacement")
    p_print.rename("p", "pressure")
    Be_print.rename("Be", "Be")
  
    ### update old variables
    _u_p_be_old.assign(_u_p_be)
    

    ### Save DefGrad to file 
    point = (0.5,0.5,0.5) # point at which Defgrad is required
    DF = I + grad(u_print)
    defGrad.assign(project(DF, WF))
    stretch_vec[i] = defGrad(point)[0]

    # Save Stress to file 
    PK_s,tempPress = CalculatePKStress(u_print,p_print,Be_print,ce,coe)
    PK_1 = DF*PK_s # this is 1st PK stress
    PK_stress.assign(project(PK_1, WF))
    stress_vec[i] = PK_stress(point)[0]
    
    # save xdmf file
    file_results.write(u_print, tt)
    file_results.write(defGrad, tt)
    file_results.write(PK_stress, tt)
    
    
### analytical solution for SLS model (Stress relaxation)
for i in range(len(time)):
    if time[i] <= Tload:
        stress_ana[i] = E1*eps_dot*(tau_sigma-tau_eps)*(1-np.exp(-time[i]/tau_eps)) \
                            +E1*(stretch_vec[i]-1) 
    else:
        eps0 = eps_dot*Tload
        sigma_t1 = E1*eps_dot*(tau_sigma-tau_eps)*(1-np.exp(-Tload/tau_eps)) \
                            +E1*((1+eps0)-1)
        stress_ana[i] = (sigma_t1-E1*eps0)*np.exp(Tload/tau_eps)*np.exp(-time[i]/tau_eps)\
                            +E1*eps0

### plot results
f = plt.figure(figsize=(12,6))
plt.plot(time, stress_vec,'r-',label='Fenics')
plt.plot(time, stress_ana,'k.',label='Analytical')
plt.xlabel('time')
plt.ylabel('stress')
plt.legend()
plt.show()

And the results are as floows:

If anyone looking for the constitutive equations, here are the papers:

Panda, Satish Kumar, and Martin Lindsay Buist. “A finite nonlinear hyper-viscoelastic model for soft biological tissues.” Journal of biomechanics 69 (2018): 121-128.

Panda, Satish K., and Martin L. Buist. “A finite element approach for gastrointestinal tissue mechanics.” International journal for numerical methods in biomedical engineering 35.12 (2019): e3269.

2 Likes