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-Tutorial which is contained in kernels/Templates_for_Tutorials, and rename it Cahn-Hilliard_Tutorial in the folder kernels

Commands
$ cd LBM_Saclay_Rech-Dev/src/kernels
$ cp -r Templates_for_Tutorials/ADE_for_CH-Tutorial ./Cahn-Hilliard_Tutorial

Remarks

  • Your new developments will be performed in the folder Cahn-Hilliard_Tutorial

  • The folder name Cahn-Hilliard_Tutorial will be used for compilation

  • All files have the extension _ADE for Advection Diffusion Equation. Rename them with _CH for 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

  1. Change strings _ADE with _CH

    • Open all files in your favorite editor (e.g. geany or codium).

    • Search in all files the strings _ADE and replace them with _CH (Ctrl H with geany).

  2. Change the problem name: in file Setup.h modify the problem name ADE with CH

    register_problem<Problem, ModelParams::Tag2Quadra>("ADE", "base");
    
    Modification with CH
    register_problem<Problem, ModelParams::Tag2Quadra>("CH", "base");
    

    Remark

    Your new problem will be named CH inside your .ini input 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 CH of LBM_Saclay either with the simple diffusion of gaussian or with the advected gaussian in the folder run_training_lbm/Tutorial01_Cahn-Hilliard

Commands

Go to the directory

$ cd LBM_Saclay_Rech-Dev/run_training_lbm/Tutorial01_Cahn-Hilliard

Run simple diffusion of gaussian

$ ../../build_CH/src/LBM_saclay TestCase_Gaussian_ADE.ini

Run 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 IMU and laplacian ILAPLAC.

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 mu will be used in the .ini input 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_t at the end of ModelParams

real_t mobility, W, sigma;

Add specific functions for Cahn-Hilliard

  • Add a new function for derivative of double-well g_prime defined 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 M2 to 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 & ILAPLAC have been declared in Index_CH.h.

  • The parameters sigma (surface tension \(\sigma\)) and W (interface thickness \(W\)) have been read and declared inside ModelParams (previous box).

  • The new function g_prime has been defined just above

  • Add a new function tauM for 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 mobility has been read and declared in ModelParams.

  • You could have used the parameter D as 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 phi0 defined 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

sign will take the values +1 or -1 in the input datafile .ini for initialize 1 inside 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_SPINODAL and RANDOM_NUCLEATION in file InitConditionsTypes.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_SPINODAL and RANDOM_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 position xphi is “regularized” with the hyperbolic tangent function phi0. Don’t forget to define it in Models_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 valModuloX and valModuloY have already been declared in file Models_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_SPINODAL and RANDOM_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-Hilliard

and 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 .vti files and csv files of subfolder Contours_CAC.

Commands for contours
  • Open all .vti files and select comp

  • Ctrl space and Cell Data to Point Data and Apply

  • Clic on contour and select field comp with value 0.5 and Apply

Commands for csv files
  • Open csv files (choose option CSV Reader) + OK + Apply

  • Crtl space + Table To Points + select Points:0 for X Column and Points:2 for Y Column

  • Play with Point Size in section Styling

Run & post-process

  • Run Spinodal decomposition: Tuto-CH_Test02-Spinodal.ini

  • Make 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.ini

  • Make 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 indices IDPHIDX and IDPHIDY

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_term for CAC model. If counter_term=0 then the Cahn-Hilliard model will be simulated. Else If counter_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_CT for 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_CH and add a new one M2_CAC for CAC

Solutions M2_CAC and M2_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 if with counter_term for 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 if is 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.ini which is contained in the folder run_training_lbm/Tutorial01_Cahn-Hilliard

  • Compare 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