Skip to content

Commit 3f412d3

Browse files
Fix yaml serialization in phonon wf + add EPFL UPET models (#1400)
* safe load yaml in phonon calcs * add upet models * update mlff docs * change default upet model to smallest omat model, test JSON store explicitly
1 parent 4e94ad4 commit 3f412d3

8 files changed

Lines changed: 83 additions & 7 deletions

File tree

docs/user/codes/forcefields.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Support is provided for the following models, which can be selected using `atoma
3636
| Neuroevolution Potential (NEP) | `NEP` | [10.1103/PhysRevB.104.104309](https://doi.org/10.1103/PhysRevB.104.104309) | Relies on `calorine` package |
3737
| Neural Equivariant Interatomic Potentials (Nequip) | `Nequip` | [10.1038/s41467-022-29939-5](https://doi.org/10.1038/s41467-022-29939-5) | Relies on the `nequip` package |
3838
| SevenNet | `SevenNet` | [10.1021/acs.jctc.4c00190](https://doi.org/10.1021/acs.jctc.4c00190) | Relies on the `sevenn` package |
39+
| Universal Point Edge Transformer (UPET) | `UPET` | [10.1038/s41467-025-65662-7](https://doi.org/10.1038/s41467-025-65662-7) | Relies on the `upet` package. Defaults to the smallest "pet-omad-xs" model. |
3940

4041
## Using custom forcefields by dictionary
4142

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ forcefields = [
6464
"mattersim>=1.0.1",
6565
"sevenn>=0.9.3",
6666
"deepmd-kit>=2.1.4",
67+
"upet>=0.2.0",
6768
]
6869
torchsim = [
6970
"torch-sim-atomistic==0.5.0; python_version >= '3.12'"
@@ -102,6 +103,7 @@ strict-forcefields-generic = [
102103
"tensorflow-cpu==2.20.0",
103104
"mattersim==1.2.1",
104105
"wandb>=0.24.0", # required for mattersim
106+
"upet>=0.2.0",
105107
]
106108
strict-forcefields-torch-limited = [
107109
"matgl==2.0.6",

src/atomate2/common/schemas/phonons.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pydantic import BaseModel, Field
1717
from pymatgen.core import Structure
1818
from pymatgen.io.phonopy import (
19-
get_ph_bs_symm_line,
19+
get_ph_bs_symm_line_from_dict,
2020
get_ph_dos,
2121
get_phonopy_structure,
2222
get_pmg_structure,
@@ -27,6 +27,7 @@
2727
from pymatgen.phonon.plotter import PhononBSPlotter, PhononDosPlotter
2828
from pymatgen.symmetry.bandstructure import HighSymmKpath
2929
from pymatgen.symmetry.kpath import KPathSeek
30+
from ruamel.yaml import YAML
3031
from typing_extensions import Self
3132

3233
from atomate2.aims.utils.units import omegaToTHz
@@ -393,8 +394,10 @@ def from_forces_born(
393394
is_band_connection=kwargs.get("band_structure_eigenvectors", False),
394395
)
395396
phonon.write_yaml_band_structure(filename=filename_band_yaml)
396-
bs_symm_line = get_ph_bs_symm_line(
397-
filename_band_yaml, labels_dict=kpath_dict, has_nac=born is not None
397+
bs_symm_line = get_ph_bs_symm_line_from_dict(
398+
YAML(typ="safe").load(Path(filename_band_yaml).read_text()),
399+
has_nac=born is not None,
400+
labels_dict=kpath_dict,
398401
)
399402
new_plotter = PhononBSPlotter(bs=bs_symm_line)
400403
new_plotter.save_plot(

src/atomate2/forcefields/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class MLFF(Enum): # TODO inherit from StrEnum when 3.11+
4545
Allegro = "Allegro"
4646
FAIRChem = "FAIRChem"
4747
MatterSim = "MatterSim"
48+
UPET = "UPET"
4849

4950
@classmethod
5051
def _missing_(cls, value: Any) -> Any:
@@ -80,6 +81,10 @@ def _missing_(cls, value: Any) -> Any:
8081
"predict_unit": {"model_name": "uma-s-1p1"},
8182
"task_name": "omat",
8283
},
84+
MLFF.UPET: {
85+
"model": "pet-omad-xs",
86+
"version": "1.0.0",
87+
},
8388
}
8489

8590

@@ -377,6 +382,11 @@ def ase_calculator(
377382

378383
calculator = MatterSimCalculator(**kwargs)
379384

385+
case MLFF.UPET:
386+
from upet.calculator import UPETCalculator
387+
388+
calculator = UPETCalculator(**kwargs)
389+
380390
elif isinstance(calculator_meta, dict):
381391
calc_cls = _load_calc_cls(calculator_meta)
382392
calculator = calc_cls(**kwargs)
@@ -444,6 +454,8 @@ def _get_pkg_name(calculator_meta: MLFF | dict[str, Any]) -> str | None:
444454
ff_pkg = "calorine"
445455
case MLFF.SevenNet:
446456
ff_pkg = "sevenn"
457+
case MLFF.UPET:
458+
ff_pkg = "upet"
447459
case _:
448460
ff_pkg = None
449461
return ff_pkg

tests/forcefields/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
try:
2121
_ = ase_calculator(mlff)
2222
_INSTALLED_MLFF[mlff.name] = True
23-
except (ImportError, ValueError):
24-
_INSTALLED_MLFF[mlff.name] = False
23+
except (ImportError, ValueError) as exc:
24+
# Note that UPET calculator raises a ValueError if no kwargs are passed
25+
# Catch that here
26+
_INSTALLED_MLFF[mlff.name] = (
27+
"'model' parameter is required when not using checkpoint_path" in str(exc)
28+
)
2529
except Exception: # noqa: BLE001
2630
# Some calculators, like GAP, require extra potential files
2731
# Generally, thesea re

tests/forcefields/flows/test_phonon.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import os
22
from pathlib import Path
3+
from tempfile import TemporaryDirectory
34

45
import pytest
56
from ase.calculators.calculator import Calculator
6-
from jobflow import Flow, run_locally
7+
from jobflow import Flow, JobStore, run_locally
78
from numpy.testing import assert_allclose
89
from pymatgen.core.structure import Structure
910
from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine
@@ -113,6 +114,26 @@ def test_phonon_wf_force_field(
113114
):
114115
# TODO brittle due to inability to adjust dtypes in CHGNetRelaxMaker
115116

117+
# See issue: https://github.com/materialsproject/atomate2/issues/1395
118+
# Testing JSON store explicitly to avoid regression for this flow
119+
json_dir = TemporaryDirectory()
120+
json_store = JobStore.from_dict_spec(
121+
{
122+
"docs_store": {
123+
"type": "JSONStore",
124+
"paths": str(Path(json_dir.name) / "output.json"),
125+
"read_only": False,
126+
},
127+
"additional_stores": {
128+
"data": {
129+
"type": "JSONStore",
130+
"paths": str(Path(json_dir.name) / "blob_output.json"),
131+
"read_only": False,
132+
}
133+
},
134+
}
135+
)
136+
116137
phonon_kwargs = dict(
117138
use_symmetrized_structure="conventional",
118139
create_thermal_displacements=False,
@@ -146,7 +167,12 @@ def test_phonon_wf_force_field(
146167
flow = phonon_maker.make(si_structure)
147168

148169
# run the flow or job and ensure that it finished running successfully
149-
responses = run_locally(flow, create_folders=True, ensure_success=True)
170+
responses = run_locally(
171+
flow, create_folders=True, ensure_success=True, store=json_store
172+
)
173+
174+
# close temp dir for JSON store
175+
json_dir.cleanup()
150176

151177
# validate the outputs
152178
ph_bs_dos_doc = responses[flow[-1].uuid][1].output

tests/forcefields/test_jobs.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,33 @@ def test_mattersim_relax_maker(si_structure: Structure, test_dir: Path):
785785
assert output.forcefield_version == get_imported_version("mattersim")
786786

787787

788+
@pytest.mark.skipif(not mlff_is_installed("UPET"), reason="upet is not installed.")
789+
def test_upet_relax_maker(si_structure: Structure, test_dir: Path):
790+
791+
# translate one atom to ensure a small number of relaxation steps are taken
792+
si_structure.translate_sites(0, [0, 0, 0.1])
793+
# generate job
794+
job = ForceFieldRelaxMaker(
795+
force_field_name="UPET",
796+
steps=25,
797+
).make(si_structure)
798+
responses = run_locally(job, ensure_success=True)
799+
output = responses[job.uuid][1].output
800+
assert isinstance(output, ForceFieldTaskDocument)
801+
assert output.output.energy == approx(-11.241942405700684, rel=1e-4)
802+
assert np.allclose(
803+
output.output.ionic_steps[-1].forces,
804+
[
805+
[-0.16317394375801086, -0.11385675519704819, 0.09085404127836227],
806+
[0.16317394375801086, 0.113856740295887, -0.09085404127836227],
807+
],
808+
rtol=1e-2,
809+
)
810+
assert len(output.output.ionic_steps) > 1
811+
assert output.output.n_steps == len(output.output.ionic_steps)
812+
assert output.forcefield_version == get_imported_version("upet")
813+
814+
788815
@pytest.mark.skipif(not mlff_is_installed("MACE"), reason="mace_torch is not installed")
789816
def test_ext_load_static_maker(si_structure: Structure):
790817
calculator_meta = {

tests/forcefields/test_md.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def test_ml_ff_md_maker(
8383
MLFF.MATPES_R2SCAN: -8.561729431152344,
8484
MLFF.FAIRChem: -5.4,
8585
MLFF.MatterSim: -5.4,
86+
MLFF.UPET: -5.6,
8687
}
8788

8889
# ASE can slightly change tolerances on structure positions

0 commit comments

Comments
 (0)