A6 One model for materials and molecules: Orb and UMA

Objectives

  • Run the same checks on crystals and on molecules with Orb-v3 and OrbMol, and with UMA 1.2 small.

  • Pass total charge and spin multiplicity to a molecular calculation.

  • Compare lattice constants, a spin gap and ionisation energies with their reference data, and measure the cost on one MI250X GCD.

Most universal MLIPs learn one potential energy surface at one level of theory. Two open routes cover both crystals and molecules:

  • Orb [9] (code): one architecture, separate checkpoints. Orb-v3 trained on OMat24 for crystals; OrbMol [10] trained on OMol25 for molecules, with total charge and spin as inputs. Weights are public.

  • UMA [1] (code): one checkpoint trained on five datasets; a task name selects the level of theory it reproduces. Weights are gated (licence approval on Hugging Face).

Task

Dataset

Reference level

Orb checkpoint

Use

omat

OMat24 [2]

PBE and PBE+U (VASP)

orb-v3-conservative-inf-omat

inorganic crystals

omol

OMol25 [3]

ωB97M-V/def2-TZVPD (ORCA)

orb-v3-conservative-omol (OrbMol)

molecules, with charge and spin

oc20

OC20

RPBE

none

catalysis, adsorbates on surfaces

odac

ODAC

PBE-D3

none

MOFs with guest molecules

omc

OMC

PBE-D3

none

molecular crystals

OrbMol and the UMA omol task read the total charge and spin multiplicity from atoms.info["charge"] and atoms.info["spin"], so the same geometry can be evaluated as a neutral singlet, a cation or a triplet. Both use the multiplicity 2S+1 (the orb-models 0.5.5 README example still shows 0; the conditioner treats 0 as unset). The crystal models ignore both keys.

Set-up on LUMI

The CSC module provides Python 3.11 and torch 2.7.1+rocm6.2.4.

  • orb-models 0.5.5 is the last release for Python 3.11 and torch 2.7; it has Orb-v3 and the first OrbMol. Releases from 0.6 need Python 3.12 and torch 2.8 or later. OrbMol-v2 (pretrained.orbmol_v2, orb-models 0.7, long-range electrostatics through nvalchemiops) is therefore not used here.

  • fairchem-core 2.21.0 is the newest release with uma-s-1p2 that still pins torch 2.8; later releases pin torch 2.13. It is installed without its dependencies, and the rest from a pinned list that keeps the module’s torch.

On a login node, in a copy of this repository at <SCRATCH>/mlip-uma:

module purge && module use /appl/local/csc/modulefiles/ && module load pytorch
python -m venv --system-site-packages .venv && source .venv/bin/activate
pip install --no-deps fairchem-core==2.21.0
pip install -r examples/uma/requirements.txt
export HF_HOME=$PWD/hf-home FAIRCHEM_CACHE_DIR=$PWD/fairchem-cache CACHED_PATH_CACHE_ROOT=$PWD/cached-path
python examples/uma/cli.py prefetch --model orb
sbatch --account=<PROJECT> scripts/lumi-uma.sbatch orb
requirements.txt
# Installed on top of the LUMI CSC PyTorch module (ROCm, Python 3.11). fairchem-core 2.21.0 pins
# torch~=2.8, so install it with --no-deps first, then these, which keep the module's torch.
# orb-models 0.5.5 is the last release for Python 3.11 and torch 2.7 (0.6 and later need 3.12 and torch 2.8).
torch==2.7.1+rocm6.2.4
ase==3.29.0
ase-db-backends==0.11.0
backoff==2.2.1
cached-path==1.8.10
clusterscope==0.0.18
dm-tree==0.1.8
e3nn==0.6.0
huggingface-hub==1.6.0
hydra-core==1.3.7
lmdb==1.6.2
monty==2026.7.16
numba==0.67.0
numpy==2.2.6
orb-models==0.5.5
orjson==3.12.0
scipy==1.17.1
submitit==1.5.4
torchtnt==0.2.4
wandb==0.30.0

UMA access

UMA is gated. Accept the licence on the model page, create a read token in your Hugging Face settings, log in once on the login node, then prefetch and submit with uma:

hf auth login   # stores the token in ~/.cache/huggingface/token
export HF_TOKEN_PATH=$HOME/.cache/huggingface/token
python examples/uma/cli.py prefetch --model uma
sbatch --account=<PROJECT> scripts/lumi-uma.sbatch uma

Code

Each backend exposes calculator(task) for omat or omol. Orb loads one checkpoint per task; UMA loads one predict unit and puts a light ASE calculator on top of it for each task. The tasks do not know which backend they run.

models.py
 1"""Two backends with one interface: `calculator(task)` for task "omat" (crystals) or "omol" (molecules)."""
 2
 3from __future__ import annotations
 4
 5import time
 6
 7import torch
 8
 9
10def device() -> str:
11    return "cuda" if torch.cuda.is_available() else "cpu"
12
13
14class Backend:
15    """Loads checkpoints on first use and records the load time of each."""
16
17    checkpoints: dict[str, str] = {}
18
19    def __init__(self, dtype: str = "float64"):
20        self.dtype = dtype
21        self.load_s: dict[str, float] = {}
22        self._calcs = {}
23
24    def checkpoint(self, task: str) -> str:
25        return self.checkpoints[task]
26
27    def calculator(self, task: str):
28        if task not in self._calcs:
29            start = time.perf_counter()
30            self._calcs[task] = self._load(task)
31            self.load_s.setdefault(self.checkpoint(task), time.perf_counter() - start)
32        return self._calcs[task]
33
34    def _load(self, task: str):
35        raise NotImplementedError
36
37
38class Orb(Backend):
39    """Orb-v3 conservative (OMat24) for crystals, OrbMol (OMol25, charge and spin) for molecules."""
40
41    name = "orb"
42    checkpoints = {"omat": "orb-v3-conservative-inf-omat", "omol": "orb-v3-conservative-omol"}
43
44    def _load(self, task: str):
45        from orb_models.forcefield import pretrained
46        from orb_models.forcefield.calculator import ORBCalculator
47
48        precision = "float64" if self.dtype == "float64" else "float32-highest"
49        loader = getattr(pretrained, self.checkpoint(task).replace("-", "_"))
50        model = loader(device=device(), precision=precision, compile=False)
51        return ORBCalculator(model, device=device())
52
53
54class UMA(Backend):
55    """UMA 1.2 small: one predict unit shared by all tasks."""
56
57    name = "uma"
58    checkpoints = {"omat": "uma-s-1p2", "omol": "uma-s-1p2"}
59
60    def _load(self, task: str):
61        from fairchem.core import FAIRChemCalculator, pretrained_mlip
62        from fairchem.core.units.mlip_unit.api.inference import inference_settings_default
63
64        if not hasattr(self, "unit"):
65            settings = inference_settings_default()
66            settings.base_precision_dtype = getattr(torch, self.dtype)
67            self.unit = pretrained_mlip.get_predict_unit(self.checkpoint(task), inference_settings=settings,
68                                                         device=device())
69        return FAIRChemCalculator(self.unit, task_name=task)
70
71
72BACKENDS = {"orb": Orb, "uma": UMA}
73PACKAGES = {"orb": ("orb-models",), "uma": ("fairchem-core", "e3nn")}
74
75
76def versions(name: str) -> dict:
77    from importlib.metadata import version
78
79    out = {p: version(p) for p in ("torch", "ase", "numpy", *PACKAGES[name])}
80    out["checkpoints"] = sorted(set(BACKENDS[name].checkpoints.values()))
81    out["gpu"] = torch.cuda.get_device_name(0) if torch.cuda.is_available() else None
82    return out
structures.py
 1"""Test systems and their reference values (sources on the lesson page)."""
 2
 3from __future__ import annotations
 4
 5from ase import Atoms
 6from ase.build import bulk, molecule
 7
 8CRYSTALS = {
 9    "Si": dict(structure="diamond", a=5.43, experiment=5.431, pbe=5.470),
10    "Cu": dict(structure="fcc", a=3.61, experiment=3.615, pbe=3.629),
11}
12
13O2_GAP_EV = 0.982
14O2_IE_EV = 12.07
15WATER_IE_EV = 12.62
16
17
18def crystal(name: str) -> Atoms:
19    spec = CRYSTALS[name]
20    return bulk(name, spec["structure"], a=spec["a"], cubic=True)
21
22
23def with_state(atoms: Atoms, charge: int, spin: int) -> Atoms:
24    """OrbMol and the UMA omol task read total charge and spin multiplicity from atoms.info."""
25    atoms = atoms.copy()
26    atoms.info.update(charge=charge, spin=spin)
27    return atoms
28
29
30def oxygen(spin: int, charge: int = 0) -> Atoms:
31    return with_state(molecule("O2"), charge=charge, spin=spin)
32
33
34def water(charge: int = 0, spin: int = 1) -> Atoms:
35    return with_state(molecule("H2O"), charge=charge, spin=spin)
tasks.py
  1"""The three checks, for any backend: lattice constants (omat), charge and spin (omol), cost per force call."""
  2
  3from __future__ import annotations
  4
  5import csv
  6import json
  7import time
  8from pathlib import Path
  9
 10import numpy as np
 11from ase.filters import FrechetCellFilter
 12from ase.optimize import BFGS
 13
 14import structures
 15
 16RESULTS = Path(__file__).resolve().parent / "results"
 17FMAX = 0.005
 18
 19
 20def outdir(model) -> Path:
 21    path = RESULTS / model.name
 22    path.mkdir(parents=True, exist_ok=True)
 23    return path
 24
 25
 26def save_csv(model, name: str, rows: list[dict]) -> None:
 27    with open(outdir(model) / name, "w", newline="") as f:
 28        writer = csv.DictWriter(f, fieldnames=list(rows[0]))
 29        writer.writeheader()
 30        writer.writerows(rows)
 31
 32
 33def save_json(model, name: str, data: dict) -> None:
 34    (outdir(model) / name).write_text(json.dumps(data, indent=2) + "\n")
 35
 36
 37def attach(atoms, calc) -> None:
 38    """ASE compares positions and cell but not atoms.info, so drop cached results before a new charge or spin."""
 39    calc.reset()
 40    atoms.calc = calc
 41
 42
 43def relax(atoms, calc, cell: bool = False):
 44    attach(atoms, calc)
 45    target = FrechetCellFilter(atoms) if cell else atoms
 46    BFGS(target, logfile=None).run(fmax=FMAX, steps=500)
 47    return atoms
 48
 49
 50def lattice(model) -> list[dict]:
 51    rows = []
 52    for name, spec in structures.CRYSTALS.items():
 53        atoms = relax(structures.crystal(name), model.calculator("omat"), cell=True)
 54        a = float(np.cbrt(atoms.get_volume()))
 55        rows.append(dict(crystal=name, model=model.checkpoint("omat"), a_A=round(a, 4), pbe_A=spec["pbe"], experiment_A=spec["experiment"],
 56                         vs_pbe_pct=round(100 * (a / spec["pbe"] - 1), 2),
 57                         vs_experiment_pct=round(100 * (a / spec["experiment"] - 1), 2)))
 58    save_csv(model, "lattice.csv", rows)
 59    return rows
 60
 61
 62def state(atoms) -> dict:
 63    return dict(energy_eV=atoms.get_potential_energy(), bond_A=round(atoms.get_distance(0, 1), 4))
 64
 65
 66def oxygen(model) -> dict:
 67    """Triplet ground state, closed-shell singlet, and the doublet cation at the triplet geometry."""
 68    calc = model.calculator("omol")
 69    triplet = relax(structures.oxygen(spin=3), calc)
 70    states = dict(triplet=state(triplet), singlet=state(relax(structures.oxygen(spin=1), calc)))
 71    cation = structures.oxygen(spin=2, charge=1)
 72    cation.positions = triplet.positions
 73    ie = single_point(cation, calc) - states["triplet"]["energy_eV"]
 74    gap = states["singlet"]["energy_eV"] - states["triplet"]["energy_eV"]
 75    return dict(states=states, gap_eV=round(gap, 3), experiment_eV=structures.O2_GAP_EV,
 76                ie_vertical_eV=round(ie, 3), ie_experiment_eV=structures.O2_IE_EV)
 77
 78
 79def single_point(atoms, calc) -> float:
 80    attach(atoms, calc)
 81    return atoms.get_potential_energy()
 82
 83
 84def geometry(atoms) -> dict:
 85    return dict(oh_A=round(atoms.get_distance(0, 1), 4), hoh_deg=round(atoms.get_angle(1, 0, 2), 1))
 86
 87
 88def water(model) -> dict:
 89    calc = model.calculator("omol")
 90    neutral = relax(structures.water(), calc)
 91    e_neutral = neutral.get_potential_energy()
 92    cation = structures.water(charge=1, spin=2)
 93    cation.positions = neutral.positions
 94    vertical = single_point(cation, calc) - e_neutral
 95    adiabatic = relax(cation, calc).get_potential_energy() - e_neutral
 96    return dict(vertical_eV=round(vertical, 3), adiabatic_eV=round(adiabatic, 3), experiment_eV=structures.WATER_IE_EV,
 97                neutral=geometry(neutral), cation=geometry(cation))
 98
 99
100def molecules(model) -> dict:
101    out = dict(model=model.checkpoint("omol"), o2=oxygen(model), water=water(model))
102    save_json(model, "molecules.json", out)
103    return out
104
105
106def seconds_per_call(atoms, calc, calls: int = 20) -> tuple[float, float]:
107    """First call (includes warm-up), then the median of later calls on rattled copies."""
108    times = []
109    for i in range(calls + 1):
110        trial = atoms.copy()
111        trial.rattle(0.01, seed=i)
112        trial.calc = calc
113        start = time.perf_counter()
114        trial.get_forces()
115        times.append(time.perf_counter() - start)
116    return times[0], float(np.median(times[1:]))
117
118
119def timing(model) -> list[dict]:
120    systems = {
121        "Si 216 atoms (omat)": (structures.crystal("Si").repeat(3), "omat"),
122        "Cu 256 atoms (omat)": (structures.crystal("Cu").repeat(4), "omat"),
123        "H2O 3 atoms (omol)": (structures.water(), "omol"),
124    }
125    rows = []
126    for label, (atoms, task) in systems.items():
127        row = dict(system=label, model=model.checkpoint(task), dtype=model.dtype, load_s="", first_call_s="",
128                   per_call_s="")
129        try:
130            first, median = seconds_per_call(atoms, model.calculator(task))
131            row.update(load_s=round(model.load_s[model.checkpoint(task)], 1), first_call_s=round(first, 3),
132                       per_call_s=round(median, 4), status="ok")
133        except RuntimeError as err:
134            row["status"] = str(err).splitlines()[0][:80]
135        rows.append(row)
136    save_csv(model, f"timing_{model.dtype}.csv", rows)
137    return rows
cli.py
17def main() -> None:
18    parser = argparse.ArgumentParser(description=__doc__)
19    parser.add_argument("step", choices=STEPS)
20    parser.add_argument("--model", choices=list(models.BACKENDS), default="orb")
21    parser.add_argument("--dtype", choices=["float32", "float64"], default="float64")
22    args = parser.parse_args()
23    model = models.BACKENDS[args.model](args.dtype)
24
25    if args.step == "prefetch":
26        for task in ("omat", "omol"):
27            model.calculator(task)
28        print("cached", model.load_s)
29    elif args.step == "versions":
30        text = json.dumps(models.versions(args.model), indent=2)
31        (tasks.outdir(model) / "versions.json").write_text(text + "\n")
32        print(text)
33    elif args.step == "run":
34        for row in tasks.lattice(model):
35            print(f"{row['crystal']}: a = {row['a_A']} Å (PBE {row['pbe_A']}, experiment {row['experiment_A']})")
36        mol = tasks.molecules(model)
37        print(f"O2 singlet-triplet gap {mol['o2']['gap_eV']} eV, water IE vertical "
38              f"{mol['water']['vertical_eV']} eV, adiabatic {mol['water']['adiabatic_eV']} eV")
39        print("load", {k: round(v, 1) for k, v in model.load_s.items()})
40    else:
41        for row in tasks.timing(model):
42            print(row)
43
44
45if __name__ == "__main__":
46    main()
lumi-uma.sbatch
31export PYTHONNOUSERSITE=1
32export HF_HOME="$root/hf-home" HF_HUB_OFFLINE=1 FAIRCHEM_CACHE_DIR="$root/fairchem-cache"
33export CACHED_PATH_CACHE_ROOT="$root/cached-path"
34export MIOPEN_USER_DB_PATH="/tmp/$USER-miopen-$SLURM_JOB_ID"
35export MIOPEN_CUSTOM_CACHE_DIR="$MIOPEN_USER_DB_PATH"
36export HIP_FORCE_DEV_KERNARG=1
37export OMP_NUM_THREADS="$SLURM_CPUS_PER_TASK"
38mkdir -p "$MIOPEN_USER_DB_PATH"
39trap 'rm -rf "$MIOPEN_USER_DB_PATH"' EXIT
40
41cd "$root/examples/uma"
42run() { srun --ntasks=1 --gpus=1 --cpu-bind=cores python cli.py "$@" --model "$model"; }
43failed=()
44run versions || failed+=(versions)
45echo "== run $model $(date +%T)"
46run run --dtype float64 || failed+=(run)
47for dtype in float32 float64; do
48    echo "== timing $model $dtype $(date +%T)"
49    run timing --dtype "$dtype" || failed+=("timing-$dtype")
50done
51echo "== done $(date +%T), failed: ${failed[*]:-none}"

What the job computes

  • omat: relax the conventional cells of Si (diamond) and Cu (fcc), positions and cell together, with BFGS to fmax 0.005 eV/Å.

  • omol, spin: relax O2 as a triplet (spin=3, the ground state) and as a singlet (spin=1); the gap is the difference of the two minima. As a control for the charge input, evaluate the O2+ doublet (charge=1, spin=2) at the triplet geometry.

  • omol, charge: relax neutral water (charge=0, spin=1), then evaluate the cation (charge=1, spin=2) at that geometry for the vertical ionisation energy, and relax it for the adiabatic one.

  • Cost: model load time, the first force call and the median of 20 later calls, in float32 and float64, one process per precision.

ASE decides whether to recompute from positions, cell and atomic numbers, not from atoms.info. A new charge or spin on the same geometry would return the cached energy, so tasks.attach resets the calculator first.

Stress on a periodic cell uses the cell determinant (torch.det), which fails in float32 on LUMI’s torch 2.7.1+rocm6.2.4 (see A3 MatGL: relaxation, MD, lattice benchmark and training), so the science runs in float64. The float32 timing run records which calls fail rather than stopping.

Reference values

Quantity

Reference

Value

Si lattice constant

experiment, 22.5 °C (CODATA 2022) [4]

5.431 Å

Si lattice constant

PBE, all-electron, static lattice [5]

5.470 Å

Cu lattice constant

experiment, 25 °C [6]

3.615 Å

Cu lattice constant

PBE, all-electron, static lattice [5]

3.629 Å

O2 a¹Δg minus X³Σg−, Te

experiment [7]

7918.1 cm−1 = 0.982 eV

O2 first ionisation energy

experiment, evaluated, adiabatic [11]

12.07 eV

H2O first ionisation energy

experiment, evaluated [8]

12.621 eV (photoelectron vertical value 12.62 eV)

The PBE lattice constants come from the equilibrium volumes of the WIEN2k reference set (data): a = (8V0)1/3 for Si and (4V0)1/3 for Cu. The crystal models reproduce PBE, so compare them first with PBE; the difference from experiment is mostly the functional, plus thermal expansion and zero-point motion, which a static relaxation leaves out.

The singlet needs care. The a¹Δg state of O2 is not a single closed-shell determinant, and OMol25 ran main-group singlets in restricted Kohn-Sham unless bonds were expected to break [3]. The OMol25 singlet is therefore a closed-shell ωB97M-V state, not the experimental a¹Δg, and the reference method itself overestimates the 0.982 eV gap; a model trained on it should too.

Results

One MI250X GCD on dev-g, torch 2.7.1+rocm6.2.4, float64; orb-models 0.5.5 and fairchem-core 2.21.0. Each job (both checks and both timing runs) took about 4 min: 3 min 39 s for Orb, 3 min 58 s for UMA.

results (CSV and JSON)
crystal,model,a_A,pbe_A,experiment_A,vs_pbe_pct,vs_experiment_pct
Si,orb-v3-conservative-inf-omat,5.4603,5.47,5.431,-0.18,0.54
Cu,orb-v3-conservative-inf-omat,3.6134,3.629,3.615,-0.43,-0.04
crystal,model,a_A,pbe_A,experiment_A,vs_pbe_pct,vs_experiment_pct
Si,uma-s-1p2,5.4607,5.47,5.431,-0.17,0.55
Cu,uma-s-1p2,3.6154,3.629,3.615,-0.37,0.01
{
  "model": "orb-v3-conservative-omol",
  "o2": {
    "states": {
      "triplet": {
        "energy_eV": -4091.321526546901,
        "bond_A": 1.1877
      },
      "singlet": {
        "energy_eV": -4089.777784485433,
        "bond_A": 1.1962
      }
    },
    "gap_eV": 1.544,
    "experiment_eV": 0.982,
    "ie_vertical_eV": 12.661,
    "ie_experiment_eV": 12.07
  },
  "water": {
    "vertical_eV": 6.068,
    "adiabatic_eV": 6.064,
    "experiment_eV": 12.62,
    "neutral": {
      "oh_A": 0.9617,
      "hoh_deg": 105.0
    },
    "cation": {
      "oh_A": 0.9689,
      "hoh_deg": 106.3
    }
  }
}
{
  "model": "uma-s-1p2",
  "o2": {
    "states": {
      "triplet": {
        "energy_eV": -4091.282627495787,
        "bond_A": 1.1971
      },
      "singlet": {
        "energy_eV": -4089.779104671426,
        "bond_A": 1.1964
      }
    },
    "gap_eV": 1.504,
    "experiment_eV": 0.982,
    "ie_vertical_eV": 12.708,
    "ie_experiment_eV": 12.07
  },
  "water": {
    "vertical_eV": 12.636,
    "adiabatic_eV": 12.552,
    "experiment_eV": 12.62,
    "neutral": {
      "oh_A": 0.9617,
      "hoh_deg": 105.0
    },
    "cation": {
      "oh_A": 0.9987,
      "hoh_deg": 106.4
    }
  }
}
system,model,dtype,load_s,first_call_s,per_call_s,status
Si 216 atoms (omat),orb-v3-conservative-inf-omat,float64,8.8,1.182,0.0583,ok
Cu 256 atoms (omat),orb-v3-conservative-inf-omat,float64,8.8,0.101,0.0973,ok
H2O 3 atoms (omol),orb-v3-conservative-omol,float64,1.1,0.027,0.022,ok
system,model,dtype,load_s,first_call_s,per_call_s,status
Si 216 atoms (omat),uma-s-1p2,float64,22.4,1.822,0.1367,ok
Cu 256 atoms (omat),uma-s-1p2,float64,22.4,0.249,0.248,ok
H2O 3 atoms (omol),uma-s-1p2,float64,22.4,0.078,0.0611,ok

Crystals

Crystal

Orb-v3 (Å)

UMA omat (Å)

PBE (Å)

Experiment (Å)

Si

5.4603 (−0.18 % vs PBE)

5.4607 (−0.17 %)

5.470

5.431

Cu

3.6134 (−0.43 %)

3.6154 (−0.37 %)

3.629

3.615

Both models land within 0.5 % of PBE, slightly below it, and within 0.002 Å of each other. Cu then matches experiment, but only because the model error and the PBE overestimate cancel; Si keeps most of the PBE error (+0.5 % against experiment).

Molecules

Quantity

OrbMol

UMA omol

Reference

O2 singlet minus triplet (eV)

1.544

1.504

0.982 (experiment, a¹Δg)

O2 bond, triplet / singlet (Å)

1.188 / 1.196

1.197 / 1.196

O2 ionisation energy, vertical (eV)

12.66

12.71

12.07 (experiment, adiabatic)

H2O ionisation energy, vertical (eV)

6.07

12.64

12.62 (experiment)

H2O ionisation energy, adiabatic (eV)

6.06

12.55

12.621 (experiment)

  • O2 spin: both models put the singlet 1.5 eV above the triplet, 0.5 eV more than experiment, as expected from a closed-shell singlet reference. The two agree to 0.04 eV, which points at the reference method rather than the models.

  • O2 charge: both vertical ionisation energies are about 0.6 eV above the adiabatic experimental value, a plausible size for a vertical value.

  • Water: UMA is within 0.02 eV of the experimental vertical value, and its cation relaxes (O–H 0.962 to 0.999 Å), lowering the energy by 0.08 eV. OrbMol gives half the ionisation energy and a cation that barely moves. The charge input reaches OrbMol (O2 is right), so this checkpoint does not describe the water cation. OrbMol-v2 adds explicit electrostatics aimed at charge states like this but needs a newer software stack than the CSC module.

A model that takes charge and spin as inputs will return a number for any combination. Check each charge and spin state against a known reference before using it.

Cost on one MI250X GCD

System

Orb load / first / per call (s)

UMA load / first / per call (s)

Si, 216 atoms

8.8 / 1.18 / 0.058

22.4 / 1.82 / 0.137

Cu, 256 atoms

8.8 / 0.10 / 0.097

22.4 / 0.25 / 0.248

H2O, 3 atoms

1.1 / 0.027 / 0.022

22.4 / 0.078 / 0.061

  • Orb loads one checkpoint per task (Orb-v3 9 s, OrbMol 1 s); UMA loads one for all tasks (22 s). These are warm loads; the first load in a fresh job step took about 30 s for Orb-v3 and 68 s for UMA.

  • Per force call, Orb-v3 is 2.3 to 2.6 times faster than UMA on these crystals.

  • The first call includes a one-off GPU warm-up, paid once per process (Si ran first; Cu reuses it).

  • Three atoms cost 20 to 60 ms per call: small molecules are bound by kernel launch and Python overhead, not by arithmetic.

  • In float32 the crystal calls fail with CUDA driver error: 209 (the cell determinant) for both models. OrbMol on the non-periodic molecule runs in float32 at 21 ms per call; for UMA the water call failed too, after the crystal failure in the same process.

Keypoints

  • Orb covers crystals and molecules with two checkpoints; UMA with one checkpoint and a task name. Either way, each model reproduces its own reference level: PBE for crystals, ωB97M-V for molecules.

  • Charge and spin multiplicity are inputs in atoms.info; ASE caching ignores them, so reset the calculator between states.

  • Crystal results agree closely between Orb-v3 and UMA. For molecules, a charge-aware model can still be wrong for one charge state: OrbMol halves the water ionisation energy, UMA reproduces it.

References

  1. B. M. Wood et al., UMA: a family of universal models for atoms. arXiv:2506.23971

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

  3. D. S. Levine et al., OMol25. arXiv:2505.08762

  4. CODATA 2022, lattice parameter of silicon, NIST. physics.nist.gov

  5. K. Lejaeghere et al., Reproducibility in density functional theory calculations of solids, Science 351, aad3000 (2016). doi:10.1126/science.aad3000

  6. M. E. Straumanis and L. S. Yu, Acta Cryst. A 25, 676 (1969). doi:10.1107/S0567739469001549

  7. NIST Chemistry WebBook, O2, constants of diatomic molecules. webbook.nist.gov

  8. NIST Chemistry WebBook, H2O, gas-phase ion energetics. webbook.nist.gov

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

  10. Orbital Materials, OrbMol: extending Orb to molecular systems (2025). orbitalindustries.com

  11. NIST Chemistry WebBook, O2, gas-phase ion energetics. webbook.nist.gov