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 Tutorial_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

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/Tutorial_Cahn-Hilliard

Commands

Go to the directory

$ cd LBM_Saclay_Rech-Dev/run_training_lbm/Tutorial_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 for Cahn-Hilliard

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
 2         collider.f[0]   = Base::get_f_val(tag, IJK, 0);
 3         collider.S0[0]  = 0.0 ;
 4         collider.feq[0] = w[0]*M0;
 5
 6         // ipop > 0
 7         for (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
2          collider.f[0]   = Base::get_f_val(tag, IJK, 0);
3          collider.S0[0]  = 0.0 ;
4          collider.feq[0] = M0 - 3.0*MU*(1 - w[0]);
5          // ipop > 0
6          for (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 Tutorial_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/Tutorial_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.

Post-process 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

Exercise

Make a video with paraview for the two other test cases:

  • Spinodal decomposition: Tuto-CH_Test02-Spinodal.ini

  • Nucleation: Tuto-CH_Test03-Nucleation.ini

4. 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