A5 NEB: migration barriers with MLIPs

Objectives

  • Explain what a climbing-image NEB computes and why barriers matter.

  • Run a Li vacancy hop in olivine LiFePO4 with MACE-MP-0b, Orb-v3 and TensorNet.

  • Judge MLIP barriers against DFT, experiment and published benchmarks.

Why barriers matter

Ion transport, defect diffusion and surface reactions are rare events: the system waits in a minimum, then crosses a saddle point. In harmonic transition-state theory the rate of one hop is

k = ν exp(−Ea / kBT),

with an attempt frequency ν of about 1012 to 1013 s−1 and the migration barrier Ea, the energy of the saddle above the initial minimum. Because Ea sits in an exponent, small errors are large in rate: at 298 K, 60 meV changes the diffusivity by about a factor of ten [18]. DFT-NEB itself carries an error of about that size, and a DFT-NEB for a cell of about 100 atoms is an expensive calculation. A universal MLIP can run the same NEB in seconds on one GPU, if its energy surface is right far from equilibrium.

The nudged elastic band

NEB: two fixed end points, moving images joined by springs, and a climbing image that finds the saddle.

  • A chain of images is placed between two relaxed minima and optimised together.

  • Each moving image feels the true force perpendicular to the band and a spring force along it. The springs only keep the images evenly spaced; they do not pull the band off the minimum-energy path.

  • In the climbing-image variant (CI-NEB), the highest image has no springs and the component of its true force along the band is inverted. It moves uphill along the path and downhill across it, and converges on the saddle [3]. The barrier is then read from that image, not interpolated between images.

  • Cost: images × optimiser steps force calls. Every image is an independent energy and force evaluation, which suits a GPU model.

The case: Li in olivine LiFePO4

Olivine LiFePO4 is a common Li-ion cathode [1]. Li moves through one-dimensional channels along [010]; hops between channels cost more than 2.5 eV [2]. Atomistic simulations predicted a curved path between neighbouring Li sites [14], and neutron diffraction later imaged it [15]. The system is well studied with DFT, so it is a fair test for a universal MLIP.

  • Cell and positions: the X-ray structure (Pnma, a = 10.332 Å, b = 6.010 Å, c = 4.692 Å) [5], from the Crystallography Open Database (entry 2100916). pymatgen expands the six symmetry-distinct sites into the 28-atom cell.

  • Supercell 1×2×2 (112 atoms, 10.3 × 12.0 × 9.4 Å), the same size as a published GGA+U study [22]. Periodic copies of the vacancy are at least 9.4 Å apart.

  • One Li at the origin is removed (111 atoms). The neighbouring Li along b, 3.005 Å away, hops into the vacancy.

  • Both end points have the same atom order, so the band is a simple interpolation between them.

structures.py
 1"""Olivine LiFePO4 supercell with one Li vacancy and the two end points of a Li hop along [010]."""
 2
 3from __future__ import annotations
 4
 5import numpy as np
 6from ase import Atoms
 7from pymatgen.core import Lattice, Structure
 8from pymatgen.io.ase import AseAtomsAdaptor
 9
10# Pnma cell and sites of LiFePO4 from X-ray diffraction (Streltsov et al., 1993; COD 2100916).
11CELL = (10.332, 6.010, 4.692)
12SITES = [
13    ("Li", (0.0, 0.0, 0.0)),
14    ("Fe", (0.28222, 0.25, 0.97472)),
15    ("P", (0.09486, 0.25, 0.41820)),
16    ("O", (0.09678, 0.25, 0.7428)),
17    ("O", (0.45710, 0.25, 0.2060)),
18    ("O", (0.16558, 0.04646, 0.2848)),
19]
20SUPERCELL = (1, 2, 2)
21
22
23def unit_cell() -> Atoms:
24    species, coords = zip(*SITES)
25    s = Structure.from_spacegroup("Pnma", Lattice.orthorhombic(*CELL), species, coords)
26    return AseAtomsAdaptor.get_atoms(s)
27
28
29def end_points(supercell: tuple[int, int, int] = SUPERCELL) -> tuple[Atoms, Atoms, int]:
30    """Remove the Li at the origin; the neighbouring Li along b hops into that vacancy.
31
32    Returns the initial and final images (same atom order) and the index of the hopping Li.
33    """
34    atoms = unit_cell().repeat(supercell)
35    atoms.wrap()
36    li = [a.index for a in atoms if a.symbol == "Li"]
37    vacancy = min(li, key=lambda i: np.linalg.norm(atoms.positions[i]))
38    target = atoms.positions[vacancy] + [0, CELL[1] / 2, 0]
39    hopper = min(li, key=lambda i: np.linalg.norm(atoms.positions[i] - target))
40    site = atoms.positions[vacancy].copy()
41    del atoms[vacancy]
42    hopper -= hopper > vacancy
43    final = atoms.copy()
44    final.positions[hopper] = site
45    return atoms, final, hopper

Bond lengths of the built cell (Li–O 2.09 to 2.19 Å, Fe–O 2.06 to 2.25 Å, P–O 1.52 to 1.56 Å) match the experimental octahedra and tetrahedra.

Models

Model

Checkpoint

Training data

Reference level

MACE-MP-0b

mace-mp-0b small

MPtrj [6]

PBE and PBE+U

Orb-v3

orb-v3-conservative-inf-omat

OMat24 [7]

PBE and PBE+U

TensorNet

TensorNet-PES-MatPES-PBE-2025.2

MatPES [8]

PBE, no U

Architectures: MACE-MP-0 [9], Orb-v3 [10], TensorNet [11].

models.py
 1"""Three universal MLIPs as ASE calculators, all in float64."""
 2
 3from __future__ import annotations
 4
 5import torch
 6
 7torch.set_default_dtype(torch.float64)
 8
 9MODELS = {
10    "MACE-MP-0b": "mace-mp-0b small (MPtrj, PBE and PBE+U)",
11    "Orb-v3": "orb-v3-conservative-inf-omat (OMat24, PBE and PBE+U)",
12    "TensorNet": "TensorNet-PES-MatPES-PBE-2025.2 (MatPES, PBE)",
13}
14MACE_MODEL = "small-0b"
15MATGL_MODEL = "TensorNet-PES-MatPES-PBE-2025.2"
16
17
18def device() -> str:
19    return "cuda" if torch.cuda.is_available() else "cpu"
20
21
22def mace():
23    from mace.calculators import mace_mp
24
25    return mace_mp(model=MACE_MODEL, device=device(), default_dtype="float64")
26
27
28def orb():
29    """orb-models 0.5.x API: 0.6 and later need Python 3.12, the LUMI module has 3.11."""
30    from orb_models.forcefield import pretrained
31    from orb_models.forcefield.calculator import ORBCalculator
32
33    model = pretrained.orb_v3_conservative_inf_omat(device=device(), precision="float64", compile=False)
34    return ORBCalculator(model, device=device())
35
36
37def tensornet():
38    import matgl
39    from matgl.ext.ase import PESCalculator
40
41    matgl.set_default_dtype("float", 64)
42    potential = matgl.load_model(MATGL_MODEL).to(device=device(), dtype=torch.float64)
43    return PESCalculator(potential, stress_unit="eV/A3")
44
45
46LOADERS = {"MACE-MP-0b": mace, "Orb-v3": orb, "TensorNet": tensornet}
47
48
49def load(name: str):
50    """Return an ASE calculator that counts its own calculate() calls in `.calls`."""
51    calc = LOADERS[name]()
52    calc.calls = 0
53    calculate = calc.calculate
54
55    def counted(*args, **kwargs):
56        calc.calls += 1
57        return calculate(*args, **kwargs)
58
59    calc.calculate = counted
60    return calc
61
62
63def versions() -> dict:
64    from importlib.metadata import version
65
66    pkgs = ("torch", "ase", "pymatgen", "mace-torch", "orb-models", "matgl")
67    out = {p: version(p) for p in pkgs}
68    out["gpu"] = torch.cuda.get_device_name(0) if torch.cuda.is_available() else None
69    out["float"] = str(torch.get_default_dtype()).removeprefix("torch.")
70    return out

On LUMI, torch 2.7.1+rocm6.2.4 fails in float32 det and prod kernels (CUDA driver error: 209), so every model runs in float64. The module has Python 3.11, so orb-models is 0.5.5 (0.6 and later need Python 3.12).

NEB set-up

  • Relax both end points at fixed cell with FIRE [12] to fmax 0.05 eV/Å.

  • Seven moving images, first guess from IDPP [13], which avoids atoms coming too close in a straight-line guess.

  • CI-NEB in ASE [4] from the start, spring constant 0.1 eV/Ų (ASE default), FIRE to fmax 0.05 eV/Å, at most 1000 steps.

  • Barrier: highest image energy minus the initial energy. Force calls: model evaluations counted in the calculator. Wall time: the NEB optimisation only.

neb.py
 63def run(name: str, images: int = 7, fmax: float = 0.05, optimiser: str = "FIRE",
 64        steps: int = 1000) -> dict:
 65    opt = OPTIMISERS[optimiser]
 66    calc = models.load(name)
 67    initial, final, hopper = structures.end_points()
 68
 69    start = time.perf_counter()
 70    relaxed = []
 71    for atoms in (initial, final):
 72        atoms.calc = calc
 73        relaxed.append(bool(opt(atoms, logfile=None).run(fmax=fmax, steps=steps)))
 74        freeze(atoms)
 75    relax = {"converged": relaxed, "force_calls": calc.calls,
 76             "wall_s": round(time.perf_counter() - start, 2)}
 77
 78    band = [initial] + [initial.copy() for _ in range(images)] + [final]
 79    for atoms in band[1:-1]:
 80        atoms.calc = ImageCalculator(calc)
 81    neb = NEB(band, climb=True)
 82    neb.interpolate(method="idpp", mic=True)
 83    calc.calls = 0
 84    start = time.perf_counter()
 85    optimizer = opt(neb, logfile=None)
 86    converged = bool(optimizer.run(fmax=fmax, steps=steps))
 87    neb_run = {"converged": converged, "steps": optimizer.nsteps, "force_calls": calc.calls,
 88               "wall_s": round(time.perf_counter() - start, 2)}
 89    for atoms in band[1:-1]:
 90        freeze(atoms)
 91
 92    tools = NEBTools(band)
 93    barrier, delta = tools.get_barrier(fit=False)
 94    fit = tools.get_fit()
 95    e0 = band[0].get_potential_energy()
 96    result = {
 97        "model": name, "description": models.MODELS[name], "atoms": len(initial),
 98        "images": images, "fmax": fmax, "optimiser": optimiser,
 99        "barrier_eV": round(float(barrier), 4), "reaction_energy_eV": round(float(delta), 4),
100        "hop_distance_A": round(float(fit.path[-1]), 4),
101        "relax": relax, "neb": neb_run,
102        "path_A": [round(float(x), 4) for x in fit.path],
103        "energies_eV": [round(float(a.get_potential_energy() - e0), 5) for a in band],
104        "fit_path_A": [round(float(x), 4) for x in fit.fit_path],
105        "fit_energies_eV": [round(float(x), 5) for x in fit.fit_energies],
106        "li_offset_xz_A": lateral_offset(band, hopper),
107        "hopper": hopper,
108    }
109    RESULTS.mkdir(exist_ok=True)
110    stem = name.lower().replace("-", "_")
111    (RESULTS / f"{stem}.json").write_text(json.dumps(result, indent=2))
112    write(RESULTS / f"{stem}_band.extxyz", band)
113    return result

One model is loaded once and shared by all images. If the images share one ASE calculator directly (allow_shared_calculator=True), its cache only holds the last image, and ASE evaluates every image again each time the optimiser asks the band for forces: about three times per step. A thin per-image wrapper keeps a cache for each image:

neb.py: ImageCalculator
24class ImageCalculator(Calculator):
25    """Per-image view of one shared model, so each image caches its own results.
26
27    With one calculator shared by all images, ASE re-evaluates every image each time
28    the optimiser asks the band for forces (about three times per step).
29    """
30
31    implemented_properties = ["energy", "forces"]
32
33    def __init__(self, model):
34        super().__init__()
35        self.model = model
36
37    def calculate(self, atoms=None, properties=("energy",), system_changes=all_changes):
38        super().calculate(atoms, properties, system_changes)
39        copy = self.atoms.copy()
40        copy.calc = self.model
41        self.results = {"energy": copy.get_potential_energy(), "forces": copy.get_forces()}

Run on LUMI

On a login node (compute nodes have no internet), in a copy of this repository at <SCRATCH>/mlip-neb:

module purge && module use /appl/local/csc/modulefiles/ && module load pytorch
python -m venv --system-site-packages .venv && source .venv/bin/activate
pip install -r examples/neb/requirements.txt
for m in MACE-MP-0b Orb-v3 TensorNet; do
    HF_HOME=$PWD/hf-home python examples/neb/cli.py prefetch --model $m
done
sbatch --account=<PROJECT> scripts/lumi-neb.sbatch

The job runs each model in its own process, then draws the figures, and ends with:

MACE-MP-0b: barrier 0.263 eV, NEB 343 calls, 12.01 s, converged True
Orb-v3: barrier 0.317 eV, NEB 329 calls, 19.37 s, converged True
TensorNet: barrier 0.143 eV, NEB 252 calls, 7.33 s, converged True
== done ..., failed: none
lumi-neb.sbatch
27export PYTHONNOUSERSITE=1
28export HF_HOME="$root/hf-home" HF_HUB_OFFLINE=1
29export MIOPEN_USER_DB_PATH="/tmp/$USER-miopen-$SLURM_JOB_ID"
30export MIOPEN_CUSTOM_CACHE_DIR="$MIOPEN_USER_DB_PATH"
31export HIP_FORCE_DEV_KERNARG=1
32export MPLCONFIGDIR="/tmp/$USER-mpl-$SLURM_JOB_ID"
33export OMP_NUM_THREADS="$SLURM_CPUS_PER_TASK"
34mkdir -p "$MIOPEN_USER_DB_PATH" "$MPLCONFIGDIR"
35trap 'rm -rf "$MIOPEN_USER_DB_PATH" "$MPLCONFIGDIR"' EXIT
36
37cd "$root/examples/neb"
38run() { srun --ntasks=1 --gpus=1 --cpu-bind=cores python cli.py "$@"; }
39failed=()
40run versions || failed+=(versions)
41for model in MACE-MP-0b Orb-v3 TensorNet; do
42    echo "== $model $(date +%T)"
43    run run --model "$model" || failed+=("$model")
44done
45run figures || failed+=(figures)
46echo "== done $(date +%T), failed: ${failed[*]:-none}"
cli.py
16def main() -> None:
17    parser = argparse.ArgumentParser(description=__doc__)
18    parser.add_argument("step", choices=STEPS)
19    parser.add_argument("--model", choices=list(models.MODELS), help="model for run (default: all)")
20    parser.add_argument("--images", type=int, default=7, help="moving images between end points")
21    parser.add_argument("--fmax", type=float, default=0.05, help="force tolerance (eV/Å)")
22    parser.add_argument("--optimiser", choices=["FIRE", "BFGS"], default="FIRE")
23    args = parser.parse_args()
24    names = [args.model] if args.model else list(models.MODELS)
25
26    if args.step == "prefetch":
27        for name in names:
28            models.load(name)
29            print("cached", name)
30    elif args.step == "versions":
31        import neb
32
33        neb.RESULTS.mkdir(exist_ok=True)
34        text = json.dumps(models.versions(), indent=2)
35        (neb.RESULTS / "versions.json").write_text(text)
36        print(text)
37    elif args.step == "run":
38        import neb
39
40        for name in names:
41            r = neb.run(name, images=args.images, fmax=args.fmax, optimiser=args.optimiser)
42            print(f"{name}: barrier {r['barrier_eV']:.3f} eV, NEB {r['neb']['force_calls']} calls, "
43                  f"{r['neb']['wall_s']} s, converged {r['neb']['converged']}")
44    else:
45        import figures
46
47        figures.main()
48
49
50if __name__ == "__main__":
51    main()

To vary one setting inside a GPU allocation, from examples/neb (this overwrites that model’s files in results/):

python cli.py run --model TensorNet --optimiser BFGS
python cli.py run --model MACE-MP-0b --images 5 --fmax 0.03

Results

Conditions: one AMD MI250X GCD, float64, one run, 111 atoms, 7 moving images, torch 2.7.1+rocm6.2.4, ase 3.29.0, pymatgen 2026.9.24, mace-torch 0.3.16, orb-models 0.5.5, matgl 4.0.3.

MACE-MP-0b

Orb-v3

TensorNet

barrier Ea

0.263 eV

0.317 eV

0.143 eV

end-point energy difference

0.000 eV

0.000 eV

0.000 eV

NEB steps

48

46

35

NEB force calls

343

329

252

NEB wall time

12.0 s

19.4 s

7.3 s

time per force call

35 ms

59 ms

29 ms

end-point relaxation (2 structures)

74 calls, 16.9 s

90 calls, 19.3 s

78 calls, 3.0 s

path length of the hopping Li

3.35 Å

3.26 Å

3.24 Å

offset from straight line at saddle

0.67 Å

0.65 Å

0.64 Å

Left: CI-NEB energy profiles (points are images, lines are the ASE fit). Right: distance of the hopping Li from the straight line between its two sites.

Relaxed supercell with all nine positions of the hopping Li (Orb-v3), seen down c and down a. Atoms deeper in the cell overlap in projection.

  • All three runs converged, and both end points have the same energy, as the symmetry of the hop requires.

  • All three find the curved path: the Li bulges 0.64 to 0.67 Å off the straight line, so it travels about 3.3 Å instead of 3.0 Å, as predicted [14] and observed [15].

  • The end-point relaxation times include loading the model and GPU warm-up; use the NEB times to compare cost.

  • With one shared calculator (allow_shared_calculator=True) the same job needed 1022, 980 and 749 force calls and 34.4, 56.3 and 20.6 s for the same barriers. The per-image wrapper saves about a factor of three.

Comparison with DFT and experiment

Method

Barrier

GGA (PW91), ferromagnetic, vacancy in LiFePO4 [2]

0.27 eV

GGA+U (U = 4.3 eV), vacancy, hole on an in-plane Fe [16]

0.29 eV

GGA+U, vacancy, hole on an out-of-plane Fe [16]

0.47 eV

GGA+U, charged vacancy, 1×2×2 cell [22]

0.32 eV

Classical shell-model potentials [14]

0.55 eV

Experiment, single-crystal impedance along b [24]

about 0.54 eV

Experiment, single-crystal muon spin relaxation [23]

0.14 to 0.17 eV

MACE-MP-0b, Orb-v3, TensorNet (this page)

0.26, 0.32, 0.14 eV

  • MACE-MP-0b and Orb-v3 fall within 0.05 eV of the DFT vacancy barriers without a localised hole (0.27 to 0.32 eV).

  • TensorNet gives about half. The missing U is not the main cause, since plain GGA gives 0.27 eV [2]; a too soft energy surface is the likely reason (next section).

  • The 0.17 eV spread between models is close to a factor of 1000 in hop rate at 298 K.

  • Experiments do not measure the same quantity. Macroscopic transport (about 0.5 eV and higher) includes antisite defects that block channels and polarons; muon spin relaxation probes local hops. Compare an MLIP with the DFT level it was trained on, not with experiment.

Reading the results

Softening: expect underestimates

Universal MLIPs are trained mostly on structures near equilibrium. Far from it, their energy surface is too flat (“softened”), and energies and forces of high-energy states such as saddle points come out too low [17].

Schematic: DFT energy surface (left) and a softened uMLIP surface (right); training points near equilibrium (orange) are reproduced, high-energy states (red) are underestimated.

Figure: B. Deng et al., npj Comput. Mater. 11, 9 (2025), Fig. 1 [17], CC BY 4.0; caption removed.

For 470 Mg2+ migration paths (reference: ApproxNEB with GGA), MACE-MP-0, CHGNet and M3GNet gave barrier mean absolute errors (MAE) of 0.34, 0.39 and 0.49 eV, and all three distributions are shifted to negative errors:

Left: DFT and uMLIP energy profiles for Mg2+ migration in V2O3(SO4)2. Right: distribution of uMLIP minus DFT barriers; most lie below zero (softening).

Figure: B. Deng et al., npj Comput. Mater. 11, 9 (2025), Fig. 3 [17], CC BY 4.0, unmodified.

The same work shows the error is systematic: fine-tuning CHGNet on one structure moved its force-slope from 0.86 to 0.97 of DFT, and ten structures cut the force MAE from 0.19 to 0.13 eV/Å (see A4 Train and fine-tune a potential).

How good are zero-shot barriers?

The largest solid-state test ran five universal MLIPs, unchanged, on 574 battery migration paths with GGA DFT-NEB references [18]. Their NEB had no climbing image and only three moving images, so each band maximum is a lower bound on the model’s own saddle. Its Orb-v3 is the checkpoint used on this page; its MACE-MP-0 is the large MPtrj model, not MACE-MP-0b small.

Parity plot of MLIP against DFT-NEB migration barriers for 574 paths and five models; inset 0 to 2 eV.

Figure: A. K. Bheemaguli, P. Xiao and G. Sai Gautam, Fig. 2 of arXiv:2512.03642 [18], CC BY 4.0, cropped to the plot.

MACE-MP-0

Orb-v3

SevenNet

CHGNet

M3GNet

MAE, all 574 paths

0.310 eV

0.336 eV

0.344 eV

0.343 eV

0.349 eV

MAE, 17 common outliers removed

0.239 eV

0.245 eV

0.251 eV

0.275 eV

0.290 eV

MAE, each model’s outliers above 1 eV removed

0.202 eV

0.198 eV

0.203 eV

0.248 eV

0.257 eV

correct good/bad at 0.5 eV

79.4 %

84.8 %

82.9 %

73.9 %

73.5 %

underestimated paths

52 %

42 %

43 %

73 %

78 %

  • An MAE of 0.2 to 0.3 eV is far too large for a quantitative rate, but good enough to sort fast from slow conductors about 80 % of the time.

  • CHGNet and M3GNet underestimate most barriers; MACE, SevenNet and Orb-v3 show no clear bias on this set.

  • The MLIP path is a good start: the MLIP images were better first guesses for DFT than a linear interpolation in about 66 % of cases (above 71 % for MACE-MP-0 and SevenNet).

Confusion matrices for classifying paths as good (below 0.5 eV) or bad conductors; in each matrix, top left true positives, top right false positives, bottom left false negatives, bottom right true negatives.

Figure: A. K. Bheemaguli, P. Xiao and G. Sai Gautam, Fig. 4 of arXiv:2512.03642 [18], CC BY 4.0, cropped to the plot.

Newer models trained on more off-equilibrium data do better. On 154 DFT-NEB ion-migration paths with a full CI-NEB workflow, barrier MAE ranged from about 0.05 eV (UMA) and 0.06 eV (MACE-MPA-0) to 0.10 to 0.17 eV for MatPES and older models, and most models still underestimated [20]. Static evaluations along the DFT paths gave similar errors, so the error comes from the energy surface, not from the NEB.

Screening, then refinement

The most reliable use of an MLIP-NEB is as a cheap first stage whose path and saddle are checked with a few DFT calculations. For 932 surface reactions (OC20NEB, RPBE), models trained only on adsorbate relaxations found transition states within 0.1 eV of DFT 91 % of the time, with a 28× speed-up [19]. The two numbers come from different model sizes; the trade-off for one model is in panel d below.

CatTSunami overview: pretrained models, the OC20NEB dataset and four ML-accelerated NEB protocols with speed-up and success rate.

Figure: B. Wander et al., Fig. 1 of arXiv:2405.02078 [19], CC BY 4.0, cropped to the figure.

Protocol (EquiformerV2, 31M)

Speed-up

Success within 0.1 eV

all ML

2200×

70 %

ML, then 3 DFT single points

88×

84 %

ML, then 2 DFT relaxations and 1 DFT single point

28×

88 %

ML pre-relaxation, then DFT NEB

3×

DFT level

(a) success rate and (b) converged fraction per model and reaction class, (c) parity of ML-assisted against DFT barriers, (d) speed-up against success rate.

Figure: B. Wander et al., Fig. 2 of arXiv:2405.02078 [19], CC BY 4.0, cropped to the figure.

  • A full reaction network (CO hydrogenation on Rh(111), about 19 000 NEBs) took 12 GPU days, against an estimated 52 GPU years with DFT.

  • ML pre-optimised bands sometimes found lower transition states than DFT alone (7 % of transfers), since cheap sampling explores more paths.

  • For organic molecules, MLIP path searches followed by DFT saddle refinement reached 96.6 % success (MACE-OMol25) with 3.8 DFT gradients per reaction, 94 to 96 % fewer than DFT alone [21].

  • Uncertainty-aware NEB, which weights forces by the model’s force covariance, is an early research direction [25].

For LiFePO4 this means: take the MLIP path and saddle geometry, then run a few DFT (+U) single points or a short DFT CI-NEB started from the MLIP band.

Magnetism, charge and PBE+U

  • Fe2+ in LiFePO4 carries a magnetic moment. These MLIPs take no spins or charges as input: they were trained mostly on spin-polarised DFT, but predict one energy surface without a magnetic state.

  • Removing a neutral Li leaves a hole on a nearby Fe (Fe3+). The models cannot place this polaron, so the result is closest to a DFT calculation with a delocalised hole.

  • Where the hole sits changes the DFT barrier by almost 0.2 eV [16], more than the spread between MACE-MP-0b and Orb-v3. Polaron effects need DFT+U or hybrid DFT.

  • MPtrj and OMat24 mix PBE and PBE+U labels; MatPES is PBE only. Know which reference a model reproduces before comparing it with a DFT+U number.

Keypoints

  • CI-NEB with a universal MLIP gives the curved Li path in LiFePO4 and a barrier in seconds on one GPU; barriers differ by a factor of two between models (0.14 to 0.32 eV).

  • Zero-shot MLIP barriers have mean errors of 0.05 to 0.5 eV and tend to be too low (softening): good for ranking and paths, not for rates.

  • Use MLIP-NEB to screen and to start DFT; refine the saddle with DFT, especially where magnetism or polarons matter.

References

  1. A. K. Padhi et al., J. Electrochem. Soc. 144, 1188 (1997). doi:10.1149/1.1837571

  2. D. Morgan, A. Van der Ven and G. Ceder, Electrochem. Solid-State Lett. 7, A30 (2004). doi:10.1149/1.1633511

  3. G. Henkelman, B. P. Uberuaga and H. Jónsson, J. Chem. Phys. 113, 9901 (2000). doi:10.1063/1.1329672

  4. A. Hjorth Larsen et al., ASE, J. Phys.: Condens. Matter 29, 273002 (2017). doi:10.1088/1361-648X/aa680e

  5. V. A. Streltsov et al., Acta Cryst. B 49, 147 (1993). doi:10.1107/S0108768192004701

  6. B. Deng et al., CHGNet and MPtrj, Nat. Mach. Intell. 5, 1031 (2023). doi:10.1038/s42256-023-00716-3

  7. L. Barroso-Luque et al., OMat24. arXiv:2410.12771

  8. A. D. Kaplan et al., MatPES. arXiv:2503.04070

  9. I. Batatia et al., MACE-MP-0. arXiv:2401.00096

  10. B. Rhodes et al., Orb-v3. arXiv:2504.06231

  11. G. Simeon and G. De Fabritiis, TensorNet. arXiv:2306.06482

  12. E. Bitzek et al., FIRE, Phys. Rev. Lett. 97, 170201 (2006). doi:10.1103/PhysRevLett.97.170201

  13. S. Smidstrup et al., IDPP, J. Chem. Phys. 140, 214106 (2014). doi:10.1063/1.4878664

  14. M. S. Islam et al., Chem. Mater. 17, 5085 (2005). doi:10.1021/cm050999v

  15. S. Nishimura et al., Nat. Mater. 7, 707 (2008). doi:10.1038/nmat2251

  16. G. K. P. Dathar et al., Chem. Mater. 23, 4032 (2011). doi:10.1021/cm201604g

  17. B. Deng et al., Systematic softening in universal machine learning interatomic potentials, npj Comput. Mater. 11, 9 (2025). doi:10.1038/s41524-024-01500-6

  18. A. K. Bheemaguli, P. Xiao and G. Sai Gautam, Evaluation of foundational machine learned interatomic potentials for migration barrier predictions, Digital Discovery 5, 1809 (2026). doi:10.1039/D5DD00534E; arXiv:2512.03642; code and data, Zenodo

  19. B. Wander et al., CatTSunami, ACS Catal. 15, 5283 (2025). doi:10.1021/acscatal.4c04272; arXiv:2405.02078

  20. K. Amirian et al., FPBench. arXiv:2609.05714; code

  21. J. Marks, J. Vandezande and J. Gomes, automated transition-state searches with MLIPs. arXiv:2604.00405

  22. K. Hoang and M. Johannes, Chem. Mater. 23, 3003 (2011). doi:10.1021/cm200725j; arXiv:1105.3492

  23. O. K. Forslund et al., single-crystal muon spin relaxation of LiFePO4. arXiv:2111.11941

  24. J. Li, W. Yao, S. Martin and D. Vaknin, Solid State Ionics 179, 2016 (2008). doi:10.1016/j.ssi.2008.06.028

  25. Y. Yu and Y. Wang, uncertainty-aware NEB and dimer methods. arXiv:2605.24401