Skip to content

Commit 27c7a52

Browse files
authored
fix(abacus): omit unavailable MD force labels (deepmodeling#1024)
Fixes deepmodeling#1000. Represent missing ABACUS MD FORCE columns as unavailable labels instead of fabricated zero arrays. Tests: `cd tests && python -m unittest test_abacus_md.TestABACUSMD` Why existing tests missed it: All existing ABACUS MD fixtures contained FORCE columns; neither the helper nor full LabeledSystem path covered a no-force dump. Coding agent: Codex Codex version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning effort: xhigh Co-authored-by: njzjz-bot <njzjz.bot@gmail.com>
1 parent 055cf69 commit 27c7a52

2 files changed

Lines changed: 53 additions & 3 deletions

File tree

dpdata/formats/abacus/md.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ def get_coords_from_dump(dumplines, natoms):
7272
)
7373
cells = np.zeros([nframes_dump, 3, 3])
7474
stresses = np.zeros([nframes_dump, 3, 3])
75-
forces = np.zeros([nframes_dump, total_natoms, 3])
75+
# A missing FORCE column means labels are unavailable, not that every
76+
# force is exactly zero. Keep this distinction so callers never train on
77+
# fabricated labels that merely look well-shaped.
78+
forces = np.zeros([nframes_dump, total_natoms, 3]) if calc_force else None
7679
coords = np.zeros([nframes_dump, total_natoms, 3])
7780
iframe = 0
7881
for iline in range(nlines):
@@ -116,6 +119,7 @@ def get_coords_from_dump(dumplines, natoms):
116119
coords[iframe, iat] *= celldm
117120

118121
if calc_force:
122+
assert forces is not None
119123
forces[iframe, iat] = np.array(
120124
[
121125
float(i)
@@ -187,7 +191,8 @@ def get_frame(fname):
187191
if np.isnan(iene):
188192
coords = np.delete(coords, i - ndump, axis=0)
189193
cells = np.delete(cells, i - ndump, axis=0)
190-
force = np.delete(force, i - ndump, axis=0)
194+
if force is not None:
195+
force = np.delete(force, i - ndump, axis=0)
191196
stress = np.delete(stress, i - ndump, axis=0)
192197
energy = np.delete(energy, i - ndump, axis=0)
193198
unconv_stru += "%d " % i # noqa: UP031
@@ -207,7 +212,8 @@ def get_frame(fname):
207212
# data['cells'][:, :, :] = cell
208213
data["coords"] = coords
209214
data["energies"] = energy
210-
data["forces"] = force
215+
if force is not None:
216+
data["forces"] = force
211217
data["virials"] = stress
212218
if not isinstance(data["virials"], np.ndarray):
213219
del data["virials"]

tests/test_abacus_md.py

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

33
import os
4+
import shutil
5+
import tempfile
46
import unittest
57

68
import numpy as np
79
from context import dpdata
810

11+
from dpdata.formats.abacus.md import get_coords_from_dump
912
from dpdata.unit import LengthConversion
1013

1114
bohr2ang = LengthConversion("bohr", "angstrom").value()
@@ -30,6 +33,47 @@ def tearDown(self):
3033
if os.path.isfile("abacus.md/water_stru"):
3134
os.remove("abacus.md/water_stru")
3235

36+
def test_missing_force_column_does_not_create_zero_labels(self):
37+
dump = """MDSTEP: 0
38+
LATTICE_CONSTANT: 1.0 Angstrom
39+
LATTICE_VECTORS
40+
1.0 0.0 0.0
41+
0.0 1.0 0.0
42+
0.0 0.0 1.0
43+
POSITION
44+
0 H 0.0 0.0 0.0
45+
46+
47+
""".splitlines()
48+
_, _, forces, _ = get_coords_from_dump(dump, [1])
49+
self.assertIsNone(forces)
50+
51+
def test_no_force_trajectory_omits_force_key(self):
52+
"""Exercise the complete format path, not just the dump helper."""
53+
with tempfile.TemporaryDirectory() as tmpdir:
54+
shutil.copy("abacus.md.nostress/INPUT", tmpdir)
55+
shutil.copy("abacus.md.nostress/STRU", tmpdir)
56+
source_out = "abacus.md.nostress/OUT.autotest"
57+
target_out = os.path.join(tmpdir, "OUT.autotest")
58+
os.mkdir(target_out)
59+
shutil.copy(os.path.join(source_out, "running_md.log"), target_out)
60+
61+
# Preserve the real trajectory framing while removing only the
62+
# optional force columns from the header and atom records.
63+
with open(os.path.join(source_out, "MD_dump")) as fp:
64+
lines = fp.readlines()
65+
with open(os.path.join(target_out, "MD_dump"), "w") as fp:
66+
for line in lines:
67+
if "FORCE" in line and "POSITION" in line:
68+
fp.write("INDEX LABEL POSITION (Angstrom)\n")
69+
elif len(line.split()) >= 8 and line.split()[0].isdigit():
70+
fp.write(" " + " ".join(line.split()[:5]) + "\n")
71+
else:
72+
fp.write(line)
73+
74+
system = dpdata.LabeledSystem(tmpdir, fmt="abacus/md")
75+
self.assertNotIn("forces", system.data)
76+
3377
def test_atom_names(self):
3478
self.assertEqual(self.system_water.data["atom_names"], ["H", "O"])
3579
self.assertEqual(self.system_Si.data["atom_names"], ["Si"])

0 commit comments

Comments
 (0)