Machine-learning interatomic potentials with MACE¶
ACEsuit MACE is a pre-trained
E(3)-equivariant graph-neural-network interatomic potential. It
predicts energy, forces, and stress on a DFT-fitted potential-energy
surface — orders of magnitude faster than an SCF, at close-to-DFT
accuracy within its trained domain. vibe-qc exposes it as a first-class
method="mace", plus direct drivers for periodic systems.
MACE is a learned potential, not a quantum-chemistry method: it solves no Schrödinger equation and produces no wavefunction. vibe-qc drives MACE’s pre-trained forward pass and attributes it as an external model — the energy is MACE’s, not a vibe-qc ab-initio total. This tutorial walks from a single molecule through geometry optimization, periodic crystals, molecules adsorbed on oxide and metal surfaces, and a comparison with vibe-qc’s own semi-empirical methods.
What you’ll learn: run MACE on molecules and crystals; optimize geometries and relax unit cells; compute adsorption energies of water and ammonia on MgO, CaO, Al₂O₃, and metal surfaces; choose a model and respect its license; and when to reach for MACE versus ab-initio or semi-empirical methods.
Installation¶
MACE is an optional dependency (a heavy PyTorch + e3nn stack), behind the
[mace] extra, on Python ≤ 3.13:
pip install 'vibe-qc[mace]'
On Python 3.14 the runtime is import-gated with a clear message (MACE’s
matscipy dependency has no 3.14 wheel yet). On macOS, vibe-qc auto-sets
KMP_DUPLICATE_LIB_OK=TRUE (its native core and PyTorch each link an
OpenMP runtime) — verified not to change results. Foundation-model
weights download on first use into ~/.cache/mace/.
1. A first calculation — an organic molecule¶
method="mace" plugs into run_job exactly like an SCF method, minus
the basis set (MACE needs none):
from pathlib import Path
from vibeqc import Molecule, run_job
# ethanol, C2H5OH
mol = Molecule.from_xyz("ethanol.xyz")
run_job(mol, method="mace", output="ethanol_mace")
ethanol_mace.out carries the energy, an MACE provenance block
(model, license, training/theory, the energy-scale caveat), and the
references; ethanol_mace.bibtex / .references cite the MACE method
paper + the foundation-model paper. There is no .molden — MACE has no
orbitals.
Energy scale. A MACE energy lives on a model-specific reference scale (each model subtracts its own per-element atomic energies), not a vibe-qc total and not comparable across models. For H₂O, MACE-MPA-0 gives ≈ −0.51 Ha; the organic MACE-OFF23 gives ≈ −76.5 Ha (near its ωB97M total). Use MACE energies for relative quantities — geometry optimization, reaction energies at fixed composition + model, adsorption energies, MD — never as absolute totals.
2. Geometry optimization¶
Add optimize=True — ASE’s BFGS drives the MACE calculator directly:
run_job(mol, method="mace", optimize=True, fmax=0.02, output="ethanol_opt")
You get ethanol_opt.traj (one frame per step) as usual. MACE’s
geometries are close to experiment — relaxing water and ammonia:
molecule |
bond |
MACE-MPA-0 |
experiment |
|---|---|---|---|
H₂O |
O–H / H–O–H |
0.970 Å / 103.9° |
0.958 Å / 104.5° |
NH₃ |
N–H / H–N–H |
1.015 Å / 107.4° |
1.012 Å / 106.7° |
3. Choosing a model — and its license¶
The default is the MIT-licensed MACE-MPA-0 (materials, 89 elements).
Pass MLIPOptions to select another model, device, or dtype:
from vibeqc.mlip import MLIPOptions
run_job(mol, method="mace",
mlip_options=MLIPOptions(model="off23-medium",
accept_academic_license=True),
output="ethanol_off23")
model key |
family |
domain |
license |
|---|---|---|---|
|
MACE-MP / MPA |
materials (89 elts) |
MIT |
|
MACE-OFF23 |
organic (H,C,N,O,F,P,S,Cl,Br,I) |
ASL |
The organic MACE-OFF23 weights are under the Academic Software
License — academic, non-commercial use only. vibe-qc raises a
PermissionError if you select an ASL model without acknowledging it
(accept_academic_license=True or VIBEQC_ACCEPT_ASL=1). MIT models are
never gated. Picking an organic model for an out-of-domain element (say a
metal) raises a clear error pointing you at a materials model.
4. Periodic crystals¶
Periodic systems use direct drivers (MACE is not wired into
run_periodic_job, which is SCF-only) — following the periodic
semi-empirical precedent. run_periodic_mace returns energy, forces, and
stress; optimize_periodic_mace_cell does a variable-cell
relaxation:
import numpy as np
from ase.build import bulk
from vibeqc.ase_periodic import atoms_to_periodic_system
from vibeqc.mlip.mace import run_periodic_mace, optimize_periodic_mace_cell
system = atoms_to_periodic_system(bulk("Si", "diamond", a=5.43))
res = run_periodic_mace(system)
print(res.energy) # Ha (model reference scale)
print(np.diag(res.stress()) * 29421.0) # GPa (Ha/bohr³ → GPa)
relaxed = optimize_periodic_mace_cell(system, fmax=0.01)
For bulk silicon this relaxes the (slightly compressed) cell — the stress drops from −1.85 GPa to ≈ 0, the energy lowers, and the lattice constant moves from 5.43 Å to MACE’s ≈ 5.47 Å equilibrium. Forces are zero by the diamond symmetry, as they should be.
5. Molecules on surfaces — adsorption energies¶
This is where a fast, broadly-trained materials model shines: thousands of single-points per second let you screen adsorption across many surfaces. The recipe is the standard one — adsorption energy
E_ads = E(slab + molecule) − E(slab) − E(molecule)
all evaluated with the same MACE model so the per-element reference energies cancel. Here is a complete, runnable example for water on MgO(100):
import numpy as np
from ase.build import bulk, surface, molecule, add_adsorbate
from ase.optimize import BFGS
from ase.constraints import FixAtoms
from vibeqc.mlip.mace import mace_calculator
# one MACE-MPA-0 calculator, reused for every structure
calc = mace_calculator()
def energy(atoms, relax=True):
a = atoms.copy(); a.calc = calc
if relax:
z = a.positions[:, 2]
a.set_constraint(FixAtoms(mask=z < z.min() + 1.2)) # freeze the bottom
BFGS(a, logfile=None).run(fmax=0.15, steps=80)
return a.get_potential_energy() # eV
# gas-phase water in a box
h2o = molecule("H2O"); h2o.set_cell([14, 14, 14]); h2o.center(); h2o.pbc = True
e_gas = energy(h2o)
# MgO(100) slab (3 layers, 2×2), relaxed
slab = surface(bulk("MgO", "rocksalt", a=4.212), (1, 0, 0), 3, vacuum=8.0) * (2, 2, 1)
e_slab = energy(slab)
# water on top of a surface Mg, relaxed
ads = slab.copy()
mg = max((i for i, z in enumerate(ads.numbers) if z == 12), key=lambda i: ads.positions[i, 2])
add_adsorbate(ads, molecule("H2O"), height=2.1, position=ads.positions[mg, :2], mol_index=0)
e_total = energy(ads)
print(f"E_ads(H2O/MgO) = {e_total - e_slab - e_gas:.3f} eV") # ≈ -0.81 eV
Running the same recipe for water and ammonia across a range of oxide and metal surfaces (MACE-MPA-0, relaxed, bottom layer fixed):
surface |
atoms |
E_ads(H₂O) / eV |
E_ads(NH₃) / eV |
|---|---|---|---|
MgO(100) |
24 |
−0.81 |
−1.16 |
CaO(100) |
24 |
−3.18 † |
−1.35 |
Al₂O₃(0001) |
60 |
−1.50 |
−1.64 |
TiO₂(110) |
36 |
−0.66 |
−1.08 |
Pt(111) |
27 |
−0.37 |
−1.42 |
Cu(111) |
27 |
−0.11 |
−0.45 |
The trends are physically sensible: ammonia (a stronger Lewis base) binds more strongly than water on every surface except CaO. The standout is † water on CaO(100): it binds far more strongly (≈ −3.2 eV) than on MgO because CaO is much more basic — in the relaxed structure one O–H points down and forms a very short hydrogen bond (≈ 1.5 Å) to a surface oxygen, the onset of the dissociative surface-hydroxyl chemistry CaO is known for. (This is a stable, reproducible MACE result — ≈ −3.16 eV across starting heights of 1.8–2.5 Å — not a stray local minimum.) The less basic MgO binds water moderately (≈ 0.8 eV); the more reactive Al₂O₃ binds both strongly; on the noble-ish Cu(111) water barely physisorbs (≈ 0.1 eV) while it adsorbs more strongly on Pt(111). A fixed-charge force field would miss the MgO < CaO basicity trend that MACE reproduces here. A handful of these single-points take seconds — the same screen at DFT would be hours to days.
These are illustrative numbers from a quick relaxation (loose
fmax, thin slabs, a single adsorption site). For publication you would converge slab thickness, k-points-equivalent supercell size, the adsorption site, andfmax— but the workflow is exactly the one above. On strongly basic surfaces like CaO, check whether the water stayed molecular or dissociated (inspect the relaxed O–H distances) — the two are different physical states with different energies. The full script is inexamples/mlip/04_surface_adsorption.py.
6. MACE versus vibe-qc’s semi-empirical methods¶
vibe-qc ships its own semi-empirical platform (DFTB, GFN2-xTB, PM6,
OMx — see semiempirical). Both are
fast, non-ab-initio energy engines, but they sit in different places:
MACE (this tutorial) |
semi-empirical (vibe-qc’s own) |
|
|---|---|---|
nature |
learned potential (pre-trained GNN) |
physics-based, vibe-qc’s own code |
accuracy (in domain) |
close to the reference DFT |
lower; qualitative–semi-quantitative |
electronic structure |
none (no orbitals/charges/gap) |
yes — charges, orbitals, HOMO–LUMO |
charge / spin states |
ignored (neutral potential) |
represented |
elements |
trained set (10 organic / 89 materials) |
any parameterised element |
transferability |
in-distribution only |
broad (physics-based) |
typical use |
accurate energies/forces/MD, screening |
electronic properties, any element |
Concretely, MACE reproduces the water and ammonia geometries above to
within ~0.01 Å and ~1° of experiment — materially closer than typical
semi-empirical methods (the MACE-OFF23 paper reports MACE-OFF beating
both GFN2-xTB and ANI-1ccx on organic benchmarks). The trade-off is that
semi-empirical methods give you a wavefunction (Mulliken charges, a gap,
frontier orbitals) and work for any parameterised element and charge
state, where MACE gives none of those and only within its training
domain. A detailed, numbers-backed comparison lives in
semiempirical_mlip_comparison.
Rule of thumb: reach for MACE when you want DFT-quality energies/forces/geometries/MD on systems inside its training domain and don’t need electronic structure; reach for semi-empirical (or ab-initio) when you need charges/orbitals/gaps, unusual elements or charge states, or a physically transferable model.
7. Visualization¶
A MACE run writes the same structural output as any vibe-qc job, so
geometry and (with optimize=True) the relaxation trajectory visualize
out of the box: moltui ethanol_opt.xyz (terminal), or pass
output_qvf=True and open the .qvf archive in vibe-view for the
3-D structure + trajectory animation + citations. There is no
orbital/density/band visualization — there is no wavefunction to plot.
See mlip § Visualization.
8. Limitations + good practice¶
No electronic structure — no charges, orbitals, gaps, spectra.
Trained-domain only — accuracy degrades out of distribution; MACE cannot tell you when you’ve left its domain. Sanity-check against DFT for unusual chemistry.
Charge / spin ignored — vibe-qc warns if you pass a non-neutral or open-shell system; the energy is the neutral value regardless.
Energy scale — model-specific reference, never an absolute total or comparable across models.
Attribution — every run cites the MACE papers; cite them in published work (
CLAUDE.md§ 10: vibe-qc drives MACE, it does not claim MACE’s energy as its own).
See also¶
mlip— the MACE user-guide reference.examples/mlip/— runnable scripts (molecular, periodic, NEB reaction path, surface adsorption).semiempirical_mlip_comparison— detailed MACE-vs-semi-empirical comparison.neb§ MACE backend —run_neb(method="mace")drives reaction-path searches on the MACE potential (analytic forces, no SCF).