From d8791284986bf2bc73743a6dc3dd21522882c70d Mon Sep 17 00:00:00 2001 From: njzjz-bot Date: Thu, 16 Jul 2026 12:53:18 +0800 Subject: [PATCH 1/3] fix(vasp): load nested OUTCAR multisystems Discover canonically named OUTCAR files recursively so the multi-system CLI path can combine one-calculation-per-directory VASP datasets. Add a regression covering nested single-frame calculations. Coding-Agent: Codex Codex-Version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- dpdata/plugins/vasp.py | 31 +++++++++++++++++++++++++++++++ tests/test_vasp_outcar.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/dpdata/plugins/vasp.py b/dpdata/plugins/vasp.py index 84d3142b..a1014c8c 100644 --- a/dpdata/plugins/vasp.py +++ b/dpdata/plugins/vasp.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from typing import TYPE_CHECKING import numpy as np @@ -82,6 +83,36 @@ def to_system(self, data, frame_idx=0, **kwargs): @Format.register("outcar") @Format.register("vasp/outcar") class VASPOutcarFormat(Format): + def from_multi_systems(self, directory, **kwargs): + """Find conventionally named OUTCAR files below ``directory``. + + VASP calculations are commonly stored one calculation per directory, + so the objects consumed by :meth:`from_labeled_system` are the OUTCAR + files themselves rather than the calculation directories. Searching + recursively also supports grouping calculations below intermediate + directories such as workflow stages or temperatures. + + Parameters + ---------- + directory : str or os.PathLike + Root directory containing VASP calculation directories. + **kwargs : dict + Additional format options. They are consumed later when each + discovered OUTCAR is loaded. + + Returns + ------- + list[str] + Deterministically ordered paths to files named ``OUTCAR``. + """ + outcar_files = [] + for root, _, files in os.walk(directory): + # Match VASP's canonical output name so unrelated files in the + # calculation tree are not accidentally parsed as OUTCAR data. + if "OUTCAR" in files: + outcar_files.append(os.path.join(root, "OUTCAR")) + return sorted(outcar_files) + @Format.post("rot_lower_triangular") def from_labeled_system( self, file_name, begin=0, step=1, convergence_check=True, **kwargs diff --git a/tests/test_vasp_outcar.py b/tests/test_vasp_outcar.py index 68856877..8716b27a 100644 --- a/tests/test_vasp_outcar.py +++ b/tests/test_vasp_outcar.py @@ -1,5 +1,8 @@ from __future__ import annotations +import os +import shutil +import tempfile import unittest import numpy as np @@ -122,6 +125,31 @@ def test(self): self.assertEqual(ss.get_nframes(), 2) +class TestVaspOUTCARMultiSystems(unittest.TestCase): + def test_loads_outcars_recursively_from_nested_directories(self): + """The CLI ``-m`` path must discover OUTCAR files, not parse directories.""" + with tempfile.TemporaryDirectory() as tmpdir: + calculation_dirs = [ + os.path.join(tmpdir, "Bond_calc", "calculation-000"), + os.path.join(tmpdir, "heating", "temperature-300", "calculation-000"), + ] + source_outcars = [ + os.path.join("poscars", "OUTCAR.Ge.vdw"), + os.path.join("poscars", "Ti-O-Ti-v6", "OUTCAR"), + ] + for calculation_dir, source_outcar in zip( + calculation_dirs, source_outcars, strict=True + ): + os.makedirs(calculation_dir) + shutil.copy(source_outcar, os.path.join(calculation_dir, "OUTCAR")) + + systems = dpdata.MultiSystems.from_file(tmpdir, fmt="vasp/outcar") + + self.assertEqual(len(systems), 2) + self.assertEqual(systems.get_nframes(), 2) + self.assertEqual({system.get_nframes() for system in systems}, {1}) + + class TestVaspAtomNamesV6(unittest.TestCase): def test(self): # in vasp v6, the key TITEL is removed. check if the atom names From 42450f96dc70663cbe9aa53b7d632d3c19d38f11 Mon Sep 17 00:00:00 2001 From: njzjz-bot Date: Tue, 21 Jul 2026 14:07:29 +0800 Subject: [PATCH 2/3] test(vasp): verify discovered system compositions Strengthen recursive OUTCAR coverage by checking that both loaded systems preserve the non-zero compositions of their source calculations. Coding-Agent: Codex Codex-Version: codex-cli 0.144.6 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- tests/test_vasp_outcar.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_vasp_outcar.py b/tests/test_vasp_outcar.py index 8716b27a..40aadc7e 100644 --- a/tests/test_vasp_outcar.py +++ b/tests/test_vasp_outcar.py @@ -128,6 +128,17 @@ def test(self): class TestVaspOUTCARMultiSystems(unittest.TestCase): def test_loads_outcars_recursively_from_nested_directories(self): """The CLI ``-m`` path must discover OUTCAR files, not parse directories.""" + + def nonzero_composition(system): + """Normalize away MultiSystems' shared zero-count type entries.""" + return frozenset( + (name, count) + for name, count in zip( + system["atom_names"], system["atom_numbs"], strict=True + ) + if count + ) + with tempfile.TemporaryDirectory() as tmpdir: calculation_dirs = [ os.path.join(tmpdir, "Bond_calc", "calculation-000"), @@ -137,6 +148,10 @@ def test_loads_outcars_recursively_from_nested_directories(self): os.path.join("poscars", "OUTCAR.Ge.vdw"), os.path.join("poscars", "Ti-O-Ti-v6", "OUTCAR"), ] + expected_compositions = { + nonzero_composition(dpdata.LabeledSystem(source, fmt="vasp/outcar")) + for source in source_outcars + } for calculation_dir, source_outcar in zip( calculation_dirs, source_outcars, strict=True ): @@ -148,6 +163,12 @@ def test_loads_outcars_recursively_from_nested_directories(self): self.assertEqual(len(systems), 2) self.assertEqual(systems.get_nframes(), 2) self.assertEqual({system.get_nframes() for system in systems}, {1}) + # MultiSystems aligns type maps and element order across systems, so + # compare the non-zero compositions rather than display formulas. + self.assertEqual( + {nonzero_composition(system) for system in systems}, + expected_compositions, + ) class TestVaspAtomNamesV6(unittest.TestCase): From 2c640e91a4416117deb4cbcef9caf07b178bf533 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:54:34 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_vasp_outcar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vasp_outcar.py b/tests/test_vasp_outcar.py index 5d83f9dc..744a8605 100644 --- a/tests/test_vasp_outcar.py +++ b/tests/test_vasp_outcar.py @@ -1,9 +1,9 @@ from __future__ import annotations +import io import os import shutil import tempfile -import io import unittest import warnings