Skip to content

Commit 5a01ccc

Browse files
Add example MP-compatible phase diagram tutorial (#1159)
* add MP workflow / phase diagram example * move job uuid to name mapper to common utils * add example of how to apply just gga / +u mixing * clean up ff eos test / add MP API key to notebook test * restructure notebook to skip certain cells * remove dupe json * disable mp api-based tutorial cell * more skip execution * pin monty in openff * switch monty / pymatgen pin for openff * bump pydantic / fix aims document model * add new doc models for vasp test data, add test data, add options in tutorial to run these * precommit * update docs with json archive method
1 parent ee2d24e commit 5a01ccc

35 files changed

+846
-25
lines changed

.github/workflows/testing.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ jobs:
205205
run: uv pip install --upgrade 'git+https://github.com/materialsproject/pymatgen@${{ github.event.client_payload.pymatgen_ref }}'
206206

207207
- name: Test Notebooks
208+
env:
209+
MP_API_KEY: ${{ secrets.MP_API_KEY }}
208210
run: |
209211
micromamba activate a2
210212
pytest --nbmake ./tutorials --ignore=./tutorials/openmm_tutorial.ipynb --ignore=./tutorials/force_fields

docs/dev/vasp_tests.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ name. For example, there cannot be two calculations called "relax". Instead you
146146
should ensure they are named something like "relax 1" and "relax 2".
147147
```
148148

149+
### 2a. Dealing with larger amounts of test data / directories.
150+
151+
Some complex workflows and tutorials might require a larger than normal amount of test data to execute.
152+
You might also realize that a branching workflow requires many steps, leading to a sprawling reference directory structure.
153+
In this case, you may want to use separate tools in `atomate2` which bundle your VASP test data into JSON format archives.
154+
To do this for a single VASP calculation directory, you might run:
155+
156+
```python
157+
from atomate2.utils.testing.vasp import VaspTestData
158+
159+
vasp_test_data = VaspTestData.from_directory(
160+
"/Users/alex/atomate2/job_2021-11-08-17-24-31-799852-28250"
161+
)
162+
vasp_test_data.to_file("tight_relax_1.json.lzma")
163+
```
164+
165+
You can use any other compression method supported by `monty.io.zpath`, such as GZIP (`.gz`) or bzip2 (`.bz2`).
166+
The LZMA compression method is shown here because this offers generally the highest compression ratio of the three and is fairly quick to decompress, but it is CPU intensive to compress.
167+
168+
The `VaspTestData` class handles removal of POTCAR copyright information, converting them to POTCAR.spec files as before.
169+
The test infrastructure using `mock_vasp` is also equipped to handle extraction of VASP files from a compressed JSON archive.
170+
149171
## 3. Copy the test data folder into atomate2
150172

151173
You can now copy the WF_NAME folder into the atomate2 test files. VASP test files live
@@ -283,10 +305,25 @@ def test_elastic(mock_vasp, clean_dir):
283305
)
284306
```
285307

286-
Note that the `mock_vasp` and `clean_dir` arguments to the test function are
308+
<b>Note:</b> The `mock_vasp` and `clean_dir` arguments to the test function are
287309
[pytest fixtures](https://docs.pytest.org/en/6.2.x/fixture.html) and are essential
288310
for the test to run successfully.
289311

312+
<b>Note:</b> If you used the `VaspTestData` method of creating JSON archives for the test data, you would use the following for `ref_paths`:
313+
314+
```py
315+
ref_paths = {
316+
"elastic relax 1/6": "Si_elastic/elastic_relax_1_6.json.lzma",
317+
"elastic relax 2/6": "Si_elastic/elastic_relax_2_6.json.lzma",
318+
"elastic relax 3/6": "Si_elastic/elastic_relax_3_6.json.lzma",
319+
"elastic relax 4/6": "Si_elastic/elastic_relax_4_6.json.lzma",
320+
"elastic relax 5/6": "Si_elastic/elastic_relax_5_6.json.lzma",
321+
"elastic relax 6/6": "Si_elastic/elastic_relax_6_6.json.lzma",
322+
"tight relax 1": "Si_elastic/tight_relax_1.json.lzma",
323+
"tight relax 2": "Si_elastic/tight_relax_2.json.lzma",
324+
}
325+
```
326+
290327
```{warning}
291328
For `mock_vasp` to work correctly, all imports needed for the test must be
292329
imported in the test function itself (rather than at the top of the file).

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ strict = [
109109
"numpy",
110110
"phonopy==2.30.1",
111111
"pydantic-settings==2.7.0",
112-
"pydantic==2.9.2",
112+
"pydantic==2.11.1",
113113
"pymatgen-analysis-defects==2025.1.18",
114114
"pymatgen==2025.2.18",
115115
"pymongo==4.10.1",
@@ -123,7 +123,7 @@ strict-openff = [
123123
"monty==2025.3.3",
124124
"openmm-mdanalysis-reporter==0.1.0",
125125
"openmm==8.1.1",
126-
"pymatgen==2025.3.10",
126+
"pymatgen==2024.11.13", # TODO: open ff is extremely sensitive to pymatgen version
127127
"mdanalysis==2.7.0"
128128
]
129129
strict-forcefields = [

src/atomate2/aims/schemas/calculation.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
from collections.abc import Sequence
88
from datetime import datetime, timezone
99
from pathlib import Path
10-
from typing import TYPE_CHECKING, Any, Optional, Union
10+
from typing import TYPE_CHECKING, Any, Optional
1111

1212
import numpy as np
1313
from ase.spectrum.band_structure import BandStructure
14+
from emmet.core.math import Matrix3D, Vector3D
1415
from jobflow.utils import ValueEnum
1516
from pydantic import BaseModel, Field
1617
from pymatgen.core import Molecule, Structure
@@ -19,10 +20,10 @@
1920
from pymatgen.io.aims.inputs import AimsGeometryIn
2021
from pymatgen.io.aims.outputs import AimsOutput
2122
from pymatgen.io.common import VolumetricData
22-
from typing_extensions import Self
2323

2424
if TYPE_CHECKING:
25-
from emmet.core.math import Matrix3D, Vector3D
25+
from typing_extensions import Self
26+
2627

2728
STORE_VOLUMETRIC_DATA = ("total_density",)
2829

@@ -93,7 +94,7 @@ class CalculationOutput(BaseModel):
9394
None, description="The final DFT energy per atom for the calculation"
9495
)
9596

96-
structure: Union[Structure, Molecule] = Field(
97+
structure: Structure | Molecule = Field(
9798
None, description="The final structure from the calculation"
9899
)
99100

@@ -127,7 +128,7 @@ class CalculationOutput(BaseModel):
127128
description="The valence band maximum, or HOMO for molecules, in eV "
128129
"(if system is not metallic)",
129130
)
130-
atomic_steps: list[Union[Structure, Molecule]] = Field(
131+
atomic_steps: list[Structure | Molecule] = Field(
131132
None, description="Structures for each ionic step"
132133
)
133134

@@ -198,7 +199,7 @@ class CalculationInput(BaseModel):
198199
The parameters passed in the control.in file
199200
"""
200201

201-
structure: Union[Structure, Molecule] = Field(
202+
structure: Structure | Molecule = Field(
202203
None, description="The input structure object"
203204
)
204205
parameters: dict[str, Any] = Field(

src/atomate2/utils/testing/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88
99
This module will hold the core logic for those tests.
1010
"""
11+
12+
from atomate2.utils.testing.common import get_job_uuid_name_map
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Define common testing utils used in atomate2."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from jobflow import Flow, Job, Response
9+
10+
11+
def get_job_uuid_name_map(job_flow_resp: Job | Flow | Response) -> dict[str, str]:
12+
"""
13+
Get all job UUIDs and map them to the job name.
14+
15+
Useful for running complex flows locally / testing in CI, where one often
16+
wants the output of a job with a specific name.
17+
18+
Parameters
19+
----------
20+
job_flow_resp : jobflow Job, Flow, or Response
21+
22+
Returns
23+
-------
24+
dict mapping string UUIDs to string names.
25+
"""
26+
uuid_to_name: dict[str, str] = {}
27+
28+
def recursive_get_job_names(
29+
flow_like: Job | Flow, uuid_to_name: dict[str, str]
30+
) -> None:
31+
if flow_jobs := getattr(flow_like, "jobs", None):
32+
for job in flow_jobs:
33+
recursive_get_job_names(job, uuid_to_name)
34+
elif replacement := getattr(flow_like, "replace", None):
35+
recursive_get_job_names(replacement, uuid_to_name)
36+
else:
37+
uuid_to_name[flow_like.uuid] = flow_like.name
38+
39+
recursive_get_job_names(job_flow_resp, uuid_to_name)
40+
return uuid_to_name

0 commit comments

Comments
 (0)