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 |
|---|---|---|---|---|
|
OMat24 [2] |
PBE and PBE+U (VASP) |
|
inorganic crystals |
|
OMol25 [3] |
ωB97M-V/def2-TZVPD (ORCA) |
|
molecules, with charge and spin |
|
OC20 |
RPBE |
none |
catalysis, adsorbates on surfaces |
|
ODAC |
PBE-D3 |
none |
MOFs with guest molecules |
|
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 throughnvalchemiops) is therefore not used here.fairchem-core2.21.0 is the newest release withuma-s-1p2that 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 |
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 |
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¶
B. M. Wood et al., UMA: a family of universal models for atoms. arXiv:2506.23971
L. Barroso-Luque et al., OMat24. arXiv:2410.12771
D. S. Levine et al., OMol25. arXiv:2505.08762
CODATA 2022, lattice parameter of silicon, NIST. physics.nist.gov
K. Lejaeghere et al., Reproducibility in density functional theory calculations of solids, Science 351, aad3000 (2016). doi:10.1126/science.aad3000
M. E. Straumanis and L. S. Yu, Acta Cryst. A 25, 676 (1969). doi:10.1107/S0567739469001549
NIST Chemistry WebBook, O2, constants of diatomic molecules. webbook.nist.gov
NIST Chemistry WebBook, H2O, gas-phase ion energetics. webbook.nist.gov
B. Rhodes et al., Orb-v3. arXiv:2504.06231
Orbital Materials, OrbMol: extending Orb to molecular systems (2025). orbitalindustries.com
NIST Chemistry WebBook, O2, gas-phase ion energetics. webbook.nist.gov