Skip to content

Commit 85a1dda

Browse files
Housekeeping (#1270)
* bump ase to 3.26 + add MTKNPT as default for NPT * improve VASP documentation around run_vasp_kwargs, custodian settings, and magmom initialization * remove deprecated forcefield makers that were slated for deletion on 1 January, 2025 * Suppress warnings in file client by default * Fallback methods in MPMorph's automatic volume determination
1 parent b412cfe commit 85a1dda

File tree

17 files changed

+252
-1025
lines changed

17 files changed

+252
-1025
lines changed

docs/user/codes/vasp.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,66 @@ The most important settings to consider are:
4242
NCORE, KPAR etc.
4343
- `VASP_VDW_KERNEL_DIR`: The path to the VASP Van der Waals kernel.
4444

45+
## FAQs
46+
47+
<b>How can I update the Custodian handlers used in a VASP job?</b>
48+
- Every `Maker` which derives from `BaseVaspMaker` (see below) has a `run_vasp_kwargs` kwarg.
49+
So, for example, to run a `StaticMaker` with only the `VaspErrorHandler` as the custodian handler, you would do this:
50+
51+
```py
52+
from atomate2.vasp.jobs.core import StaticMaker
53+
from custodian.vasp.handlers import VaspErrorHandler
54+
55+
maker = StaticMaker(run_vasp_kwargs={"handlers": [VaspErrorHandler]})
56+
```
57+
58+
<b>How can I change the other Custodian settings used to run VASP?</b>
59+
- These can be set through `run_vasp_kwargs.custodian_kwargs`:
60+
61+
```py
62+
maker = StaticMaker(
63+
run_vasp_kwargs={
64+
"custodian_kwargs": {
65+
"max_errors_per_job": 5,
66+
"monitor_freq": 100,
67+
}
68+
}
69+
)
70+
```
71+
For all possible `custodian_kwargs`, see the [`Custodian` class](https://github.com/materialsproject/custodian/blob/aa02baf5bc2a1883c5f8a8b6808340eeae324a99/src/custodian/custodian.py#L68).
72+
NB: You cannot set the following four `Custodian` fields using `custodian_kwargs`: `handlers`, `jobs`, `validators`, `max_errors`, and `scratch_dir`.
73+
74+
<b>Can I change how VASP is run for each per-`Maker`/`Job`?</b>
75+
Yes! Still using the `run_vasp_kwargs` and either the `vasp_cmd`, which represents the path to (and including) `vasp_std`, and `vasp_gamma_cmd`, which is the path to `vasp_gam`:
76+
77+
```py
78+
maker = StaticMaker(run_vasp_kwargs={"vasp_cmd": "/path/to/vasp_std"})
79+
```
80+
81+
<b>How can I use non-colinear VASP?</b>
82+
The same method as before applies, you can simply set:
83+
84+
```py
85+
vasp_ncl_path = "/path/to/vasp_ncl"
86+
maker = StaticMaker(
87+
run_vasp_kwargs={"vasp_cmd": vasp_ncl_path, "vasp_gamma_cmd": vasp_ncl_path}
88+
)
89+
```
90+
91+
<b>How can I update the magnetic moments (MAGMOM) tag used to start a calculation?</b>
92+
You can specify MAGMOM using a `dict` of defined values, such as:
93+
94+
```py
95+
from pymatgen.io.vasp.sets import MPRelaxSet
96+
97+
vis = MPRelaxSet(user_incar_settings={"MAGMOM": {"In": 0.5, "Ga": 0.5, "As": -0.5}})
98+
```
99+
You can also specify different magnetic moments for different oxidation states, such as `{"Ga3+": 0.25}`.
100+
However, note that `"Ga0+"`, which has been assigned zero-valued oxidation state, is distinct from `"Ga"`, which has not been assigned an oxidation state.
101+
102+
Alternatively, MAGMOM can be set by giving a structure assigned magnetic moments: `structure.add_site_property("magmom", list[float])`.
103+
This will override the default MAGMOM settings of a VASP input set.
104+
45105
(vasp_workflows)=
46106

47107
## List of VASP workflows

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ defects = [
5151
"python-ulid>=2.7",
5252
]
5353
forcefields = [
54-
"ase>=3.25.0",
54+
"ase>=3.26.0",
5555
"calorine>=3.0",
5656
"chgnet>=0.2.2",
5757
"mace-torch>=0.3.3",
@@ -63,7 +63,7 @@ forcefields = [
6363
"torchdata<=0.7.1", # TODO: remove when issue fixed
6464
]
6565
approxneb = ["pymatgen-analysis-diffusion>=2024.7.15"]
66-
ase = ["ase>=3.25.0"]
66+
ase = ["ase>=3.26.0"]
6767
ase-ext = ["tblite>=0.3.0; platform_system=='Linux'"]
6868
openmm = [
6969
"mdanalysis>=2.8.0",
@@ -95,7 +95,7 @@ tests = [
9595
]
9696
strict = [
9797
"PyYAML==6.0.2",
98-
"ase==3.25.0",
98+
"ase==3.26.0",
9999
"cclib==1.8.1",
100100
"click==8.2.1",
101101
"custodian==2025.8.13",

src/atomate2/ase/md.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import contextlib
66
import io
7+
import logging
78
import os
89
import sys
910
import time
@@ -41,6 +42,8 @@
4142

4243
from atomate2.ase.schemas import AseMoleculeTaskDoc, AseStructureTaskDoc
4344

45+
logger = logging.getLogger(__name__)
46+
4447

4548
class MDEnsemble(Enum):
4649
"""Define known MD ensembles."""
@@ -60,12 +63,13 @@ class DynamicsPresets(Enum):
6063
nvt_nose_hoover = "ase.md.npt.NPT"
6164
npt_berendsen = "ase.md.nptberendsen.NPTBerendsen"
6265
npt_nose_hoover = "ase.md.npt.NPT" # noqa: PIE796
66+
npt_nose_hoover_chain = "ase.md.nose_hoover_chain.MTKNPT"
6367

6468

6569
default_dynamics = {
6670
MDEnsemble.nve: "velocityverlet",
6771
MDEnsemble.nvt: "langevin",
68-
MDEnsemble.npt: "nose-hoover",
72+
MDEnsemble.npt: "nose-hoover-chain",
6973
}
7074

7175
_valid_dynamics: dict[MDEnsemble, set[str]] = {}
@@ -251,18 +255,23 @@ def _get_ensemble_defaults(self) -> None:
251255
if (
252256
(
253257
isinstance(self.dynamics, DynamicsPresets)
254-
and DynamicsPresets(self.dynamics) == DynamicsPresets.npt_berendsen
258+
and DynamicsPresets(self.dynamics)
259+
== DynamicsPresets.npt_nose_hoover
255260
)
256261
or (
257262
isinstance(self.dynamics, type)
258263
and issubclass(self.dynamics, MolecularDynamics)
259-
and self.dynamics.__name__ == "NPTBerendsen"
264+
and self.dynamics.__name__ == "NPT"
260265
)
261-
or (isinstance(self.dynamics, str) and self.dynamics == "berendsen")
266+
or (isinstance(self.dynamics, str) and self.dynamics == "nose-hoover")
262267
):
263-
stress_kwarg = "pressure_au"
264-
else:
268+
logger.warning(
269+
"The `NPT` module in ASE is no longer recommended."
270+
"Users are advised to switch to Nose-Hoover chain / MTKNPT."
271+
)
265272
stress_kwarg = "externalstress"
273+
else:
274+
stress_kwarg = "pressure_au"
266275

267276
self.ase_md_kwargs[stress_kwarg] = self.ase_md_kwargs.get(
268277
stress_kwarg, self.p_schedule[0] * 1e3 * units.bar
@@ -340,8 +349,8 @@ def run_ase(
340349
if self.dynamics not in _valid_dynamics[self.ensemble]:
341350
raise ValueError(
342351
f"{self.dynamics} thermostat not available for "
343-
f"{self.ensemble.value}."
344-
f"Available {self.ensemble.value} thermostats are:"
352+
f"{self.ensemble.value}. "
353+
f"Available {self.ensemble.value} thermostats are: "
345354
" ".join(_valid_dynamics[self.ensemble])
346355
)
347356

@@ -391,7 +400,10 @@ def run_ase(
391400
def _callback(dyn: MolecularDynamics = md_runner) -> None:
392401
if self.ensemble == MDEnsemble.nve:
393402
return
394-
dyn.set_temperature(temperature_K=self.t_schedule[dyn.nsteps])
403+
if hasattr(dyn, "_temperature_K"):
404+
dyn._temperature_K = self.t_schedule[dyn.nsteps] # noqa: SLF001
405+
else:
406+
dyn.set_temperature(temperature_K=self.t_schedule[dyn.nsteps])
395407
if self.ensemble == MDEnsemble.nvt:
396408
return
397409

src/atomate2/ase/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,22 @@ def relax(
465465
write_atoms.calc = self.calculator
466466
else:
467467
write_atoms = atoms
468+
469+
# ase==3.26.0 change: writing FixAtoms and FixCartesian
470+
# constraints to extxyz supported.
471+
# Write only these constraints to extxyz
472+
if len(write_atoms.constraints) > 0:
473+
from ase.constraints import FixAtoms, FixCartesian
474+
475+
write_atoms_constraints = [
476+
cons
477+
for cons in write_atoms.constraints
478+
if isinstance(cons, FixAtoms | FixCartesian)
479+
]
480+
del write_atoms.constraints
481+
for cons in write_atoms_constraints:
482+
write_atoms.set_constraint(cons)
483+
468484
ase_write(
469485
final_atoms_object_file, write_atoms, format="extxyz", append=True
470486
)

src/atomate2/common/jobs/mpmorph.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
import os
1717
from pathlib import Path
1818
from tempfile import TemporaryDirectory
19+
from typing import TYPE_CHECKING
1920

2021
import numpy as np
2122
import pandas as pd
2223
from jobflow import Job
2324
from pymatgen.core import Composition, Molecule, Structure
2425
from pymatgen.io.packmol import PackmolBoxGen
2526

27+
if TYPE_CHECKING:
28+
from pymatgen.core import Element, Species
29+
2630
_DEFAULT_AVG_VOL_FILE = Path("~/.cache/atomate2").expanduser() / "db_avg_vols.json.gz"
2731
if not _DEFAULT_AVG_VOL_FILE.parents[0].exists():
2832
os.makedirs(_DEFAULT_AVG_VOL_FILE.parents[0], exist_ok=True)
@@ -80,14 +84,11 @@ def get_average_volume_from_mp_api(
8084
with MPRester(api_key=mp_api_key) as mpr:
8185
comp_entries = mpr.get_entries(composition.reduced_formula, inc_structure=True)
8286

83-
vols = None
84-
85-
if len(comp_entries) > 0:
86-
vols = [
87-
entry.structure.volume / entry.structure.num_sites for entry in comp_entries
88-
]
87+
vols = [
88+
entry.structure.volume / entry.structure.num_sites for entry in comp_entries
89+
]
8990

90-
else:
91+
if not vols:
9192
# Find all Materials project entries containing the elements in the
9293
# desired composition to estimate starting volume.
9394
with MPRester() as mpr:
@@ -104,6 +105,27 @@ def get_average_volume_from_mp_api(
104105

105106
vols = [entry.structure.volume / entry.structure.num_sites for entry in entries]
106107

108+
# Fallback: mix atomic volume by relative weight in composition
109+
if not vols:
110+
by_comp: dict[Element | Species, list[float]] = {
111+
ele: [] for ele in composition.elements
112+
}
113+
for entry in _entries:
114+
if len(entry.composition.elements) == 1:
115+
by_comp[entry.composition.elements[0]].append(
116+
entry.structure.volume / entry.structure.num_sites
117+
)
118+
vols = [
119+
coeff * np.mean(by_comp[ele]) / composition.num_atoms
120+
for ele, coeff in composition.items()
121+
]
122+
123+
if any(not v for v in by_comp.values()):
124+
raise ValueError(
125+
"No unary data for "
126+
f"{', '.join(str(k) for k, v in by_comp.items() if not v)}."
127+
)
128+
107129
return np.mean(vols)
108130

109131

@@ -250,10 +272,10 @@ def get_entry_from_dict(chem_env: str) -> dict | None:
250272
return {k: data[k].squeeze() for k in ("avg_vol", "count")}
251273
return None
252274

253-
chem_env_key = _get_chem_env_key_from_composition(
275+
full_chem_env_key = _get_chem_env_key_from_composition(
254276
composition, ignore_oxi_states=ignore_oxi_states
255277
)
256-
if (avg_vol := get_entry_from_dict(chem_env_key)) is not None:
278+
if (avg_vol := get_entry_from_dict(full_chem_env_key)) is not None:
257279
return avg_vol["avg_vol"]
258280

259281
vols = []
@@ -269,6 +291,19 @@ def get_entry_from_dict(chem_env: str) -> dict | None:
269291
vols.append(avg_vol["avg_vol"] * avg_vol["count"])
270292
counts += avg_vol["count"]
271293

294+
# Fallback, relative weight of monatomic volumes
295+
if counts == 0:
296+
by_comp = {ele: get_entry_from_dict(ele.name) for ele in composition.elements}
297+
if any(v is None for v in by_comp.values()):
298+
raise ValueError(
299+
"No unary data for "
300+
f"{', '.join(str(k) for k, v in by_comp.items() if v is None)}"
301+
)
302+
return (
303+
sum(coeff * by_comp[ele]["avg_vol"] for ele, coeff in composition.items())
304+
/ composition.num_atoms
305+
)
306+
272307
return sum(vols) / counts
273308

274309

0 commit comments

Comments
 (0)