Skip to content

Commit 70a66a2

Browse files
authored
fix(vasp): collect OUTCAR files from multiple directories (#1034)
1 parent a166808 commit 70a66a2

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

dpdata/plugins/vasp.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from typing import TYPE_CHECKING
45

56
import numpy as np
@@ -82,6 +83,36 @@ def to_system(self, data, frame_idx=0, **kwargs):
8283
@Format.register("outcar")
8384
@Format.register("vasp/outcar")
8485
class VASPOutcarFormat(Format):
86+
def from_multi_systems(self, directory, **kwargs):
87+
"""Find conventionally named OUTCAR files below ``directory``.
88+
89+
VASP calculations are commonly stored one calculation per directory,
90+
so the objects consumed by :meth:`from_labeled_system` are the OUTCAR
91+
files themselves rather than the calculation directories. Searching
92+
recursively also supports grouping calculations below intermediate
93+
directories such as workflow stages or temperatures.
94+
95+
Parameters
96+
----------
97+
directory : str or os.PathLike
98+
Root directory containing VASP calculation directories.
99+
**kwargs : dict
100+
Additional format options. They are consumed later when each
101+
discovered OUTCAR is loaded.
102+
103+
Returns
104+
-------
105+
list[str]
106+
Deterministically ordered paths to files named ``OUTCAR``.
107+
"""
108+
outcar_files = []
109+
for root, _, files in os.walk(directory):
110+
# Match VASP's canonical output name so unrelated files in the
111+
# calculation tree are not accidentally parsed as OUTCAR data.
112+
if "OUTCAR" in files:
113+
outcar_files.append(os.path.join(root, "OUTCAR"))
114+
return sorted(outcar_files)
115+
85116
@Format.post("rot_lower_triangular")
86117
def from_labeled_system(
87118
self, file_name, begin=0, step=1, convergence_check=True, **kwargs

tests/test_vasp_outcar.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22

33
import io
4+
import os
5+
import shutil
6+
import tempfile
47
import unittest
58
import warnings
69

@@ -202,6 +205,52 @@ def test(self):
202205
self.assertEqual(ss.get_nframes(), 2)
203206

204207

208+
class TestVaspOUTCARMultiSystems(unittest.TestCase):
209+
def test_loads_outcars_recursively_from_nested_directories(self):
210+
"""The CLI ``-m`` path must discover OUTCAR files, not parse directories."""
211+
212+
def nonzero_composition(system):
213+
"""Normalize away MultiSystems' shared zero-count type entries."""
214+
return frozenset(
215+
(name, count)
216+
for name, count in zip(
217+
system["atom_names"], system["atom_numbs"], strict=True
218+
)
219+
if count
220+
)
221+
222+
with tempfile.TemporaryDirectory() as tmpdir:
223+
calculation_dirs = [
224+
os.path.join(tmpdir, "Bond_calc", "calculation-000"),
225+
os.path.join(tmpdir, "heating", "temperature-300", "calculation-000"),
226+
]
227+
source_outcars = [
228+
os.path.join("poscars", "OUTCAR.Ge.vdw"),
229+
os.path.join("poscars", "Ti-O-Ti-v6", "OUTCAR"),
230+
]
231+
expected_compositions = {
232+
nonzero_composition(dpdata.LabeledSystem(source, fmt="vasp/outcar"))
233+
for source in source_outcars
234+
}
235+
for calculation_dir, source_outcar in zip(
236+
calculation_dirs, source_outcars, strict=True
237+
):
238+
os.makedirs(calculation_dir)
239+
shutil.copy(source_outcar, os.path.join(calculation_dir, "OUTCAR"))
240+
241+
systems = dpdata.MultiSystems.from_file(tmpdir, fmt="vasp/outcar")
242+
243+
self.assertEqual(len(systems), 2)
244+
self.assertEqual(systems.get_nframes(), 2)
245+
self.assertEqual({system.get_nframes() for system in systems}, {1})
246+
# MultiSystems aligns type maps and element order across systems, so
247+
# compare the non-zero compositions rather than display formulas.
248+
self.assertEqual(
249+
{nonzero_composition(system) for system in systems},
250+
expected_compositions,
251+
)
252+
253+
205254
class TestVaspAtomNamesV6(unittest.TestCase):
206255
def test(self):
207256
# in vasp v6, the key TITEL is removed. check if the atom names

0 commit comments

Comments
 (0)