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

medium-mpa-0 (default), small, medium, large

MACE-MP / MPA

materials (89 elts)

MIT

off23-small, off23-medium, off23-large

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, and fmax, 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 in examples/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