08 - Surfaces#
This script demonstrates surface slab calculations, calculating the surface energy of Cu(111).
import numpy as np
from ase.build import bulk, fcc111
from ase.constraints import FixAtoms
from vasp import Vasp
print("=" * 60)
print("Surface Slab Calculations")
print("=" * 60)
print()
Part 1: Bulk copper reference#
print("Part 1: Bulk Cu reference calculation")
print("-" * 40)
print()
cu_bulk = bulk('Cu', 'fcc', a=3.6)
calc_bulk = Vasp(
label='results/surfaces/cu_bulk',
atoms=cu_bulk,
xc='PBE',
encut=400,
kpts=(12, 12, 12),
ismear=1,
sigma=0.1,
)
energy_bulk = calc_bulk.potential_energy
e_per_atom = energy_bulk / len(cu_bulk)
print(f" Total energy: {energy_bulk:.6f} eV")
print(f" Energy per atom: {e_per_atom:.6f} eV/atom")
print()
Part 2: Cu(111) slab#
print("Part 2: Cu(111) slab calculation")
print("-" * 40)
print()
# Create Cu(111) slab with 4 layers and vacuum
slab = fcc111('Cu', size=(2, 2, 4), vacuum=12.0, a=3.6)
print("Slab structure:")
print(" Surface: Cu(111)")
print(" Size: 2x2 supercell, 4 layers")
print(f" Atoms: {len(slab)}")
print(" Vacuum: 12.0 Å")
print()
# Fix bottom two layers
positions_z = slab.positions[:, 2]
z_sorted = np.sort(np.unique(np.round(positions_z, decimals=2)))
z_threshold = z_sorted[1] + 0.1 # Fix bottom 2 layers
constraint = FixAtoms(mask=positions_z <= z_threshold)
slab.set_constraint(constraint)
n_fixed = sum(positions_z <= z_threshold)
print(f" Fixed atoms (bottom 2 layers): {n_fixed}")
print()
# Slab calculation with dipole correction
calc_slab = Vasp(
label='results/surfaces/cu111_slab',
atoms=slab,
xc='PBE',
encut=400,
kpts=(6, 6, 1), # Gamma-centered, 1 in z for slab
ismear=1,
sigma=0.1,
ldipol=True, # Dipole correction
idipol=3, # Correct in z direction
isif=2, # Relax positions only (not cell)
ibrion=1, # Quasi-Newton (more robust than CG)
potim=0.5, # Ionic step size
nsw=50, # Max ionic steps
ediffg=-0.02, # Force convergence: 0.02 eV/Å
)
energy_slab = calc_slab.potential_energy
print(f" Total energy: {energy_slab:.6f} eV")
print()
Part 3: Calculate surface energy#
print("Part 3: Surface energy calculation")
print("-" * 40)
print()
n_atoms_slab = len(slab)
# Surface area (only one side counted since symmetric slab)
cell = slab.get_cell()
area = np.linalg.norm(np.cross(cell[0], cell[1]))
# Surface energy (per surface, slab has two surfaces)
e_surface_ev = (energy_slab - n_atoms_slab * e_per_atom) / (2 * area)
# Convert eV/Ų to J/m²
ev_per_ang2_to_j_per_m2 = 16.0217663
e_surface_j = e_surface_ev * ev_per_ang2_to_j_per_m2
print(f" Number of atoms in slab: {n_atoms_slab}")
print(f" Surface area: {area:.4f} Ų")
print(f" Surface energy: {e_surface_ev:.6f} eV/Ų")
print(f" Surface energy: {e_surface_j:.4f} J/m²")
print(" Experimental: ~1.83 J/m²")
print()
Part 4: Work function (from electrostatic potential)#
print("Part 4: Work function estimation")
print("-" * 40)
print()
# Static calculation for accurate LOCPOT
calc_static = Vasp(
label='results/surfaces/cu111_static',
atoms=slab,
xc='PBE',
encut=400,
kpts=(6, 6, 1),
ismear=1,
sigma=0.1,
ldipol=True,
idipol=3,
lvtot=True, # Write LOCPOT
)
_ = calc_static.potential_energy
fermi = calc_static.results.get('fermi_level', 0.0)
# Try to read work function from LOCPOT
try:
wf = calc_static.get_work_function()
print(f" Fermi level: {fermi:.4f} eV")
print(f" Vacuum level: {wf + fermi:.4f} eV")
print(f" Work function: {wf:.4f} eV")
print(" Experimental Cu(111): 4.94 eV")
except Exception:
print(f" Fermi level: {fermi:.4f} eV")
print(" Work function calculation requires LOCPOT analysis")
print(" Experimental Cu(111): 4.94 eV")
print()
Part 5: Convergence with slab thickness#
print("Part 5: Surface energy vs. slab thickness")
print("-" * 40)
print()
print("Testing convergence with number of layers:")
print()
# Quick convergence test with different layer counts
layer_counts = [3, 4, 5, 6]
surface_energies = []
for n_layers in layer_counts:
slab_test = fcc111('Cu', size=(2, 2, n_layers), vacuum=12.0, a=3.6)
calc_test = Vasp(
label=f'results/surfaces/cu111_{n_layers}L',
atoms=slab_test,
xc='PBE',
encut=400,
kpts=(6, 6, 1),
ismear=1,
sigma=0.1,
)
e_test = calc_test.potential_energy
n_atoms = len(slab_test)
e_surf = (e_test - n_atoms * e_per_atom) / (2 * area) * ev_per_ang2_to_j_per_m2
surface_energies.append(e_surf)
print(f" {n_layers} layers: {e_surf:.4f} J/m²")
print()
print(f" Variation: {max(surface_energies) - min(surface_energies):.4f} J/m²")
print()
Summary#
print("=" * 60)
print("Summary")
print("=" * 60)
print()
print("Cu(111) Surface Properties:")
print(f" Surface energy: {e_surface_j:.3f} J/m² (exp: ~1.83 J/m²)")
print(" Work function: ~4.9 eV (exp: 4.94 eV)")
print()
print("Key points:")
print(" - Use sufficient vacuum (10-15 Å)")
print(" - Apply dipole correction (LDIPOL=True)")
print(" - Converge with slab thickness")
print(" - Fix bottom layers during relaxation")
print()
print("Next: Try 09_adsorption/ for molecules on surfaces.")