This program was contributed by Ester Comellas <ester.comellas@upc.edu>.
It comes without any warranty or support by its authors or the authors of deal.II.
This program is part of the deal.II code gallery and consists of the following files (click to inspect):
Pictures from this code gallery program
Annotated version of readme.md
Readme file for nonlinear-poro-viscoelasticity
Overview
We implemented a nonlinear poro-viscoelastic formulation with the aim of characterising brain tissue response to cyclic loading. Our model captures both experimentally observed fluid flow and conditioning aspects of brain tissue behavior in addition to its well-established nonlinear, preconditioning, hysteretic, and tension-compression asymmetric characteristics.
The tissue is modelled as a biphasic material consisting of an immiscible aggregate of a nonlinear viscoelastic solid skeleton saturated with pore fluid. The governing equations are linearised using automatic differentiation and solved monolithically for the unknown solid displacements and fluid pore pressure values.
A detailed description of the formulation, its verification, and the results obtained can be found in:
-
E. Comellas, S. Budday, J.-P. Pelteret, G. A. Holzapfel and P. Steinmann (2020), Modeling the porous and viscous responses of human brain tissue behavior, Computer Methods in Applied Mechanics and Engineering, 113128. DOI: 10.1016/j.cma.2020.113128.
In this paper we show that nonlinear poroelasticity alone can reproduce consolidation experiments, yet it is insufficient to capture stress conditioning due to cyclic loading. We also discuss how the poroelastic response exhibits preconditioning and hysteresis in the fluid flow space, with porous and viscous effects being highly interrelated.
Quick facts about the code
-
Biphasic material following the Theory of Porous Media
-
Nonlinear finite viscoelasticity built on Ogden hyperelasticity
-
Darcy-like fluid flow
-
Spatial discretisation with continuous Q2-P1 Lagrangian finite elements
-
Temporal discretisation with a stable implicit one-step backward differentiation method
-
Newton-Raphson scheme to solve the nonlinear system of governing equations
-
Forward mode automatic differentiation with the number of derivative components chosen at run-time (Sacado library within Trilinos package) to linearise the governing equations (and, implicitly, the constitutive laws)
-
Trilinos direct solver for the (non-symmetric) linear system of equations using a monolithic scheme
-
Parallelization through Threading Building Blocks and across nodes via MPI (using Trilinos linear algebra)
-
Based on step-44 and the code gallery contributions 'Quasi-Static Finite-Strain Compressible Elasticity' and 'Quasi-Static Finite-Strain Quasi-incompressible Visco-elasticity'
-
Only works in 3D
Running the code
Requirements
-
MPI and Trilinos (built with the Sacado library) must be enabled
Compiling and running
Similar to the example programs, run
cmake -DDEAL_II_DIR=/path/to/deal.II .
in this directory to configure the problem.
You can switch between debug and release mode by calling either
or
The problem may then be run in serial mode with
and in parallel (in this case, on 6
processors) with
mpirun -np 6 ./nonlinear-poro-viscoelasticity
Alternatively, to keep it a bit more tidy, create a folder, e.g. run and copy the input file in there. Then type:
mpirun -np 6 -wdir run ../nonlinear-poro-viscoelasticity
All the input files used to produce the results shown in the paper are provided in in the input-files folder. Simply replace the parameters.prm file in the main directory. For the verification examples, run the python script 'run-multi-calc.py' instead:
The 'run-multi-calc.py' and 'runPoro.sh' files provided must both be in the main directory. This will automatically generate the required input files and run them in sequence.
Reference for this work
If you use this program as a basis for your own work, please consider citing the paper referenced in the introduction. The initial version of this work was contributed to the deal.II project by E. Comellas and J-P. Pelteret.
Recommended literature
-
W. Ehlers and G. Eipper (1999), Finite Elastic Deformations in Liquid-Saturated and Empty Porous Solids, Transport in Porous Media 34(1/3):179-191. DOI: 10.1023/A:1006565509095.
-
S. Reese and S. Govindjee (2001), A theory of finite viscoelasticity and numerical aspects, International Journal of Solids and Structures 35(26-27):3455-3482. DOI: 10.1016/S0020-7683(97)00217-5.
-
G. Franceschini, D. Bigoni, P. Regitnig and G. A. Holzapfel (2006), Brain tissue deforms similarly to filled elastomers and follows consolidation theory, Journal of the Mechanics and Physics of Solids 54(12):2592-2620. DOI: 10.1016/j.jmps.2006.05.004.
-
S. Budday, G. Sommer, J. Haybaeck, P. Steinmann, G. A. Holzapfel and E. Kuhl (2017), Rheological characterization of human brain tissue, Acta Biomaterialia 60:315-329. DOI: 10.1016/j.actbio.2017.06.024.
-
G.A. Holzapfel (2001), Nonlinear Solid Mechanics. A Continuum Approach for Engineering, John Wiley & Sons. ISBN: 978-0-471-82319-3.
Results
The results shown here are a selection of those presented and discussed in the paper referenced in the introduction.
Consolidation experiments
We reproduce the uniaxial consolidation experiments of brain tissue from the seminal paper by Franceschini et al. (2006) using a reduction of our formulation to nonlinear poroelasticity without the viscous component. The following geometry (320 cells and 9544 degrees of freedom), boundary conditions, and loading are used:
The material properties are:
-
First Lamé parameter: 334kPa
-
Ogden parameters: μ1=1.044kPa, μ2=1.183kPa, α1=4.309, and α2=7.736
-
Initial solid volume fraction: 0.80
-
Isotropic initial intrinsic permeability: 8.0e-11mm2
-
Deformation-dependency control parameter for the permeability: 40
-
Fluid viscosity: 0.89mPa·s
We consider the effect of gravity in our formulation, with the simplifying assumption that the fluid and solid density are both 0.997mg/mm3. Using these parameters and simulation conditions, we obtain a reasonable fit to the experimental curve:
What is interesting about these results is that they show that nonlinear poroelasticity alone, i.e. with a purely hyperelastic solid component, can capture the consolidation behavior of brain tissue in response to an oedometric test. However, as we will see in the next section, the viscous component is necessary to also capture stress conditioning due to cyclic loading.
Cyclic experiments under multiple loading modes
Budday et al. (2017) performed an exhaustive set of cyclic loading experiments on brain tissue. They showed in this and subsequent publications that a finite viscoelastic Ogden model is able to predict essential features like nonlinearity, preconditioning, hysteresis, and tension-compression asymmetry. However, such a monophasic viscoelastic formulation can only implicitly capture the effects due to the extracellular fluid flow within the tissue. This was the motivation behind the formulation presented here: to provide a comprehensive model for brain tissue response that will enable us to explore the porous and viscous effects in its mechanical response to a wide range of loading scenarios.
As an example, we show here the results for cyclic compressive loading. The following geometry (512 cells and 15468 degrees of freedom), boundary conditions, and loading are used:
The material properties are:
-
First Lamé parameter: 24.5kPa
-
Ogden hyperelastic parameters: μ∞,1=-83.9Pa, and α∞,1=-11.92
-
Ogden viscous parameters: μ1=-2100Pa, and α1=-2.2
-
Deformation-independent viscosity of the solid: 14kPa·s
-
Initial solid volume fraction: 0.75
-
Isotropic initial intrinsic permeability: 1.0e-8mm2
-
Deformation-dependency control parameter for the permeability: 50
-
Fluid viscosity: 0.89mPa·s
To simplify the problem, we neglect the effect of gravity for this example. Here is an animation of our results, visualised with Paraview:
To compare with the experimental results, our code computes the nominal stress on the loading surface and provides the values for each timestep in the "data-for-gnuplot.sol" output file. We can then plot compressive nominal stress versus the averaged vertical stretch:
We see that viscosity is required in the solid component to reproduce the preconditioning and hysteretic response seen in the experiments. We were somewhat surprised by these results, because we were expecting to see preconditioning and hysteresis with poroelasticity alone. We discussed this in a conference talk:
-
E. Comellas, J.-P. Pelteret, S. Budday and P. Steinmann (2018). Unsolved issues in the numerical modelling of experimentally-observed porous effects in brain tissue behaviour, 6th European Conference on Computational Mechanics and 7th European Conference on Computational Fluid Dynamics (ECCM-ECFD 2018), Glasgow (UK), 11th–15th June 2018. DOI: 10.13140/RG.2.2.18553.29283.
So, we set out to explore why this is not the case. For that, we studied the porous and viscous dissipation, which are computed in the code and also provided in the "data-for-gnuplot.sol" output file. We determined that there is dissipation occurring in the poroelastic case, but we were barely seeing it in the stress-stretch plot. Even if we played around with the material parameters to increase the amount of dissipation, the slight hysteresis in our plot barely changed. Where is all that energy going?
We found the answer by looking closely into the thermodynamic basis of our constitutive equations. The viscous dissipation is a function of the viscous part of the stress tensor and the viscosity of the solid component. However, the porous dissipation depends on the porous material parameters and the seepage velocity, i.e. the relative velocity of the fluid with respect to the deforming solid component, which is proportional to the gradient of the pressure. Now, the plots we were studying are in the displacement-related space. We were looking for the porous dissipation in the wrong place! Our unknowns, the displacements and pressure, are slightly coupled. This is why we see a bit of porous dissipation in the stress-stretch plot for the poroelastic example.
Indeed, when we computed an equivalent to the nominal stress but in the pressure-related space, we found the "missing" dissipation. The "accumulated outgoing fluid volume" in the pressure-space is the equivalent to the nominal stress in the displacement-space. We plot it versus the fluid reaction pressure on the loading surface, which can be loosely seen as an equivalent to the averaged vertical stretch in the previous plots.
Our takeaways
In developing this formulation and running the examples provided in this code gallery, we have realised the importance of the loading and boundary conditions selected in the problems. Inhomogeneous displacements and pressure distributions require a fine enough discretization as well as a careful set-up of representative loading and boundary conditions. Without inhomogeneous stretch and pressure maps, the predicted viscous and porous behaviours of the material will not be realistic.
We have started to explore the relation between viscous and porous effects in response to different types of loading. Interestingly, the fluid accumulated outside the sample increases for each loading cycle in the poroelastic model, while it decreases for the poro-viscoelastic one.
The evolution of pressure in the centre of the loading surface, where we observe a pressure concentration due to loading, has a noticeably different pattern for the two models. Note also the delay between the peak pressure values and peak displacement ones.
We are not sure yet how much of these differences is due to the incorporation of the viscous component, and how much can be explained by the choice of material parameters. (Note that the solid part of the poroelastic example has been adjusted with the Franceschini experiment, but the poro-viscoelastic one used the Budday experiments.) We will keep exploring with more numerical examples, but additional experiments are needed to adjust our material parameters in order to better distinguish the porous and viscous parts of brain tissue response to loading.
We hope the community finds this code useful, whether it is for brain mechanics or other applications, and look forward to learning and discussing about new insights gained about the interrelations of the viscous, porous, and elastic parts of the nonlinear poro-viscoelastic formulation.
import fileinput
import sys
import subprocess
import os
import shutil
import select
import time
parallelProcesses = 1
processes = {}
jobnames = {}
x = open("parameters.prm", 'w')
x.write("subsection Finite element system\n")
x.write(" set Polynomial degree displ = 2\n")
x.write(" set Polynomial degree pore = 1\n")
x.write(" set Quadrature order = 3\n")
x.write("end\n\n")
x.write("subsection Geometry\n")
x.write(" set Geometry type = Ehlers_tube_step_load\n")
x.write(" set Global refinement = 1\n")
x.write(" set Grid scale = 1\n")
x.write(" set Load type = pressure\n")
x.write(" set Load value = -7.5e6\n")
x.write(" set Drained pressure = 0\n")
x.write("end\n\n")
x.write("subsection Material properties \n")
x.write(" set material = Neo-Hooke\n")
x.write(" set lambda = 8.375e6\n")
x.write(" set shear modulus = 5.583e6\n")
x.write(" set seepage definition = Ehlers\n")
x.write(" set initial solid volume fraction = 0.67\n")
x.write(" set kappa = 0\n")
x.write(" set initial Darcy coefficient = 1.0e-4\n")
x.write(" set fluid weight = 1.0e4\n")
x.write(" set gravity term = false\n")
x.write("end\n\n")
x.write("subsection Nonlinear solver\n")
x.write(" set Max iterations Newton-Raphson = 20\n")
x.write(" set Tolerance force = 1.0e-6\n")
x.write(" set Tolerance displacement = 1.0e-6\n")
x.write(" set Tolerance pore pressure = 1.0e-6\n")
x.write("end\n\n")
x.write("subsection Time\n")
x.write(" set End time = 10.0\n")
x.write(" set Time step size = 0.002\n")
x.write("end\n\n")
x.write("subsection Output parameters\n")
x.write(" set Time step number output = 5\n")
x.write("end\n")
x.close()
kappas = ["0","1","2","5"]
for kappa in kappas:
jobname = "Ehlers_step_load_kappa_"+kappa
x = fileinput.input("parameters.prm", inplace=1)
for line in x:
if "set kappa" in line:
line = " set kappa = "+kappa+"\n"
print (line),
x.close()
process = subprocess.Popen("./runPoro.sh " + jobname, shell=True)
executable = "RESULTS/calcDir_" + jobname
results = "RESULTS/resultDir_" + jobname
time.sleep(1)
while not os.path.exists(executable) and not os.path.exists(results):
time.sleep(1)
print ("waiting for executable to be copied")
processes[process.pid] = process
jobnames[process.pid] = jobname
while len(processes) >= parallelProcesses:
pid, status = os.wait()
if pid in processes:
if status == 0:
print ("Job %30s successful" % jobnames[pid])
del processes[pid]
del jobnames[pid]
while processes:
pid, status = os.wait()
if pid in processes:
if status == 0:
print ("Job %30s successful" % jobnames[pid])
del processes[pid]
del jobnames[pid]
jobname = "Ehlers_increase_load_kappa_0"
x = fileinput.input("parameters.prm", inplace=1)
for line in x:
if "set Geometry type" in line:
line = " set Geometry type = Ehlers_tube_increase_load\n"
if "set kappa" in line:
line = " set kappa = 0\n"
print (line),
x.close()
process = subprocess.Popen("./runPoro.sh " + jobname, shell=True)
executable = "RESULTS/calcDir_" + jobname
results = "RESULTS/resultDir_" + jobname
time.sleep(1)
while not os.path.exists(executable) and not os.path.exists(results):
time.sleep(1)
print ("waiting for executable to be copied")
processes[process.pid] = process
jobnames[process.pid] = jobname
while len(processes) >= parallelProcesses:
pid, status = os.wait()
if pid in processes:
if status == 0:
print ("Job %30s successful" % jobnames[pid])
del processes[pid]
del jobnames[pid]
jobname = "Ehlers_cube"
x = fileinput.input("parameters.prm", inplace=1)
for line in x:
if "set Geometry type" in line:
line = " set Geometry type = Ehlers_cube_consolidation\n"
if "set Global refinement" in line:
line = " set Global refinement = 2\n"
if "set Time step size" in line:
line = " set Time step size = 0.01\n"
if "set Time step number output" in line:
line = " set Time step number output = 1\n"
print (line),
x.close()
process = subprocess.Popen("./runPoro.sh " + jobname, shell=True)
executable = "RESULTS/calcDir_" + jobname
results = "RESULTS/resultDir_" + jobname
time.sleep(1)
while not os.path.exists(executable) and not os.path.exists(results):
time.sleep(1)
print ("waiting for executable to be copied")
processes[process.pid] = process
jobnames[process.pid] = jobname
while len(processes) >= parallelProcesses:
pid, status = os.wait()
if pid in processes:
if status == 0:
print ("Job %30s successful" % jobnames[pid])
del processes[pid]
del jobnames[pid]
print (" _ _ _ _ _ _ _ _ _ ")
print (" /_\ | | | (_)___| |__ ___ __ ___ _ __ _ __| |___| |_ ___ __| | |")
print (" / _ \| | | | / _ \ '_ (_-< / _/ _ \ ' \| '_ \ / -_) _/ -_) _ |_|")
print ("/_/ \_\_|_| _/ \___/_.__/__/ \__\___/_|_|_| .__/_\___|\__\___\__,_(_)")
print (" |__/ |_| ")
Annotated version of input-files/5-Ehlers-verification/runPoro.sh
#!/bin/bash
# first parameter is the name for directory where the calculation is made
echo " _ _ _ _ _ "
echo " _ _ _ _ _ _ _ _ (_)_ _ __ _ __| |___ __ _| | (_|_) "
echo "| '_| || | ' \| ' \| | ' \/ _ | / _ / -_) _ | |_| | | _ _ _ "
echo "|_| \_,_|_||_|_||_|_|_||_\__, | \__,_\___\__,_|_(_)_|_| (_) (_) (_) "
echo " |___/ "
echo "DEAL_II_DIR: " @f$DEAL_II_DIR
# Define and print tmpdir, where calculations are made and resultsdir where results will be stored
maindir=`pwd`
tmpdir=@f$maindir/RESULTS/calcDir_@f$1
resultdir=@f$maindir/RESULTS/resultDir_@f$1
echo "Main directory :" @f$maindir
echo "Directory for calculations:" @f$tmpdir
echo "Directory for results :" @f$resultdir
# change to temporary job directory
mkdir -p @f$tmpdir
cd @f$tmpdir
# copy stuff from location where job was submitted to temporary directory
cp -r @f$maindir/parameters.prm . && echo "Copying input file succeeded" || echo "Copying input file failed"
cp -r @f$maindir/nonlinear-poro-viscoelasticity . && echo "Copying executable succeeded" || echo "Copying executable failed"
# run code (change num of parallel processes if desired)
touch out.log
touch err.log
COUNTER=0
while [ @f$COUNTER -lt 10 ]; do
COUNTER=@f$((COUNTER+1))
echo "Start run " @f$COUNTER
mpirun -np 6 ./nonlinear-poro-viscoelasticity >>out.log 2>err.log
if [ @f$? -eq 0 ]; then
echo FEM Code OK
break
else
echo @f$?
echo FEM Code FAILED
fi
done
# create folder for output and copy parameter file and results into it
mkdir -p @f$resultdir
mkdir -p @f$resultdir/Paraview-files
cp parameters.prm @f$resultdir/
cp *.sol @f$resultdir/
cp solution.* @f$resultdir/Paraview-files/
# get rid of the temporary job dir
rm -rf @f$tmpdir
We start by including all the necessary deal.II header files and some C++ related ones. They have been discussed in detail in previous tutorial programs, so you need only refer to past tutorials for details.
LinearOperator< Range, Domain, Payload > linear_operator(const OperatorExemplar &, const Matrix &)
We create a namespace for everything that relates to the nonlinear poro-viscoelastic formulation, and import all the deal.II function and class names into it:
Run-time parameters
Set up a ParameterHandler object to read in the parameter choices at run-time introduced by the user through the file "parameters.prm"
Finite Element system
Here we specify the polynomial order used to approximate the solution, both for the displacements and pressure unknowns. The quadrature order should be adjusted accordingly.
{
"Displacement system polynomial order");
"Pore pressure system polynomial order");
"Gauss quadrature order");
{
void enter_subsection(const std::string &subsection, const bool create_path_if_needed=true)
long int get_integer(const std::string &entry_string) const
void declare_entry(const std::string &entry, const std::string &default_value, const Patterns::PatternBase &pattern=Patterns::Anything(), const std::string &documentation="", const bool has_to_be_set=false)
Geometry
These parameters are related to the geometry definition and mesh generation. We select the type of problem to solve and introduce the desired load values.
{
"|Ehlers_tube_increase_load"
"|Ehlers_cube_consolidation"
"|Franceschini_consolidation"
"|Budday_cube_tension_compression"
"|Budday_cube_tension_compression_fully_fixed"
"|Budday_cube_shear_fully_fixed"),
"Type of geometry used. "
"For Ehlers verification examples see Ehlers and Eipper (1999). "
"For Franceschini brain consolidation see Franceschini et al. (2006)"
"For Budday brain examples see Budday et al. (2017)");
"Global refinement level");
"Global grid scaling factor");
"Number of times each set of 3 cycles is repeated, only for "
"Budday_cube_tension_compression and Budday_cube_tension_compression_fully_fixed. "
"Load value is doubled in second set, load rate is kept constant."
"Final time indicates end of second cycle set.");
"Prescribed fluid flow. Not implemented in any example yet.");
"Increase of pressure value at drained boundary w.r.t the atmospheric pressure.");
{
std::string get(const std::string &entry_string) const
double get_double(const std::string &entry_name) const
Materials
Here we select the type of material for the solid component and define the corresponding material parameters. Then we define he fluid data, including the type of seepage velocity definition to use.
{
"Type of material used in the problem");
"First Lamé parameter for extension function related to compactation point in solid material [Pa].");
"shear modulus for Neo-Hooke materials [Pa].");
"The type of eigen solver to be used for Ogden and visco-Ogden models.");
"Shear material parameter 'mu1' for Ogden material [Pa].");
"Shear material parameter 'mu2' for Ogden material [Pa].");
"Shear material parameter 'mu1' for Ogden material [Pa].");
"Stiffness material parameter 'alpha1' for Ogden material [-].");
"Stiffness material parameter 'alpha2' for Ogden material [-].");
"Stiffness material parameter 'alpha3' for Ogden material [-].");
"Shear material parameter 'mu1' for first viscous mode in Ogden material [Pa].");
"Shear material parameter 'mu2' for first viscous mode in Ogden material [Pa].");
"Shear material parameter 'mu1' for first viscous mode in Ogden material [Pa].");
"Stiffness material parameter 'alpha1' for first viscous mode in Ogden material [-].");
"Stiffness material parameter 'alpha2' for first viscous mode in Ogden material [-].");
"Stiffness material parameter 'alpha3' for first viscous mode in Ogden material [-].");
"Deformation-independent viscosity parameter 'eta_1' for first viscous mode in Ogden material [-].");
"Type of formulation used to define the seepage velocity in the problem. "
"Choose between Markert formulation of deformation-dependent intrinsic permeability "
"and Ehlers formulation of deformation-dependent Darcy flow coefficient.");
"Initial porosity (solid volume fraction, 0 < n_0s < 1)");
"Deformation-dependency control parameter for specific permeability (kappa >= 0)");
"Initial intrinsic permeability parameter [m^2] (isotropic permeability). To be used with Markert formulation.");
"Effective shear viscosity parameter of the fluid [Pa·s, (N·s)/m^2]. To be used with Markert formulation.");
"Initial Darcy flow coefficient [m/s] (isotropic permeability). To be used with Ehlers formulation.");
"Effective weight of the fluid [N/m^3]. To be used with Ehlers formulation.");
"Gravity term considered (true) or neglected (false)");
"Real (or effective) density of the fluid");
"Real (or effective) density of the solid");
"Direction of gravity (unit vector 0 for x, 1 for y, 2 for z)");
"Value of gravity (be careful to have consistent units!)");
{
SymmetricTensorEigenvectorMethod
Solid
Fluid
Gravity effects
AssertThrow(
false, ExcMessage(
"Markert seepage velocity formulation requires the definition of "
"'initial intrinsic permeability' and 'fluid viscosity' greater than 0.0."));
AssertThrow(
false, ExcMessage(
"Ehler seepage velocity formulation requires the definition of "
"'initial Darcy coefficient' and 'fluid weight' greater than 0.0."));
AssertThrow(
false, ExcMessage(
"Unknown eigen solver selected."));
bool get_bool(const std::string &entry_name) const
#define AssertThrow(cond, exc)
Nonlinear solver
We now define the tolerances and the maximum number of iterations for the Newton-Raphson scheme used to solve the nonlinear system of governing equations.
{
"Number of Newton-Raphson iterations allowed");
"Force residual tolerance");
"Displacement error tolerance");
"Pore pressure error tolerance");
{
Time
Here we set the timestep size \( \varDelta t \) and the simulation end-time.
{
"Time step size. The value must be larger than the displacement error tolerance defined.");
{
Output
We can choose the frequency of the data for the output files.
{
"Paraview output files to generate.");
"Output data for time steps multiple of the given "
"Output data associated with integration point values"
" averaged on elements or on nodes.");
{
All parameters
We finally consolidate all of the above structures into a single container that holds all the run-time selections.
{
{
FESystem::declare_parameters(prm);
Geometry::declare_parameters(prm);
Materials::declare_parameters(prm);
NonlinearSolver::declare_parameters(prm);
Time::declare_parameters(prm);
OutputParam::declare_parameters(prm);
{
FESystem::parse_parameters(prm);
Geometry::parse_parameters(prm);
Materials::parse_parameters(prm);
NonlinearSolver::parse_parameters(prm);
Time::parse_parameters(prm);
OutputParam::parse_parameters(prm);
Time class
A simple class to store time data. For simplicity we assume a constant time step size.
Constitutive equation for the solid component of the biphasic material
Base class: generic hyperelastic material
The "extra" Kirchhoff stress in the solid component is the sum of isochoric and a volumetric part: \(\mathbf{\tau} = \mathbf{\tau}_E^{(\bullet)} + \mathbf{\tau}^{\textrm{vol}}\). The deviatoric part changes depending on the type of material model selected: Neo-Hooken hyperelasticity, Ogden hyperelasticiy, or a single-mode finite viscoelasticity based on the Ogden hyperelastic model. In this base class we declare it as a virtual function, and it will be defined for each model type in the corresponding derived class. We define here the volumetric component, which depends on the extension function \(U(J_S)\) selected, and in this case is the same for all models. We use the function proposed by Ehlers & Eipper 1999 doi:10.1023/A:1006565509095. We also define some public functions to access and update the internal variables.
template <
int dim,
typename NumberType = Sacado::Fad::DFad<
double> >
:
lambda (parameters.lambda),
{
return ( NumberType(lambda * (1.0-
n_OS)*(1.0-
n_OS)
#define Assert(cond, exc)
DEAL_II_HOST constexpr Number determinant(const SymmetricTensor< 2, dim, Number > &)
Derived class: Neo-Hookean hyperelastic material
template <
int dim,
typename NumberType = Sacado::Fad::DFad<
double> >
:
Standard Neo-Hooke
DEAL_II_HOST constexpr SymmetricTensor< 2, dim, Number > symmetrize(const Tensor< 2, dim, Number > &t)
Neo-Hooke in terms of principal stretches
const std::array< std::pair< NumberType, Tensor< 1, dim, NumberType > >, dim >
for (
unsigned int d=0; d<dim; ++d)
DEAL_II_HOST constexpr SymmetricTensor< 4, dim, Number > outer_product(const SymmetricTensor< 2, dim, Number > &t1, const SymmetricTensor< 2, dim, Number > &t2)
std::array< std::pair< Number, Tensor< 1, dim, Number > >, std::integral_constant< int, dim >::value > eigenvectors(const SymmetricTensor< 2, dim, Number > &T, const SymmetricTensorEigenvectorMethod method=SymmetricTensorEigenvectorMethod::ql_implicit_shifts)
Derived class: Ogden hyperelastic material
template <
int dim,
typename NumberType = Sacado::Fad::DFad<
double> >
:
mu({parameters.mu1_infty,
alpha({parameters.alpha1_infty,
parameters.alpha3_infty})
std::vector<double>
alpha;
const std::array< std::pair< NumberType, Tensor< 1, dim, NumberType > >, dim >
for (
unsigned int i = 0; i < 3; ++i)
for (
unsigned int A = 0; A < dim; ++A)
::VectorizedArray< Number, width > pow(const ::VectorizedArray< Number, width > &, const Number p)
Derived class: Single-mode Ogden viscoelastic material
We use the finite viscoelastic model described in Reese & Govindjee (1998) doi:10.1016/S0020-7683(97)00217-5 The algorithm for the implicit exponential time integration is given in Budday et al. (2017) doi: 10.1016/j.actbio.2017.06.024
template <
int dim,
typename NumberType = Sacado::Fad::DFad<
double> >
:
parameters.alpha3_infty}),
parameters.alpha2_mode_1,
parameters.alpha3_mode_1}),
{}
const std::array< std::pair< NumberType, Tensor< 1, dim, NumberType > >, dim >
for (
int a = 0; a < dim; ++a)
const double tolerance = 1
e-8;
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int b = 0;
b < dim; ++
b)
for (
unsigned int a = 0; a < dim; ++a)
"viscoelastic exponential time integration algorithm."));
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int a = 0; a < dim; ++a)
static ::ExceptionBase & ExcMessage(std::string arg1)
SymmetricTensor< 2, dim, Number > e(const Tensor< 2, dim, Number > &F)
SymmetricTensor< 2, dim, Number > b(const Tensor< 2, dim, Number > &F)
Tensor< 2, dim, Number > F(const Tensor< 2, dim, Number > &Grad_u)
::VectorizedArray< Number, width > log(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > exp(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > abs(const ::VectorizedArray< Number, width > &)
DEAL_II_HOST constexpr SymmetricTensor< 2, dim, Number > invert(const SymmetricTensor< 2, dim, Number > &)
Store history
for (
unsigned int a = 0; a < dim; ++a)
for (
unsigned int b = 0; b < dim; ++b)
std::array< std::pair< NumberType, Tensor< 1, dim, NumberType > >, dim >
eigen_B;
for (
unsigned int i = 0; i < 3; ++i)
for (
unsigned int A = 0; A < dim; ++A)
for (
unsigned int i = 0; i < 3; ++i)
for (
int p = 0; p < dim; ++p)
for (
unsigned int i = 0; i < 3; ++i)
for (
int p = 0; p < dim; ++p)
for (
unsigned int i = 0; i < 3; ++i)
for (
int p = 0; p < dim; ++p)
long double gamma(const unsigned int n)
Constitutive equation for the fluid component of the biphasic material
We consider two slightly different definitions to define the seepage velocity with a Darcy-like law. Ehlers & Eipper 1999, doi:10.1023/A:1006565509095 Markert 2007, doi:10.1007/s11242-007-9107-6 The selection of one or another is made by the user via the parameters file.
template <
int dim,
typename NumberType = Sacado::Fad::DFad<
double> >
:
"Material_Darcy_Fluid --> Only Markert "
"and Ehlers formulations have been implemented."));
"Material_Darcy_Fluid --> Only Markert and Ehlers "
"formulations have been implemented."));
const NumberType fraction = (1.0 - (
n_OS /
det_F) )/(1.0 -
n_OS);
static ::ExceptionBase & ExcInternalError()
As seen in step-18, the PointHistory
class offers a method for storing data at the quadrature points. Here each quadrature point holds a pointer to a material description. Thus, different material models can be used in different regions of the domain. Among other data, we choose to store the "extra" Kirchhoff stress \(\boldsymbol{\tau}_E\) and the dissipation values \(\mathcal{D}_p\) and \(\mathcal{D}_v\).
template <
int dim,
typename NumberType = Sacado::Fad::DFad<
double> >
{
if (parameters.mat_type ==
"Neo-Hooke")
else if (parameters.mat_type ==
"Ogden")
else if (parameters.mat_type ==
"visco-Ogden")
{
if (parameters.gravity_term ==
true)
= parameters.density_SR * parameters.solid_vol_frac
* (
det_F_AD - parameters.solid_vol_frac);
gravity_vector[parameters.gravity_direction] = parameters.gravity_value;
std::shared_ptr< Material_Hyperelastic<dim, NumberType> >
solid_material;
std::shared_ptr< Material_Darcy_Fluid<dim, NumberType> >
fluid_material;
Nonlinear poro-viscoelastic solid
The Solid class is the central class as it represents the problem at hand: the nonlinear poro-viscoelastic solid
Generate mesh
Define points for post-processing
Set up the finite element system to be solved:
Extract sub-blocks from the global matrix
Several functions to assemble the system and right hand side matrices using multithreading.
typename ActiveSelector::active_cell_iterator active_cell_iterator
Define boundary conditions
virtual std::pair<types::boundary_id,types::boundary_id>
const int &direction)
const = 0;
Create and update the quadrature points.
Solve non-linear system using a Newton-Raphson scheme
Solve the linearized equations using a direct solver
Retrieve the solution
Store the converged values of the internal variables at the end of each timestep
Post-processing and writing data to files
const double current_time,
const double current_time,
Headers and footer for the output files
For parallel communication
const unsigned int n_mpi_processes;
const unsigned int this_mpi_process;
A collection of the parameters used to describe the problem setup
Declare an instance of dealii Triangulation class (mesh)
const ::parallel::distributed::Triangulation< dim, spacedim > * triangulation
Keep track of the current time and the time spent evaluating certain functions
A storage object for quadrature point information.
Integers to store polynomial degree (needed for output)
Declare an instance of dealii FESystem class (finite element definition)
Declare an instance of dealii DoFHandler class (assign DoFs to mesh)
Integer to store DoFs per element (this value will be used often)
const unsigned int dofs_per_cell;
Declare an instance of dealii Extractor objects used to retrieve information from the solution vectors We will use "u_fe" and "p_fluid_fe"as subscript in operator [] expressions on FEValues and FEFaceValues objects to extract the components of the displacement vector and fluid pressure, respectively.
Description of how the block-system is arranged. There are 3 blocks: 0 - vector DOF displacements u 1 - scalar DOF fluid pressure p_fluid
static const unsigned int n_blocks = 2;
static const unsigned int n_components = dim+1;
Extractors
Block data
DoF index data
Declare an instance of dealii QGauss class (The Gauss-Legendre family of quadrature rules for numerical integration) Gauss Points in element, with n quadrature points (in each space direction <dim> )
Gauss Points on element faces (used for definition of BCs)
Integer to store num GPs per element (this value will be used often)
const unsigned int n_q_points;
Integer to store num GPs per face (this value will be used often)
Declare an instance of dealii AffineConstraints class (linear constraints on DoFs due to hanging nodes or BCs)
Declare an instance of dealii classes necessary for FE system set-up and assembly Store elements of tangent matrix (indicated by SparsityPattern class) as sparse matrix (more efficient)
Right hand side vector of forces
Total displacement values + pressure (accumulated solution to FE system)
Non-block system for the direct solver. We will copy the block system into these to solve the linearized system of equations.
We define variables to store norms and update norms and normalisation factors.
{
numbers::NumberTraits< Number >::real_type norm() const
Declare several instances of the "Error" structure
Methods to calculate error measures
Print information to screen
NOTE: In all functions, we pass by reference (&), so these functions work on the original copy (not a clone copy), modifying the input variables inside the functions will change them outside the function.
Implementation of the Solid
class
Public interface
We initialise the Solid class using data extracted from the parameter file.
:
n_mpi_processes (
Utilities::
MPI::n_mpi_processes(mpi_communicator)),
this_mpi_process (
Utilities::
MPI::this_mpi_process(mpi_communicator)),
time(parameters.end_time, parameters.delta_t),
dofs_per_cell (fe.dofs_per_cell),
Assert(dim==3, ExcMessage(
"This problem only works in 3 space dimensions."));
The class destructor simply clears the data held by the DOFHandler
Runs the 3D solid problem
The current solution increment is defined as a block vector to reflect the structure of the PDE system, with multiple solution components
Open file
if (this_mpi_process == 0)
outfile.open(
"console-output.sol");
Generate mesh
Assign DOFs and create the stiffness and right-hand-side force vector
Define points for post-processing
if (this_mpi_process == 0)
Print results to output file
if (parameters.outfiles_requested ==
"true")
Increment time step (=load step) NOTE: In solving the quasi-static problem, the time becomes a loading parameter, i.e. we increase the loading linearly with time, making the two concepts interchangeable.
Print information on screen
pcout <<
"\n CST = make constraints";
pcout <<
"\n ASM_SYS = assemble system";
pcout <<
"\n SLV = linear solver \n";
Print information on file
outfile <<
"\n CST = make constraints";
outfile <<
"\n ASM_SYS = assemble system";
outfile <<
"\n SLV = linear solver \n";
while ( (time.get_end() - time.get_current()) > -1.0*parameters.tol_u )
Initialize the current solution increment to zero
Solve the non-linear system using a Newton-Rapshon scheme
Add the computed solution increment to total solution
Store the converged values of the internal variables
Output results
if (( (time.get_timestep()%parameters.timestep_output) == 0 )
&& (parameters.outfiles_requested ==
"true") )
Increment the time step (=load step)
Print the footers and close files
if (this_mpi_process == 0)
NOTE: ideally, we should close the outfile here [ >> outfile.close (); ] But if we do, then the timer output will not be printed. That is why we leave it open.
Private interface
We define the structures needed for parallelization with Threading Building Blocks (TBB) Tangent matrix and right-hand side force vector assembly structures. PerTaskData_ASM stores local contributions
std::vector<types::global_dof_index> local_dof_indices;
:
cell_matrix(dofs_per_cell, dofs_per_cell),
local_dof_indices(dofs_per_cell)
ScratchData_ASM stores larger objects used during the assembly
template <
typename NumberType>
Integration helper
Quadrature point solution
std::vector<NumberType> local_dof_values;
shape function values
std::vector<std::vector<Tensor<1,dim>>>
Nx;
shape function gradients
std::vector<std::vector<Tensor<2,dim, NumberType>>>
grad_Nx;
std::vector<std::vector<SymmetricTensor<2,dim, NumberType>>>
symm_grad_Nx;
:
local_dof_values(
fe_cell.dofs_per_cell),
local_dof_values(
rhs.local_dof_values),
const unsigned int n_dofs_per_cell =
Nx_p_fluid[0].size();
Assert(local_dof_values.size() == n_dofs_per_cell, ExcInternalError());
for (
unsigned int k = 0;
k < n_dofs_per_cell; ++
k)
local_dof_values[
k] = 0.0;
Assert(
Nx.size() == n_q_points, ExcInternalError());
for (
unsigned int k = 0;
k < n_dofs_per_cell; ++
k)
Define the boundary conditions on the mesh
{
pcout <<
" CST " << std::flush;
if (constraints.is_inhomogeneously_constrained(i) ==
true)
constraints.set_inhomogeneity(i,0.0);
Set-up the FE system
Determine number of components per block
The DOF handler is initialised and we renumber the grid in an efficient manner.
void component_wise(DoFHandler< dim, spacedim > &dof_handler, const std::vector< unsigned int > &target_component=std::vector< unsigned int >())
void Cuthill_McKee(DoFHandler< dim, spacedim > &dof_handler, const bool reversed_numbering=false, const bool use_constraints=false, const std::vector< types::global_dof_index > &starting_indices=std::vector< types::global_dof_index >())
Count the number of DoFs in each block
Setup the sparsity pattern and tangent matrix
locally_owned_dofs.
clear();
for (
unsigned int b=0; b<n_blocks; ++b)
Print information on screen
pcout <<
"\nTriangulation:\n"
<<
" Number of active cells: "
for (
unsigned int p=0; p<n_mpi_processes; ++p)
pcout << (p==0 ?
' ' :
'+')
pcout <<
" Number of degrees of freedom: "
for (
unsigned int p=0; p<n_mpi_processes; ++p)
pcout << (p==0 ?
' ' :
'+')
pcout <<
" Number of degrees of freedom per block: "
<<
"[n_u, n_p_fluid] = ["
Print information to file
<<
" Number of active cells: "
for (
unsigned int p=0; p<n_mpi_processes; ++p)
outfile <<
" Number of degrees of freedom: "
for (
unsigned int p=0; p<n_mpi_processes; ++p)
outfile <<
" Number of degrees of freedom per block: "
<<
"[n_u, n_p_fluid] = ["
We optimise the sparsity pattern to reflect this structure and prevent unnecessary data creation for the right-diagonal block components.
for (
unsigned int ii = 0;
ii < n_components; ++
ii)
for (
unsigned int jj = 0;
jj < n_components; ++
jj)
Identify "zero" matrix components of FE-system (The two components do not couple)
The rest of components always couple
false, this_mpi_process);
void make_sparsity_pattern(const DoFHandler< dim, spacedim > &dof_handler, SparsityPatternBase &sparsity_pattern, const AffineConstraints< number > &constraints={}, const bool keep_constrained_dofs=true, const types::subdomain_id subdomain_id=numbers::invalid_subdomain_id)
Reinitialize the (sparse) tangent matrix with the given sparsity pattern.
Initialize the right hand side and solution vectors with number of DoFs
Non-block system
false, this_mpi_process);
Set up the quadrature point history
Component extractors: used to extract sub-blocks from the global matrix Description of which local element DOFs are attached to which block component
for (
unsigned int k = 0;
k < fe.dofs_per_cell; ++
k)
const unsigned int k_group = fe.system_to_base_index(
k).first.first;
Set-up quadrature point history (QPH) data objects
pcout <<
"\nSetting up quadrature point data..." << std::endl;
outfile <<
"\nSetting up quadrature point data..." << std::endl;
Create QPH data objects.
Setup the initial quadrature point data using the info stored in parameters
for (; cell!=
endc; ++cell)
Assert(cell->is_locally_owned(), ExcInternalError());
Assert(cell->subdomain_id() == this_mpi_process, ExcInternalError());
const std::vector<std::shared_ptr<PointHistory<dim, ADNumberType> > >
Assert(
lqph.size() == n_q_points, ExcInternalError());
Solve the non-linear system using a Newton-Raphson scheme
Print the load step
Declare newton_update vector (solution of a Newton iteration), which must have as many positions as global DoFs.
Reset the error storage objects
Declare and initialize iterator for the Newton-Raphson algorithm steps
Iterate until error is below tolerance or max number iterations are reached
Initialize global stiffness matrix and global force vector to zero
Apply boundary conditions
Compute the rhs residual (error between external and internal forces in FE system)
error_residual in first iteration is stored to normalize posterior error measures
Determine the normalised residual error
If both errors are below the tolerances, exit the loop. We need to check the residual vector directly for convergence in the load steps where no external forces or displacements are imposed.
pcout <<
"\n ***** CONVERGED! ***** "
outfile <<
"\n ***** CONVERGED! ***** "
Solve the linearized system
Compute the displacement error
error_update in first iteration is stored to normalize posterior error measures
Determine the normalised Newton update error
Determine the normalised residual error
Print error values
pcout <<
" | " << std::fixed << std::setprecision(3)
Update
If maximum allowed number of iterations for Newton algorithm are reached, print non-convergence message and abort program
Prints the header for convergence info on console
static const unsigned int l_width = 120;
for (
unsigned int i = 0; i <
l_width; ++i)
pcout <<
"\n SOLVER STEP | SYS_RES "
<<
"RES_NORM RES_U RES_P "
<<
"NU_NORM NU_U NU_P " << std::endl;
outfile <<
"\n SOLVER STEP | SYS_RES "
<<
"RES_NORM RES_U RES_P "
<<
"NU_NORM NU_U NU_P " << std::endl;
for (
unsigned int i = 0; i <
l_width; ++i)
pcout << std::endl << std::endl;
Prints the footer for convergence info on console
static const unsigned int l_width = 120;
for (
unsigned int i = 0; i <
l_width; ++i)
pcout << std::endl << std::endl;
pcout <<
"Relative errors:" << std::endl
outfile <<
"Relative errors:" << std::endl
Determine the true residual error for the problem
Determine the true Newton update error for the problem
Compute the total solution, which is valid at any Newton step. This is required as, to reduce computational error, the total solution is only updated at the end of the timestep.
Cell interpolation -> Ghosted vector
Compute elemental stiffness tensor and right-hand side force vector, and assemble into global ones
{
timerfile.enter_subsection(
"Assemble system");
pcout <<
" ASM_SYS " << std::flush;
outfile <<
" ASM_SYS " << std::flush;
Info given to FEValues and FEFaceValues constructors, to indicate which data will be needed at each element.
@ update_values
Shape function values.
@ update_normal_vectors
Normal vectors.
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
@ update_quadrature_points
Transformed quadrature points.
Setup a copy of the data structures required for the process and pass them, along with the memory addresses of the assembly functions to the WorkStream object for processing
for (; cell !=
endc; ++cell)
Assert(cell->is_locally_owned(), ExcInternalError());
Assert(cell->subdomain_id() == this_mpi_process, ExcInternalError());
Add the local elemental contribution to the global stiffness tensor We do it twice, for the block and the non-block systems
{
constraints.distribute_local_to_global(data.cell_matrix,
constraints.distribute_local_to_global(data.cell_matrix,
Compute stiffness matrix and corresponding rhs for one element
Assert(cell->is_locally_owned(), ExcInternalError());
scratch.fe_values_ref.reinit(cell);
cell->get_dof_indices(data.local_dof_indices);
Setup automatic differentiation
for (
unsigned int k = 0;
k < dofs_per_cell; ++
k)
Initialise the dofs for the cell using the current solution.
scratch.local_dof_values[
k] = scratch.solution_total[data.local_dof_indices[
k]];
Mark this cell DoF as an independent variable
scratch.local_dof_values[
k].diff(
k, dofs_per_cell);
Update the quadrature point solution Compute the values and gradients of the solution in terms of the AD variables
for (
unsigned int q = 0;
q < n_q_points; ++
q)
for (
unsigned int k = 0;
k < dofs_per_cell; ++
k)
const unsigned int k_group = fe.system_to_base_index(
k).first.first;
scratch.fe_values_ref[
u_fe].gradient(
k,
q);
for (
unsigned int dd = 0;
dd < dim; ++
dd)
for (
unsigned int ee = 0;
ee < dim; ++
ee)
scratch.solution_grads_u_total[
q][
dd][
ee]
scratch.solution_values_p_fluid_total[
q]
+= scratch.local_dof_values[
k] *
Nx_p;
for (
unsigned int dd = 0;
dd < dim; ++
dd)
scratch.solution_grads_p_fluid_total[
q][
dd]
Set up pointer "lgph" to the PointHistory object of this element
const std::vector<std::shared_ptr<const PointHistory<dim, ADNumberType> > >
Assert(
lqph.size() == n_q_points, ExcInternalError());
Precalculate the element shape function values and gradients
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
const unsigned int i_group = fe.system_to_base_index(i).first.first;
scratch.grad_Nx_p_fluid[
q_point][i] =
Assemble the stiffness matrix and rhs vector
Get some info from constitutive model of solid
Get some info from constitutive model of fluid
Define some aliases to make the assembly process easier to follow
const std::vector<Tensor<1,dim>> &
Nu = scratch.Nx[
q_point];
const std::vector<SymmetricTensor<2, dim, ADNumberType>>
const std::vector<double> &
Np = scratch.Nx_p_fluid[
q_point];
const std::vector<Tensor<1, dim, ADNumberType> > &
grad_Np
= scratch.grad_Nx_p_fluid[
q_point];
const double JxW = scratch.fe_values_ref.JxW(
q_point);
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
const unsigned int i_group = fe.system_to_base_index(i).first.first;
Assemble the Neumann contribution (external force contribution).
if (cell->face(face)->at_boundary() ==
true)
scratch.fe_face_values_ref.reinit(cell, face);
= scratch.fe_face_values_ref.normal_vector(
f_q_point);
= scratch.fe_face_values_ref.quadrature_point(
f_q_point);
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
const unsigned int i_group = fe.system_to_base_index(i).first.first;
= fe.system_to_component_index(i).first;
= scratch.fe_face_values_ref.shape_value(i,
f_q_point);
= scratch.fe_face_values_ref.shape_value(i,
f_q_point);
Linearise the residual
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
data.cell_rhs(i) -=
R_i.val();
for (
unsigned int j=0;
j<dofs_per_cell; ++
j)
data.cell_matrix(i,
j) +=
R_i.fastAccessDx(
j);
Store the converged values of the internal variables
for (; cell!=
endc; ++cell)
const std::vector<std::shared_ptr<PointHistory<dim, ADNumberType> > >
Solve the linearized equations
{
pcout <<
" SLV " << std::flush;
Copy the non-block solution back to block system
for (
unsigned int i=0; i<locally_owned_dofs.n_elements(); ++i)
= locally_owned_dofs.nth_index_in_set(i);
Class to compute gradient of the pressure
:
for (
unsigned int p=0; p<
input_data.solution_gradients.size(); ++p)
for (
unsigned int d=0; d<dim; ++d)
#define AssertDimension(dim1, dim2)
Print results to vtu file
const double current_time,
Declare local variables with number of stress components & assign value according to "dim" value
Declare local vectors to store values OUTPUT AVERAGED ON ELEMENTS ----------------------------------------—
OUTPUT AVERAGED ON NODES -------------------------------------------— We need to create a new FE space with a single dof per node to avoid duplication of the output on nodes for our problem with dim+1 dofs.
We need to create a new FE space with a dim dof per node to be able to output data on nodes in vector form
Declare and initialize local unit vectors (to construct tensor basis)
for (
unsigned int i=0; i<dim; ++i)
Declare an instance of the material class object
if (parameters.mat_type ==
"Neo-Hooke")
else if (parameters.mat_type ==
"Ogden")
else if (parameters.mat_type ==
"visco-Ogden")
Assert (
false, ExcMessage(
"Material type not implemented"));
Define a local instance of FEValues to compute updated values required to calculate stresses
Iterate through elements (cells) and Gauss Points
start cell loop
Assert(cell->is_locally_owned(), ExcInternalError());
Assert(cell->subdomain_id() == this_mpi_process, ExcInternalError());
material_id(cell->active_cell_index())=
static_cast<int>(cell->material_id());
start gauss point loop
const std::vector<std::shared_ptr<const PointHistory<dim,ADNumberType>>>
Assert(
lqph.size() == n_q_points, ExcInternalError());
Cauchy stress
for (
unsigned int i=0; i<dim; ++i)
for (
unsigned int j=0;
j<dim; ++
j)
Volumes
Green-Lagrange strain
Seepage velocity
Dissipations
OUTPUT AVERAGED ON ELEMENTS ----------------------------------------— Both average on elements and on nodes is NOT weighted with the integration point volume, i.e., we assume equal contribution of each integration point to the average. Ideally, it should be weighted, but I haven't invested time in getting it to work properly.
if (parameters.outtype ==
"elements")
for (
unsigned int j=0;
j<dim; ++
j)
OUTPUT AVERAGED ON NODES ----------------------------------------—
else if (parameters.outtype ==
"nodes")
cell_v->vertex_dof_index(v, 0);
for (
unsigned int k=0;
k<dim; ++
k)
Different nodes might have different amount of contributions, e.g., corner nodes have less integration points contributing to the averaged. This is why we need a counter and divide at the end, outside the cell loop.
if (parameters.outtype ==
"nodes")
for (
unsigned int k=0;
k<dim; ++
k)
for (
unsigned int i=0; i<dim; ++i)
SymmetricTensor< 2, dim, Number > d(const Tensor< 2, dim, Number > &F, const Tensor< 2, dim, Number > &dF_dt)
T sum(const T &t, const MPI_Comm mpi_communicator)
Add the results to the solution to create the output file for Paraview
std::vector<DataComponentInterpretation::DataComponentInterpretation>
data_out.add_data_vector(material_id,
"material_id");
@ component_is_part_of_vector
Integration point results --------------------------------------------------------—
if (parameters.outtype ==
"elements")
else if (parameters.outtype ==
"nodes")
std::vector<DataComponentInterpretation::DataComponentInterpretation>
<< "."
<< ".vtu";
<< ".pvtu";
{
data_out.write_vtu(output);
std::string int_to_string(const unsigned int value, const unsigned int digits=numbers::invalid_unsigned_int)
We have a collection of files written in parallel This next set of steps should only be performed by master process
if (this_mpi_process == 0)
List of all files written out at this timestep by all processors
for (
unsigned int p=0; p<n_mpi_processes; ++p)
Time dependent data master file
void write_pvd_record(std::ostream &out, const std::vector< std::pair< double, std::string > > ×_and_names)
Print results to plotting file
const double current_time,
Variables needed to print the solution file for plotting
Auxiliary variables needed for mpi processing
Declare an instance of the material class object
if (parameters.mat_type ==
"Neo-Hooke")
else if (parameters.mat_type ==
"Ogden")
else if (parameters.mat_type ==
"visco-Ogden")
Assert (
false, ExcMessage(
"Material type not implemented"));
Define a local instance of FEValues to compute updated values required to calculate stresses
Iterate through elements (cells) and Gauss Points
start cell loop
for (; cell!=
endc; ++cell)
Assert(cell->is_locally_owned(), ExcInternalError());
Assert(cell->subdomain_id() == this_mpi_process, ExcInternalError());
start gauss point loop
const std::vector<std::shared_ptr<const PointHistory<dim,ADNumberType>>>
Assert(
lqph.size() == n_q_points, ExcInternalError());
Volumes
Seepage velocity
Dissipations
Compute reaction force on load boundary & total fluid flow across drained boundary. Define a local instance of FEFaceValues to compute values required to calculate reaction force
start face loop
Reaction force
if (cell->face(face)->at_boundary() ==
true &&
Get displacement gradients for current face
Get pressure for current element
start gauss points on faces loop
Compute deformation gradient from displacements gradient (present configuration)
const std::vector<std::shared_ptr<const PointHistory<dim,ADNumberType>>>
Assert(
lqph.size() == n_q_points, ExcInternalError());
Cauchy stress
for (
unsigned int i=0; i<dim; ++i)
for (
unsigned int j=0;
j<dim; ++
j)
Fluid flow
if (cell->face(face)->at_boundary() ==
true &&
(cell->face(face)->boundary_id() ==
cell->face(face)->boundary_id() ==
Get displacement gradients for current face
Get pressure gradients for current face
start gauss points on faces loop
Deformation gradient and inverse from displacements gradient (present configuration)
const std::vector<std::shared_ptr<const PointHistory<dim,ADNumberType>>>
Assert(
lqph.size() == n_q_points, ExcInternalError());
Seepage velocity
for (
unsigned int i=0; i<dim; ++i)
Sum the results from different MPI process and then add to the reaction_force vector In theory, the solution on each surface (each cell) only exists in one MPI process so, we add all MPI process, one will have the solution and the others will be zero
for (
unsigned int d=0; d<dim; ++d)
Same for total fluid flow, and for porous and viscous dissipations
Extract solution for tracked vectors Copying an MPI::BlockVector into MPI::Vector is not possible, so we copy each block of MPI::BlockVector into an MPI::Vector And then we copy the MPI::Vector into "normal" Vectors
if (this_mpi_process == 0)
Append the pressure solution vector to the displacement solution vector, creating a single solution vector equivalent to the original BlockVector so FEFieldFunction will work with the dof_handler_ref.
for (
unsigned int d=0; d<(dim+1); ++d)
For values close to zero, set to 0.0
if (abs(
update[d])<1.5*parameters.tol_u)
Write the results to the plotting file. Add two blank lines between cycles in the cyclic loading examples so GNUPLOT can detect each cycle as a different block
if (( (parameters.geom_type ==
"Budday_cube_tension_compression_fully_fixed")||
(parameters.geom_type ==
"Budday_cube_tension_compression")||
(parameters.geom_type ==
"Budday_cube_shear_fully_fixed") ) &&
( (abs(current_time - parameters.end_time/3.) <0.9*parameters.delta_t)||
(abs(current_time - 2.*parameters.end_time/3.)<0.9*parameters.delta_t) ) &&
parameters.num_cycle_sets == 1 )
if (( (parameters.geom_type ==
"Budday_cube_tension_compression_fully_fixed")||
(parameters.geom_type ==
"Budday_cube_tension_compression")||
(parameters.geom_type ==
"Budday_cube_shear_fully_fixed") ) &&
( (abs(current_time - parameters.end_time/9.) <0.9*parameters.delta_t)||
(abs(current_time - 2.*parameters.end_time/9.)<0.9*parameters.delta_t)||
(abs(current_time - 3.*parameters.end_time/9.)<0.9*parameters.delta_t)||
(abs(current_time - 5.*parameters.end_time/9.)<0.9*parameters.delta_t)||
(abs(current_time - 7.*parameters.end_time/9.)<0.9*parameters.delta_t) ) &&
parameters.num_cycle_sets == 2 )
for (
unsigned int d=0;
d<dim; ++
d)
plotpointfile << std::setw(15) << parameters.drained_pressure <<
",";
for (
unsigned int d=0;
d<(3*dim+2); ++
d)
for (
unsigned int d=0;
d<(dim+1); ++
d)
for (
unsigned int d=0;
d<dim; ++
d)
for (
unsigned int d=0;
d<dim; ++
d)
for (
unsigned int d=0;
d<dim; ++
d)
Header for console output file
outputfile <<
"/*-----------------------------------------------------------------------------------------";
outputfile <<
"\n\n Poro-viscoelastic formulation to solve nonlinear solid mechanics problems using deal.ii";
outputfile <<
"\n\n Problem setup by E Comellas and J-P Pelteret, University of Erlangen-Nuremberg, 2018";
outputfile <<
"\n\n/*-----------------------------------------------------------------------------------------";
Header for plotting output file
plotpointfile <<
"#\n# *** Solution history for tracked vertices -- DOF: 0 = Ux, 1 = Uy, 2 = Uz, 3 = P ***"
for (
unsigned int d=0; d<dim; ++d)
plotpointfile <<
"# The reaction force is the integral over the loaded surfaces in the "
<<
"undeformed configuration of the Cauchy stress times the normal surface unit vector.\n"
<<
"# reac(p) corresponds to the volumetric part of the Cauchy stress due to the pore fluid pressure"
<<
" and reac(E) corresponds to the extra part of the Cauchy stress due to the solid contribution."
<<
"# The fluid flow is the integral over the drained surfaces in the "
<<
"undeformed configuration of the seepage velocity times the normal surface unit vector."
unsigned int columns = 24;
for (
unsigned int d=1;
d<columns; ++
d)
<< std::right << std::setw(16) <<
"Time,"
<< std::right << std::setw(16) <<
"ref vol,"
<< std::right << std::setw(16) <<
"def vol,"
<< std::right << std::setw(16) <<
"solid vol,";
for (
unsigned int d=0;
d<(dim+1); ++
d)
<<
"P" << p <<
"[" <<
d <<
"],";
for (
unsigned int d=0;
d<dim; ++
d)
<<
"reaction [" <<
d <<
"],";
for (
unsigned int d=0;
d<dim; ++
d)
<<
"reac(p) [" <<
d <<
"],";
for (
unsigned int d=0;
d<dim; ++
d)
<<
"reac(E) [" <<
d <<
"],";
<< std::right<< std::setw(16)<<
"porous dissip,"
<< std::right<< std::setw(15)<<
"viscous dissip"
Footer for console output file
Copy "parameters" file at end of output file.
std::ifstream
infile(
"parameters.prm");
std::string content =
"";
for(i=0 ;
infile.eof()!=
true ; i++)
if(aux==
'\n') content +=
'#';
content.erase(content.end()-1);
outputfile <<
"\n\n\n\n PARAMETERS FILE USED IN THIS COMPUTATION: \n#"
Footer for plotting output file
Copy "parameters" file at end of output file.
std::ifstream
infile(
"parameters.prm");
std::string content =
"";
for(i=0 ;
infile.eof()!=
true ; i++)
if(aux==
'\n') content +=
'#';
content.erase(content.end()-1);
<<
"# PARAMETERS FILE USED IN THIS COMPUTATION:" << std::endl
Verification examples from Ehlers and Eipper 1999
We group the definition of the geometry, boundary and loading conditions specific to the verification examples from Ehlers and Eipper 1999 into specific classes.
Base class: Tube geometry and boundary conditions
:
Solid<dim> (parameters)
this->triangulation.reset_manifold(0);
this->triangulation.refine_global(
std::max (1U, this->parameters.global_refinement));
this->triangulation.reset_manifold(0);
if (this->time.get_timestep() < 2)
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->x_displacement)|
this->fe.component_mask(
this->y_displacement) ) );
(this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) |
this->fe.component_mask(
this->z_displacement) ));
virtual std::pair<types::boundary_id,types::boundary_id>
return std::make_pair(2,2);
virtual std::vector<double>
const int &direction)
const override
AssertThrow(
false,
ExcMessage(
"Displacement loading not implemented for Ehlers verification examples."));
void cylinder(Triangulation< dim > &tria, const double radius=1., const double half_length=1.)
static constexpr double PI
::VectorizedArray< Number, width > max(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)
Derived class: Step load example
if (this->parameters.load_type ==
"pressure")
return this->parameters.load * N;
Derived class: Load increasing example
if (this->parameters.load_type ==
"pressure")
const double initial_time = this->time.get_delta_t();
const double final_time = this->time.get_end();
const double current_time = this->time.get_current();
Class: Consolidation cube
:
Solid<dim> (parameters)
this->triangulation.refine_global(
std::max (1U, this->parameters.global_refinement));
this->triangulation.begin_active(),
endc = this->triangulation.end();
for (; cell !=
endc; ++cell)
if (cell->face(face)->at_boundary() ==
true &&
cell->face(face)->center()[2] == 1.0 *
this->parameters.scale)
if (cell->face(face)->center()[0] < 0.5 *
this->parameters.scale &&
cell->face(face)->center()[1] < 0.5 *
this->parameters.scale)
cell->face(face)->set_boundary_id(100);
cell->face(face)->set_boundary_id(101);
if (this->time.get_timestep() < 2)
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
this->fe.component_mask(
this->x_displacement));
this->fe.component_mask(
this->x_displacement));
this->fe.component_mask(
this->y_displacement));
this->fe.component_mask(
this->y_displacement));
( this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) |
this->fe.component_mask(
this->z_displacement) ));
if (this->parameters.load_type ==
"pressure")
return this->parameters.load * N;
virtual std::pair<types::boundary_id,types::boundary_id>
return std::make_pair(101,101);
virtual std::vector<double>
const int &direction)
const override
AssertThrow(
false,
ExcMessage(
"Displacement loading not implemented for Ehlers verification examples."));
void hyper_rectangle(Triangulation< dim, spacedim > &tria, const Point< dim > &p1, const Point< dim > &p2, const bool colorize=false)
Franceschini experiments
:
Solid<dim> (parameters)
const double radius = 0.5;
const double height = 0.27; //8.1 mm for 30 mm radius
const double height = 0.23;
if (cell->face(face)->at_boundary() ==
true)
if (cell->face(face)->center()[2] == 0.0)
cell->face(face)->set_boundary_id(1);
else if (cell->face(face)->center()[2] == height)
cell->face(face)->set_boundary_id(2);
cell->face(face)->set_boundary_id(0);
this->triangulation.refine_global(
std::max (1U, this->parameters.global_refinement));
void hyper_ball(Triangulation< dim > &tria, const Point< dim > ¢er=Point< dim >(), const double radius=1., const bool attach_spherical_manifold_on_boundary_cells=false)
void extrude_triangulation(const Triangulation< 2, 2 > &input, const unsigned int n_slices, const double height, Triangulation< 3, 3 > &result, const bool copy_manifold_ids=false, const std::vector< types::manifold_id > &manifold_priorities={})
tracked_vertices[0][2] = 0.27*this->parameters.scale;
if (this->time.get_timestep() < 2)
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->x_displacement)|
this->fe.component_mask(
this->y_displacement) ) );
(this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) |
this->fe.component_mask(
this->z_displacement) ));
(this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) ));
virtual std::pair<types::boundary_id,types::boundary_id>
return std::make_pair(1,2);
virtual std::vector<double>
const int &direction)
const override
AssertThrow(
false, ExcMessage(
"Displacement loading not implemented for Franceschini examples."));
if (this->parameters.load_type ==
"pressure")
return (this->parameters.load * N);
Examples to reproduce experiments by Budday et al. 2017
We group the definition of the geometry, boundary and loading conditions specific to the examples to reproduce experiments by Budday et al. 2017 into specific classes.
Base class: Cube geometry and loading pattern
:
Solid<dim> (parameters)
this->triangulation.begin_active(),
endc = this->triangulation.end();
for (; cell !=
endc; ++cell)
if (cell->face(face)->at_boundary() ==
true &&
( cell->face(face)->boundary_id() == 0 ||
cell->face(face)->boundary_id() == 1 ||
cell->face(face)->boundary_id() == 2 ||
cell->face(face)->boundary_id() == 3 ) )
cell->face(face)->set_boundary_id(100);
this->triangulation.refine_global(
std::max (1U, this->parameters.global_refinement));
virtual std::pair<types::boundary_id,types::boundary_id>
return std::make_pair(100,100);
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
Derived class: Uniaxial boundary conditions
if (this->time.get_timestep() < 2)
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
this->fe.component_mask(
this->z_displacement) );
cell = this->dof_handler_ref.begin_active(),
endc = this->dof_handler_ref.end();
for (; cell !=
endc; ++cell)
constraints.add_line(cell->vertex_dof_index(
node, 0));
constraints.add_line(cell->vertex_dof_index(
node, 1));
if (this->parameters.load_type ==
"displacement")
this->fe.component_mask(direction));
if (this->parameters.load_type ==
"pressure")
const double current_time = this->time.get_current();
const double final_time = this->time.get_end();
virtual std::vector<double>
const int &direction)
const override
if ( (boundary_id == 5) && (direction == 2) )
const double current_time = this->time.get_current();
const double final_time = this->time.get_end();
const double delta_time = this->time.get_delta_t();
if (this->parameters.num_cycle_sets == 1)
if ( current_time <= (final_time*1.0/3.0) )
(2.0*
num_cycles*current_time/(final_time*1.0/3.0) + 0.5)));
(2.0*
num_cycles*current_time / (final_time*2.0/3.0)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
Derived class: No lateral displacement in loading surfaces
if (this->time.get_timestep() < 2)
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) |
this->fe.component_mask(
this->z_displacement) ));
if (this->parameters.load_type ==
"displacement")
this->fe.component_mask(direction) );
(this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) ));
if (this->parameters.load_type ==
"pressure")
const double current_time = this->time.get_current();
const double final_time = this->time.get_end();
virtual std::vector<double>
const int &direction)
const override
if ( (boundary_id == 5) && (direction == 2) )
const double current_time = this->time.get_current();
const double final_time = this->time.get_end();
const double delta_time = this->time.get_delta_t();
if (this->parameters.num_cycle_sets == 1)
if ( current_time <= (final_time*1.0/3.0) )
(2.0*
num_cycles*current_time/(final_time*1.0/3.0) + 0.5)));
(2.0*
num_cycles*current_time / (final_time*2.0/3.0)
Derived class: No lateral or vertical displacement in loading surface
if (this->time.get_timestep() < 2)
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->pressure)));
(this->fe.component_mask(
this->x_displacement) |
this->fe.component_mask(
this->y_displacement) |
this->fe.component_mask(
this->z_displacement) ));
if (this->parameters.load_type ==
"displacement")
this->fe.component_mask(direction));
(this->fe.component_mask(
this->y_displacement) |
this->fe.component_mask(
this->z_displacement) ));
if (this->parameters.load_type ==
"pressure")
const double current_time = this->time.get_current();
const double final_time = this->time.get_end();
virtual std::vector<double>
const int &direction)
const override
if ( (boundary_id == 4) && (direction == 0) )