Skip to article frontmatterSkip to article content

Project 1: Grand Canonical Monte Carlo Simulations of Competitive Adsorption

Introduction

In surface science, competitive adsorption refers to the scenario where multiple species (adsorbates) compete for adsorption sites on a surface. This phenomenon is crucial in heterogeneous catalysis, separation processes, and sensor technology, where the presence of one adsorbate can significantly influence the adsorption behavior of another.

In this project, you will write a Python program that uses the grand canonical Monte Carlo (GCMC) method and the Metropolis algorithm to simulate competitive adsorption of two different adsorbate species on a two-dimensional (2D) square lattice. By varying parameters such as chemical potential, temperature, and interaction energies, you will compute the phase diagram of the system.

Competitive Adsorption on Surfaces

Two-Component Adsorption Model

Source
# Import necessary libraries
import matplotlib.pyplot as plt
import numpy as np

# Create figure and subplots
fig, axs = plt.subplots(1, 3, figsize=(15, 5))

# Left panel: Types of Sites
ax = axs[0]
ax.set_title('Types of Sites')
grid_size = 5

# Draw grid
for x in range(grid_size + 1):
    ax.plot([x, x], [0, grid_size], color='black', linewidth=0.5)
for y in range(grid_size + 1):
    ax.plot([0, grid_size], [y, y], color='black', linewidth=0.5)

# Define site types
empty_sites = [(1, 1), (3, 3)]
adsorbate_A_sites = [(2, 2), (4, 1)]
adsorbate_B_sites = [(1, 3), (3, 1)]

# Plot empty sites
for (x, y) in empty_sites:
    ax.plot(x + 0.5, y + 0.5, 's', color='white', markersize=40, markeredgecolor='black')

# Plot adsorbate A sites
for (x, y) in adsorbate_A_sites:
    ax.plot(x + 0.5, y + 0.5, 'o', color='blue', markersize=20)
    ax.text(x + 0.5, y + 0.5, 'A', ha='center', va='center', color='white', fontsize=12)

# Plot adsorbate B sites
for (x, y) in adsorbate_B_sites:
    ax.plot(x + 0.5, y + 0.5, 'o', color='red', markersize=20)
    ax.text(x + 0.5, y + 0.5, 'B', ha='center', va='center', color='white', fontsize=12)

ax.set_xlim(0, grid_size)
ax.set_ylim(0, grid_size)
ax.set_aspect('equal')
ax.set_xticks([])
ax.set_yticks([])

# Middle panel: Adsorption Energies
ax = axs[1]
ax.set_title('Adsorption Energies')

# Draw surface line
ax.plot([0, 5], [0, 0], color='black', linewidth=2)
ax.text(2.5, -0.5, 'Surface', ha='center', fontsize=12)

# Adsorbate A approaching surface
ax.plot(1, 3, 'o', color='blue', markersize=20)
ax.arrow(1, 2.5, 0, -2.0, head_width=0.2, head_length=0.3, fc='blue', ec='blue')
ax.text(1, 3.5, r'$\epsilon_A$', ha='center', color='blue', fontsize=12)

# Adsorbate B approaching surface
ax.plot(4, 3, 'o', color='red', markersize=20)
ax.arrow(4, 2.5, 0, -2.0, head_width=0.2, head_length=0.3, fc='red', ec='red')
ax.text(4, 3.5, r'$\epsilon_B$', ha='center', color='red', fontsize=12)

ax.set_xlim(0, 5)
ax.set_ylim(-1, 4)
ax.axis('off')

# Right panel: Interaction Energies
ax = axs[2]
ax.set_title('Interaction Energies')

# Adsorbates on surface
ax.plot(2, 2, 'o', color='blue', markersize=20)
ax.text(2, 2, 'A', ha='center', va='center', color='white', fontsize=12)
ax.plot(3, 2, 'o', color='blue', markersize=20)
ax.text(3, 2, 'A', ha='center', va='center', color='white', fontsize=12)
ax.plot(2.5, 1, 'o', color='red', markersize=20)
ax.text(2.5, 1, 'B', ha='center', va='center', color='white', fontsize=12)

# Interaction between A-A
ax.plot([2, 3], [2, 2], color='black', linestyle='--')
ax.text(2.5, 2.1, r'$\epsilon_{AA}$', ha='center', fontsize=12)

# Interaction between A-B
ax.plot([2, 2.5], [2, 1], color='black', linestyle='--')
ax.text(2.4, 1.5, r'$\epsilon_{AB}$', ha='center', fontsize=12)

ax.set_xlim(1, 4)
ax.set_ylim(0.5, 3)
ax.axis('off')

plt.tight_layout()
plt.show()
<Figure size 1500x500 with 3 Axes>

Consider a 2D square lattice representing the surface, where each lattice site can be empty (white squares in the left panel), occupied by adsorbate A (blue circles), or occupied by adsorbate B (red circles). The adsorption behavior is governed by adsorption energies ϵA\epsilon_A and ϵB\epsilon_B (middle panel) and interaction energies ϵAA\epsilon_{AA}, ϵBB\epsilon_{BB}, and ϵAB\epsilon_{AB} (right panel).

Lattice Model for Two Species

In this lattice model, the total energy of the system is given by

E=i(niAϵA+niBϵB)+12i,j(niAnjAϵAA+niBnjBϵBB+niAnjBϵAB+niBnjAϵAB)E = \sum_{i} \left( n_i^A \epsilon_A + n_i^B \epsilon_B \right) + \frac{1}{2} \sum_{\langle i,j \rangle} \left( n_i^A n_j^A \epsilon_{AA} + n_i^B n_j^B \epsilon_{BB} + n_i^A n_j^B \epsilon_{AB} + n_i^B n_j^A \epsilon_{AB} \right)

where niAn_i^A and niBn_i^B are the number of adsorbates A and B at site ii, respectively. If site ii is empty, niA=niB=0n_i^A = n_i^B = 0. If site ii is occupied by adsorbate A, niA=1n_i^A = 1 and niB=0n_i^B = 0. If site ii is occupied by adsorbate B, niA=0n_i^A = 0 and niB=1n_i^B = 1. The first sum runs over all lattice sites, and the second sum runs over all pairs of neighboring sites. The factor 12\frac{1}{2} avoids double-counting interactions.

Grand Canonical Ensemble

In the grand canonical ensemble, this system is open to exchange particles and energy with a reservoir at fixed chemical potentials μA\mu_A and μB\mu_B for adsorbates A and B, respectively, and temperature TT. The probability distribution function for the system is proportional to

P({niA,niB})exp(βΩ)P(\{n_i^A, n_i^B\}) \propto \exp\left(-\beta \Omega\right)

where Ω\Omega is the grand potential given by

Ω=EμANAμBNB\Omega = E - \mu_A N_A - \mu_B N_B

and β=1kT\beta = \frac{1}{kT} is the inverse temperature. The numbers of adsorbates A and B are denoted by NAN_A and NBN_B, respectively.

Grand Canonical Monte Carlo (GCMC) Simulations

Metropolis Algorithm Adapted for GCMC

In the grand canonical ensemble, the Metropolis algorithm is adapted to allow for particle addition and removal moves.

Psuedocode for GCMC Simulation

Initialize Lattice

FUNCTION initialize_lattice(size):
    CREATE a 2D array 'lattice' of dimensions size x size
    INITIALIZE all elements of 'lattice' to 0  # 0 represents empty sites
    RETURN 'lattice'

Compute Neighbor Indices with Periodic Boundary Conditions

FUNCTION compute_neighbor_indices(size):
    CREATE an empty dictionary 'neighbor_indices'
    FOR x FROM 0 TO size - 1:
        FOR y FROM 0 TO size - 1:
            neighbors = [
                ((x - 1) MOD size, y),
                ((x + 1) MOD size, y),
                (x, (y - 1) MOD size),
                (x, (y + 1) MOD size)
            ]
            neighbor_indices[(x, y)] = neighbors
    RETURN 'neighbor_indices'

Calculate Interaction Energy

FUNCTION calculate_interaction_energy(lattice, site, particle, neighbor_indices, epsilon_AA, epsilon_BB, epsilon_AB):
    SET x, y TO coordinates of 'site'
    SET 'interaction_energy' TO 0
    FOR each 'neighbor' in neighbor_indices[(x, y)]:
        SET 'neighbor_particle' TO lattice[neighbor]
        IF 'neighbor_particle' IS NOT 0:
            IF 'particle' IS 1:  # Particle A
                IF 'neighbor_particle' IS 1:
                    ADD 'epsilon_AA' TO 'interaction_energy'
                ELSE:  # Neighbor is Particle B
                    ADD 'epsilon_AB' TO 'interaction_energy'
            ELSE:  # Particle B
                IF 'neighbor_particle' IS 2:
                    ADD 'epsilon_BB' TO 'interaction_energy'
                ELSE:  # Neighbor is Particle A
                    ADD 'epsilon_AB' TO 'interaction_energy'
    RETURN 'interaction_energy'

Attempt to Add or Remove a Particle

FUNCTION attempt_move(lattice, N_A, N_B, N_empty, neighbor_indices, params):
    SET 'size' TO dimension of 'lattice'
    SET 'N_sites' TO size * size
    SET 'beta' TO 1 / params['T']
    EXTRACT parameters:
        'epsilon_A', 'epsilon_B', 'epsilon_AA', 'epsilon_BB', 'epsilon_AB', 'mu_A', 'mu_B' FROM 'params'

    DECIDE whether to add or remove a particle (50% chance each)
    IF adding a particle:
        IF 'N_empty' IS 0:
            RETURN 'N_A', 'N_B', 'N_empty'  # No empty sites available
        SELECT a random 'site' from empty sites in 'lattice'
        DECIDE which particle to add (A or B) with equal probability
        IF adding Particle A:
            SET 'particle' TO 1
            SET 'mu' TO 'mu_A'
            SET 'epsilon' TO 'epsilon_A'
            SET 'N_s' TO 'N_A'
        ELSE:  # Adding Particle B
            SET 'particle' TO 2
            SET 'mu' TO 'mu_B'
            SET 'epsilon' TO 'epsilon_B'
            SET 'N_s' TO 'N_B'
        CALCULATE 'delta_E' = 'epsilon' + calculate_interaction_energy(...)
        CALCULATE acceptance probability 'acc_prob' = MIN[1, (N_empty) / (N_s + 1) * exp(-beta * (delta_E - mu))]
        GENERATE a random number 'r' between 0 and 1
        IF 'r' < 'acc_prob':
            SET lattice[site] = 'particle'
            UPDATE counts:
                IF 'particle' IS 1:
                    INCREMENT 'N_A' by 1
                ELSE:
                    INCREMENT 'N_B' by 1
            DECREMENT 'N_empty' by 1
    ELSE:  # Removing a particle
        IF 'N_sites' - 'N_empty' IS 0:
            RETURN 'N_A', 'N_B', 'N_empty'  # No particles to remove
        SELECT a random 'site' from occupied sites in 'lattice'
        GET 'particle' at 'site'
        IF 'particle' IS 1:
            SET 'mu' TO 'mu_A'
            SET 'epsilon' TO 'epsilon_A'
            SET 'N_s' TO 'N_A'
        ELSE:  # Particle B
            SET 'mu' TO 'mu_B'
            SET 'epsilon' TO 'epsilon_B'
            SET 'N_s' TO 'N_B'
        CALCULATE 'delta_E' = -'epsilon' - calculate_interaction_energy(...)
        CALCULATE acceptance probability 'acc_prob' = MIN[1, N_s / (N_empty + 1) * exp(-beta * (delta_E + mu))]
        GENERATE a random number 'r' between 0 and 1
        IF 'r' < 'acc_prob':
            SET lattice[site] = 0  # Remove particle
            UPDATE counts:
                IF 'particle' IS 1:
                    DECREMENT 'N_A' by 1
                ELSE:
                    DECREMENT 'N_B' by 1
            INCREMENT 'N_empty' by 1
    RETURN 'N_A', 'N_B', 'N_empty'

Run the GCMC Simulation

FUNCTION run_simulation(size, n_steps, params):
    INITIALIZE 'lattice' using initialize_lattice(size)
    COMPUTE 'neighbor_indices' using compute_neighbor_indices(size)
    SET 'N_sites' TO size * size
    INITIALIZE counts:
        'N_A' = 0
        'N_B' = 0
        'N_empty' = 'N_sites'
    CREATE arrays 'coverage_A' and 'coverage_B' of length 'n_steps'

    FOR 'step' FROM 0 TO 'n_steps' - 1:
        UPDATE 'N_A', 'N_B', 'N_empty' by calling attempt_move(...)
        SET coverage_A[step] = 'N_A' / 'N_sites'
        SET coverage_B[step] = 'N_B' / 'N_sites'

    RETURN 'lattice', 'coverage_A', 'coverage_B'

Plot Lattice Configuration

FUNCTION plot_lattice(lattice, ax, title):
    SET 'size' TO dimension of 'lattice'
    FOR x FROM 0 TO 'size' - 1:
        FOR y FROM 0 TO 'size' - 1:
            IF lattice[x, y] IS 1:
                PLOT a red circle at position (x + 0.5, y + 0.5) on axis 'ax'
            ELSE IF lattice[x, y] IS 2:
                PLOT a blue circle at position (x + 0.5, y + 0.5) on axis 'ax'
    SET axis limits and labels:
        x-axis from 0 to 'size', y-axis from 0 to 'size'
        REMOVE x and y tick labels
        ENABLE minor grid lines
        SET title of 'ax' to 'title'
    RETURN 'ax'

Example Simulation and Phase Diagram

Here is an example Python code snippet that runs a grand canonical Monte Carlo simulation for competitive adsorption on a 2D lattice and generates a phase diagram showing the coverage of adsorbates A and B as a function of chemical potential μA\mu_A and temperature TT. The code also plots the final lattice configurations at specific points in the phase diagram.

import numpy as np
import matplotlib.pyplot as plt

# Parameters
size = 4
n_steps = 10000
mus_A = np.linspace(-0.2, 0, 7)
Ts = np.linspace(0.001, 0.019, 7)
params = []
for mu_A in mus_A:
    for T in Ts:
        params.append({
            'epsilon_A': -0.1,
            'epsilon_B': -0.1,
            'epsilon_AA': 0,
            'epsilon_BB': 0,
            'epsilon_AB': 0,
            'mu_A': mu_A,
            'mu_B': -0.1,
            'T': T  # Temperature (in units of k)
        })

# Run the simulation
np.random.seed(42)
final_lattice = np.zeros((len(mus_A), len(Ts), size, size))
mean_coverage_A = np.zeros((len(mus_A), len(Ts)))
mean_coverage_B = np.zeros((len(mus_A), len(Ts)))
for i, param in enumerate(params):
    lattice, coverage_A, coverage_B = run_simulation(size, n_steps, param)
    final_lattice[i // len(Ts), i % len(Ts)] = lattice
    mean_coverage_A[i // len(Ts), i % len(Ts)] = np.mean(coverage_A[-1000:])
    mean_coverage_B[i // len(Ts), i % len(Ts)] = np.mean(coverage_B[-1000:])

# Plot the T-mu_A phase diagram
fig, axs = plt.subplots_mosaic([[0, 1, 2], [3, 4, 5]], figsize=(6.5, 4.5))

# Mean coverage of A
axs[0].pcolormesh(mus_A, Ts, mean_coverage_A.T, cmap='viridis', vmin=0, vmax=1)
axs[0].set_title(r'$\langle \theta_A \rangle$')
axs[0].set_xlabel(r'$\mu_A$')
axs[0].set_ylabel(r'$T$')

# Mean coverage of B
axs[1].pcolormesh(mus_A, Ts, mean_coverage_B.T, cmap='viridis', vmin=0, vmax=1)
axs[1].set_title(r'$\langle \theta_B \rangle$')
axs[1].set_xlabel(r'$\mu_A$')
axs[1].set_yticks([])

# Mean total coverage
cax = axs[2].pcolormesh(mus_A, Ts, mean_coverage_A.T + mean_coverage_B.T, cmap='viridis', vmin=0, vmax=1)
axs[2].set_title(r'$\langle \theta_A + \theta_B \rangle$')
axs[2].set_xlabel(r'$\mu_A$')
axs[2].set_yticks([])
fig.colorbar(cax, ax=axs[2], location='right', fraction=0.1)

# Plot the final lattice configuration

# mu_A = -0.2 eV and T = 0.01 / k
axs[3] = plot_lattice(final_lattice[0, 3], axs[3], r'$\mu_A = -0.2$ eV, $T = 0.01 / k$')

# mu_A = -0.1 eV and T = 0.01 / k
axs[4] = plot_lattice(final_lattice[3, 3], axs[4], r'$\mu_A = -0.1$ eV, $T = 0.01 / k$')

# mu_A = 0 eV and T = 0.01 / k
axs[5] = plot_lattice(final_lattice[6, 3], axs[5], r'$\mu_A = 0$ eV, $T = 0.01 / k$')

plt.tight_layout()
plt.show()
Phase Diagram

The phase diagram shows the mean coverage of adsorbates A and B on the surface as a function of chemical potential μA\mu_A and temperature TT. The lattice configurations at specific points in the phase diagram illustrate the adsorption behavior of the two species under different conditions.

Project Description

Scenario

You work for a chemical company that produces ammonia using the Haber-Bosch process. The company is interested in understanding the competitive adsorption of nitrogen and hydrogen on the catalyst surface to optimize the reaction conditions. Your task is to simulate the adsorption behavior of nitrogen and hydrogen on a 2D lattice and analyze the effects of varying chemical potentials and interaction energies. The company wants you to provide a detailed report on the phase diagram of the system and the implications for the ammonia synthesis process.

You decide to use a grand canonical Monte Carlo simulation to model the competitive adsorption of nitrogen and hydrogen on a 2D square lattice. You will vary the chemical potential of hydrogen (μH\mu_{\text{H}}) and hold the chemical potential of nitrogen (μN\mu_{\text{N}}) constant because nitrogen is in excess in the reaction. You do not know the exact values of the adsorption and interaction energies, so you will explore a range of parameters to understand the system’s behavior. You will generate phase diagrams showing the coverage of nitrogen and hydrogen on the surface and analyze how different parameters affect the adsorption behavior. Finally, you will write a report summarizing your findings and discussing the implications for the ammonia synthesis process.

You decide to explore the following sets of parameters:

You will run the simulation for each set of parameters and generate phase diagrams showing the coverage of nitrogen and hydrogen on the surface. You will analyze the phase diagrams and discuss the implications of the results for the ammonia synthesis process. Your report should include figures of the phase diagrams and lattice configurations for each parameter set and a detailed discussion of the physical interpretation of the results. You should also discuss how the competitive adsorption behavior of nitrogen and hydrogen influences the ammonia synthesis process and suggest potential strategies for optimizing the reaction conditions.

Requirements

Optional Enhancements (Five Bonus Points Each)