Intro to adsorption energies#
To introduce OCP we start with using it to calculate adsorption energies for a simple, atomic adsorbate where we specify the site we want to the adsorption energy for. Conceptually, you do this like you would do it with density functional theory. You create a slab model for the surface, place an adsorbate on it as an initial guess, run a relaxation to get the lowest energy geometry, and then compute the adsorption energy using reference states for the adsorbate.
You do have to be careful in the details though. Some OCP model/checkpoint combinations return a total energy like density functional theory would, but some return an “adsorption energy” directly. You have to know which one you are using. In this example, the model we use returns an “adsorption energy”.
Intro to Adsorption energies#
Adsorption energies are always a reaction energy (an adsorbed species relative to some implied combination of reactants). There are many common schemes in the catalysis literature.
For example, you may want the adsorption energy of oxygen, and you might compute that from this reaction:
1/2 O2 + slab -> slab-O
DFT has known errors with the energy of a gas-phase O2 molecule, so it’s more common to compute this energy relative to a linear combination of H2O and H2. The suggested reference scheme for consistency with OC20 is a reaction
x CO + (x + y/2 - z) H2 + (z-x) H2O + w/2 N2 + * -> CxHyOzNw*
Here, x=y=w=0, z=1, so the reaction ends up as
-H2 + H2O + * -> O*
or alternatively,
H2O + * -> O* + H2
It is possible through thermodynamic cycles to compute other reactions. If we can look up rH1 below and compute rH2
H2 + 1/2 O2 -> H2O re1 = -3.03 eV, from exp
H2O + * -> O* + H2 re2 # Get from UMA
Then, the adsorption energy for
1/2O2 + * -> O*
is just re1 + re2.
Based on https://atct.anl.gov/Thermochemical%20Data/version%201.118/species/?species_number=986, the formation energy of water is about -3.03 eV at standard state experimentally. You could also compute this using DFT, but you would probably get the wrong answer for this.
The first step is getting a checkpoint for the model we want to use. UMA is currently the state-of-the-art model and will provide total energy estimates at the RPBE level of theory if you use the “OC20” task.
Need to install fairchem-core or get UMA access or getting permissions/401 errors?
Install the necessary packages using pip, uv etc
Get access to any necessary huggingface gated models
Get and login to your Huggingface account
Request access to https://huggingface.co/facebook/UMA
Create a Huggingface token at https://huggingface.co/settings/tokens/ with the permission “Permissions: Read access to contents of all public gated repos you can access”
Add the token as an environment variable using
huggingface-cli login
or by setting the HF_TOKEN environment variable.
If you find your kernel is crashing, it probably means you have exceeded the allowed amount of memory. This checkpoint works fine in this example, but it may crash your kernel if you use it in the NRR example.
This next cell will automatically download the checkpoint from huggingface and load it.
from __future__ import annotations
from fairchem.core import FAIRChemCalculator, pretrained_mlip
predictor = pretrained_mlip.get_predict_unit("uma-s-1")
calc = FAIRChemCalculator(predictor, task_name="oc20")
/home/runner/work/_tool/Python/3.12.11/x64/lib/python3.12/site-packages/torchtnt/utils/version.py:12: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
import pkg_resources
WARNING:root:device was not explicitly set, using device='cuda'.
Next we can build a slab with an adsorbate on it. Here we use the ASE module to build a Pt slab. We use the experimental lattice constant that is the default. This can introduce some small errors with DFT since the lattice constant can differ by a few percent, and it is common to use DFT lattice constants. In this example, we do not constrain any layers.
from ase.build import add_adsorbate, fcc111
from ase.optimize import BFGS
# reference energies from a linear combination of H2O/N2/CO/H2!
atomic_reference_energies = {
"H": -3.477,
"N": -8.083,
"O": -7.204,
"C": -7.282,
}
re1 = -3.03
slab = fcc111("Pt", size=(2, 2, 5), vacuum=20.0)
slab.pbc = True
adslab = slab.copy()
add_adsorbate(adslab, "O", height=1.2, position="fcc")
slab.set_calculator(calc)
opt = BFGS(slab)
opt.run(fmax=0.05, steps=100)
slab_e = slab.get_potential_energy()
adslab.set_calculator(calc)
opt = BFGS(adslab)
opt.run(fmax=0.05, steps=100)
adslab_e = adslab.get_potential_energy()
# Energy for ((H2O-H2) + * -> *O) + (H2 + 1/2O2 -> H2) leads to 1/2O2 + * -> *O!
adslab_e - slab_e - atomic_reference_energies["O"] + re1
/tmp/ipykernel_10197/3752951811.py:17: FutureWarning: Please use atoms.calc = calc
slab.set_calculator(calc)
Step Time Energy fmax
BFGS: 0 20:12:30 -104.710392 0.707696
BFGS: 1 20:12:30 -104.767891 0.605448
BFGS: 2 20:12:30 -104.919425 0.369264
BFGS: 3 20:12:30 -104.952878 0.441350
BFGS: 4 20:12:30 -105.029999 0.467592
BFGS: 5 20:12:30 -105.091452 0.365230
BFGS: 6 20:12:31 -105.128722 0.195039
BFGS: 7 20:12:31 -105.143315 0.048837
Step Time Energy fmax
BFGS: 0 20:12:31 -110.055657 1.762238
/tmp/ipykernel_10197/3752951811.py:22: FutureWarning: Please use atoms.calc = calc
adslab.set_calculator(calc)
BFGS: 1 20:12:31 -110.239036 0.996881
BFGS: 2 20:12:31 -110.389564 0.747598
BFGS: 3 20:12:31 -110.441199 0.818394
BFGS: 4 20:12:31 -110.557349 0.688434
BFGS: 5 20:12:31 -110.631226 0.497311
BFGS: 6 20:12:31 -110.687287 0.690726
BFGS: 7 20:12:31 -110.737886 0.729330
BFGS: 8 20:12:32 -110.774870 0.435695
BFGS: 9 20:12:32 -110.786661 0.199878
BFGS: 10 20:12:32 -110.789558 0.080687
BFGS: 11 20:12:32 -110.790038 0.058016
BFGS: 12 20:12:32 -110.790286 0.044015
-1.4729707438775743
It is good practice to look at your geometries to make sure they are what you expect.
import matplotlib.pyplot as plt
from ase.visualize.plot import plot_atoms
fig, axs = plt.subplots(1, 2)
plot_atoms(slab, axs[0])
plot_atoms(slab, axs[1], rotation=("-90x"))
axs[0].set_axis_off()
axs[1].set_axis_off()

import matplotlib.pyplot as plt
from ase.visualize.plot import plot_atoms
fig, axs = plt.subplots(1, 2)
plot_atoms(adslab, axs[0])
plot_atoms(adslab, axs[1], rotation=("-90x"))
axs[0].set_axis_off()
axs[1].set_axis_off()

How did we do? We need a reference point. In the paper below, there is an atomic adsorption energy for O on Pt(111) of about -4.264 eV. This is for the reaction O + * -> O*. To convert this to the dissociative adsorption energy, we have to add the reaction:
1/2 O2 -> O D = 2.58 eV (expt)
to get a comparable energy of about -1.68 eV. There is about ~0.2 eV difference (we predicted -1.47 eV above, and the reference comparison is -1.68 eV) to account for. The biggest difference is likely due to the differences in exchange-correlation functional. The reference data used the PBE functional, and eSCN was trained on RPBE data. To additional places where there are differences include:
Difference in lattice constant
The reference energy used for the experiment references. These can differ by up to 0.5 eV from comparable DFT calculations.
How many layers are relaxed in the calculation
Some of these differences tend to be systematic, and you can calibrate and correct these, especially if you can augment these with your own DFT calculations.
See convergence study for some additional studies of factors that influence this number.
Exercises#
Explore the effect of the lattice constant on the adsorption energy.
Try different sites, including the bridge and top sites. Compare the energies, and inspect the resulting geometries.
Trends in adsorption energies across metals.#
Xu, Z., & Kitchin, J. R. (2014). Probing the coverage dependence of site and adsorbate configurational correlations on (111) surfaces of late transition metals. J. Phys. Chem. C, 118(44), 25597–25602. http://dx.doi.org/10.1021/jp508805h
These are atomic adsorption energies:
O + * -> O*
We have to do some work to get comparable numbers from OCP
H2 + 1/2 O2 -> H2O re1 = -3.03 eV
H2O + * -> O* + H2 re2 # Get from UMA
O -> 1/2 O2 re3 = -2.58 eV
Then, the adsorption energy for
O + * -> O*
is just re1 + re2 + re3.
Here we just look at the fcc site on Pt. First, we get the data stored in the paper.
Next we get the structures and compute their energies. Some subtle points are that we have to account for stoichiometry, and normalize the adsorption energy by the number of oxygens.
First we get a reference energy from the paper (PBE, 0.25 ML O on Pt(111)).
import json
with open("energies.json") as f:
edata = json.load(f)
with open("structures.json") as f:
sdata = json.load(f)
edata["Pt"]["O"]["fcc"]["0.25"]
-4.263842000000002
Next, we load data from the SI to get the geometry to start from.
with open("structures.json") as f:
s = json.load(f)
sfcc = s["Pt"]["O"]["fcc"]["0.25"]
Next, we construct the atomic geometry, run the geometry optimization, and compute the energy.
re3 = -2.58 # O -> 1/2 O2 re3 = -2.58 eV
from ase import Atoms
adslab = Atoms(sfcc["symbols"], positions=sfcc["pos"], cell=sfcc["cell"], pbc=True)
# Grab just the metal surface atoms
slab = adslab[adslab.arrays["numbers"] == adslab.arrays["numbers"][0]]
adsorbates = adslab[~(adslab.arrays["numbers"] == adslab.arrays["numbers"][0])]
slab.set_calculator(calc)
opt = BFGS(slab)
opt.run(fmax=0.05, steps=100)
adslab.set_calculator(calc)
opt = BFGS(adslab)
opt.run(fmax=0.05, steps=100)
re2 = (
adslab.get_potential_energy()
- slab.get_potential_energy()
- sum([atomic_reference_energies[x] for x in adsorbates.get_chemical_symbols()])
)
nO = 0
for atom in adslab:
if atom.symbol == "O":
nO += 1
re2 += re1 + re3
print(re2 / nO)
Step Time Energy fmax
BFGS: 0 20:12:34 -82.871048 0.991408
/tmp/ipykernel_10197/647904475.py:10: FutureWarning: Please use atoms.calc = calc
slab.set_calculator(calc)
BFGS: 1 20:12:34 -82.925868 0.745700
BFGS: 2 20:12:35 -83.018081 0.333948
BFGS: 3 20:12:35 -83.023148 0.299890
BFGS: 4 20:12:35 -83.030485 0.229305
BFGS: 5 20:12:35 -83.036234 0.153249
BFGS: 6 20:12:35 -83.039629 0.091953
BFGS: 7 20:12:35 -83.040797 0.065548
BFGS: 8 20:12:35 -83.041530 0.068049
BFGS: 9 20:12:35 -83.041911 0.048328
Step Time Energy fmax
BFGS: 0 20:12:35 -88.773220 0.312913
/tmp/ipykernel_10197/647904475.py:14: FutureWarning: Please use atoms.calc = calc
adslab.set_calculator(calc)
BFGS: 1 20:12:35 -88.776449 0.272291
BFGS: 2 20:12:36 -88.787274 0.105157
BFGS: 3 20:12:36 -88.788948 0.100239
BFGS: 4 20:12:36 -88.791513 0.116284
BFGS: 5 20:12:36 -88.793694 0.099435
BFGS: 6 20:12:36 -88.795381 0.091206
BFGS: 7 20:12:36 -88.796142 0.096744
BFGS: 8 20:12:36 -88.796645 0.077230
BFGS: 9 20:12:36 -88.797139 0.036562
-4.161228991253062
Site correlations#
This cell reproduces a portion of a figure in the paper. We compare oxygen adsorption energies in the fcc and hcp sites across metals and coverages. These adsorption energies are highly correlated with each other because the adsorption sites are so similar.
At higher coverages, the agreement is not as good. This is likely because the model is extrapolating and needs to be fine-tuned.
import time
from tqdm import tqdm
t0 = time.time()
data = {"fcc": [], "hcp": []}
refdata = {"fcc": [], "hcp": []}
for metal in ["Cu", "Ag", "Pd", "Pt", "Rh", "Ir"]:
print(metal)
for site in ["fcc", "hcp"]:
for adsorbate in ["O"]:
for coverage in tqdm(["0.25"]):
entry = s[metal][adsorbate][site][coverage]
adslab = Atoms(
entry["symbols"],
positions=entry["pos"],
cell=entry["cell"],
pbc=True,
)
# Grab just the metal surface atoms
adsorbates = adslab[
~(adslab.arrays["numbers"] == adslab.arrays["numbers"][0])
]
slab = adslab[adslab.arrays["numbers"] == adslab.arrays["numbers"][0]]
slab.set_calculator(calc)
opt = BFGS(slab)
opt.run(fmax=0.05, steps=100)
adslab.set_calculator(calc)
opt = BFGS(adslab)
opt.run(fmax=0.05, steps=100)
re2 = (
adslab.get_potential_energy()
- slab.get_potential_energy()
- sum(
[
atomic_reference_energies[x]
for x in adsorbates.get_chemical_symbols()
]
)
)
nO = 0
for atom in adslab:
if atom.symbol == "O":
nO += 1
re2 += re1 + re3
data[site] += [re2 / nO]
refdata[site] += [edata[metal][adsorbate][site][coverage]]
f"Elapsed time = {time.time() - t0} seconds"
Cu
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:37 -48.909898 0.648664
/tmp/ipykernel_10197/1356342052.py:33: FutureWarning: Please use atoms.calc = calc
slab.set_calculator(calc)
BFGS: 1 20:12:37 -48.933204 0.542517
BFGS: 2 20:12:37 -49.000104 0.281186
BFGS: 3 20:12:37 -49.002298 0.255891
BFGS: 4 20:12:37 -49.011423 0.142128
BFGS: 5 20:12:37 -49.015710 0.106526
BFGS: 6 20:12:37 -49.017446 0.059934
BFGS: 7 20:12:37 -49.017903 0.051310
BFGS: 8 20:12:37 -49.018336 0.045122
Step Time Energy fmax
BFGS: 0 20:12:37 -55.207153 0.352144
/tmp/ipykernel_10197/1356342052.py:37: FutureWarning: Please use atoms.calc = calc
adslab.set_calculator(calc)
BFGS: 1 20:12:38 -55.209778 0.276583
BFGS: 2 20:12:38 -55.217150 0.155893
BFGS: 3 20:12:38 -55.219399 0.164019
BFGS: 4 20:12:38 -55.223018 0.122984
BFGS: 5 20:12:38 -55.225368 0.082931
BFGS: 6 20:12:38 -55.227167 0.070956
BFGS: 7 20:12:38 -55.228235 0.088503
BFGS: 8 20:12:38 -55.229333 0.100302
BFGS: 9 20:12:38 -55.230439 0.076144
BFGS: 10 20:12:38 -55.230999 0.047299
100%|██████████| 1/1 [00:02<00:00, 2.12s/it]
100%|██████████| 1/1 [00:02<00:00, 2.12s/it]
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:39 -48.935744 0.556482
BFGS: 1 20:12:39 -48.953546 0.474347
BFGS: 2 20:12:39 -49.008534 0.210306
BFGS: 3 20:12:39 -49.009794 0.200665
BFGS: 4 20:12:39 -49.017044 0.072477
BFGS: 5 20:12:39 -49.017929 0.039939
Step Time Energy fmax
BFGS: 0 20:12:39 -55.123491 0.310584
BFGS: 1 20:12:39 -55.125386 0.241926
BFGS: 2 20:12:39 -55.130100 0.125352
BFGS: 3 20:12:40 -55.131741 0.147008
BFGS: 4 20:12:40 -55.134775 0.135930
BFGS: 5 20:12:40 -55.136893 0.110807
BFGS: 6 20:12:40 -55.138170 0.059611
BFGS: 7 20:12:40 -55.138669 0.052031
BFGS: 8 20:12:40 -55.139100 0.066685
BFGS: 9 20:12:40 -55.139747 0.069465
BFGS: 10 20:12:40 -55.140299 0.045438
100%|██████████| 1/1 [00:01<00:00, 1.83s/it]
100%|██████████| 1/1 [00:01<00:00, 1.83s/it]
Ag
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:40 -33.025614 0.630174
BFGS: 1 20:12:41 -33.044428 0.547941
BFGS: 2 20:12:41 -33.111288 0.171413
BFGS: 3 20:12:41 -33.113080 0.157253
BFGS: 4 20:12:41 -33.114517 0.142916
BFGS: 5 20:12:41 -33.117277 0.113049
BFGS: 6 20:12:41 -33.121223 0.112236
BFGS: 7 20:12:41 -33.124449 0.074632
BFGS: 8 20:12:41 -33.125439 0.035989
Step Time Energy fmax
BFGS: 0 20:12:41 -38.163234 0.098049
BFGS: 1 20:12:41 -38.164159 0.096574
BFGS: 2 20:12:42 -38.175116 0.083650
BFGS: 3 20:12:42 -38.176062 0.094338
BFGS: 4 20:12:42 -38.178598 0.101133
BFGS: 5 20:12:42 -38.181050 0.084281
BFGS: 6 20:12:42 -38.182881 0.065589
BFGS: 7 20:12:42 -38.183785 0.060144
BFGS: 8 20:12:42 -38.184160 0.037711
100%|██████████| 1/1 [00:01<00:00, 1.93s/it]
100%|██████████| 1/1 [00:01<00:00, 1.93s/it]
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:42 -33.047713 0.553945
BFGS: 1 20:12:43 -33.061966 0.487415
BFGS: 2 20:12:43 -33.116795 0.122879
BFGS: 3 20:12:43 -33.117631 0.113218
BFGS: 4 20:12:43 -33.118950 0.096734
BFGS: 5 20:12:43 -33.120806 0.082419
BFGS: 6 20:12:43 -33.123819 0.079679
BFGS: 7 20:12:43 -33.125431 0.044132
Step Time Energy fmax
BFGS: 0 20:12:43 -38.093480 0.090438
BFGS: 1 20:12:43 -38.094295 0.086651
BFGS: 2 20:12:43 -38.103534 0.120505
BFGS: 3 20:12:44 -38.104587 0.108166
BFGS: 4 20:12:44 -38.106962 0.071383
BFGS: 5 20:12:44 -38.108239 0.063370
BFGS: 6 20:12:44 -38.109018 0.051515
BFGS: 7 20:12:44 -38.109808 0.072603
BFGS: 8 20:12:44 -38.110548 0.067930
BFGS: 9 20:12:44 -38.110977 0.041004
100%|██████████| 1/1 [00:01<00:00, 1.93s/it]
100%|██████████| 1/1 [00:01<00:00, 1.93s/it]
Pd
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:44 -70.161981 0.627545
BFGS: 1 20:12:44 -70.186816 0.504642
BFGS: 2 20:12:45 -70.238253 0.189074
BFGS: 3 20:12:45 -70.239964 0.180014
BFGS: 4 20:12:45 -70.244726 0.146995
BFGS: 5 20:12:45 -70.247895 0.116365
BFGS: 6 20:12:45 -70.250246 0.081063
BFGS: 7 20:12:45 -70.251179 0.060312
BFGS: 8 20:12:45 -70.251964 0.042875
Step Time Energy fmax
BFGS: 0 20:12:45 -76.122212 0.213012
BFGS: 1 20:12:45 -76.125373 0.189420
BFGS: 2 20:12:45 -76.140839 0.181206
BFGS: 3 20:12:46 -76.143068 0.161940
BFGS: 4 20:12:46 -76.146864 0.132163
BFGS: 5 20:12:46 -76.149676 0.108933
BFGS: 6 20:12:46 -76.152001 0.092636
BFGS: 7 20:12:46 -76.153183 0.085759
BFGS: 8 20:12:46 -76.153982 0.065606
BFGS: 9 20:12:46 -76.154480 0.045744
100%|██████████| 1/1 [00:02<00:00, 2.03s/it]
100%|██████████| 1/1 [00:02<00:00, 2.03s/it]
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:46 -70.194493 0.451440
BFGS: 1 20:12:46 -70.208545 0.372765
BFGS: 2 20:12:47 -70.242539 0.175289
BFGS: 3 20:12:47 -70.243691 0.162880
BFGS: 4 20:12:47 -70.249799 0.073682
BFGS: 5 20:12:47 -70.250639 0.072098
BFGS: 6 20:12:47 -70.251439 0.055157
BFGS: 7 20:12:47 -70.252051 0.034610
Step Time Energy fmax
BFGS: 0 20:12:47 -75.892184 0.170435
BFGS: 1 20:12:47 -75.895145 0.151332
BFGS: 2 20:12:47 -75.906051 0.158189
BFGS: 3 20:12:47 -75.907864 0.150576
BFGS: 4 20:12:48 -75.912122 0.127365
BFGS: 5 20:12:48 -75.915196 0.116076
BFGS: 6 20:12:48 -75.917587 0.084766
BFGS: 7 20:12:48 -75.918673 0.069600
BFGS: 8 20:12:48 -75.919252 0.048511
100%|██████████| 1/1 [00:01<00:00, 1.82s/it]
100%|██████████| 1/1 [00:01<00:00, 1.82s/it]
Pt
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:48 -82.871048 0.991408
BFGS: 1 20:12:48 -82.925868 0.745701
BFGS: 2 20:12:48 -83.018081 0.333948
BFGS: 3 20:12:49 -83.023148 0.299890
BFGS: 4 20:12:49 -83.030486 0.229305
BFGS: 5 20:12:49 -83.036234 0.153249
BFGS: 6 20:12:49 -83.039630 0.091952
BFGS: 7 20:12:49 -83.040796 0.065548
BFGS: 8 20:12:49 -83.041530 0.068049
BFGS: 9 20:12:49 -83.041910 0.048327
Step Time Energy fmax
BFGS: 0 20:12:49 -88.773220 0.312913
BFGS: 1 20:12:50 -88.776449 0.272290
BFGS: 2 20:12:50 -88.787275 0.105157
BFGS: 3 20:12:50 -88.788948 0.100239
BFGS: 4 20:12:50 -88.791513 0.116283
BFGS: 5 20:12:50 -88.793694 0.099436
BFGS: 6 20:12:50 -88.795380 0.091202
BFGS: 7 20:12:50 -88.796142 0.096747
BFGS: 8 20:12:50 -88.796645 0.077226
BFGS: 9 20:12:50 -88.797139 0.036559
100%|██████████| 1/1 [00:02<00:00, 2.34s/it]
100%|██████████| 1/1 [00:02<00:00, 2.34s/it]
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:51 -82.954207 0.669494
BFGS: 1 20:12:51 -82.979728 0.543763
BFGS: 2 20:12:51 -83.033172 0.200051
BFGS: 3 20:12:51 -83.034708 0.180726
BFGS: 4 20:12:51 -83.040170 0.076918
BFGS: 5 20:12:51 -83.040803 0.058416
BFGS: 6 20:12:51 -83.041526 0.048637
Step Time Energy fmax
BFGS: 0 20:12:51 -88.359104 0.227136
BFGS: 1 20:12:51 -88.361884 0.198911
BFGS: 2 20:12:51 -88.371241 0.110721
BFGS: 3 20:12:52 -88.372672 0.114553
BFGS: 4 20:12:52 -88.376632 0.095077
BFGS: 5 20:12:52 -88.378873 0.079802
BFGS: 6 20:12:52 -88.380259 0.061946
BFGS: 7 20:12:52 -88.380724 0.068262
BFGS: 8 20:12:52 -88.381067 0.055387
BFGS: 9 20:12:52 -88.381353 0.025352
100%|██████████| 1/1 [00:01<00:00, 1.83s/it]
100%|██████████| 1/1 [00:01<00:00, 1.83s/it]
Rh
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:52 -100.184556 0.679183
BFGS: 1 20:12:52 -100.209786 0.592727
BFGS: 2 20:12:53 -100.277909 0.175048
BFGS: 3 20:12:53 -100.280900 0.128371
BFGS: 4 20:12:53 -100.289038 0.066837
BFGS: 5 20:12:53 -100.290136 0.053351
BFGS: 6 20:12:53 -100.290651 0.045018
Step Time Energy fmax
BFGS: 0 20:12:53 -106.908737 0.208901
BFGS: 1 20:12:53 -106.911481 0.173523
BFGS: 2 20:12:53 -106.917689 0.053177
BFGS: 3 20:12:53 -106.918231 0.050722
BFGS: 4 20:12:54 -106.918579 0.042915
100%|██████████| 1/1 [00:01<00:00, 1.32s/it]
100%|██████████| 1/1 [00:01<00:00, 1.32s/it]
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:54 -100.163399 0.715416
BFGS: 1 20:12:54 -100.195280 0.619180
BFGS: 2 20:12:54 -100.278722 0.270302
BFGS: 3 20:12:54 -100.281483 0.200542
BFGS: 4 20:12:54 -100.288211 0.079572
BFGS: 5 20:12:54 -100.289963 0.056580
BFGS: 6 20:12:54 -100.290656 0.044816
Step Time Energy fmax
BFGS: 0 20:12:54 -106.865187 0.171078
BFGS: 1 20:12:55 -106.867944 0.141427
BFGS: 2 20:12:55 -106.874490 0.062456
BFGS: 3 20:12:55 -106.875161 0.056680
BFGS: 4 20:12:55 -106.875533 0.046880
100%|██████████| 1/1 [00:01<00:00, 1.32s/it]
100%|██████████| 1/1 [00:01<00:00, 1.32s/it]
Ir
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:55 -124.224302 1.179416
BFGS: 1 20:12:55 -124.298547 0.928835
BFGS: 2 20:12:55 -124.409064 0.149721
BFGS: 3 20:12:55 -124.412492 0.137846
BFGS: 4 20:12:55 -124.417156 0.062426
BFGS: 5 20:12:56 -124.417815 0.041027
Step Time Energy fmax
BFGS: 0 20:12:56 -130.594653 0.381607
BFGS: 1 20:12:56 -130.605550 0.292489
BFGS: 2 20:12:56 -130.621251 0.114387
BFGS: 3 20:12:56 -130.624740 0.111581
BFGS: 4 20:12:56 -130.625517 0.083603
BFGS: 5 20:12:56 -130.626561 0.072352
BFGS: 6 20:12:56 -130.627576 0.088237
BFGS: 7 20:12:56 -130.628070 0.075932
BFGS: 8 20:12:56 -130.628447 0.042618
100%|██████████| 1/1 [00:01<00:00, 1.62s/it]
100%|██████████| 1/1 [00:01<00:00, 1.63s/it]
0%| | 0/1 [00:00<?, ?it/s]
Step Time Energy fmax
BFGS: 0 20:12:57 -124.215234 1.158102
BFGS: 1 20:12:57 -124.298643 0.910750
BFGS: 2 20:12:57 -124.409635 0.167763
BFGS: 3 20:12:57 -124.412695 0.207504
BFGS: 4 20:12:57 -124.416624 0.161092
BFGS: 5 20:12:57 -124.417881 0.081306
BFGS: 6 20:12:57 -124.418151 0.038266
Step Time Energy fmax
BFGS: 0 20:12:57 -130.487515 0.407218
BFGS: 1 20:12:57 -130.498984 0.272069
BFGS: 2 20:12:58 -130.511822 0.132224
BFGS: 3 20:12:58 -130.515791 0.077771
BFGS: 4 20:12:58 -130.516512 0.076785
BFGS: 5 20:12:58 -130.516852 0.063182
BFGS: 6 20:12:58 -130.518034 0.046361
100%|██████████| 1/1 [00:01<00:00, 1.52s/it]
100%|██████████| 1/1 [00:01<00:00, 1.52s/it]
'Elapsed time = 21.657994031906128 seconds'
First, we compare the computed data and reference data. There is a systematic difference of about 0.5 eV due to the difference between RPBE and PBE functionals, and other subtle differences like lattice constant differences and reference energy differences. This is pretty typical, and an expected deviation.
plt.plot(refdata["fcc"], data["fcc"], "r.", label="fcc")
plt.plot(refdata["hcp"], data["hcp"], "b.", label="hcp")
plt.plot([-5.5, -3.5], [-5.5, -3.5], "k-")
plt.xlabel("Ref. data (DFT)")
plt.ylabel("UMA-OC20 prediction");

Next we compare the correlation between the hcp and fcc sites. Here we see the same trends. The data falls below the parity line because the hcp sites tend to be a little weaker binding than the fcc sites.
plt.plot(refdata["hcp"], refdata["fcc"], "r.")
plt.plot(data["hcp"], data["fcc"], ".")
plt.plot([-6, -1], [-6, -1], "k-")
plt.xlabel("$H_{ads, hcp}$")
plt.ylabel("$H_{ads, fcc}$")
plt.legend(["DFT (PBE)", "UMA-OC20"]);

Exercises#
You can also explore a few other adsorbates: C, H, N.
Explore the higher coverages. The deviations from the reference data are expected to be higher, but relative differences tend to be better. You probably need fine tuning to improve this performance. This data set doesn’t have forces though, so it isn’t practical to do it here.
Next steps#
In the next step, we consider some more complex adsorbates in nitrogen reduction, and how we can leverage OCP to automate the search for the most stable adsorbate geometry. See the next step.
Convergence study#
In Calculating adsorption energies we discussed some possible reasons we might see a discrepancy. Here we investigate some factors that impact the computed energies.
In this section, the energies refer to the reaction 1/2 O2 -> O*.
Effects of number of layers#
Slab thickness could be a factor. Here we relax the whole slab, and see by about 4 layers the energy is converged to ~0.02 eV.
for nlayers in [3, 4, 5, 6, 7, 8]:
slab = fcc111("Pt", size=(2, 2, nlayers), vacuum=10.0)
slab.pbc = True
slab.set_calculator(calc)
opt_slab = BFGS(slab, logfile=None)
opt_slab.run(fmax=0.05, steps=100)
slab_e = slab.get_potential_energy()
adslab = slab.copy()
add_adsorbate(adslab, "O", height=1.2, position="fcc")
adslab.pbc = True
adslab.set_calculator(calc)
opt_adslab = BFGS(adslab, logfile=None)
opt_adslab.run(fmax=0.05, steps=100)
adslab_e = adslab.get_potential_energy()
print(
f"nlayers = {nlayers}: {adslab_e - slab_e - atomic_reference_energies['O'] + re1:1.2f} eV"
)
/tmp/ipykernel_10197/338101817.py:5: FutureWarning: Please use atoms.calc = calc
slab.set_calculator(calc)
/tmp/ipykernel_10197/338101817.py:14: FutureWarning: Please use atoms.calc = calc
adslab.set_calculator(calc)
nlayers = 3: -1.60 eV
nlayers = 4: -1.47 eV
nlayers = 5: -1.47 eV
nlayers = 6: -1.46 eV
nlayers = 7: -1.47 eV
nlayers = 8: -1.47 eV
Effects of relaxation#
It is common to only relax a few layers, and constrain lower layers to bulk coordinates. We do that here. We only relax the adsorbate and the top layer.
This has a small effect (0.1 eV).
from ase.constraints import FixAtoms
for nlayers in [3, 4, 5, 6, 7, 8]:
slab = fcc111("Pt", size=(2, 2, nlayers), vacuum=10.0)
slab.set_constraint(FixAtoms(mask=[atom.tag > 1 for atom in slab]))
slab.pbc = True
slab.set_calculator(calc)
opt_slab = BFGS(slab, logfile=None)
opt_slab.run(fmax=0.05, steps=100)
slab_e = slab.get_potential_energy()
adslab = slab.copy()
add_adsorbate(adslab, "O", height=1.2, position="fcc")
adslab.set_constraint(FixAtoms(mask=[atom.tag > 1 for atom in adslab]))
adslab.pbc = True
adslab.set_calculator(calc)
opt_adslab = BFGS(adslab, logfile=None)
opt_adslab.run(fmax=0.05, steps=100)
adslab_e = adslab.get_potential_energy()
print(
f"nlayers = {nlayers}: {adslab_e - slab_e - atomic_reference_energies['O'] + re1:1.2f} eV"
)
/tmp/ipykernel_10197/1426773950.py:8: FutureWarning: Please use atoms.calc = calc
slab.set_calculator(calc)
/tmp/ipykernel_10197/1426773950.py:18: FutureWarning: Please use atoms.calc = calc
adslab.set_calculator(calc)
nlayers = 3: -1.48 eV
nlayers = 4: -1.35 eV
nlayers = 5: -1.35 eV
nlayers = 6: -1.34 eV
nlayers = 7: -1.35 eV
nlayers = 8: -1.35 eV
Unit cell size#
Coverage effects are quite noticeable with oxygen. Here we consider larger unit cells. This effect is large, and the results don’t look right, usually adsorption energies get more favorable at lower coverage, not less. This suggests fine-tuning could be important even at low coverages.
for size in [1, 2, 3, 4, 5]:
slab = fcc111("Pt", size=(size, size, 5), vacuum=10.0)
slab.set_constraint(FixAtoms(mask=[atom.tag > 1 for atom in slab]))
slab.pbc = True
slab.set_calculator(calc)
opt_slab = BFGS(slab, logfile=None)
opt_slab.run(fmax=0.05, steps=100)
slab_e = slab.get_potential_energy()
adslab = slab.copy()
add_adsorbate(adslab, "O", height=1.2, position="fcc")
adslab.set_constraint(FixAtoms(mask=[atom.tag > 1 for atom in adslab]))
adslab.pbc = True
adslab.set_calculator(calc)
opt_adslab = BFGS(adslab, logfile=None)
opt_adslab.run(fmax=0.05, steps=100)
adslab_e = adslab.get_potential_energy()
print(
f"({size}x{size}): {adslab_e - slab_e - atomic_reference_energies['O'] + re1:1.2f} eV"
)
/tmp/ipykernel_10197/3371624330.py:7: FutureWarning: Please use atoms.calc = calc
slab.set_calculator(calc)
/tmp/ipykernel_10197/3371624330.py:17: FutureWarning: Please use atoms.calc = calc
adslab.set_calculator(calc)
(1x1): -0.11 eV
(2x2): -1.35 eV
(3x3): -1.38 eV
(4x4): -1.41 eV
(5x5): -1.42 eV
Summary#
As with DFT, you should take care to see how these kinds of decisions affect your results, and determine if they would change any interpretations or not.