Slabs and adsorbates¶
vibeqc.build is the native (no-ASE) helper for surface-catalysis
inputs. It generates genuine two-dimensional :class:PeriodicSystem
objects (dim=2): two in-plane lattice vectors, atoms at their real
Cartesian z, and no vacuum gap.
The submodule is deliberately small: closed-form surface unit cells for
the most common low-index facets of fcc, bcc, and hcp metals. For
exotic facets or non-metals, build a PeriodicSystem directly from
coordinates (see crystal_lattices).
A slab needs no vacuum¶
A Gaussian-basis code does not need a vacuum gap to describe a surface.
Only the two in-plane lattice vectors are periodic; the direction normal
to the slab is simply never summed over. vibe-qc therefore treats a slab
as a true dim=2 object and evaluates its electrostatics with the
rigorous vacuum-free 2D Ewald gauge (Parry 1975; de Leeuw and Perram
1979), exposed as CoulombMethod.SLAB_EWALD_2D and selected
automatically. This is the same choice CRYSTAL makes for its SLAB
mode.
PeriodicSystem still stores a full 3x3 lattice, because AO integrals
and spglib both want a 3D cell. For dim=2 the third column a3 is
auto-synthesized along the slab normal and is not physical: it is
bookkeeping only, and the SCF energy is invariant to its length. Do not
treat it as a vacuum gap, and do not tune it.
Warning
Migration from the old vacuum convention. Before v0.16.0,
vq.slab(...) returned a dim=3 cell whose third lattice vector was a
load-bearing vacuum gap, and slab jobs were routed through the bulk 3D
Coulomb builders. That was a plane-wave idiom, and it was wrong: a slab
run as a 3D crystal of sheets a3 apart yields an energy that depends
on a3 and on the k-mesh.
vq.slab(...)now returnsdim=2. Passperiodic_z=Trueto get the legacydim=3-with-vacuum cell, which is useful only as a 3D-with-vacuum reference.For
dim=2thevacuum=argument no longer affects the physics. It is retained purely as viewer / density-grid extent metadata onSlabInfo.Slab energies computed before this change are not comparable to the new ones. The new ones are
a3-invariant.
Build a slab straight from lattice vectors with vq.slab_2d, passing
the atoms at their real z (bohr):
import vibeqc as vq
# A graphene layer: two in-plane vectors + real-z atoms. No vacuum.
sheet = vq.slab_2d(
[4.6487, 0.0000, 0.0],
[2.3244, 4.0259, 0.0],
[vq.Atom(6, [0.0000, 0.0000, 0.0]),
vq.Atom(6, [2.3244, 1.3420, 0.0])],
)
assert sheet.dim == 2 # a3 is synthesized for you, and is non-physical
A 5-layer Fe(100) slab with N₂ adsorbed¶
import vibeqc as vq
# 5-layer Fe(100), 2×2 lateral cell. This is a dim=2 slab: no vacuum.
# The default lattice constant (2.866 Å) comes from
# vq.BULK_LATTICE_CONSTANTS; override with a=...
slab_sys, info = vq.slab(
"Fe", facet=(1, 0, 0), n_layers=5,
supercell=(2, 2),
multiplicity=21, # 5×4 = 20 atoms × 4 unpaired e-/Fe ≈ FM start guess
)
assert slab_sys.dim == 2
# Side-on N₂ at a bridge site, 1.9 Å above the top Fe layer.
slab_with_n2 = vq.place_adsorbate(
slab_sys, "N2", info=info,
site="bridge", orientation="side-on", height=1.9,
bond_length=1.10,
)
The info object (a :class:SlabInfo) carries the per-atom layer
index, which feeds the frozen-substrate relaxation pattern below.
Built-in adsorbate library¶
vq.build_molecule(name, bond_length=...) returns a small library of
common adsorbate geometries (in Å):
Name |
Atoms |
Default geometry |
|---|---|---|
|
1-2 |
linear along z |
|
2 |
linear along z |
|
3-5 |
gas-phase neutral geometry |
For anything else, pass an explicit ((symbol, (x, y, z)), …) sequence
in Å.
Adsorption sites¶
place_adsorbate(slab, ads, site=..., info=info) resolves named sites
against the primitive surface cell:
Site |
fcc(100), bcc(100) |
fcc(111), hcp(0001) |
bcc(110) |
|---|---|---|---|
|
atop a metal |
atop a metal |
atop a metal |
|
2-fold bridge |
2-fold bridge |
short bridge |
|
4-fold hollow |
(degenerate w/ fcc/hcp) |
quasi-3-fold |
|
- |
hcp(110) absent, fcc-only |
- |
|
- |
offset 3-fold hollow |
- |
For full control, pass position=(x, y) in Å (relative to the lattice
origin) instead of site=.
Frozen-substrate relaxation¶
Important
Anything force-driven (relaxation, NEB, FD Hessian) needs dim=3
today, because slab analytic gradients are not shipped yet. Build the
cell with periodic_z=True for these workflows, and accept the
vacuum-image error that convention carries. Single-point energies should
use the dim=2 default.
The standard pattern: freeze the bottom layers and relax only the top
layers + adsorbate. :func:vibeqc.relax_atoms takes a
freeze_indices= argument; :meth:SlabInfo.bottom_layer_indices
produces the list:
# dim=3 with a real vacuum gap: required for forces.
slab3d, info3d = vq.slab("Fe", facet=(1, 0, 0), n_layers=5,
vacuum=12.0, supercell=(2, 2),
multiplicity=21, periodic_z=True)
slab3d_n2 = vq.place_adsorbate(slab3d, "N2", info=info3d,
site="bridge", orientation="side-on",
height=1.9, bond_length=1.10)
freeze = info3d.bottom_layer_indices(3) # bottom 3 of 5 layers
opt = vq.relax_atoms(
slab3d_n2,
basis_name="sto-3g",
kmesh=vq.monkhorst_pack(slab3d_n2, [2, 2, 1]),
method="UKS",
functional="pbe",
freeze_indices=freeze,
)
Internally the optimizer pins the fractional coordinates of the frozen
atoms via L-BFGS-B box bounds and zeros their gradient components, so
the reported |grad| reflects only the free degrees of freedom.
Animate the relaxation in vibe-view¶
Pass output_trajectory="stem" and relax_atoms writes a
vibe-view-renderable QVF archive on exit, one frame per accepted
L-BFGS-B step, with the initial geometry as frame 0 and the
converged geometry as the last frame:
opt = vq.relax_atoms(
slab_with_n2,
basis_name="sto-3g",
kmesh=vq.monkhorst_pack(slab_with_n2, [2, 2, 1]),
method="UKS",
functional="pbe",
freeze_indices=freeze,
output_trajectory="slab_n2_relax", # → slab_n2_relax.qvf
)
The archive ships as QVF v2 (per-frame lattice + dim) for any
PeriodicSystem input, so vibe-view’s renderer (per the
“Periodic reaction paths (QVF v2)” section)
draws the unit cell and wraps atoms across in-plane periodic
boundaries automatically. Default output_trajectory=None is a
no-op, no per-step capture overhead.
Open-shell + multi-k for metallic slabs¶
On a dim=2 slab, open-shell UKS routes through the vacuum-free
SLAB_EWALD_2D gauge, at Gamma and multi-k. Metallic slabs typically
need a finite k-mesh in the surface plane, pass kpoints= to
:func:vibeqc.run_periodic_job. The slab normal is not periodic, so its
entry is always 1:
vq.run_periodic_job(
slab_with_n2,
basis=vq.BasisSet(slab_with_n2.unit_cell_molecule(), "sto-3g"),
method="UKS",
functional="pbe",
jk_method="auto", # -> SLAB_EWALD_2D on a dim=2 slab
kpoints=[4, 4, 1], # k-mesh in surface plane, 1 along the normal
)
method="UHF" on a slab is not supported yet and raises; use UKS.
On a dim=3 cell (periodic_z=True), open-shell dispatches via
BIPOLE as before.
Warning
Metallic slabs and smearing. smearing_temperature is wired through
the GDF / GPW / GAPW / RIJCOSX routes, none of which accept a dim=2
slab, so a slab that needs Fermi-Dirac smearing to converge cannot be
smeared on the 2D route today. Until slab smearing lands, run such
systems as dim=3 with thick vacuum (vq.slab(..., periodic_z=True)).
Dispersion correction for periodic systems¶
vq.compute_d3bj_periodic returns the D3-BJ dispersion energy per
unit cell. Two backends:
backend="dftd3"(recommended), Grimme’s reference Fortran library via the optionaldftd3Python package. Bit-exact periodic D3-BJ. Install withpip install dftd3orpip install -e '.[dispersion]'.backend="builtin", vibe-qc’s native C++ D3-BJ on a supercell expansion of the unit cell. Approximate (CN values near the supercell boundary are wrong); the per-cell energy converges with larger supercell. Use for elements not yet covered by the dftd3 install or to stay external-dependency-free.backend="auto"(default), picks dftd3 when available.
Standalone call:
import vibeqc as vq
slab_sys, info = vq.slab("Fe", facet=(1, 0, 0), n_layers=5,
supercell=(2, 2), multiplicity=21)
res = vq.compute_d3bj_periodic(slab_sys, "pbe", with_gradient=True)
print(res.energy, "Ha per cell;", res.backend, res.supercell)
compute_d3bj_periodic is dimensionality-aware: on a dim=2 slab it
replicates only in-plane, so the reported supercell looks like
(3, 3, 1).
Or inline with run_periodic_job via the dispersion= keyword. Leave
jk_method at its default "auto", which resolves a dim=2 slab to
the vacuum-free SLAB_EWALD_2D gauge:
vq.run_periodic_job(
slab_with_n2,
basis=vq.BasisSet(slab_with_n2.unit_cell_molecule(), "sto-3g"),
method="UKS",
functional="pbe",
jk_method="auto", # -> SLAB_EWALD_2D for dim=2
kpoints=[4, 4, 1], # the normal axis is never sampled
dispersion="pbe", # or True (uses the current functional)
dispersion_backend="auto",
)
Note
Passing an explicit bulk jk_method ("gdf", "bipole", "gpw",
"gapw", "rijcosx") to a dim=2 slab now raises
NotImplementedError rather than silently returning an a3-dependent
energy. Use "auto" (or "slab_ewald_2d"). method="UHF" on a slab
also raises for now; use RHF, RKS, or UKS.
The dispersion piece is logged as a separate block in the .out
file and the SCF result is wrapped with a _DispersionAugmented
proxy that exposes .energy_total = E_SCF + E_disp (the bare
.energy is unchanged).
Pass the slab PeriodicSystem unchanged. For a dim=2 slab the lattice
sum runs over the two in-plane axes only, so the dispersion sum is
naturally bounded along the normal.
What’s not here yet¶
The following surface-catalysis features are tracked in
docs/roadmap.md but not yet shipped:
Slab analytic gradients. The
SLAB_EWALD_2Dgauge ships energies (RHF / RKS / UKS, Gamma and multi-k); its analytic gradient is still being written, so geometry optimisation and forces on adim=2slab raise. As a stopgap, build the cell withperiodic_z=Trueand optimise thedim=3-with-vacuum system, accepting thea3-dependent error that convention carries.Open-shell HF (
UHF) on a slab. UseUKSinstead.Density-fitted (GDF) and grid (GPW) 2D Coulomb, for large slabs where the 2D-Ewald sum becomes the bottleneck.
Relaxed bond-length / angle scans on the surface.
Nudged elastic band (NEB / CI-NEB).
Periodic D4 (the molecular D4 path already exists; periodic generalisation pending).
The Hubbard-U correction (+U) for strongly-correlated d/f
electrons is now shipped, see
DFT+U for the full surface (vq.HubbardSite(...)
on run_periodic_job, energies + Fock + per-spin per-k path);
periodic +U gradients are still being plumbed.
If you need any of these urgently, file an issue.