ASE interface

UPET ships an ASE-compatible calculator, UPETCalculator, that wraps any UPET model and exposes the standard ASE get_potential_energy / get_forces / get_stress API. This page covers the full surface of ASE-driven workflows: basic evaluation, loading from a local checkpoint, non-conservative forces and stresses, uncertainty quantification, rotational averaging, empirical dispersion corrections, electronic DOS prediction and dataset visualization.

See also

For runnable end-to-end ASE workflows — single-point evaluation, geometry optimization and MD in the NVE / NVT / NPT ensembles — see the example gallery.

Basic usage

To perform a single-structure evaluation, build an ASE Atoms object and attach a UPETCalculator. The model name is formed by combining the model family and the size, e.g. pet-mad-s, pet-omat-l. See Available models for the full list.

from upet.calculator import UPETCalculator
from ase.build import bulk

atoms = bulk("Si", cubic=True, a=5.43, crystalstructure="diamond")
calculator = UPETCalculator(model="pet-mad-s", version="1.5.0", device="cpu")
atoms.calc = calculator

energy = atoms.get_potential_energy()
forces = atoms.get_forces()

These ASE methods are ideal for single-structure evaluations, but inefficient when you need to run a pre-defined dataset through the model. For efficient batched evaluation, see Batched evaluation with metatrain and the README_BATCHED.md reference.

If the version argument is not specified, the latest available version of the model is downloaded and used by default:

from upet.calculator import UPETCalculator

# uses the latest version of the PET-MAD-S model by default
calculator = UPETCalculator(model="pet-mad-s", device="cpu")

You can list the available versions for any model with upet.list_upet():

from upet import list_upet

list_upet(model="pet-mad", size="s")  # PET-MAD of size S
list_upet(model="pet-mad")             # all PET-MAD sizes
list_upet()                            # all available UPET models

Loading from a local checkpoint

You can download a checkpoint from the HuggingFace repository directly:

wget https://huggingface.co/lab-cosmo/upet/resolve/main/models/pet-mad-s-v1.5.0.ckpt

and then load the calculator from that local file (useful for fine-tuned checkpoints or reproducibility):

from upet.calculator import UPETCalculator

calculator = UPETCalculator(checkpoint_path="pet-mad-s-v1.5.0.ckpt", device="cpu")

Non-conservative (direct) forces and stresses

By default, UPET models compute conservative forces and stresses as derivatives of the energy via automatic differentiation. UPET models also support the direct prediction of forces and stresses as separate targets, which typically gives a 2–3x speedup. However, as discussed in this preprint, non-conservative forces and stresses require additional care to avoid undesired effects during molecular dynamics.

To use non-conservative forces and stresses, set non_conservative=True when constructing the calculator:

from upet.calculator import UPETCalculator
from ase.build import bulk

atoms = bulk("Si", cubic=True, a=5.43, crystalstructure="diamond")
calculator = UPETCalculator(
    model="pet-mad-s", version="1.5.0", device="cpu", non_conservative=True
)
atoms.calc = calculator

energy = atoms.get_potential_energy()   # computed as usual
forces = atoms.get_forces()             # predicted as a separate target
stresses = atoms.get_stress()           # predicted as a separate target

More details on making direct-force MD simulations reliable are in the Atomistic Cookbook.

Uncertainty quantification

UPET models can also estimate the uncertainty of the energy prediction. This is important when probing the model on data that may be substantially different from its training distribution, or when propagating uncertainty to derived observables (phase transition temperatures, diffusion coefficients, and so on).

Note

Uncertainty quantification is only available for a subset of checkpoints (currently pet-mad-s v1.0.2, pet-mad-xs v1.5.0 and pet-mad-s v1.5.0). See Available models for the full list.

Use the get_energy_uncertainty and get_energy_ensemble methods of UPETCalculator:

from upet.calculator import UPETCalculator
from ase.build import bulk

atoms = bulk("Si", cubic=True, a=5.43, crystalstructure="diamond")
calculator = UPETCalculator(model="pet-mad-s", version="1.5.0", device="cpu")
atoms.calc = calculator

energy = atoms.get_potential_energy()
energy_uncertainty = calculator.get_energy_uncertainty(atoms, per_atom=False)
energy_ensemble = calculator.get_energy_ensemble(atoms, per_atom=False)

Both methods accept a per_atom flag, which controls whether the quantity is reported per atom or for the whole system. For the methodology, see the LLPR and shallow ensembles papers.

Rotational averaging

By design, UPET models are not exactly equivariant with respect to rotations and inversions. The equivariance error is typically much smaller than the overall model error, but in some cases (geometry optimization or phonon calculations of highly symmetric structures) it can be beneficial to enforce additional rotational averaging.

Pass a rotational_average_order to the calculator:

from upet.calculator import UPETCalculator
from ase.build import bulk

atoms = bulk("Si", cubic=True, a=5.43, crystalstructure="diamond")
calculator = UPETCalculator(
    model="pet-mad-s",
    version="1.5.0",
    device="cpu",
    rotational_average_order=3,
)
atoms.calc = calculator
energy = atoms.get_potential_energy()
forces = atoms.get_forces()
stresses = atoms.get_stress()

Predictions are averaged over a Lebedev quadrature of the O(3) group at the requested order (here 3). Higher orders are more accurate but costlier.

By default, all transformed structures are evaluated in a single batch, which can be memory-intensive for large systems. Reduce memory pressure by setting rotational_average_batch_size to a smaller value:

from upet.calculator import UPETCalculator

calculator = UPETCalculator(
    model="pet-mad-s",
    version="1.5.0",
    device="cpu",
    rotational_average_order=3,
    rotational_average_batch_size=8,
)

After the evaluation, rotational averaging error statistics are available on the calculator’s results dictionary:

energy_rot_std = calculator.results["energy_rot_std"]
forces_rot_std = calculator.results["forces_rot_std"]
stresses_rot_std = calculator.results["stresses_rot_std"]

Empirical dispersion corrections (D3)

You can combine the UPET calculator with the torch-based D3 dispersion correction of pfnet-research, torch-dftd. Install it inside your UPET environment:

pip install torch-dftd

Then combine both calculators with ase.calculators.mixing.SumCalculator:

import torch
from ase.calculators.mixing import SumCalculator
from torch_dftd.torch_dftd3_calculator import TorchDFTD3Calculator
from upet.calculator import UPETCalculator

device = "cuda" if torch.cuda.is_available() else "cpu"

calc_upet = UPETCalculator(model="pet-mad-s", version="1.5.0", device=device)
dft_d3 = TorchDFTD3Calculator(device=device, xc="r2scan", damping="bj")

combined_calc = SumCalculator([calc_upet, dft_d3])
atoms.calc = combined_calc

DOS, Fermi levels and bandgaps (PET-MAD-DOS)

The UPET package also exposes the PET-MAD-DOS model for predicting electronic density of states of materials, their Fermi levels and bandgaps, via PETMADDOSCalculator:

from ase.build import bulk
from upet.calculator import PETMADDOSCalculator

atoms = bulk("Si", cubic=True, a=5.43, crystalstructure="diamond")
pet_mad_dos_calculator = PETMADDOSCalculator(version="latest", device="cpu")

energies, dos = pet_mad_dos_calculator.calculate_dos(atoms)

Per-atom DOS and batched evaluation on a list of structures are also supported:

# per-atom DOS
energies, dos_per_atom = pet_mad_dos_calculator.calculate_dos(atoms, per_atom=True)

# multiple structures
atoms_1 = bulk("Si", cubic=True, a=5.43, crystalstructure="diamond")
atoms_2 = bulk("C", cubic=True, a=3.55, crystalstructure="diamond")
energies, dos = pet_mad_dos_calculator.calculate_dos(
    [atoms_1, atoms_2], per_atom=False
)

Bandgap and Fermi level predictions are available via dedicated methods:

bandgap = pet_mad_dos_calculator.calculate_bandgap(atoms)
fermi_level = pet_mad_dos_calculator.calculate_efermi(atoms)

You can also re-use a previously computed DOS:

bandgap = pet_mad_dos_calculator.calculate_bandgap(atoms, dos=dos)
fermi_level = pet_mad_dos_calculator.calculate_efermi(atoms, dos=dos)

The same is supported for a list of Atoms:

bandgaps = pet_mad_dos_calculator.calculate_bandgap([atoms_1, atoms_2], dos=dos)
fermi_levels = pet_mad_dos_calculator.calculate_efermi([atoms_1, atoms_2], dos=dos)

Dataset visualization with the PET-MAD featurizer

You can use the last-layer features of the PET-MAD model, together with a pre-trained sketch-map dimensionality reduction, to obtain 2D and 3D representations of a dataset (e.g. to identify structural or chemical motifs). It can be used as a stand-alone feature builder or combined with the chemiscope viewer for interactive visualization.

import ase.io
import chemiscope
from upet.explore import PETMADFeaturizer

featurizer = PETMADFeaturizer(version="latest")

# Load structures
frames = ase.io.read("dataset.xyz", ":")

# Compute features only
features = featurizer(frames, None)

# Or create an interactive Jupyter visualization
chemiscope.explore(frames, featurize=featurizer)