Skip to content

Commit db0cc99

Browse files
authored
fix(dftbplus): parse all geometry and force rows (#1018)
1 parent 4ee3029 commit db0cc99

2 files changed

Lines changed: 103 additions & 28 deletions

File tree

dpdata/formats/dftbplus/output.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,42 +38,52 @@ def read_dftb_plus(
3838
symbols = None
3939
forces = None
4040
energy = None
41+
natoms = None
4142
with open_file(fn_1) as f:
42-
flag = 0
43-
for line in f:
44-
if flag == 1:
45-
flag += 1
46-
elif flag == 2:
47-
components = line.split()
48-
flag += 1
49-
elif line.startswith("Geometry"):
50-
flag = 1
51-
coord = []
52-
symbols = []
53-
elif flag in (3, 4, 5, 6):
54-
s = line.split()
55-
components_num = int(s[1])
56-
symbols.append(components[components_num - 1])
43+
lines = iter(f)
44+
for line in lines:
45+
if not line.startswith("Geometry"):
46+
continue
47+
# GenFormat declares the atom count on the first line after the
48+
# opening brace and the element table on the next line. The old
49+
# state machine hard-coded four geometry rows, so ammonia-sized
50+
# fixtures passed while larger molecules were truncated.
51+
count_line = next(lines).split()
52+
natoms = int(count_line[0])
53+
coordinate_mode = count_line[1].upper()
54+
components = next(lines).split()
55+
coord = []
56+
symbols = []
57+
for _ in range(natoms):
58+
s = next(lines).split()
59+
symbols.append(components[int(s[1]) - 1])
5760
coord.append([float(s[2]), float(s[3]), float(s[4])])
58-
flag += 1
59-
if flag == 7:
60-
flag = 0
61+
if coordinate_mode == "F":
62+
# Fractional GenFormat coordinates are expressed in the
63+
# lattice-vector basis that follows the atom records.
64+
origin = np.array([float(value) for value in next(lines).split()])
65+
lattice = np.array(
66+
[[float(value) for value in next(lines).split()] for _ in range(3)]
67+
)
68+
coord = (np.asarray(coord) @ lattice + origin).tolist()
69+
elif coordinate_mode not in {"C", "S"}:
70+
raise ValueError(
71+
f"unsupported GenFormat coordinate mode: {coordinate_mode}"
72+
)
73+
break
74+
if natoms is None:
75+
raise ValueError("GenFormat Geometry block not found in DFTB+ input")
6176
with open_file(fn_2) as f:
62-
flag = 0
63-
for line in f:
77+
lines = iter(f)
78+
for line in lines:
6479
if line.startswith("Total Forces"):
65-
flag = 8
6680
forces = []
67-
elif flag in (8, 9, 10, 11):
68-
s = line.split()
69-
forces.append([float(s[1]), float(s[2]), float(s[3])])
70-
flag += 1
71-
if flag == 12:
72-
flag = 0
81+
for _ in range(natoms):
82+
s = next(lines).split()
83+
forces.append([float(s[1]), float(s[2]), float(s[3])])
7384
elif line.startswith("Total energy:"):
7485
s = line.split()
7586
energy = float(s[2])
76-
flag = 0
7787

7888
symbols = np.array(symbols)
7989
forces = np.array(forces)

tests/test_dftbplus.py

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

33
import unittest
4+
from io import StringIO
45

56
import numpy as np
67
from comp_sys import CompLabeledSys, IsNoPBC
78
from context import dpdata
89

10+
from dpdata.formats.dftbplus.output import read_dftb_plus
11+
912

1013
class TestDeepmdLoadAmmonia(unittest.TestCase, CompLabeledSys, IsNoPBC):
1114
def setUp(self):
@@ -55,6 +58,68 @@ def setUp(self):
5558
self.f_places = 6
5659
self.v_places = 6
5760

61+
def test_parser_reads_more_than_four_atoms(self):
62+
input_text = """Geometry = GenFormat {
63+
5 C
64+
C H
65+
1 1 0.0 0.0 0.0
66+
2 2 1.0 0.0 0.0
67+
3 2 0.0 1.0 0.0
68+
4 2 0.0 0.0 1.0
69+
5 1 1.0 1.0 1.0
70+
}
71+
"""
72+
output_text = """Total energy: -1.0 H
73+
Total Forces
74+
1 0.1 0.2 0.3
75+
2 0.4 0.5 0.6
76+
3 0.7 0.8 0.9
77+
4 1.0 1.1 1.2
78+
5 1.3 1.4 1.5
79+
"""
80+
symbols, coords, energy, forces = read_dftb_plus(
81+
StringIO(input_text), StringIO(output_text)
82+
)
83+
self.assertEqual(len(symbols), 5)
84+
self.assertEqual(coords.shape, (5, 3))
85+
self.assertEqual(forces.shape, (5, 3))
86+
self.assertEqual(energy, -1.0)
87+
88+
def test_parser_converts_fractional_genformat_coordinates(self):
89+
input_text = """Geometry = GenFormat {
90+
2 F
91+
H O
92+
1 1 0.5 0.0 0.0
93+
2 2 0.0 0.5 0.0
94+
0.1 0.2 0.3
95+
2.0 0.0 0.0
96+
0.0 4.0 0.0
97+
0.0 0.0 6.0
98+
}
99+
"""
100+
output_text = """Total energy: -1.0 H
101+
Total Forces
102+
1 0.1 0.2 0.3
103+
2 0.4 0.5 0.6
104+
"""
105+
106+
symbols, coords, _, _ = read_dftb_plus(
107+
StringIO(input_text), StringIO(output_text)
108+
)
109+
110+
np.testing.assert_array_equal(symbols, ["H", "O"])
111+
np.testing.assert_allclose(
112+
coords,
113+
[[1.1, 0.2, 0.3], [0.1, 2.2, 0.3]],
114+
)
115+
116+
def test_parser_rejects_input_without_genformat_geometry(self):
117+
with self.assertRaisesRegex(ValueError, "Geometry block not found"):
118+
read_dftb_plus(
119+
StringIO("Hamiltonian = DFTB {}\n"),
120+
StringIO("Total energy: -1.0 H\n"),
121+
)
122+
58123

59124
if __name__ == "__main__":
60125
unittest.main()

0 commit comments

Comments
 (0)