Skip to content

Commit e6c9476

Browse files
Add updated MP workflows + cleanup (#1139)
* Update pyproject.toml * Update pyproject.toml * test a new pymatgen version * Update pyproject.toml * Update test_utils.py * redraft mp flows * split off openff tests * remove deprecated flows (completely forgot at the start of the year * fix pin in strict openff wf * correctly partition tests? * revert openff_md test ref change * openff test over py310-312 --------- Co-authored-by: J. George <[email protected]>
1 parent 3ddc48b commit e6c9476

File tree

10 files changed

+361
-336
lines changed

10 files changed

+361
-336
lines changed

.github/workflows/testing.yml

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858

5959
- name: Install conda dependencies
6060
run: |
61-
micromamba install -n a2 -c conda-forge enumlib packmol bader openbabel openff-toolkit==0.16.2 openff-interchange==0.3.22 --yes
61+
micromamba install -n a2 -c conda-forge enumlib packmol bader --yes
6262
6363
- name: Install dependencies
6464
run: |
@@ -87,7 +87,7 @@ jobs:
8787
# However this `splitting-algorithm` means that tests cannot depend sensitively on the order they're executed in.
8888
run: |
8989
micromamba activate a2
90-
pytest --splits 3 --group ${{ matrix.split }} --durations-path tests/.pytest-split-durations --splitting-algorithm least_duration --ignore=tests/ase --cov=atomate2 --cov-report=xml
90+
pytest --splits 3 --group ${{ matrix.split }} --durations-path tests/.pytest-split-durations --splitting-algorithm least_duration --ignore=tests/ase --ignore=tests/openff_md --ignore=tests/openmm_md --cov=atomate2 --cov-report=xml
9191
9292
9393
- uses: codecov/codecov-action@v1
@@ -97,6 +97,62 @@ jobs:
9797
name: coverage${{ matrix.split }}
9898
file: ./coverage.xml
9999

100+
test-openff:
101+
# prevent this action from running on forks
102+
if: github.repository == 'materialsproject/atomate2'
103+
104+
services:
105+
local_mongodb:
106+
image: mongo:4.0
107+
ports:
108+
- 27017:27017
109+
110+
runs-on: ubuntu-latest
111+
defaults:
112+
run:
113+
shell: bash -l {0} # enables conda/mamba env activation by reading bash profile
114+
strategy:
115+
matrix:
116+
python-version: ["3.10","3.11","3.12"]
117+
118+
steps:
119+
- name: Check out repo
120+
uses: actions/checkout@v4
121+
122+
- name: Set up micromamba
123+
uses: mamba-org/setup-micromamba@main
124+
125+
- name: Create mamba environment
126+
run: |
127+
micromamba create -n a2 python=${{ matrix.python-version }} --yes
128+
129+
- name: Install uv
130+
run: micromamba run -n a2 pip install uv
131+
132+
- name: Install conda dependencies
133+
run: |
134+
micromamba install -n a2 -c conda-forge enumlib packmol bader openbabel openff-toolkit==0.16.2 openff-interchange==0.3.22 --yes
135+
136+
- name: Install dependencies
137+
run: |
138+
micromamba activate a2
139+
python -m pip install --upgrade pip
140+
uv pip install .[strict-openff,tests]
141+
142+
- name: Install pymatgen from master if triggered by pymatgen repo dispatch
143+
if: github.event_name == 'repository_dispatch' && github.event.action == 'pymatgen-ci-trigger'
144+
run: |
145+
micromamba activate a2
146+
uv pip install --upgrade 'git+https://github.com/materialsproject/pymatgen@${{ github.event.client_payload.pymatgen_ref }}'
147+
148+
- name: Test split ${{ matrix.split }}
149+
env:
150+
MP_API_KEY: ${{ secrets.MP_API_KEY }}
151+
152+
run: |
153+
micromamba activate a2
154+
pytest tests/{openff_md,openmm_md}
155+
100156
test-notebooks-and-ase:
101157
# prevent this action from running on forks
102158
if: github.repository == 'materialsproject/atomate2'

pyproject.toml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies = [
3030
"custodian>=2024.4.18",
3131
"emmet-core>=0.84.3rc3",
3232
"jobflow>=0.1.11",
33-
"monty>=2024.7.30",
33+
"monty>=2024.12.10",
3434
"numpy",
3535
"pydantic-settings>=2.0.3",
3636
"pydantic>=2.0.1",
@@ -104,23 +104,27 @@ strict = [
104104
"ijson==3.3.0",
105105
"jobflow==0.1.19",
106106
"lobsterpy==0.4.9",
107-
"mdanalysis==2.7.0",
108-
"monty==2024.10.21",
109-
"mp-api==0.43.0",
107+
"monty==2025.1.9",
108+
"mp-api==0.45.3",
110109
"numpy",
111-
"openmm-mdanalysis-reporter==0.1.0",
112-
"openmm==8.1.1",
113110
"phonopy==2.30.1",
114111
"pydantic-settings==2.7.0",
115112
"pydantic==2.9.2",
116113
"pymatgen-analysis-defects==2024.10.22",
117-
"pymatgen==2024.11.13",
114+
"pymatgen==2025.2.18",
118115
"pymongo==4.10.1",
119116
"python-ulid==3.0.0",
120117
"seekpath==2.1.0",
121118
"tblite==0.3.0; python_version < '3.12'",
122119
"typing-extensions==4.12.2",
123120
]
121+
strict-openff = [
122+
"mdanalysis==2.7.0",
123+
"monty==2024.12.10",
124+
"openmm-mdanalysis-reporter==0.1.0",
125+
"openmm==8.1.1",
126+
"pymatgen==2024.11.13",
127+
]
124128
strict-forcefields = [
125129
"calorine==3.0",
126130
"chgnet==0.4.0",

src/atomate2/common/jobs/utils.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@
22

33
from __future__ import annotations
44

5+
import os
56
from typing import TYPE_CHECKING
67

78
from jobflow import Response, job
9+
from monty.os.path import zpath
10+
from pymatgen.command_line.bader_caller import bader_analysis_from_path
811
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
912

1013
from atomate2 import SETTINGS
1114

1215
if TYPE_CHECKING:
16+
from collections.abc import Sequence
17+
from pathlib import Path
18+
1319
from pymatgen.core import Structure
1420

1521

@@ -137,3 +143,62 @@ def retrieve_structure_from_materials_project(
137143
output=structure,
138144
stored_data={"task_id": task_id, "database_version": database_version},
139145
)
146+
147+
148+
@job
149+
def remove_workflow_files(
150+
directories: Sequence[str], file_names: Sequence[str], allow_zpath: bool = True
151+
) -> list[str]:
152+
"""
153+
Remove files from previous jobs.
154+
155+
For example, at the end of an MP flow, WAVECAR files are generated
156+
that take up a lot of disk space.
157+
This utility can automatically remove them following a workflow.
158+
159+
Parameters
160+
----------
161+
makers : Sequence[Maker, Flow, Job]
162+
Jobs in the flow on which to remove files.
163+
file_names : Sequence[str]
164+
The list of file names to remove, ex. ["WAVECAR"] rather than a full path
165+
allow_zpath : bool = True
166+
Whether to allow checking for gzipped output using `monty.os.zpath`
167+
168+
Returns
169+
-------
170+
list[str] : list of removed files
171+
"""
172+
abs_paths = [os.path.abspath(dir_name.split(":")[-1]) for dir_name in directories]
173+
174+
removed_files = []
175+
for job_dir in abs_paths:
176+
for file in file_names:
177+
file_name = os.path.join(job_dir, file)
178+
if allow_zpath:
179+
file_name = zpath(file_name)
180+
181+
if os.path.isfile(file_name):
182+
removed_files.append(file_name)
183+
os.remove(file_name)
184+
185+
return removed_files
186+
187+
188+
@job
189+
def bader_analysis(dir_name: str | Path, suffix: str | None = None) -> dict:
190+
"""Run Bader charge analysis as a job.
191+
192+
Parameters
193+
----------
194+
dir_name : str or Path
195+
The name of the directory to run Bader in.
196+
suffix : str or None (default)
197+
Suffixes of the files to filter by.
198+
199+
Returns
200+
-------
201+
dict of bader charge analysis which is JSONable.
202+
"""
203+
dir_name = os.path.abspath(str(dir_name).split(":")[-1])
204+
return bader_analysis_from_path(dir_name, suffix=suffix or "")

src/atomate2/common/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,18 @@ def parse_additional_json(dir_name: Path) -> dict[str, Any]:
194194
if key not in ("custodian", "transformations", "FW"):
195195
additional_json[key] = loadfn(filename, cls=None)
196196
return additional_json
197+
198+
199+
def _recursive_get_dir_names(jobs: list, dir_names: list) -> None:
200+
"""Recursively get all `output.dir_name` from a list of jobs.
201+
202+
Parameters
203+
----------
204+
jobs : list of jobs, Makers, Flows, etc.
205+
dir_names : a list to add the `dir_name`'s to.
206+
"""
207+
for a_job in jobs:
208+
if (sub_jobs := getattr(a_job, "jobs", None)) is not None:
209+
_recursive_get_dir_names(sub_jobs, dir_names)
210+
else:
211+
dir_names.append(a_job.output.dir_name)

src/atomate2/vasp/flows/mp.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
from jobflow import Flow, Maker
1616
from pymatgen.io.vasp.sets import LobsterSet
1717

18+
from atomate2.common.jobs.utils import remove_workflow_files
19+
from atomate2.common.utils import _recursive_get_dir_names
1820
from atomate2.lobster.jobs import LobsterMaker
1921
from atomate2.vasp.flows.core import DoubleRelaxMaker
2022
from atomate2.vasp.flows.lobster import VaspLobsterMaker
2123
from atomate2.vasp.jobs.mp import (
24+
MP24PreRelaxMaker,
25+
MP24RelaxMaker,
26+
MP24StaticMaker,
2227
MPGGARelaxMaker,
2328
MPGGAStaticMaker,
2429
MPMetaGGARelaxMaker,
@@ -29,6 +34,7 @@
2934
logger = logging.getLogger(__name__)
3035

3136
if TYPE_CHECKING:
37+
from collections.abc import Sequence
3238
from pathlib import Path
3339

3440
from pymatgen.core.structure import Structure
@@ -194,6 +200,95 @@ def make(self, structure: Structure, prev_dir: str | Path | None = None) -> Flow
194200
return Flow(jobs=jobs, output=output, name=self.name)
195201

196202

203+
@dataclass
204+
class MP24DoubleRelaxMaker(DoubleRelaxMaker):
205+
"""MP24 PBEsol + r2SCAN double relaxation workflow.
206+
207+
Parameters
208+
----------
209+
name : str
210+
Name of the flows produced by this maker.
211+
relax_maker1 : .BaseVaspMaker
212+
Maker to generate the first relaxation.
213+
relax_maker2 : .BaseVaspMaker
214+
Maker to generate the second relaxation.
215+
"""
216+
217+
name: str = "MP24 double relax"
218+
relax_maker1: Maker | None = field(default_factory=MP24PreRelaxMaker)
219+
relax_maker2: Maker = field(
220+
default_factory=lambda: MP24RelaxMaker(
221+
copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR", "CHGCAR")}
222+
)
223+
)
224+
225+
226+
@dataclass
227+
class MP24DoubleRelaxStaticMaker(Maker):
228+
"""MP24 workflow to relax a structure with r2SCAN.
229+
230+
Optionally, files can be automatically cleaned following completion
231+
of the workflow. By default, WAVECAR files are removed.
232+
233+
Parameters
234+
----------
235+
name : str
236+
Name of the flows produced by this maker.
237+
relax_maker : .BaseVaspMaker
238+
Maker to generate the relaxation.
239+
static_maker : .BaseVaspMaker
240+
Maker to generate the static calculation before the relaxation.
241+
clean_files : Sequence of str or None
242+
If a list of strings, names of files to remove following the workflow.
243+
By default, this removes the WAVECAR files (gzipped or not).
244+
"""
245+
246+
name: str = "MP24 r2SCAN workflow"
247+
relax_maker: Maker = field(default_factory=MP24DoubleRelaxMaker)
248+
static_maker: Maker = field(
249+
default_factory=lambda: MP24StaticMaker(
250+
copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR", "CHGCAR")}
251+
)
252+
)
253+
clean_files: Sequence[str] | None = ("WAVECAR",)
254+
255+
def make(self, structure: Structure, prev_dir: str | Path | None = None) -> Flow:
256+
"""Relax a structure with r2SCAN.
257+
258+
Parameters
259+
----------
260+
structure : .Structure
261+
A pymatgen structure object.
262+
prev_dir : str or Path or None
263+
A previous VASP calculation directory to copy output files from.
264+
265+
Returns
266+
-------
267+
Flow
268+
A flow containing the MP relaxation workflow.
269+
"""
270+
relax_flow = self.relax_maker.make(structure=structure, prev_dir=prev_dir)
271+
272+
static_job = self.static_maker.make(
273+
structure=relax_flow.output.structure, prev_dir=relax_flow.output.dir_name
274+
)
275+
276+
jobs = [relax_flow, static_job]
277+
278+
self.clean_files = self.clean_files or []
279+
if len(self.clean_files) > 0:
280+
directories: list[str] = []
281+
_recursive_get_dir_names(jobs, directories)
282+
cleanup = remove_workflow_files(
283+
directories=directories,
284+
file_names=self.clean_files,
285+
allow_zpath=True,
286+
)
287+
jobs += [cleanup]
288+
289+
return Flow(jobs=jobs, output=static_job.output, name=self.name)
290+
291+
197292
# update potcars to 54, use correct W potcar
198293
# use staticmaker for compatibility
199294
@dataclass

0 commit comments

Comments
 (0)