Implementation of a Cahn-Hilliard model
Objective of this 1st tutorial
The objective of this tutorial is to implement a kernel to simulate the Cahn-Hilliard equation (Eq. (508) with chemical potential defined by Eq. (507)). The starting point is the kernel ADE_for_CH-Tutorial which simulates the Advection-Diffusion Equation (ADE) with the lattice Boltzmann method. This tutorial provides all code lines to write in each file for simulating three test cases with the Cahn-Hilliard model: serpentine, spinodal decomposition and nucleation. The verification tests will be carried out with the input files (.ini) contained in the folder Tutorial01_Cahn-Hilliard/ in run_training_lbm.
It is assumed that you have already downloaded LBM_Saclay_Rech-Dev and you are working on your own branch (see Before programming: create a new branch from master).
1. Create a new kernel CH for Cahn-Hilliard
Copy the ADE kernel template and rename all files with new extension _CH
Copy the folder
ADE_for_CH-Tutorialwhich is contained inkernels/Templates_for_Tutorials, and rename itCahn-Hilliard_Tutorialin the folderkernels
Commands
$ cd LBM_Saclay_Rech-Dev/src/kernels $ cp -r Templates_for_Tutorials/ADE_for_CH-Tutorial ./Cahn-Hilliard_TutorialRemarks
Your new developments will be performed in the folder
Cahn-Hilliard_TutorialThe folder name
Cahn-Hilliard_Tutorialwill be used for compilation
All files have the extension
_ADEfor Advection Diffusion Equation. Rename them with_CHfor Cahn-Hilliard.
Commands
$ cd Cahn-Hilliard_Tutorial $ mv Index_ADE.h Index_CH.h $ mv LBMScheme_ADE.h LBMScheme_CH.h $ mv Models_ADE.h Models_CH.h $ mv Problem_ADE.h Problem_CH.h
Edit them and change strings _ADE with _CH
Change strings
_ADEwith_CHOpen all files in your favorite editor (e.g.
geanyorcodium).Search in all files the strings
_ADEand replace them with_CH(Ctrl Hwithgeany).
Change the problem name: in file
Setup.hmodify the problem nameADEwithCHregister_problem<Problem, ModelParams::Tag2Quadra>("ADE", "base");
Modification with
CHregister_problem<Problem, ModelParams::Tag2Quadra>("CH", "base");Remark
Your new problem will be named
CHinside your.iniinput file i.e. in section[lbm]your problem must be:[lbm] problem=CH
Compile your new kernel
Generate your
makefile(only once)
Commands
$ cd LBM_Saclay_Rech-Dev $ mkdir build_CH $ cd build_CH $ cmake -DKokkos_ENABLE_OPENMP=ON -DUSE_HDF5=ON -DPROBLEM=Cahn-Hilliard_Tutorial ..
Compile
Command
$ make -j 22
Run with input files of ADE
Execute your new kernel
CHof LBM_Saclay either with the simple diffusion of gaussian or with the advected gaussian in the folderrun_training_lbm/Tutorial01_Cahn-Hilliard
Commands
Go to the directory
$ cd LBM_Saclay_Rech-Dev/run_training_lbm/Tutorial01_Cahn-HilliardRun simple diffusion of gaussian
$ ../../build_CH/src/LBM_saclay TestCase_Gaussian_ADE.iniRun advected gaussian
$ ../../build_CH/src/LBM_saclay TestCase_Moving-Gaussian_ADE.ini
Check the solutions with paraview.
2. Modifications inside each file of CH
Advice
During your implementation, it is strongly recommended to compile regularly (make or make -j 22) to detect any typos.
For compilation errors, it is useful to write the terminal outputs inside a file e.g. compil.log:
$ make -j 22 2>&1 | tee compil.log
or alternatively compil2.log
$ make VERBOSE=1 2>&1 | tee compil2.log
Declaration of new fields
In enum ComponentIndex, only the indices for macroscopic fields such as concentration \(c\) (IC) and velocity components \(u_x\) (IU), \(u_y\) (IV) & \(u_z\) (IW) are declared:
enum ComponentIndex { IU , /*!< X velocity / momentum index */ IV , /*!< Y velocity / momentum index */ IW , /*!< Z velocity / momentum index */ IC , /*!< Phase field index */ COMPONENT_SIZE /*!< invalid index, just counting number of fields */ };
Add the new indices for chemical potential
IMUand laplacianILAPLAC.
Solution
enum ComponentIndex { IU , /*!< X velocity / momentum index */ IV , /*!< Y velocity / momentum index */ IW , /*!< Z velocity / momentum index */ IC , /*!< Phase field index */ IMU , /*!< Chemical potential */ ILAPLAC, /*!< Laplacien of Phase field */ COMPONENT_SIZE /*!< invalid index, just counting number of fields */ };
Add new outputs
In struct index2names only the strings for concentration and velocity components are written:
map[IC ] = "comp"; map[IU ] = "vx"; map[IV ] = "vy"; map[IW ] = "vz";
Add a new output for chemical potential.
Solution
map[IC ] = "comp"; map[IU ] = "vx"; map[IV ] = "vy"; map[IW ] = "vz"; map[IMU] = "mu";Remark
The keyword
muwill be used in the.iniinput file to write the chemical potential.
Read and declare Cahn-Hilliard parameters in ModelParams
The Cahn-Hillard equation involves three parameters: the surface tension \(\sigma\), the interface thickness \(W\) and the mobility \(\mathcal{M}_\phi\).
Read and declare Cahn-Hilliard parameters in
ModelParams
Solution
Read parameters in input file:
mobility = configMap.getFloat("params", "mobility", 1.0); W = configMap.getFloat("params", "W" , 1.0); sigma = configMap.getFloat("params", "sigma" , 1.0);Don’t forget to declare them as
real_tat the end ofModelParamsreal_t mobility, W, sigma;
Add specific functions for Cahn-Hilliard
Add a new function for derivative of double-well
g_primedefined by the first term inside the bracket of Eq. (507).
Solution
g_prime// ======================================================= // Derivative of double-well KOKKOS_INLINE_FUNCTION real_t g_prime(real_t c) const { return 16.0 * c * (1.0 - c) * (1.0 - 2.0 * c); }
Add a new function
M2to compute the chemical potential \(\mu\) (MU) defined by Eq. (507)
Solution function
M2// ======================================================= // second order moment of feq KOKKOS_INLINE_FUNCTION real_t M2(EquationTag1 tag, const LBMState &lbmState) const { const real_t mu = sigma*1.5/W * (g_prime(lbmState[IC])-SQR(W)* lbmState[ILAPLAC]); return mu; }Remarks
IC&ILAPLAChave been declared inIndex_CH.h.The parameters
sigma(surface tension \(\sigma\)) andW(interface thickness \(W\)) have been read and declared insideModelParams(previous box).The new function
g_primehas been defined just above
Add a new function
tauMfor collision rate using the mobility \(\mathcal{M}_\phi\) of Cahn-Hilliard equation
Solution
tauM// ======================================================= // relaxation coef for LBM scheme of phase field equation KOKKOS_INLINE_FUNCTION real_t tauM (EquationTag1 tag, const LBMState &lbmState) const { real_t tau = 0.5 + (e2 * mobility * dt / SQR(dx)); return (tau); }Remark
The parameter
mobilityhas been read and declared inModelParams.You could have used the parameter
Das the mobility coefficient. In that case, that new function is useless. But take care, especially if a future coupling with a transport equation is planned.
Add the standard hyperbolic tangent function
phi0defined by Eq. (385) (for initial condition)
Solution
phi0// ================================================ // Hyperbolic tangent solution KOKKOS_INLINE_FUNCTION real_t phi0(real_t x) const { return 0.5 * (1 + tanh(sign * 2.0 * x / W)); }Remark
signwill take the values+1or-1in the input datafile.inifor initialize1inside or outside the bubble.
Add conditions and keywords for several initial conditions
Currently only one condition exits for intial condition:
if (initTypeStr == "gaussian") initType = ADE_GAUSSIAN;
Add the necessary conditions for three Cahn-Hilliard initializations
Solution
if (initTypeStr == "gaussian") initType = ADE_GAUSSIAN; else if (initTypeStr == "serpentine") initType = SERPENTINE; else if (initTypeStr == "random") initType = RANDOM_SPINODAL; else if (initTypeStr == "nucleation") initType = RANDOM_NUCLEATION;Warning
Don’t forget to declare all keywords
SERPENTINE,RANDOM_SPINODALandRANDOM_NUCLEATIONin fileInitConditionsTypes.h
Function setup_collider with BGK
The function setup_collider is written for solving Advection-Diffusion Equation. It must be modified for solving Cahn-Hilliard equation.
First, the 2nd order moment, is currently \(c\)
const real_t M0 = Model.MomentM0(tag, lbmState); const RVect<dim> uc = Model.MomentM1<dim>(tag, lbmState); const real_t M2 = Model.MomentM2(tag, lbmState);Modify the call of function to compute \(\mu\) instead of \(c\)
Solution
//const real_t M2 = Model.MomentM2(tag, lbmState); const real_t MU = Model.M2(tag, lbmState);
Next, the appropriate function must be called for collision rate to take into account the mobility \(\mathcal{M}_\phi\)
// compute collision rate collider.tau = Model.tau(tag, lbmState);Solution
collider.tau = Model.tauM(tag, lbmState);
Finally, the equilibrium distribution function is currently written for ADE (lines 4 & 10)
1// ipop = 0 2collider.f[0] = Base::get_f_val(tag, IJK, 0); 3collider.S0[0] = 0.0 ; 4collider.feq[0] = w[0]*M0; 5 6// ipop > 0 7for (int ipop = 1; ipop < npop; ++ipop) { 8 collider.f[ipop] = this->get_f_val(tag, IJK, ipop); 9 collider.S0[ipop] = 0.0 ; 10 collider.feq[ipop] = w[ipop] * (M2 + c_cs2 * Base::compute_scal(ipop, uc));
Modify it to take into account \(c\) (moment 0) and \(\mu\) (moment 2) (see SMEMaG course)
Solution
1// ipop = 0 2collider.f[0] = Base::get_f_val(tag, IJK, 0); 3collider.S0[0] = 0.0 ; 4collider.feq[0] = M0 - 3.0*MU*(1 - w[0]); 5// ipop > 0 6for (int ipop = 1; ipop < npop; ++ipop) { 7 collider.f[ipop] = this->get_f_val(tag, IJK, ipop); 8 collider.S0[ipop] = 0.0 ; 9 collider.feq[ipop] = 3.0*MU*w[ipop] + w[ipop] * c_cs2 * Base::compute_scal(ipop, uc);
Function init_macro
Only one initial condition exists for ADE with ADE_GAUSSIAN:
if (Model.initType == ADE_GAUSSIAN) { c = Model.ampl * exp( - ( SQR(x-Model.x0) / (2 * SQR(Model.sigmax)) + SQR(y-Model.y0) / (2 * SQR(Model.sigmay)) )) ; vx = Model.initVX; vy = Model.initVY; }
Write three initial conditions corresponding to keywords
SERPENTINE,RANDOM_SPINODALandRANDOM_NUCLEATION
Solution for serpentine
else if (Model.initType == SERPENTINE) { const real_t pi = M_PI; xphi = (Model.r0 - sqrt(SQR(x - Model.x0) + SQR(y - Model.y0))); c = Model.phi0(xphi) ; vx = - pi*Model.U0 * cos(pi*((x/Model.L)-0.5)) * sin(pi*((y/Model.L)-0.5)); vy = pi*Model.U0 * sin(pi*((x/Model.L)-0.5)) * cos(pi*((y/Model.L)-0.5)); }Warning
At line
c = Model.phi0(xphi) ;the positionxphiis “regularized” with the hyperbolic tangent functionphi0. Don’t forget to define it inModels_CH.h.Solution for spinodal
else if (Model.initType == RANDOM_SPINODAL) { c = rand_gen.drand(); }Solution for nucleation
else if (Model.initType == RANDOM_NUCLEATION) { if (IJK[IX] % Model.valModuloX || IJK[IY] % Model.valModuloY ) { c = 0.2 ; } else { real_t r = 0.0; r = rand_gen.drand(); c = (9.0+r)/10.0 ; } }Remark
Both integers
valModuloXandvalModuloYhave already been declared in fileModels_CH.
Function update_macro
Currently, only the concentration and the velocity are updated:
const real_t c = moment_c; const real_t vx = Model.initVX; const real_t vy = Model.initVY; this->set_lbm_val(IJK, IC , c ); this->set_lbm_val(IJK, IU , vx ); this->set_lbm_val(IJK, IV , vy );
Add for chemical potential and add a condition for velocity of SERPENTINE option
Solution
const real_t c = moment_c; real_t vx = Model.initVX; real_t vy = Model.initVY; const real_t mu = Model.M2(tagC, lbmStatePrev); if (Model.initType == SERPENTINE) { const real_t pi = M_PI; vx = - pi*Model.U0 * cos(pi*((x/Model.L)-0.5)) * sin(pi*((y/Model.L)-0.5)); vy = pi*Model.U0 * sin(pi*((x/Model.L)-0.5)) * cos(pi*((y/Model.L)-0.5)); } this->set_lbm_val(IJK, IC , c ); this->set_lbm_val(IJK, IU , vx ); this->set_lbm_val(IJK, IV , vy ); this->set_lbm_val(IJK, IMU, mu);
Function update_macro_grad
Currently the function is empty because for ADE, there is no need to compute additional gradients or laplacian with the LB method.
KOKKOS_INLINE_FUNCTION void update_macro_grad(IVect<dim> IJK) const { }
This is not the case for Cahn-Hilliard equation because a laplacian is involved in the definition of chemical potential. Add the computation of laplacian.
Solution
KOKKOS_INLINE_FUNCTION void update_macro_grad(IVect<dim> IJK) const { real_t laplace_c = Base::compute_laplacian(IJK, IC, BOUNDARY_EQUATION_1); Base::set_lbm_val(IJK, ILAPLAC, laplace_c); }
Add the keywords of initial conditions
In file InitConditionsTypes.h, two keywords are currently declared:
enum InitCondition{ PHASE_FIELD_INIT_UNDEFINED, ADE_GAUSSIAN };
Add the three new keywords for Cahn-Hilliard:
SERPENTINE,RANDOM_SPINODALandRANDOM_NUCLEATION.
Solution
enum InitCondition{ PHASE_FIELD_INIT_UNDEFINED, ADE_GAUSSIAN, RANDOM_SPINODAL, RANDOM_NUCLEATION, SERPENTINE };
3. Verifications of your implementation
Three .ini input files are available in the directory Tutorial01_Cahn-Hilliard to check your implementation for each initial condition:
Tuto-CH_Test01-Serpentine.ini
Tuto-CH_Test02-Spinodal.ini
Tuto-CH_Test03-Nucleation.ini
It is recommended to start with Tuto-CH_Test01-Serpentine.ini because comparisons can be done with csv files contained in the folder Contours_CAC.
Run Tuto-CH_Test01-Serpentine.ini
Run
Tuto-CH_Test01-Serpentine.ini
Commands
Go to the appropriate folder
$ cd run_training_lbm/Tutorial01_Cahn-Hilliardand run LBM_Saclay
$ ../../build_CH/src/LBM_saclay Tuto-CH_Test01-Serpentine.ini
Once the simulations is over, post-process your results with paraview.
Open paraview (e.g. version 5.11) and compare the contours from Cahn-Hilliard
.vtifiles andcsvfiles of subfolderContours_CAC.Commands for contours
Open all
.vtifiles and selectcomp
Ctrl spaceandCell Data to Point DataandApplyClic on
contourand select fieldcompwith value0.5andApplyCommands for csv files
Open
csvfiles (choose optionCSV Reader) +OK+ApplyCrtl space +
Table To Points+ selectPoints:0for X Column andPoints:2for Y ColumnPlay with
Point Sizein sectionStyling
Run & post-process
Run Spinodal decomposition:
Tuto-CH_Test02-Spinodal.iniMake a video with paraview (version 5.11)
$ path-to-pvpython/pvpython Make-Movie-Spinodal_PV511.pvpy
You must obtain the video below
Video: simulation of spinodal decomposition
Run & post-process
Run Nucleation:
Tuto-CH_Test03-Nucleation.iniMake a video with paraview (version 5.11)
$ path-to-pvpython/pvpython Make-Movie-Nucleation_PV511.pvpy
You must obtain the video below
Video: simulation of spinodal decomposition
4. From Cahn-Hilliard toward Conservative Allen-Cahn model
Modificatons inside three files
Mathematical model & LBM scheme
Now we want to simulate the conservative Allen-Cahn model Eq. (469).
The LBM scheme is the second method in LBM scheme
Declaration of new fields
In
enum ComponentIndex, add new indicesIDPHIDXandIDPHIDY
Solution
enum ComponentIndex { IU , /*!< X velocity / momentum index */ IV , /*!< Y velocity / momentum index */ IW , /*!< Z velocity / momentum index */ IC , /*!< Phase field index */ IMU , /*!< Chemical potential */ ILAPLAC, /*!< Laplacien of Phase field */ IDPHIDX, /*!< Grad X of Phase field */ IDPHIDY, /*!< Grad Y of Phase field */ COMPONENT_SIZE /*!< invalid index, just counting number of fields */ };
struct ModelParams
Add a new constant value \(\epsilon=10^{-16}\) for computation of normal vector \(\boldsymbol{n}\)
Solution
using LBMState = typename Kokkos::Array<real_t, COMPONENT_SIZE>; static constexpr real_t NORMAL_EPSILON = 1.0e-16;
Read and declare a new flag (real)
counter_termfor CAC model. Ifcounter_term=0then the Cahn-Hilliard model will be simulated. Else Ifcounter_term=1, the CAC model will be solved.
Solution
Read
counter_term = configMap.getFloat("params","counter_term",0.0);
Declare
real_t counter_term;
Add specific functions for CAC
Add a function
Source_CTfor counter term \((4/W)M_{\phi}\phi(1-\phi)\boldsymbol{n}\)
Solution
Source_CT// ======================================================= // Source term for counter term of phase field equation KOKKOS_INLINE_FUNCTION RVect2 Source_CT (EquationTag1 tag, const LBMState& lbmState) const { const real_t norm = sqrt( SQR(lbmState[IDPHIDX]) + SQR(lbmState[IDPHIDY]))+NORMAL_EPSILON; real_t force_ct = Mphi * 4.0*lbmState[IPHI]*(1.0-lbmState[IPHI])/ W / norm; RVect2 term; term[IX] = force_ct * lbmState[IDPHIDX]; term[IY] = force_ct * lbmState[IDPHIDY]; return term; }
Rename the second order function for CH
M2_CHand add a new oneM2_CACfor CAC
Solutions
M2_CACandM2_CH// ======================================================= // Second order moment of feq for CAC model KOKKOS_INLINE_FUNCTION real_t M2_CAC(EquationTag1 tag, const LBMState &lbmState) const { return lbmState[IC]; } // ======================================================= // second order moment of feq for CH model KOKKOS_INLINE_FUNCTION real_t M2_CH(EquationTag1 tag, const LBMState &lbmState) const { const real_t mu = sigma * 1.5 / W * (g_prime(lbmState[IC]) - SQR(W) * lbmState[ILAPLAC]); return mu; }
Function setup_collider
Add
if-else ifwithcounter_termfor Cahn-Hilliard or Conservative Allen-Cahn
Solution
LBMState lbmState; Base::template setupLBMState2<LBMState, COMPONENT_SIZE>(IJK, lbmState); if (Model.counter_term == 0.0) { const real_t comp = Model.M0(tag, lbmState); const RVect<dim> uc = Model.M1<dim>(tag, lbmState); const real_t MU = Model.M2_CH(tag, lbmState); // compute collision rate collider.tau = Model.tau(tag, lbmState); // ipop = 0 collider.f[0] = Base::get_f_val(tag, IJK, 0); collider.S0[0] = 0.0 ; collider.feq[0] = comp -3.0*MU*(1 - w[0]); // ipop > 0 for (int ipop = 1; ipop < npop; ++ipop) { collider.f[ipop] = this->get_f_val(tag, IJK, ipop); collider.S0[ipop] = 0.0 ; collider.feq[ipop] = 3.0*MU*w[ipop] + w[ipop] * c_cs2 * Base::compute_scal(ipop, uc); } } else if (Model.counter_term == 1.0) { }
The second part
else ifis empty. Add the necessary code lines to solve CAC model
Solution
else if (Model.counter_term == 1.0) { const real_t M0 = Model.M0(tag, lbmState); const RVect<dim> M1 = Model.M1<dim>(tag, lbmState); const real_t M2 = Model.M2_CAC(tag, lbmState); const RVect2 G_ct = Model.Source_CT(tag, lbmState); // compute collision rate collider.tau = Model.tau(tag, lbmState); real_t staudx = 1.0/((collider.tau-0.5)*dx/e2); for (int ipop=0; ipop<npop; ++ipop) { collider.f[ipop ] = this->get_f_val(tag,IJK,ipop); collider.S0[ipop ] = w[ipop] * dt * staudx * Base::compute_scal(ipop,G_ct); collider.feq[ipop] = w[ipop] * (M2 + c_cs2 * Base::compute_scal(ipop, M1) ) - 0.5 * (collider.S0[ipop]); } }
Function update_macro_grad
Update gradient computation for \(\boldsymbol{n}=\boldsymbol{\nabla}\phi/|\boldsymbol{\nabla}\phi|\)
Solution
// ============================================================= // Update gradients of macrosopic variables KOKKOS_INLINE_FUNCTION void update_macro_grad(IVect<dim> IJK) const { real_t laplace_c = Base::compute_laplacian(IJK, IC, BOUNDARY_EQUATION_1); Base::set_lbm_val(IJK, ILAPLAC, laplace_c); RVect<dim> gradPhi; this->compute_gradient(gradPhi, IJK, IC, BOUNDARY_EQUATION_1); this->set_lbm_val(IJK, IDPHIDX, gradPhi[IX]); this->set_lbm_val(IJK, IDPHIDY, gradPhi[IY]); }
Verifications of your implementation
Verification of your implementation
Run your CAC model with the input file
Tuto-CH_Test04_CAC_Serpentine.iniwhich is contained in the folderrun_training_lbm/Tutorial01_Cahn-HilliardCompare your contours with those in the subfolder
Contours_CAC
5. Push your developments on Codev-Tuleap
Save your developments on codev-tuleap.cea.fr
Once your verifications are carried out, save only your implementation (not your output .vti files). See commands on After programming: push your developments
Section author: Alain Cartalade