Skip to content

Commit a39f6d4

Browse files
shyuepclaudeJaGeo
authored
Update FF dependencies. (#1495)
* Update dependencies. Remove mattersim. * Update pyproject. * Fix CI: update forcefield dep-group matrix to numpy-limited The e3nn-limited extra was removed from pyproject.toml but the testing matrix still referenced it. uv silently skips missing extras, so torch was never installed, breaking test-forcefields collection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix forcefield and electrode tests for matgl 4.x and emmet-core 0.87 matgl >= 4.0 removed the DGL backend: matgl.config.BACKEND and the matgl.ext._ase_{dgl,pyg} modules no longer exist, and all potentials load through the unified matgl.ext.ase.PESCalculator. Update the matgl calculator branch accordingly, drop the obsolete _set_matgl_backend helper, and point CHGNet at the PyG weights (BowenD-UCB/CHGNet-PyG-*). emmet-core 0.87.1 (pulled in by the pymatgen 2026.5.4 bump) dropped the battery_id argument from InsertionElectrodeDoc.from_entries; battery_id is now a computed property derived from each entry's material_id, which is already set before the call. Remove the stale argument. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Remove dead DGL tests and skip guards matgl >= 4.0 removed the DGL backend, so the DGL-specific forcefield tests no longer exercise a real code path: - Drop test_m3gnet_static_maker / test_m3gnet_relax_maker, which monkeypatched the removed matgl.config.BACKEND to 'DGL'. - Drop test_m3gnet_pot, skipped for DGL/PyTorch 2.4 incompatibility and pinned to the legacy M3GNet-MP-2021.2.8 weights matgl 3.x dropped. - Remove the 'import dgl' fallback and the now-always-true 'dgl is None' clauses from the CHGNet skip guards, which had been silently skipping the CHGNet relax tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Unpin dependencies and declare conflicting extras via tool.uv Relax == pins to >= across the strict, strict-forcefields-*, jdftx, fireworks, torchsim extras and the tests/docs dependency groups so uv is free to resolve compatible versions; the uv.lock still captures exact versions for reproducibility. Documented constraints are kept: the strict-openff pymatgen pin (openff is pymatgen-sensitive) and the numpy<3 / pymongo / netCDF4 upper bounds. Add [tool.uv] conflicts declaring the four strict-forcefields-* extras and strict-openff as mutually exclusive. These pull in incompatible stacks (nequip needs numpy<2 via matscipy; deepmd/tensorflow and nequip-allegro need numpy>=2; mace/torch-dftd pin different e3nn/torch), so uv lock now builds a separate resolution per group instead of failing to satisfy all at once. Re-enable strict-forcefields-e3nn-limited and add it to the CI test matrix to keep the matrix in sync with the extras. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add uv.lock and remove it from gitignore. * Update uvloc. * Fix torchsim reporter for torch-sim 0.6 read-only filenames torch-sim-atomistic 0.6 makes TrajectoryReporter.filenames a read-only property and opens the trajectory files in the constructor, so the post-construction 'reporter.filenames = [...]' assignment raised AttributeError. Resolve the paths up front and pass them into the constructor instead. Surfaced by unpinning torch-sim-atomistic to >=0.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Require torch-sim-atomistic>=0.6.0 and drop filenames compat shim Pin torch-sim-atomistic to >=0.6.0 now that the reporter code targets its read-only filenames property, and simplify the handling to mutate the (already deep-copied) reporter dict in place. Add the torchsim extra to the [tool.uv] conflicts set: torch-sim 0.6 is incompatible with strict-forcefields-generic (tensorflow pin), and CI already installs torchsim in its own isolated job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update uvlock. * Fix jdftx test collection under pytest 9.1 pytest 9.1 rejects a fixture that both declares params= and is overridden by an indirect @parametrize on the same test, raising 'duplicate parametrization of task_name' at collection. The task_name fixture is only consumed by test_taskdoc, which always supplies the value via indirect parametrization, so drop the redundant fixture params and keep it a pure indirect fixture. Surfaced by unpinning pytest to >=9.0.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Set fail-fast: false on test-forcefields and test-non-ase matrices A failure in one matrix job was cancelling its still-running siblings, masking the real per-group results (slow forcefields installs never finished). Disable fail-fast so each dep-group and split reports independently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cap phonopy<4 to fix Gruneisen force-constants regression phonopy 4.x changed force-constants/primitive-axis handling, breaking the phonon.save -> phonopy.load round-trip in the Gruneisen workflow ('Force constants shape disagrees with crystal structure setting'). Unpinning let phonopy float to 4.2.1; cap below the breaking major version (resolves to 3.5.1) until the workflow is made 4.x-compatible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Run torch-limited forcefield tests single-process to avoid OOM The torch-limited dep-group loads the heaviest MLIP stack (matgl + nequip + torch with bundled CUDA libs). Under 'pytest -n auto' the parallel xdist workers each hold a copy of the models and exceed the runner's memory, killing the runner mid-test ('received a shutdown signal'). Use -n 1 for torch-limited; other groups keep -n auto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test influence of neb forcefield test * test influence of neb forcefield test * update test * update test * update test * update test * fix test * fix test * fix test * split torch tests further * deactive more tests * activate this back in * grueneisen * fix import * fix import * add more tests back * switch to mace * fix tests * move tests to mace * fix qha tests * format * fix linting * fix linting * go back to previous forcefield installations * remove energy criterion due to forcefield change * make tests strict again * make tests strict again and fix accidential changes * fix linting test files * upgrade upet --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: jgeorge <janine.george@bam.de>
1 parent 48d255c commit a39f6d4

14 files changed

Lines changed: 127 additions & 214 deletions

File tree

.github/workflows/testing.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ jobs:
3838
run:
3939
shell: bash -l {0} # enables conda/mamba env activation by reading bash profile
4040
strategy:
41+
fail-fast: false
4142
matrix:
4243
python-version: ["3.12"]
43-
dep-group: ["generic", "torch-limited", "e3nn-limited"]
44+
dep-group: ["generic", "torch-limited", "e3nn-limited", "numpy-limited"]
4445

4546
steps:
4647
- name: Check out repo
@@ -79,9 +80,13 @@ jobs:
7980
env:
8081
MP_API_KEY: ${{ secrets.MP_API_KEY }}
8182

83+
# torch-limited loads the heaviest MLIP stack (matgl + nequip + torch);
84+
# running it single-process avoids the OOM that kills the runner when
85+
# xdist spawns multiple workers each holding a copy of the models.
8286
run: |
8387
micromamba activate a2
84-
pytest -n auto --cov=atomate2 --cov-report=xml tests/forcefields
88+
pytest -n ${{ matrix.dep-group == 'torch-limited' && '1' || 'auto' }} --cov=atomate2 --cov-report=xml tests/forcefields
89+
#pytest -n auto --cov=atomate2 --cov-report=xml tests/forcefields
8590
8691
- name: Forcefield tutorial
8792
if: matrix.dep-group == 'torch-limited'
@@ -113,6 +118,7 @@ jobs:
113118
run:
114119
shell: bash -l {0} # enables conda/mamba env activation by reading bash profile
115120
strategy:
121+
fail-fast: false
116122
matrix:
117123
python-version: ["3.11", "3.12"]
118124
split: [1, 2, 3]

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ develop-eggs
2929
.installed.cfg
3030
lib
3131
lib64
32-
uv.lock
3332

3433
# Installer logs
3534
pip-log.txt

.mypy_cache/3.12/cache.db

Whitespace-only changes.

pyproject.toml

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ aims = ["pymatgen-io-aims>=0.0.5", "pymatgen>=2025.10.7"]
4747
amset = ["amset>=0.4.15", "pydash"]
4848
cclib = ["cclib>=1.8.1"]
4949
mp = ["mp-api>=0.37.5"]
50-
phonons = ["phonopy>=2.43.6", "seekpath>=2.0.0"]
50+
# phonopy 4.x changed force-constants/primitive-axis handling, breaking the
51+
# phonon.save -> phonopy.load round-trip used by the Grüneisen workflow
52+
# ("Force constants shape disagrees with crystal structure setting").
53+
phonons = ["phonopy>=2.43.6,<4", "seekpath>=2.0.0"]
5154
lobster = ["ijson>=3.2.2", "lobsterpy>=0.6.0"]
5255
defects = [
5356
"dscribe>=1.2.0",
@@ -60,7 +63,7 @@ ase-ext = ["tblite>=0.3.0; platform_system=='Linux'"]
6063
forcefields-demo = ["chgnet>=0.3.8","atomate2[ase]"]
6164

6265
torchsim = [
63-
"torch-sim-atomistic==0.5.0; python_version >= '3.12'"
66+
"torch-sim-atomistic==0.6.0; python_version >= '3.12'"
6467
]
6568
jdftx = ["pymatgen==2026.5.4"]
6669
approxneb = ["pymatgen-analysis-diffusion>=2024.7.15"]
@@ -95,31 +98,21 @@ strict-forcefields-generic = [
9598
"deepmd-kit==3.1.3",
9699
"tensorflow-cpu==2.21.0; sys_platform == 'linux'",
97100
"tensorflow==2.21.0; sys_platform == 'darwin' or sys_platform == 'win32'",
98-
"mattersim==1.2.3",
99-
"wandb>=0.24.0", # required for mattersim
100-
"upet>=0.2.1",
101+
# "mattersim>=1.2.3", # need to be activated again
102+
"wandb==0.24.0", # required for mattersim
103+
"upet==0.2.5",
101104
]
102105
strict-forcefields-torch-limited = [
103-
"matgl==3.0.2",
104-
# Note that DGL stopped building new releases for Mac + Windows after 2.2.0.
105-
# That enforces a simultaneous pin on torch / torchdata
106-
# Linux users can acces newer versions of dgl / torch / torchdata via conda.
107-
# Mac / Windows users will need to install from source
108-
"dgl==2.2.0; sys_platform == 'darwin' or sys_platform == 'win32'",
109-
"dgl<=2.4.0; sys_platform == 'linux'",
110-
"torch==2.11.0; sys_platform == 'darwin' or sys_platform == 'win32'",
111-
"torch==2.2.0; sys_platform == 'linux'",
112-
"torchdata==0.7.1",
106+
"matgl==4.0.2",
113107
"nequip==0.17.1", # requires numpy<2 because of matscipy
114-
"numpy==1.26.4",
115108
]
109+
116110
strict-forcefields-e3nn-limited = [
117-
"mace-torch==0.3.16",
111+
"mace-torch==0.3.15",
118112
"torch-dftd==0.5.3",
119113
]
120114
strict-forcefields-numpy-limited = [
121115
"nequip-allegro==0.8.2",
122-
"numpy==1.26.4",
123116
]
124117

125118
strict = [

src/atomate2/common/jobs/electrode.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,7 @@ def get_insertion_electrode_doc(
202202
ient.data["material_id"] = AlphaID(int(ULID.from_str(ient.entry_id)))
203203
else:
204204
ient.data["material_id"] = ient.entry_id
205-
return InsertionElectrodeDoc.from_entries(
206-
computed_entries, working_ion_entry, battery_id=None
207-
)
205+
return InsertionElectrodeDoc.from_entries(computed_entries, working_ion_entry)
208206

209207

210208
@job

src/atomate2/forcefields/utils.py

Lines changed: 22 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,10 @@ def ase_calculator(
304304

305305
if (
306306
isinstance(calculator_meta, str)
307-
and (calculator_meta in map(str, MLFF) or calculator_meta in MLFF)
307+
and (
308+
calculator_meta in map(str, MLFF)
309+
or calculator_meta in {m.value for m in MLFF}
310+
)
308311
) or isinstance(calculator_meta, MLFF):
309312
calculator_name = MLFF(calculator_meta)
310313

@@ -349,23 +352,22 @@ def ase_calculator(
349352
)
350353

351354
import matgl
352-
353-
# matgl >= 3.0 dropped the legacy MP-2021.2.8 / MPtrj weights and
354-
# the GitHub `pretrained_models/` fallback. All pre-trained
355-
# weights now live on the `materialyze` HF org with the
356-
# ``<Architecture>-PES-<Dataset>-<Func>-<Version>`` naming.
357-
# See https://huggingface.co/materialyze for the canonical list.
355+
from matgl.ext.ase import PESCalculator
356+
357+
# matgl >= 4.0 removed the DGL backend; matgl now targets
358+
# PyTorch Geometric exclusively and all potentials load through
359+
# the single ``matgl.ext.ase.PESCalculator``. Pre-trained weights
360+
# use the ``<Architecture>-PES-<Dataset>-<Func>-<Version>`` naming
361+
# and live on the ``materialyze`` HF org (resolved from bare names
362+
# by ``load_model``), except the CHGNet PyG weights, hosted under
363+
# ``BowenD-UCB``. See https://huggingface.co/materialyze.
358364
match calculator_name:
359365
case MLFF.M3GNet:
360-
# Only the MatPES-trained PyG M3GNet potential is
361-
# distributed on HF for matgl 3.x.
362366
path = kwargs.get("path", "M3GNet-PES-MatPES-PBE-2025.2")
363-
backend = "PYG"
364367
case MLFF.CHGNet:
365-
# The MatPES-trained CHGNet weights remain DGL-backed.
366-
path = kwargs.get("path", "CHGNet-PES-MatPES-PBE-2025.2.10")
367-
backend = "DGL"
368-
368+
path = kwargs.get(
369+
"path", "BowenD-UCB/CHGNet-PyG-MatPES-PBE-2025.2.10"
370+
)
369371
case MLFF.MATPES_R2SCAN | MLFF.MATPES_PBE:
370372
# ``calculator_name.value`` is e.g. "MatPES-PBE";
371373
# take the suffix to construct the HF repo name.
@@ -376,20 +378,11 @@ def ase_calculator(
376378
"path",
377379
f"{architecture}-PES-MatPES-{functional}-{version}",
378380
)
379-
backend = "PYG"
380-
381-
_set_matgl_backend(backend)
382381

383382
if default_dtype is not None:
384383
matgl.set_default_dtype(default_dtype)
385384

386-
matgl_calc = getattr(
387-
import_module(f"matgl.ext._ase_{matgl.config.BACKEND.lower()}"),
388-
"PESCalculator",
389-
None,
390-
)
391-
392-
calculator = matgl_calc(matgl.load_model(path), **kwargs)
385+
calculator = PESCalculator(matgl.load_model(path), **kwargs)
393386

394387
case MLFF.MACE | MLFF.MACE_MP_0 | MLFF.MACE_MPA_0 | MLFF.MACE_MP_0B3:
395388
from mace.calculators import MACECalculator, mace_mp
@@ -440,9 +433,11 @@ def ase_calculator(
440433

441434
calculator = getattr(
442435
NequIPCalculator,
443-
"from_compiled_model"
444-
if hasattr(NequIPCalculator, "from_compiled_model")
445-
else "from_deployed_model",
436+
(
437+
"from_compiled_model"
438+
if hasattr(NequIPCalculator, "from_compiled_model")
439+
else "from_deployed_model"
440+
),
446441
)(**kwargs)
447442

448443
case MLFF.FAIRChem:
@@ -492,37 +487,6 @@ def _load_calc_cls(
492487
return MontyDecoder().process_decoded(calculator_meta)
493488

494489

495-
def _set_matgl_backend(backend: str) -> None:
496-
"""Switch the active matgl backend, reloading dependent submodules.
497-
498-
matgl reads ``matgl.config.BACKEND`` once when ``matgl.models`` /
499-
``matgl.apps.pes`` are first imported, and the conditional imports inside
500-
those packages decide which backend-specific classes (e.g.
501-
``matgl.models.CHGNet``) are exposed. Mutating ``matgl.config.BACKEND``
502-
after those modules have been loaded does not re-run the imports, so a
503-
second forcefield call requesting a different backend in the same Python
504-
process raises ``AttributeError: module 'matgl.models' has no attribute
505-
'CHGNet'`` when ``matgl.load_model`` looks up the class.
506-
507-
Reload the affected submodules so subsequent loads pick the correct
508-
backend-specific implementations. No-op if the backend is already set.
509-
"""
510-
import importlib
511-
import sys
512-
513-
import matgl
514-
515-
if backend == matgl.config.BACKEND:
516-
return
517-
518-
matgl.config.BACKEND = backend
519-
# Submodules that read BACKEND at import time and need to be re-evaluated
520-
# so that backend-specific classes/functions get re-bound.
521-
for modname in ("matgl.models", "matgl.apps.pes", "matgl.apps"):
522-
if modname in sys.modules:
523-
importlib.reload(sys.modules[modname])
524-
525-
526490
@contextmanager
527491
def revert_default_dtype() -> Generator[None]:
528492
"""Context manager for torch.default_dtype.

src/atomate2/torchsim/core.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,15 @@ def process_trajectory_reporter_dict(
163163
for i, props in prop_calculators_typed.items()
164164
}
165165

166+
# ``filenames`` is a read-only property and the trajectory files are opened
167+
# in the constructor, so resolve the paths before passing them in.
168+
trajectory_reporter_dict["filenames"] = [
169+
Path(p).resolve() for p in trajectory_reporter_dict.get("filenames", [])
170+
]
166171
trajectory_reporter = ts.TrajectoryReporter(
167172
**trajectory_reporter_dict, prop_calculators=prop_calculators_functions
168173
)
169174

170-
trajectory_reporter.filenames = [
171-
Path(p).resolve() for p in trajectory_reporter_dict.get("filenames", [])
172-
]
173-
174175
reporter_details = TrajectoryReporterDetails(
175176
state_frequency=trajectory_reporter.state_frequency,
176177
trajectory_kwargs=trajectory_reporter.trajectory_kwargs,

tests/forcefields/flows/test_approx_neb.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
def test_approx_neb_from_endpoints(test_dir, clean_dir):
12-
pytest.importorskip("matgl")
12+
pytest.importorskip("mace")
1313

1414
vasp_aneb_dir = test_dir / "vasp" / "ApproxNEB"
1515

@@ -21,7 +21,7 @@ def test_approx_neb_from_endpoints(test_dir, clean_dir):
2121
]
2222

2323
flow = ForceFieldApproxNebFromEndpointsMaker(
24-
image_relax_maker=ForceFieldStaticMaker(force_field_name="MATPES_R2SCAN")
24+
image_relax_maker=ForceFieldStaticMaker(force_field_name="MACE_MP_0B3"),
2525
).make("Zn", endpoints, vasp_aneb_dir / "host_structure_relax_2/outputs/CHGCAR.bz2")
2626

2727
response = run_locally(flow)
@@ -31,17 +31,14 @@ def test_approx_neb_from_endpoints(test_dir, clean_dir):
3131
}
3232

3333
assert isinstance(output["collate_images_single_hop"], NebResult)
34-
# The MatPES-r2SCAN TensorNet was retrained for matgl 3.x (v2025.2), so
35-
# exact-energy references no longer apply. Verify the path structure:
36-
# 7 finite energies, endpoints degenerate, all in a sensible band for the
37-
# Zn host structure (~-1500 eV total).
34+
# Initially, this test was written with MATPES_PBE, but had to be
35+
# changed to MACE_MP_0B3, so exact-energy references no longer apply.
36+
3837
energies = output["collate_images_single_hop"].energies
3938
assert len(energies) == 7
4039
assert all(e is not None for e in energies)
4140
# endpoints (i=0, 6) should be ~degenerate by construction
4241
assert energies[0] == pytest.approx(energies[-1], rel=1e-3)
43-
# all energies in a physical band consistent with the host
44-
assert all(-2000.0 < e < -1000.0 for e in energies)
4542

4643
assert len(output["collate_images_single_hop"].images) == 7
4744
assert all(

tests/forcefields/flows/test_gruneisen.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from atomate2.forcefields.flows.gruneisen import GruneisenMaker
1818
from atomate2.forcefields.flows.phonons import PhononMaker
19+
from atomate2.forcefields.jobs import ForceFieldRelaxMaker
1920

2021

2122
def test_gruneisen_wf_ff(clean_dir, si_structure: Structure, tmp_path: Path):
@@ -33,6 +34,12 @@ def test_gruneisen_wf_ff(clean_dir, si_structure: Structure, tmp_path: Path):
3334
store_force_constants=False,
3435
prefer_90_degrees=False,
3536
),
37+
const_vol_relax_maker=ForceFieldRelaxMaker(
38+
force_field_name="CHGNet", relax_kwargs={"fmax": 0.01}, relax_cell=False
39+
),
40+
bulk_relax_maker=ForceFieldRelaxMaker(
41+
force_field_name="CHGNet", relax_kwargs={"fmax": 0.01}
42+
),
3643
).make(structure=si_structure)
3744

3845
# run the flow or job and ensure that it finished running successfully

tests/forcefields/flows/test_phonon.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,11 @@ def test_phonon_wf_force_field(
191191
# references are kept for the `chgnet` package path.
192192
assert_allclose(
193193
ph_bs_dos_doc.free_energies,
194-
[3164.0, 3053.0, 2351.0, 999.0, -868.0]
195-
if is_matgl_chgnet
196-
else [5271.300306, 5162.674841, 4353.717375, 2698.616337, 343.125174],
194+
(
195+
[3164.0, 3053.0, 2351.0, 999.0, -868.0]
196+
if is_matgl_chgnet
197+
else [5271.300306, 5162.674841, 4353.717375, 2698.616337, 343.125174]
198+
),
197199
atol=1000,
198200
)
199201

@@ -229,9 +231,11 @@ def test_phonon_wf_force_field(
229231
# CHGNet weights distributed by matgl 3.x.
230232
assert_allclose(
231233
ph_bs_dos_doc.entropies,
232-
[0.0, 3.46, 10.50, 16.31, 20.85]
233-
if is_matgl_chgnet
234-
else [0.0, 3.733666, 12.536534, 20.344558, 26.627292],
234+
(
235+
[0.0, 3.46, 10.50, 16.31, 20.85]
236+
if is_matgl_chgnet
237+
else [0.0, 3.733666, 12.536534, 20.344558, 26.627292]
238+
),
235239
atol=2,
236240
)
237241
# heat_capacities and internal_energies depend strongly on the phonon

0 commit comments

Comments
 (0)