# Adsorption and reaction of species A and B2¶

Turning from simple adsorption events towards surface reactions, we demonstrate here the $$A+B_2$$ reaction on a fcc(100) surface and a truncated octahedral nanoparticle. The reaction is modeled using the following reactions:

$\begin{split}A(g) + ^* \longleftrightarrow A^* \\ A^* + ^* \longleftrightarrow * + A^* \\ B_2(g) + 2^* \longleftrightarrow 2B^* \\ B^* + ^* \longleftrightarrow ^*+B^* \\ A^*+B^* \longrightarrow AB(g) + 2^*\end{split}$

Thus additional to the already described events of single atom and dissociative adsorption, the diffusion of species $$A$$ and $$B$$, as well as the formation and desorption of $$AB$$ has to be implemented. In the following we present first the event classes which are additionally needed and are the same for the nanoparticle and the surface.

## Define events¶

The defined events are stored in user_events.py. For each possible type of event, a class is derived from NeighborKMC.base.events.EventBase. In this case, we need to define seven different events, the adsorption, desorption and diffusion of species A and B2, and the reaction between both species. The adsorption and diffusion of A and B2 are defined as before. For the A diffusion we derive a class to contain the event:

class ADiffEvent(EventBase):
def __init__(self, params):
EventBase.__init__(self, params)


The constructor __init__(self,params) attaches relevant parameters to the object. We need a function possible(self,system, site, other_site) that returns True if the event is possible on the current site-pair. Here we assume that the species A is assigned to the coverage number:2. A diffusion event can take place if the site is covered with A and the neighbor site empty or vice versa.

def possible(self, system, site, other_site):
# If site is uncovered
if (system.sites[site].covered == 2 and system.sites[other_site].covered == 0) or (system.sites[site].covered == 0 and system.sites[other_site].covered == 2):
return True
else:
return False


Now we also need to define a function get_rate(self, system, i_site, other_site) that returns the rate constant. To keep this as simple as possible, the rate constant is chosen to be $$R=1$$.

def get_rate(self, system, i_site, other_site):
R = 1.
return R


Each event requires a method do_event(self,system, site, other_site) to perform modifications to the site-occupations when fired:

def do_event(self, system, site, other_site):
old_cov_site = system.sites[site].covered
old_cov_other_site = system.sites[other_site].covered
system.sites[site].covered = old_cov_other_site
system.sites[other_site].covered = old_cov_site


In this case, the coverage of the site and other_site are interchanged.

To take care of the correct time evolution of the MonteCoffee we introduce an additional block which returns if either neighboring sites are involved or not. Here no neighboring sites are involved, thus we return False.

def get_involve_other(self):
return True


The diffusion of species B is exactly as for species A implemented, with the difference that B is represented by 1 in the code.

For the reaction, A and B have to be present on either site or other_site. So the reaction becomes possible if:

def possible(self, system, site, other_site):
if (system.sites[site].covered == 1 and system.sites[other_site].covered == 2) or (system.sites[site].covered == 2 and system.sites[other_site].covered == 1):
return True
else:
return False


The rate constant can be chosen according to:

def get_rate(self, system, site, other_site):
R = .1
return R


and after the formation of AB, both sites: code:site and code:other_site are emptied:

def do_event(self, system, site, other_site):
system.sites[site].covered = 0
system.sites[other_site].covered = 0


In this example, it is assumed that the desorption of the formed product AB is instantaneous without the possibility to re-absorb and split into A and B.

## Reaction over a (100) surface¶

For the reaction over a (100) surface we use ASE to define the sites and their neighbors:

from ase.build import fcc(100)
from user_sites import Site

surface = fcc100("Pt", a=latt_param, size=(20,20,1))
sites = []

# Create a site for each surface-atom:
for i in range(len(atoms)):
sites.append(Site(stype=0,
covered=0, ind=i))


A 20x20 lattice is used to ensure convergence of the coverage and each atom site is connected to its four neighbors. The in Fig. 1 shown coverage is based on the following rate constants: Adsorption, desorption and diffusion rate: 1 s-1 and the reaction rate: 0.1 s-1. It should be noted the mean-field rate of 1 s-1 for B2 adsorption/desorption corresponds to 0.5 s-1 in the kMC model. With the reaction being the rate limiting step, all coverages are 1/3 after an equilibration period as can be seen in Fig. 1.

## Reaction over a nanoparticle¶

Similar to the (100) surface, we employ again ASE to define the sites, this time constructing a truncated octahedron consisting of 260 atoms of which 144 are exposed on the surface. In contrast to the surface, we are going to use two different type of sites, stype=0 representing the (111) facet sites and stype=1 representing the (100) facet sites, edges and corners. In the inset of Fig. 2 the different types of sites are visualized.

The atoms are defined as follows:

atoms = Octahedron("Pt", 8, cutoff=3, latticeconstant = a)
sites = []
write('trunc_octa.traj', atoms) # see how the nanoparticle looks like


but to assign the site types to the surface atoms, we use the coordination number to distinguish them, the (111) facet sites having a coordination number of 9. The coordination number of each atom is calculated and a list, surface_atom_ids created in which the surface atom ids’ (coordination number < 12) are stored.

 CNS = np.zeros(len(atoms))
for i, at in enumerate(atoms):
pcur = at.position
dp = np.sqrt([(p[0] - pcur[0]) ** 2. + (p[1] - pcur[1]) ** 2. +
(p[2] - pcur[2]) ** 2. for p in atoms.positions])
CNS[i] = len([val for val in dp if 0. < val < a / np.sqrt(2) + 0.01])
surface_atom_ids = [i for i in range(len(CNS)) if CNS[i] < 12]


In difference to the (100) surface, each site has now stype assigned which is either 0 or 1. For the (100) surface all sites have the same stype=0.

for i,indic in enumerate(surface_atom_ids):
if CNS[indic] == 9:
sstype = 0
else:
sstype = 1
sites.append(Site(stype=sstype, covered=0, ind=indic))


The neighbor list is calculated for the surface atom shell only (the atoms saved in the sites-list). All atoms for the nanoparticle do not have the same number of direct neighbors.

In the following we keep all rates fixed to the (100) surface ones, beside the rates for the A adsorption. For the A adsorption we are going to employ different, site-dependent adsorption rates. Therefore, beside ensuring that the site is empty, also the stype has to be considered to determine the adsorption rate. That is done in the following way in the AAdsEvent :

def get_rate(self, system, site, other_site):
if system.sites[site].stype == 0:
R = 1.
elif system.sites[site].stype == 1:
R = 10.
return  R


To see the effect of the rate of the A adsorption on the turn over frequency (TOF) of the simulation, we study four different combinations: First using either a rate constant of 1 s-1 or 10 s-1 on both sites and second by using the mixed cases, having 1 s-1 for stype=0 and 10 s-1 for stype=1 or vice versa. The results can be seen in Fig. 2. In the case of employing the same rate for the A adsorption as for the B adsorption the TOF is the highest, and with having a 10 times faster A adsorption than B adsorption, it being the lowest. In the case of high A adsorption, the sites are blocked leading to poisoning. For the mixed cases, the TOF is higher for the one with rate stype=0: 10 s-1. Not as many sites with stype=0 exist and therefore the A poisoning is less pronounce.