3D Materials Lab: Tutorials
  • 3D Materials Lab: Tutorials
  • ➡️Defects
    • Intro to Defect Calculations
    • Interpreting Defect and Energy Level Diagrams
  • ➡️Transport
    • Boltzmann Transport Theory and Thermoelectric Performance
  • ➡️Polarization
    • Introduction to Polarization Calculations
  • ➡️Dynamics in crystalline solids
    • First-principles Calculations of Atomic Diffusion in Crystalline Solids
    • Tutorial on Using Nudged Elastic Band (NEB) method
    • Calculating Polarization Switching Barrier using SS-NEB
    • Pylada High-throughput Workflow for SS-NEB Calculations
  • ➡️Installing Pylada on HPC
    • Instructions
  • ➡️ALLOY MODELING
    • Create alloy models
Powered by GitBook
On this page
  • Modification to exiting Pylada
  • Customized Pylada workflow
  • User-defined inputs
Export as PDF
  1. Dynamics in crystalline solids

Pylada High-throughput Workflow for SS-NEB Calculations

This tutorial is based on VASP+VTST, vtstscripts, and Pylada

Written by Cheng-Wei Lee (clee2 [at] mines [dot] edu)

This tutorial assumes that users already have the relaxed endpoints for (SS-)NEB calculations, which can be also done in a high-throughput fashion using Pylada scripts like HT_relax.py or any other Python workflow that can automate the VASP calculations. The tutorials cover three topics: (1) modification to existing Pylada package, (2) customized Pylada workflows, and (3) user defined inputs.

Modification to exiting Pylada

Since nudged elastic band (NEB) calculations are performed differently from the typical VASP calculations, i.e., there are multiple working subfolders within a working folder, there are two required modifications to the published Pylada package.

modification 1 applies to "pylada/vasp/functional.py"

At line 865-868 (see blow) of the python script, there is a IF condition block to ensure that the typical VASP runs are executed properly. Since the file structure are different for NEB calculations from typical VASP calculations, we need to comment these lines out as a temporary solution.

 865         ## Last yield should be an extraction object.
 866         #if not program.success:
 867         #    raise RuntimeError("Vasp failed to execute correctly.")
 868         #return program

modification 2 applies to "pylada/vasp/keywords.py"

For the two blocks that determine ICHARG and ISTART for continuing VASP calculations, the original Pylada package only consider the main working folder and we need to add one of the working subfolders to the checklist:

original ones at line 702 and 796

            directories = [outdir]

modified one for both

            directories = [outdir, outdir+'/01/']

Customized Pylada workflow

Here's the customized Pylada workflow that takes the number of NEB iterations and the number of intermediate NEB images as input.


######################################

class SSNEB_Chain(object):

    # Defining the folder tree structure
    def __init__(self, no_iterations=10, no_images=24):
        self.no_iterations = no_iterations
        self.fnames = ['iter_%s'%(x+1) for x in range(no_iterations)]
        self.no_images = no_images

    # Defining the Extraction object
    def Extract(self, jobdir):
        '''
        The success condition for the ss-neb calculation is based on all images
        for each image, success = True when a VASP run is finished properly (with the timing information shown at the end of OUTCAR) 
        '''
        from pylada.vasp import MassExtract, Extract
        extract = MassExtract(jobdir)
        #extract = Extract(jobdir)
        success={}
        from glob import iglob
        if self.no_images > 99: digit = '/???'
        else: digit = '/??'
        for image in iglob(jobdir+digit):
            success[image]=Extract(image).success
        #for name in self.fnames:
        #    success[name]=Extract(jobdir+'/'+name).success
        success=all(success.values())
        #success = all(extract.success.values())
        extract.success=success
        return extract

    def __call__(self, structure, outdir=None, vasp=None, **kwargs ):

        from copy import deepcopy
        from os import getcwd
        import os
        from os.path import exists
        from os.path import join
        from pylada.misc import RelativePath
        from pylada.error import ExternalRunFailed
        from pylada.vasp.extract import Extract, MassExtract
        from pylada.vasp import Vasp
        from pylada.vasp.relax import Relax

        # make this function stateless.
        structure_ = structure.copy()
        outdir = getcwd() if outdir is None else RelativePath(outdir).path

        ############## NEB Loop ########################
        for name in self.fnames:
            #print("Stage of "+name)
            iternum = int(name.split('_')[-1])
            fulldir = join(outdir, name)
            # create the folder if it does not exist
            if not os.path.exists(fulldir):
                os.makedirs(fulldir)
            # check if the given iteration finished
            if exists(fulldir+'/progress.dat'): continue
   
            # restart from unfinished calculations; 
            # make sure it is not a empty CONTCAR before mv CONTCAR POSCAR
            if exists(outdir+'/01/CONTCAR') and os.path.getsize(outdir+'/01/CONTCAR') > 0:
               for i in range(1,self.no_images+1):
                   num = '%02d'%(i)
                   os.system('mv %s %s'%(outdir+'/'+num+'/CONTCAR',outdir+'/'+num+'/POSCAR'))

            ## functional
            neb = Vasp(copy=vasp)
            neb.add_keyword('IMAGES', self.no_images) 
            if iternum > 1: 
                neb.istart = 1  # use previous step's WF; but get new FFT grid
                neb.icharg = 0 
            params = deepcopy(kwargs)
            
            ## if this calculation has not been done, run it
            output = neb(structure_, outdir=outdir, **params)
            #print(name+" finished")
            
            # run nebefs.pl and record volume, energy, force, and stress
            os.chdir(outdir)
            os.system('/home/clee2/software/vtstscripts-967/nebefs.pl > %s'%(fulldir+'/progress.dat')) 
            os.system('cp stdout %s'%(fulldir+'/stdout'))
            #os.chdir('-')
            # move the CONTCAR to POSCAR and cp the new POSCAR to fulldir,i.e., the progress folders
            for i in range(1,self.no_images+1):
                num = '%02d'%(i)
                os.system('mv %s %s'%(outdir+'/'+num+'/CONTCAR',outdir+'/'+num+'/POSCAR'))
                os.system('cp %s %s'%(outdir+'/'+num+'/POSCAR',fulldir+'/POSCAR_'+num)) 
            
            if os.system('grep "reached required accuracy" %s'%(outdir+'/stdout'))  == 0:
                break   
            else: 
                # remove the OUTCAR in 01/ such that extract.success remains False before SS-NEB converges 
                os.system('/home/clee2/software/vtstscripts-967/nebbarrier.pl')
                os.system('rm %s'%(outdir+'/01/OUTCAR'))
            #if not output.success:
            #    raise ExternalRunFailed(name+" calculation did not succeed.") 
        #########################

        return self.Extract(outdir)

The user needs to modify the path to vtstscripts

User-defined inputs

Here's an exemplar Pylada submission script to utilize the customized Pylada workflow above. Pylada will prepare the INCAR, KPOINTS, POTCAR for each provided pair of endpoints and interact with HPC system to submit VASP jobs. User still need to create the initial intermediate images.

"custom_chain_SSNEB.py" is the name of the workflow above
from pylada.crystal import read
from pylada.vasp.specie import U
from copy import deepcopy
from glob import iglob
import sys
from custom_chain_SSNEB import SSNEB_Chain
######################################

from pylada.vasp import Vasp
vasp=Vasp()

vasp.program = '/home/clee2/software/vasp.5.4.4_VTST/bin/vasp_std'

vasp.prec       = "accurate"
vasp.ediff      = 1.0e-6        # total, not per atom
vasp.ediffg     = -1.0e-2     
vasp.encut      = 340.0
vasp.lplane     = True
vasp.addgrid    = True
vasp.npar       = 4
vasp.ismear     = 0
vasp.sigma      = 0.01
vasp.isym       = 0
vasp.kpoints    = "\n0\nAuto\n20"
vasp.lorbit     = 10
vasp.lcharg     = True
vasp.lwave      = True
vasp.lmaxmix    = 4
vasp.lpead      = False
vasp.algo       = "Normal"

### Key SS-NEB parameters defined here
vasp.ibrion     = 3   # needed for VTST
vasp.nelmin     = 5
vasp.isif       = 3   # for ss-neb; 
vasp.nsw        = 10  # small number like 10 to have frequent restart and updated Fourier grid
vasp.add_keyword('POTIM', 0) # needed for VTST 
vasp.add_keyword('ICHAIN', 0)
vasp.add_keyword('IOPT', 3)
vasp.add_keyword('SPRING',-5)
vasp.add_keyword('LCLIMB',False)
vasp.add_keyword('LNEBCELL',True)

pseudoDir = '~/software/pseudos'
vasp.add_specie = "Ag", pseudoDir + "/Ag", U("dudarev", "d", 5.0)
vasp.add_specie = "Al", pseudoDir + "/Al"
vasp.add_specie = "As", pseudoDir + "/As"
vasp.add_specie = "Au", pseudoDir + "/Au", U("dudarev", "d", 3.0)
vasp.add_specie = "B",  pseudoDir + "/B"
vasp.add_specie = "Ba", pseudoDir + "/Ba"
vasp.add_specie = "Be", pseudoDir + "/Be"
vasp.add_specie = "Bi", pseudoDir + "/Bi"
vasp.add_specie = "Br", pseudoDir + "/Br"
vasp.add_specie = "C",  pseudoDir + "/C"
vasp.add_specie = "Ca", pseudoDir + "/Ca"
vasp.add_specie = "Cd", pseudoDir + "/Cd"
vasp.add_specie = "Cl", pseudoDir + "/Cl"
vasp.add_specie = "Co", pseudoDir + "/Co", U("dudarev", "d", 3.0)
vasp.add_specie = "Cr", pseudoDir + "/Cr", U("dudarev", "d", 3.0)
vasp.add_specie = "Cs", pseudoDir + "/Cs"
vasp.add_specie = "Cu", pseudoDir + "/Cu", U("dudarev", "d" , 5.0)
vasp.add_specie = "F",  pseudoDir + "/F"
vasp.add_specie = "Fe", pseudoDir + "/Fe", U("dudarev", "d", 3.0)
vasp.add_specie = "Ga", pseudoDir + "/Ga"
vasp.add_specie = "Ge", pseudoDir + "/Ge"
vasp.add_specie = "H",  pseudoDir + "/H"
vasp.add_specie = "Hf", pseudoDir + "/Hf", U("dudarev", "d", 3.0)
vasp.add_specie = "Hg", pseudoDir + "/Hg"
vasp.add_specie = "I",  pseudoDir + "/I"
vasp.add_specie = "In", pseudoDir + "/In"
vasp.add_specie = "Ir", pseudoDir + "/Ir", U("dudarev", "d", 3.0)
vasp.add_specie = "K",  pseudoDir + "/K"
vasp.add_specie = "La", pseudoDir + "/La", U("dudarev", "d", 3.0)
vasp.add_specie = "Li", pseudoDir + "/Li"
vasp.add_specie = "Mg", pseudoDir + "/Mg"
vasp.add_specie = "Mn", pseudoDir + "/Mn", U("dudarev", "d", 3.0)
vasp.add_specie = "Mo", pseudoDir + "/Mo", U("dudarev", "d", 3.0)
vasp.add_specie = "N",  pseudoDir + "/N"
vasp.add_specie = "Na", pseudoDir + "/Na"
vasp.add_specie = "Nb", pseudoDir + "/Nb", U("dudarev", "d", 3.0)
vasp.add_specie = "Ni", pseudoDir + "/Ni", U("dudarev", "d", 3.0)
vasp.add_specie = "O",  pseudoDir + "/O"
vasp.add_specie = "P",  pseudoDir + "/P"
vasp.add_specie = "Pb", pseudoDir + "/Pb"
vasp.add_specie = "Pd", pseudoDir + "/Pd", U("dudarev", "d", 3.0)
vasp.add_specie = "Pt", pseudoDir + "/Pt", U("dudarev", "d", 3.0)
vasp.add_specie = "Rb", pseudoDir + "/Rb"
vasp.add_specie = "Rh", pseudoDir + "/Rh", U("dudarev", "d", 3.0)
vasp.add_specie = "Ru", pseudoDir + "/Ru", U("dudarev", "d", 3.0)
vasp.add_specie = "S",  pseudoDir + "/S"
vasp.add_specie = "Sb", pseudoDir + "/Sb"
vasp.add_specie = "Sc", pseudoDir + "/Sc", U("dudarev", "d", 3.0)
vasp.add_specie = "Se", pseudoDir + "/Se"
vasp.add_specie = "Si", pseudoDir + "/Si"
vasp.add_specie = "Sn", pseudoDir + "/Sn"
vasp.add_specie = "Sr", pseudoDir + "/Sr"
vasp.add_specie = "Ta", pseudoDir + "/Ta", U("dudarev", "d", 3.0)
vasp.add_specie = "Tl", pseudoDir + "/Tl"
vasp.add_specie = "Te", pseudoDir + "/Te"
vasp.add_specie = "Ti", pseudoDir + "/Ti", U("dudarev", "d", 3.0)
vasp.add_specie = "V",  pseudoDir + "/V",  U("dudarev", "d", 3.0)
vasp.add_specie = "W",  pseudoDir + "/W",  U("dudarev", "d", 3.0)
vasp.add_specie = "Y",  pseudoDir + "/Y",  U("dudarev", "d", 3.0)
vasp.add_specie = "Zn", pseudoDir + "/Zn"
vasp.add_specie = "Zr", pseudoDir + "/Zr", U("dudarev", "d", 3.0)
vasp.add_specie = "Gd", pseudoDir + "/Gd_3"

structures = {}
# to have name and structure (one of the end image)
for names in iglob('end_images/AlHfN_p083_random*/'):
    strc = read.poscar(names+'POSCAR_p/CONTCAR')  # e.g. pick the polar one for ferroelectric switching
    name = names.replace('end_images/','SS-NEB_alloy/')
    structures[name] = strc


#-------- setting up the jobfolder -----
from IPython.core.interactiveshell import InteractiveShell
from pylada.jobfolder import JobFolder
from pylada import interactive
from copy import deepcopy

jobfolder = JobFolder()

numI=39
for name, structure in structures.items():
    job = jobfolder / name
    job.functional = SSNEB_Chain(no_iterations=50,no_images=numI)
    job.params["structure"] = structure # use the polar structure to have POTCAR 
    job.params['vasp'] = vasp
    

interactive.jobfolder = jobfolder
%savefolders ht_ssneb_alloy_AlHfN_p083.pkl
%explore ht_ssneb_alloy_AlHfN_p083.pkl

Here an exmeplar script based on Pylada and vtstscript to generate initial neb intermediate images

User needs to change the path to vtstscript

import numpy as np
import os 
from pylada.crystal import read
from glob import iglob
from pylada.vasp import Extract


# loop through all compounds

# choose an integer number for the number of intermediate images, 
numI = 39  # 
enforce = False # determine whether to create new POSCARs and overwrite existing poscars

# get current working directory
pathcwd = os.getcwd()

for names in iglob(pathcwd+'/end_images/AlHfN_p083_random*/'):
    strc_p = names+'POSCAR_p/CONTCAR'
    strc_a = names+'POSCAR_a/CONTCAR'
    if Extract(names+'POSCAR_p/').success == True and Extract(names+'POSCAR_a/').success == True:
        print('Relaxation of '+names+' finished')
        strc = read.poscar(strc_p)
        name = names.replace('/end_images/','/SS-NEB_alloy/')
        if not os.path.exists(name):
             os.makedirs(name)
             print("Create new folder "+name)
             os.chdir(name)
             os.system('/home/clee2/software/vtstscripts-967/nebmake.pl %s %s %s'%(strc_p,strc_a,numI))  # 0.5 is that half of the atoms is cation ; 
             os.chdir(pathcwd)
        elif enforce == True:
             os.chdir(name)
             os.system('/home/clee2/software/vtstscripts-967/nebmake.pl %s %s %s'%(strc_p,strc_a,numI)) 
             os.chdir(pathcwd)
         
         # copy OUTCAR to first and last folder
        first = name+'00/'
        last  = name+ "%02d/" % (int(numI+1))
        os.system('cp %s %s'%(names+'POSCAR_p/OUTCAR',first))
        os.system('cp %s %s'%(names+'POSCAR_a/OUTCAR',last))
PreviousCalculating Polarization Switching Barrier using SS-NEBNextInstructions

Last updated 11 months ago

➡️