pymdkit 1.2.12__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymdkit-1.2.12/src/pymdkit.egg-info → pymdkit-1.3.0}/PKG-INFO +4 -3
- {pymdkit-1.2.12 → pymdkit-1.3.0}/README.md +2 -2
- {pymdkit-1.2.12 → pymdkit-1.3.0}/pyproject.toml +2 -1
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/__init__.py +1 -1
- pymdkit-1.3.0/src/pymdkit/commands/_vasp_output.py +397 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/nep_rmse.py +76 -63
- {pymdkit-1.2.12 → pymdkit-1.3.0/src/pymdkit.egg-info}/PKG-INFO +4 -3
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit.egg-info/requires.txt +1 -0
- pymdkit-1.2.12/src/pymdkit/commands/_vasp_output.py +0 -174
- {pymdkit-1.2.12 → pymdkit-1.3.0}/LICENSE +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/setup.cfg +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/add_groups.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/electrostatic_energy.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/final_energy.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/gather_contcar.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/stable_entry.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/stru2xyz.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit.egg-info/SOURCES.txt +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.2.12 → pymdkit-1.3.0}/src/pymdkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -17,6 +17,7 @@ Requires-Dist: numpy
|
|
|
17
17
|
Requires-Dist: ase
|
|
18
18
|
Requires-Dist: pymatgen
|
|
19
19
|
Requires-Dist: mp-api
|
|
20
|
+
Requires-Dist: py4vasp-core
|
|
20
21
|
Requires-Dist: pyxtal
|
|
21
22
|
Requires-Dist: dpdata
|
|
22
23
|
Dynamic: license-file
|
|
@@ -66,7 +67,7 @@ directory for job sub-folders automatically.
|
|
|
66
67
|
| `final-energy` | List VASP job final energies from lowest to highest |
|
|
67
68
|
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
68
69
|
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
69
|
-
| `nep-rmse` | Compute NEP energy/force/stress RMSE with
|
|
70
|
+
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
70
71
|
| `perturb` | Generate perturbed structures with dpdata |
|
|
71
72
|
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
72
73
|
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
|
|
@@ -116,7 +117,7 @@ pymdkit substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta
|
|
|
116
117
|
pymdkit electrostatic-energy -i Li3YCl6.cif
|
|
117
118
|
pymdkit electrostatic-energy -if Li3YCl6-all
|
|
118
119
|
pymdkit nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
|
|
119
|
-
pymdkit nep-rmse -select
|
|
120
|
+
pymdkit nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
|
|
120
121
|
pymdkit perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
|
|
121
122
|
|
|
122
123
|
pymdkit rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
@@ -43,7 +43,7 @@ directory for job sub-folders automatically.
|
|
|
43
43
|
| `final-energy` | List VASP job final energies from lowest to highest |
|
|
44
44
|
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
45
45
|
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
46
|
-
| `nep-rmse` | Compute NEP energy/force/stress RMSE with
|
|
46
|
+
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
47
47
|
| `perturb` | Generate perturbed structures with dpdata |
|
|
48
48
|
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
49
49
|
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
|
|
@@ -93,7 +93,7 @@ pymdkit substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta
|
|
|
93
93
|
pymdkit electrostatic-energy -i Li3YCl6.cif
|
|
94
94
|
pymdkit electrostatic-energy -if Li3YCl6-all
|
|
95
95
|
pymdkit nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
|
|
96
|
-
pymdkit nep-rmse -select
|
|
96
|
+
pymdkit nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
|
|
97
97
|
pymdkit perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
|
|
98
98
|
|
|
99
99
|
pymdkit rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pymdkit"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "A unified command-line toolkit for atomistic / MD structure workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -28,6 +28,7 @@ dependencies = [
|
|
|
28
28
|
"ase",
|
|
29
29
|
"pymatgen",
|
|
30
30
|
"mp-api",
|
|
31
|
+
"py4vasp-core",
|
|
31
32
|
"pyxtal",
|
|
32
33
|
"dpdata",
|
|
33
34
|
]
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Shared VASP output readers.
|
|
2
|
+
|
|
3
|
+
Reader priority is always:
|
|
4
|
+
|
|
5
|
+
vaspout.h5 > vasprun.xml > OUTCAR
|
|
6
|
+
|
|
7
|
+
Commands that extract properties from VASP outputs should use this helper so
|
|
8
|
+
newer vaspout.h5 files are preferred consistently across pymdkit.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import math
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class VaspOutput:
|
|
20
|
+
folder: Path
|
|
21
|
+
source: str
|
|
22
|
+
parser: object
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def final_energy(self):
|
|
26
|
+
return getattr(self.parser, "final_energy")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def final_structure(self):
|
|
30
|
+
return getattr(self.parser, "final_structure", None)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ionic_steps(self):
|
|
34
|
+
return getattr(self.parser, "ionic_steps", [])
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def converged_electronic(self):
|
|
38
|
+
return bool(getattr(self.parser, "converged_electronic", False))
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def converged(self):
|
|
42
|
+
return bool(getattr(self.parser, "converged", self.converged_electronic))
|
|
43
|
+
|
|
44
|
+
def get_computed_entry(self, inc_structure=True, entry_id=None):
|
|
45
|
+
if not hasattr(self.parser, "get_computed_entry"):
|
|
46
|
+
from pymatgen.entries.computed_entries import ComputedStructureEntry
|
|
47
|
+
|
|
48
|
+
structure = self.final_structure
|
|
49
|
+
if structure is None:
|
|
50
|
+
structure_file = None
|
|
51
|
+
for name in ("CONTCAR", "POSCAR"):
|
|
52
|
+
candidate = self.folder / name
|
|
53
|
+
if candidate.exists():
|
|
54
|
+
structure_file = candidate
|
|
55
|
+
break
|
|
56
|
+
if structure_file is None:
|
|
57
|
+
raise AttributeError("get_computed_entry")
|
|
58
|
+
structure = _read_structure(structure_file)
|
|
59
|
+
return ComputedStructureEntry(
|
|
60
|
+
structure=structure,
|
|
61
|
+
energy=self.final_energy,
|
|
62
|
+
parameters={"run_type": "GGA"},
|
|
63
|
+
entry_id=entry_id or self.folder.name,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
entry = self.parser.get_computed_entry(
|
|
67
|
+
inc_structure=inc_structure,
|
|
68
|
+
entry_id=entry_id or self.folder.name,
|
|
69
|
+
)
|
|
70
|
+
if entry.parameters.get("run_type") == "PBE":
|
|
71
|
+
entry.parameters["run_type"] = "GGA"
|
|
72
|
+
return entry
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_structure(path):
|
|
76
|
+
from pymatgen.core import Structure
|
|
77
|
+
|
|
78
|
+
return Structure.from_file(str(path))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class OutcarAdapter:
|
|
82
|
+
"""Small adapter for the limited properties available from OUTCAR alone."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, path):
|
|
85
|
+
from pymatgen.io.vasp.outputs import Outcar
|
|
86
|
+
|
|
87
|
+
self.path = Path(path)
|
|
88
|
+
self.outcar = Outcar(str(path))
|
|
89
|
+
self._text = self.path.read_text(errors="ignore")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def final_energy(self):
|
|
93
|
+
for attr in ("final_energy", "final_fr_energy", "final_energy_wo_entrp"):
|
|
94
|
+
if hasattr(self.outcar, attr):
|
|
95
|
+
value = getattr(self.outcar, attr)
|
|
96
|
+
if value is not None:
|
|
97
|
+
return value
|
|
98
|
+
raise AttributeError("final_energy")
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def ionic_steps(self):
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def final_structure(self):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def converged_electronic(self):
|
|
110
|
+
markers = (
|
|
111
|
+
"aborting loop because EDIFF is reached",
|
|
112
|
+
"reached required accuracy",
|
|
113
|
+
)
|
|
114
|
+
return any(marker in self._text for marker in markers)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def converged(self):
|
|
118
|
+
return (self.converged_electronic
|
|
119
|
+
and "reached required accuracy" in self._text)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _last_scalar(value):
|
|
124
|
+
try:
|
|
125
|
+
import numpy as np
|
|
126
|
+
except ImportError:
|
|
127
|
+
np = None
|
|
128
|
+
|
|
129
|
+
if np is not None:
|
|
130
|
+
try:
|
|
131
|
+
array = np.asarray(value, dtype=float)
|
|
132
|
+
except (TypeError, ValueError):
|
|
133
|
+
return None
|
|
134
|
+
if array.size == 0:
|
|
135
|
+
return None
|
|
136
|
+
return float(array.reshape(-1)[-1])
|
|
137
|
+
if isinstance(value, (int, float)) and math.isfinite(value):
|
|
138
|
+
return float(value)
|
|
139
|
+
if isinstance(value, (list, tuple)) and value:
|
|
140
|
+
return _last_scalar(value[-1])
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _find_scalar_quantity(data, names):
|
|
145
|
+
if isinstance(data, dict):
|
|
146
|
+
for name in names:
|
|
147
|
+
if name in data:
|
|
148
|
+
value = _last_scalar(data[name])
|
|
149
|
+
if value is not None:
|
|
150
|
+
return value
|
|
151
|
+
for value in data.values():
|
|
152
|
+
found = _find_scalar_quantity(value, names)
|
|
153
|
+
if found is not None:
|
|
154
|
+
return found
|
|
155
|
+
elif isinstance(data, (list, tuple)):
|
|
156
|
+
for value in reversed(data):
|
|
157
|
+
found = _find_scalar_quantity(value, names)
|
|
158
|
+
if found is not None:
|
|
159
|
+
return found
|
|
160
|
+
return None
|
|
161
|
+
def _last_numeric_array(value):
|
|
162
|
+
try:
|
|
163
|
+
import numpy as np
|
|
164
|
+
except ImportError:
|
|
165
|
+
np = None
|
|
166
|
+
|
|
167
|
+
if np is not None:
|
|
168
|
+
try:
|
|
169
|
+
array = np.asarray(value, dtype=float)
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
return None
|
|
172
|
+
if array.size == 0:
|
|
173
|
+
return None
|
|
174
|
+
while array.ndim > 2 and array.shape[0] > 0:
|
|
175
|
+
array = array[-1]
|
|
176
|
+
if array.ndim == 0:
|
|
177
|
+
return float(array)
|
|
178
|
+
return array.tolist()
|
|
179
|
+
|
|
180
|
+
if isinstance(value, (int, float)) and math.isfinite(value):
|
|
181
|
+
return float(value)
|
|
182
|
+
if isinstance(value, (list, tuple)) and value:
|
|
183
|
+
return _last_numeric_array(value[-1])
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _find_quantity(data, names):
|
|
188
|
+
if isinstance(data, dict):
|
|
189
|
+
for name in names:
|
|
190
|
+
if name in data:
|
|
191
|
+
value = _last_numeric_array(data[name])
|
|
192
|
+
if value is not None:
|
|
193
|
+
return value
|
|
194
|
+
for value in data.values():
|
|
195
|
+
found = _find_quantity(value, names)
|
|
196
|
+
if found is not None:
|
|
197
|
+
return found
|
|
198
|
+
elif isinstance(data, (list, tuple)):
|
|
199
|
+
for value in reversed(data):
|
|
200
|
+
found = _find_quantity(value, names)
|
|
201
|
+
if found is not None:
|
|
202
|
+
return found
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _last_stress_array(value):
|
|
208
|
+
try:
|
|
209
|
+
import numpy as np
|
|
210
|
+
except ImportError:
|
|
211
|
+
np = None
|
|
212
|
+
|
|
213
|
+
if np is not None:
|
|
214
|
+
try:
|
|
215
|
+
array = np.asarray(value, dtype=float)
|
|
216
|
+
except (TypeError, ValueError):
|
|
217
|
+
return None
|
|
218
|
+
if array.size == 0:
|
|
219
|
+
return None
|
|
220
|
+
while array.ndim > 2 and array.shape[0] > 0:
|
|
221
|
+
array = array[-1]
|
|
222
|
+
if array.ndim == 2 and array.shape[1] in (6, 9):
|
|
223
|
+
array = array[-1]
|
|
224
|
+
if array.ndim == 0:
|
|
225
|
+
return float(array)
|
|
226
|
+
return array.tolist()
|
|
227
|
+
|
|
228
|
+
if isinstance(value, (int, float)) and math.isfinite(value):
|
|
229
|
+
return float(value)
|
|
230
|
+
if isinstance(value, (list, tuple)) and value:
|
|
231
|
+
if value and isinstance(value[0], (list, tuple)) and len(value[0]) in (6, 9):
|
|
232
|
+
return _last_stress_array(value[-1])
|
|
233
|
+
return _last_stress_array(value[-1])
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _find_stress_quantity(data, names):
|
|
238
|
+
if isinstance(data, dict):
|
|
239
|
+
for name in names:
|
|
240
|
+
if name in data:
|
|
241
|
+
value = _last_stress_array(data[name])
|
|
242
|
+
if value is not None:
|
|
243
|
+
return value
|
|
244
|
+
for value in data.values():
|
|
245
|
+
found = _find_stress_quantity(value, names)
|
|
246
|
+
if found is not None:
|
|
247
|
+
return found
|
|
248
|
+
elif isinstance(data, (list, tuple)):
|
|
249
|
+
for value in reversed(data):
|
|
250
|
+
found = _find_stress_quantity(value, names)
|
|
251
|
+
if found is not None:
|
|
252
|
+
return found
|
|
253
|
+
return None
|
|
254
|
+
class Py4VaspAdapter:
|
|
255
|
+
"""Adapter for reading vaspout.h5 with the official py4vasp package."""
|
|
256
|
+
|
|
257
|
+
def __init__(self, path):
|
|
258
|
+
import py4vasp
|
|
259
|
+
|
|
260
|
+
self.path = Path(path)
|
|
261
|
+
self.folder = self.path.parent
|
|
262
|
+
self.calculation = py4vasp.Calculation.from_path(str(self.folder))
|
|
263
|
+
self._final_energy = None
|
|
264
|
+
self._final_structure = None
|
|
265
|
+
self._ionic_steps = None
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def final_energy(self):
|
|
269
|
+
if self._final_energy is None:
|
|
270
|
+
data = self.calculation.energy.to_dict()
|
|
271
|
+
value = _find_scalar_quantity(data, (
|
|
272
|
+
"free energy TOTEN",
|
|
273
|
+
"free_energy",
|
|
274
|
+
"free energy",
|
|
275
|
+
"energy",
|
|
276
|
+
"total",
|
|
277
|
+
))
|
|
278
|
+
if value is None:
|
|
279
|
+
raise AttributeError("final_energy")
|
|
280
|
+
self._final_energy = value
|
|
281
|
+
return self._final_energy
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def final_structure(self):
|
|
285
|
+
if self._final_structure is None:
|
|
286
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
287
|
+
|
|
288
|
+
structure = self.calculation.structure
|
|
289
|
+
atoms = None
|
|
290
|
+
for getter in (
|
|
291
|
+
lambda: structure.to_ase(),
|
|
292
|
+
lambda: structure[-1].to_ase(),
|
|
293
|
+
lambda: structure[:].to_ase()[-1],
|
|
294
|
+
):
|
|
295
|
+
try:
|
|
296
|
+
atoms = getter()
|
|
297
|
+
break
|
|
298
|
+
except Exception:
|
|
299
|
+
continue
|
|
300
|
+
if atoms is None:
|
|
301
|
+
raise AttributeError("final_structure")
|
|
302
|
+
self._final_structure = AseAtomsAdaptor.get_structure(atoms)
|
|
303
|
+
return self._final_structure
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def ionic_steps(self):
|
|
307
|
+
if self._ionic_steps is None:
|
|
308
|
+
step = {}
|
|
309
|
+
try:
|
|
310
|
+
force_data = self.calculation.force.to_dict()
|
|
311
|
+
forces = _find_quantity(force_data, ("forces", "force"))
|
|
312
|
+
if forces is not None:
|
|
313
|
+
step["forces"] = forces
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
try:
|
|
317
|
+
stress_data = self.calculation.stress.to_dict()
|
|
318
|
+
stress = _find_stress_quantity(stress_data, ("stress", "stresses"))
|
|
319
|
+
if stress is not None:
|
|
320
|
+
step["stress"] = stress
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
self._ionic_steps = [step] if step else []
|
|
324
|
+
return self._ionic_steps
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def converged_electronic(self):
|
|
328
|
+
outcar = self.folder / "OUTCAR"
|
|
329
|
+
if outcar.exists():
|
|
330
|
+
try:
|
|
331
|
+
return OutcarAdapter(outcar).converged_electronic
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def converged(self):
|
|
338
|
+
outcar = self.folder / "OUTCAR"
|
|
339
|
+
if outcar.exists():
|
|
340
|
+
try:
|
|
341
|
+
return OutcarAdapter(outcar).converged
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
return self.converged_electronic
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _parse_vaspout(path):
|
|
348
|
+
return Py4VaspAdapter(path)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _parse_vasprun(path):
|
|
352
|
+
from pymatgen.io.vasp.outputs import Vasprun
|
|
353
|
+
|
|
354
|
+
return Vasprun(str(path), parse_dos=False, parse_eigen=False)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _parse_outcar(path):
|
|
358
|
+
return OutcarAdapter(path)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def read_vasp_output(folder, require=("final_energy",)):
|
|
362
|
+
"""Read the best available VASP output in *folder*.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
folder: VASP job folder.
|
|
366
|
+
require: Attribute names that must be available on the parser.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
VaspOutput or None if no parser can provide the requested properties.
|
|
370
|
+
"""
|
|
371
|
+
folder = Path(folder)
|
|
372
|
+
candidates = (
|
|
373
|
+
("vaspout.h5", _parse_vaspout),
|
|
374
|
+
("vasprun.xml", _parse_vasprun),
|
|
375
|
+
("OUTCAR", _parse_outcar),
|
|
376
|
+
)
|
|
377
|
+
for filename, parser_fn in candidates:
|
|
378
|
+
path = folder / filename
|
|
379
|
+
if not path.exists():
|
|
380
|
+
continue
|
|
381
|
+
try:
|
|
382
|
+
parser = parser_fn(path)
|
|
383
|
+
output = VaspOutput(folder=folder, source=filename, parser=parser)
|
|
384
|
+
for attr in require:
|
|
385
|
+
value = getattr(output, attr)
|
|
386
|
+
if value is None:
|
|
387
|
+
raise AttributeError(attr)
|
|
388
|
+
return output
|
|
389
|
+
except Exception:
|
|
390
|
+
continue
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def has_vasp_output(folder):
|
|
395
|
+
folder = Path(folder)
|
|
396
|
+
return any((folder / name).exists()
|
|
397
|
+
for name in ("vaspout.h5", "vasprun.xml", "OUTCAR", "CONTCAR"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Compute and
|
|
1
|
+
"""Compute and terminal-plot NEP train RMSE for energy, force, and stress."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ import math
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
COMMAND = "nep-rmse"
|
|
10
|
-
HELP = "Compute NEP energy/force/stress RMSE and draw
|
|
10
|
+
HELP = "Compute NEP energy/force/stress RMSE and draw terminal plots."
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
@@ -18,10 +18,12 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
18
18
|
help="NEP force_train.out file.")
|
|
19
19
|
parser.add_argument("-stress-train", dest="stress_train", default="stress_train.out",
|
|
20
20
|
help="NEP stress_train.out file.")
|
|
21
|
-
parser.add_argument("-select
|
|
21
|
+
parser.add_argument("-select", dest="select_candidate", action="store_true",
|
|
22
22
|
help="Interactively select candidate structures from train.xyz.")
|
|
23
|
+
parser.add_argument("-select-candidate", dest="select_candidate",
|
|
24
|
+
action="store_true", help=argparse.SUPPRESS)
|
|
23
25
|
parser.add_argument("-xyz", default="train.xyz",
|
|
24
|
-
help="Training trajectory for -select
|
|
26
|
+
help="Training trajectory for -select mode.")
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def load_training(args):
|
|
@@ -101,7 +103,10 @@ def write_rmse_report(sections):
|
|
|
101
103
|
def axis_range(*tables):
|
|
102
104
|
import numpy as np
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
available = [t for t in tables if t is not None and len(t)]
|
|
107
|
+
if not available:
|
|
108
|
+
return -1.0, 1.0
|
|
109
|
+
merged = np.vstack(available)
|
|
105
110
|
lo = float(np.min(merged))
|
|
106
111
|
hi = float(np.max(merged))
|
|
107
112
|
pad = 0.1 * (hi - lo) if hi > lo else 1.0
|
|
@@ -116,54 +121,45 @@ def metric_value(metric, vals):
|
|
|
116
121
|
return vals["stress_GPa"], "GPa", 4
|
|
117
122
|
|
|
118
123
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
def _place_point(grid, x, y, xmin, xmax, marker):
|
|
125
|
+
width = len(grid[0])
|
|
126
|
+
height = len(grid)
|
|
127
|
+
if xmax == xmin:
|
|
128
|
+
return
|
|
129
|
+
col = int(round((x - xmin) / (xmax - xmin) * (width - 1)))
|
|
130
|
+
row = int(round((xmax - y) / (xmax - xmin) * (height - 1)))
|
|
131
|
+
if 0 <= row < height and 0 <= col < width:
|
|
132
|
+
grid[row][col] = marker
|
|
125
133
|
|
|
126
|
-
def place(x, y, char):
|
|
127
|
-
if xmax == xmin:
|
|
128
|
-
return
|
|
129
|
-
col = int(round((x - xmin) / (xmax - xmin) * (width - 1)))
|
|
130
|
-
row = int(round((xmax - y) / (xmax - xmin) * (height - 1)))
|
|
131
|
-
if 0 <= row < height and 0 <= col < width:
|
|
132
|
-
grid[row][col] = char
|
|
133
|
-
|
|
134
|
-
for i in range(width):
|
|
135
|
-
x = xmin + (xmax - xmin) * i / max(width - 1, 1)
|
|
136
|
-
place(x, x, "/")
|
|
137
134
|
|
|
135
|
+
def _plot_panel(table, candidate_table=None, accurate_table=None,
|
|
136
|
+
width=56, height=18):
|
|
137
|
+
xmin, xmax = axis_range(table, candidate_table, accurate_table)
|
|
138
|
+
grid = [[" " for _ in range(width)] for _ in range(height)]
|
|
138
139
|
if candidate_table is not None or accurate_table is not None:
|
|
139
140
|
if accurate_table is not None:
|
|
140
141
|
for x, y in accurate_table:
|
|
141
|
-
|
|
142
|
+
_place_point(grid, x, y, xmin, xmax, "\u25CB")
|
|
142
143
|
if candidate_table is not None:
|
|
143
144
|
for x, y in candidate_table:
|
|
144
|
-
|
|
145
|
-
legend = ".=candidate, o=accurate, /=DFT=NEP"
|
|
145
|
+
_place_point(grid, x, y, xmin, xmax, "\u25CF")
|
|
146
146
|
else:
|
|
147
147
|
for x, y in table:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
print("|" + "".join(row) + "|")
|
|
164
|
-
print("+" + "-" * width + "+")
|
|
165
|
-
print(f"x: {xlabel}")
|
|
166
|
-
print(f"range: {xmin:.6g} to {xmax:.6g}; {legend}")
|
|
148
|
+
_place_point(grid, x, y, xmin, xmax, "\u25CB")
|
|
149
|
+
return ["".join(row) for row in grid]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _rmse_plot_title(all_vals, candidate_vals=None, accurate_vals=None):
|
|
153
|
+
parts = []
|
|
154
|
+
for metric, label in (("energy", "Energy"), ("force", "Force"), ("stress", "Stress")):
|
|
155
|
+
value, unit, prec = metric_value(metric, all_vals)
|
|
156
|
+
text = f"{label}: all {value:.{prec}f} {unit}"
|
|
157
|
+
if candidate_vals is not None and accurate_vals is not None:
|
|
158
|
+
cand, _, _ = metric_value(metric, candidate_vals)
|
|
159
|
+
acc, _, _ = metric_value(metric, accurate_vals)
|
|
160
|
+
text += f", selected {cand:.{prec}f}, unselected {acc:.{prec}f}"
|
|
161
|
+
parts.append(text)
|
|
162
|
+
return " | ".join(parts)
|
|
167
163
|
|
|
168
164
|
|
|
169
165
|
def plot_rmse(all_vals, energy_txt, force_txt, stress_txt,
|
|
@@ -171,19 +167,18 @@ def plot_rmse(all_vals, energy_txt, force_txt, stress_txt,
|
|
|
171
167
|
candidate_vals=None, accurate_vals=None):
|
|
172
168
|
candidate_tables = candidate_tables or {}
|
|
173
169
|
accurate_tables = accurate_tables or {}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
panels = [
|
|
171
|
+
_plot_panel(energy_txt, candidate_tables.get("energy"), accurate_tables.get("energy")),
|
|
172
|
+
_plot_panel(force_txt, candidate_tables.get("force"), accurate_tables.get("force")),
|
|
173
|
+
_plot_panel(stress_txt, candidate_tables.get("stress"), accurate_tables.get("stress")),
|
|
174
|
+
]
|
|
175
|
+
width = len(panels[0][0])
|
|
176
|
+
print("\nNEP RMSE plot")
|
|
177
|
+
print(_rmse_plot_title(all_vals, candidate_vals, accurate_vals))
|
|
178
|
+
print("\u250C" + ("\u2500" * width) + "\u252C" + ("\u2500" * width) + "\u252C" + ("\u2500" * width) + "\u2510")
|
|
179
|
+
for row in range(len(panels[0])):
|
|
180
|
+
print("\u2502" + panels[0][row] + "\u2502" + panels[1][row] + "\u2502" + panels[2][row] + "\u2502")
|
|
181
|
+
print("\u2514" + ("\u2500" * width) + "\u2534" + ("\u2500" * width) + "\u2534" + ("\u2500" * width) + "\u2518")
|
|
187
182
|
|
|
188
183
|
def frame_atom_slices(frames):
|
|
189
184
|
slices = []
|
|
@@ -203,19 +198,37 @@ def select_indices(energy_txt, proportion):
|
|
|
203
198
|
return np.sort(order[:n_candidate]), np.sort(order[n_candidate:])
|
|
204
199
|
|
|
205
200
|
|
|
201
|
+
def _equal_frame_rows(table, frames, label):
|
|
202
|
+
if len(frames) == 0:
|
|
203
|
+
return []
|
|
204
|
+
if len(table) % len(frames) != 0:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"{label} rows ({len(table)}) cannot be divided among "
|
|
207
|
+
f"{len(frames)} structures for selection mode")
|
|
208
|
+
rows_per_frame = len(table) // len(frames)
|
|
209
|
+
return [slice(i * rows_per_frame, (i + 1) * rows_per_frame)
|
|
210
|
+
for i in range(len(frames))]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _rows_from_slices(slices, indices):
|
|
214
|
+
rows = []
|
|
215
|
+
for idx in indices:
|
|
216
|
+
rows.extend(range(slices[idx].start, slices[idx].stop))
|
|
217
|
+
return rows
|
|
218
|
+
|
|
219
|
+
|
|
206
220
|
def subset_tables(energy_txt, force_txt, stress_txt, frames, indices):
|
|
207
221
|
import numpy as np
|
|
208
222
|
|
|
209
223
|
atom_slices = frame_atom_slices(frames)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
force_rows =
|
|
213
|
-
|
|
214
|
-
force_rows.extend(range(atom_slices[idx].start * 3, atom_slices[idx].stop * 3))
|
|
224
|
+
force_slices = [slice(s.start * 3, s.stop * 3) for s in atom_slices]
|
|
225
|
+
stress_slices = _equal_frame_rows(stress_txt, frames, "stress_train")
|
|
226
|
+
force_rows = _rows_from_slices(force_slices, indices)
|
|
227
|
+
stress_rows = _rows_from_slices(stress_slices, indices)
|
|
215
228
|
return (
|
|
216
229
|
energy_txt[indices],
|
|
217
230
|
force_txt[force_rows] if force_rows else np.empty((0, 2)),
|
|
218
|
-
stress_txt[
|
|
231
|
+
stress_txt[stress_rows] if stress_rows else np.empty((0, 2)),
|
|
219
232
|
)
|
|
220
233
|
|
|
221
234
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -17,6 +17,7 @@ Requires-Dist: numpy
|
|
|
17
17
|
Requires-Dist: ase
|
|
18
18
|
Requires-Dist: pymatgen
|
|
19
19
|
Requires-Dist: mp-api
|
|
20
|
+
Requires-Dist: py4vasp-core
|
|
20
21
|
Requires-Dist: pyxtal
|
|
21
22
|
Requires-Dist: dpdata
|
|
22
23
|
Dynamic: license-file
|
|
@@ -66,7 +67,7 @@ directory for job sub-folders automatically.
|
|
|
66
67
|
| `final-energy` | List VASP job final energies from lowest to highest |
|
|
67
68
|
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
68
69
|
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
69
|
-
| `nep-rmse` | Compute NEP energy/force/stress RMSE with
|
|
70
|
+
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
70
71
|
| `perturb` | Generate perturbed structures with dpdata |
|
|
71
72
|
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
72
73
|
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
|
|
@@ -116,7 +117,7 @@ pymdkit substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta
|
|
|
116
117
|
pymdkit electrostatic-energy -i Li3YCl6.cif
|
|
117
118
|
pymdkit electrostatic-energy -if Li3YCl6-all
|
|
118
119
|
pymdkit nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
|
|
119
|
-
pymdkit nep-rmse -select
|
|
120
|
+
pymdkit nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
|
|
120
121
|
pymdkit perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
|
|
121
122
|
|
|
122
123
|
pymdkit rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
"""Shared VASP output readers.
|
|
2
|
-
|
|
3
|
-
Reader priority is always:
|
|
4
|
-
|
|
5
|
-
vaspout.h5 > vasprun.xml > OUTCAR
|
|
6
|
-
|
|
7
|
-
Commands that extract properties from VASP outputs should use this helper so
|
|
8
|
-
newer vaspout.h5 files are preferred consistently across pymdkit.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class VaspOutput:
|
|
19
|
-
folder: Path
|
|
20
|
-
source: str
|
|
21
|
-
parser: object
|
|
22
|
-
|
|
23
|
-
@property
|
|
24
|
-
def final_energy(self):
|
|
25
|
-
return getattr(self.parser, "final_energy")
|
|
26
|
-
|
|
27
|
-
@property
|
|
28
|
-
def final_structure(self):
|
|
29
|
-
return getattr(self.parser, "final_structure", None)
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
def ionic_steps(self):
|
|
33
|
-
return getattr(self.parser, "ionic_steps", [])
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def converged_electronic(self):
|
|
37
|
-
return bool(getattr(self.parser, "converged_electronic", False))
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def converged(self):
|
|
41
|
-
return bool(getattr(self.parser, "converged", self.converged_electronic))
|
|
42
|
-
|
|
43
|
-
def get_computed_entry(self, inc_structure=True, entry_id=None):
|
|
44
|
-
if not hasattr(self.parser, "get_computed_entry"):
|
|
45
|
-
from pymatgen.entries.computed_entries import ComputedStructureEntry
|
|
46
|
-
|
|
47
|
-
structure_file = None
|
|
48
|
-
for name in ("CONTCAR", "POSCAR"):
|
|
49
|
-
candidate = self.folder / name
|
|
50
|
-
if candidate.exists():
|
|
51
|
-
structure_file = candidate
|
|
52
|
-
break
|
|
53
|
-
if structure_file is None:
|
|
54
|
-
raise AttributeError("get_computed_entry")
|
|
55
|
-
structure = _read_structure(structure_file)
|
|
56
|
-
return ComputedStructureEntry(
|
|
57
|
-
structure=structure,
|
|
58
|
-
energy=self.final_energy,
|
|
59
|
-
parameters={"run_type": "GGA"},
|
|
60
|
-
entry_id=entry_id or self.folder.name,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
entry = self.parser.get_computed_entry(
|
|
64
|
-
inc_structure=inc_structure,
|
|
65
|
-
entry_id=entry_id or self.folder.name,
|
|
66
|
-
)
|
|
67
|
-
if entry.parameters.get("run_type") == "PBE":
|
|
68
|
-
entry.parameters["run_type"] = "GGA"
|
|
69
|
-
return entry
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _read_structure(path):
|
|
73
|
-
from pymatgen.core import Structure
|
|
74
|
-
|
|
75
|
-
return Structure.from_file(str(path))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class OutcarAdapter:
|
|
79
|
-
"""Small adapter for the limited properties available from OUTCAR alone."""
|
|
80
|
-
|
|
81
|
-
def __init__(self, path):
|
|
82
|
-
from pymatgen.io.vasp.outputs import Outcar
|
|
83
|
-
|
|
84
|
-
self.path = Path(path)
|
|
85
|
-
self.outcar = Outcar(str(path))
|
|
86
|
-
self._text = self.path.read_text(errors="ignore")
|
|
87
|
-
|
|
88
|
-
@property
|
|
89
|
-
def final_energy(self):
|
|
90
|
-
for attr in ("final_energy", "final_fr_energy", "final_energy_wo_entrp"):
|
|
91
|
-
if hasattr(self.outcar, attr):
|
|
92
|
-
value = getattr(self.outcar, attr)
|
|
93
|
-
if value is not None:
|
|
94
|
-
return value
|
|
95
|
-
raise AttributeError("final_energy")
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def ionic_steps(self):
|
|
99
|
-
return []
|
|
100
|
-
|
|
101
|
-
@property
|
|
102
|
-
def final_structure(self):
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
@property
|
|
106
|
-
def converged_electronic(self):
|
|
107
|
-
markers = (
|
|
108
|
-
"aborting loop because EDIFF is reached",
|
|
109
|
-
"reached required accuracy",
|
|
110
|
-
)
|
|
111
|
-
return any(marker in self._text for marker in markers)
|
|
112
|
-
|
|
113
|
-
@property
|
|
114
|
-
def converged(self):
|
|
115
|
-
return (self.converged_electronic
|
|
116
|
-
and "reached required accuracy" in self._text)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _parse_vaspout(path):
|
|
120
|
-
from pymatgen.io.vasp.outputs import Vaspout
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
return Vaspout(str(path), parse_dos=False, parse_eigen=False)
|
|
124
|
-
except TypeError:
|
|
125
|
-
return Vaspout(str(path))
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _parse_vasprun(path):
|
|
129
|
-
from pymatgen.io.vasp.outputs import Vasprun
|
|
130
|
-
|
|
131
|
-
return Vasprun(str(path), parse_dos=False, parse_eigen=False)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _parse_outcar(path):
|
|
135
|
-
return OutcarAdapter(path)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def read_vasp_output(folder, require=("final_energy",)):
|
|
139
|
-
"""Read the best available VASP output in *folder*.
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
folder: VASP job folder.
|
|
143
|
-
require: Attribute names that must be available on the parser.
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
VaspOutput or None if no parser can provide the requested properties.
|
|
147
|
-
"""
|
|
148
|
-
folder = Path(folder)
|
|
149
|
-
candidates = (
|
|
150
|
-
("vaspout.h5", _parse_vaspout),
|
|
151
|
-
("vasprun.xml", _parse_vasprun),
|
|
152
|
-
("OUTCAR", _parse_outcar),
|
|
153
|
-
)
|
|
154
|
-
for filename, parser_fn in candidates:
|
|
155
|
-
path = folder / filename
|
|
156
|
-
if not path.exists():
|
|
157
|
-
continue
|
|
158
|
-
try:
|
|
159
|
-
parser = parser_fn(path)
|
|
160
|
-
output = VaspOutput(folder=folder, source=filename, parser=parser)
|
|
161
|
-
for attr in require:
|
|
162
|
-
value = getattr(output, attr)
|
|
163
|
-
if value is None:
|
|
164
|
-
raise AttributeError(attr)
|
|
165
|
-
return output
|
|
166
|
-
except Exception:
|
|
167
|
-
continue
|
|
168
|
-
return None
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def has_vasp_output(folder):
|
|
172
|
-
folder = Path(folder)
|
|
173
|
-
return any((folder / name).exists()
|
|
174
|
-
for name in ("vaspout.h5", "vasprun.xml", "OUTCAR", "CONTCAR"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|