Skip to content

Commit c71799f

Browse files
newtontechOpenClaw Botpre-commit-ci[bot]
authored
fix: Support CP2K 2025 output format for energy and forces (fixes #850) (#978)
This is a recreation of #947 which was closed because the head repository was deleted. Adds support for parsing CP2K 2025 version output files. **Changes in CP2K 2025 format:** 1. Energy line format changed from: `ENERGY| Total FORCE_EVAL ( QS ) energy (a.u.):` to: `ENERGY| Total FORCE_EVAL ( QS ) energy [hartree]` 2. Forces output format changed from `ATOMIC FORCES in [a.u.]` table to `FORCES| Atomic forces [hartree/bohr]` with `FORCES| Atom x y z |f|` prefix lines **Implementation:** - Detect CP2K 2025 format by checking for 'energy [hartree]' in the content - Parse energy from new '[hartree]' format - Parse forces from new 'FORCES|' prefixed lines - Maintain backward compatibility with CP2K 2023 format **Review comments addressed since #947:** - Raise clear RuntimeError when energy cannot be parsed from CP2K 2025 format - Fix literal `\n` in test fixture - Replace truthiness checks with explicit None checks in test helper - Add numeric value assertions to edge case tests - Add force value assertions to header filtering tests **Testing:** - Added test file for CP2K 2025 format (tests/cp2k/cp2k_2025_output/) - Added regression test for CP2K 2023 backward compatibility - Added edge case tests for whitespace, header lines, and atomic forces variants - All 110 CP2K tests pass - Previously approved by @wanghan-iapcm <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * CP2K 2025 output format is now supported with enhanced energy and force data extraction capabilities * **Tests** * Comprehensive test coverage added for CP2K 2025 format parsing, including edge cases and backward compatibility validation for earlier CP2K versions <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: OpenClaw Bot <bot@openclaw.dev> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7d75096 commit c71799f

10 files changed

Lines changed: 345 additions & 10 deletions

File tree

dpdata/formats/cp2k/output.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,9 @@ def get_frames(fname):
402402
fp.close()
403403
return [], [], [], [], [], [], [], None
404404

405+
# Check if this is CP2K 2025 format
406+
is_cp2k_2025 = "energy [hartree]" in content
407+
405408
# search duplicated header
406409
fp.seek(0)
407410
header_idx = []
@@ -430,16 +433,54 @@ def get_frames(fname):
430433
atom_symbol_idx_list.append(ii.split()[1])
431434

432435
if "ENERGY|" in ii:
433-
energy = ii.split()[8]
434-
if " Atom Kind " in ii:
435-
force_flag = True
436-
force_idx = idx
437-
if force_flag:
438-
if idx > force_idx:
439-
if "SUM OF ATOMIC FORCES" in ii:
440-
force_flag = False
441-
else:
442-
force.append(ii.split()[3:6])
436+
# CP2K 2025 format: ENERGY| Total FORCE_EVAL ( QS ) energy [hartree] -7.364190264587725
437+
# CP2K 2023 format: ENERGY| Total FORCE_EVAL ( QS ) energy (a.u.): -1766.225653832774242
438+
if is_cp2k_2025:
439+
# Find the energy value after "[hartree]"
440+
parts = ii.split()
441+
energy_2025 = None
442+
try:
443+
hartree_idx = parts.index("[hartree]")
444+
energy_2025 = parts[hartree_idx + 1]
445+
except (ValueError, IndexError):
446+
# Fallback: try to find energy value in the line
447+
for part in reversed(parts):
448+
try:
449+
float(part)
450+
energy_2025 = part
451+
break
452+
except ValueError:
453+
continue
454+
if energy_2025 is None:
455+
raise RuntimeError(
456+
f"Cannot parse energy from CP2K 2025 output line: {ii.strip()}"
457+
)
458+
energy = energy_2025
459+
else:
460+
energy = ii.split()[8]
461+
462+
# CP2K 2025 force format: FORCES| prefix lines
463+
if is_cp2k_2025:
464+
if (
465+
"FORCES|" in ii
466+
and "Atom x y z" not in ii
467+
and "Atomic forces" not in ii
468+
):
469+
parts = ii.split()
470+
# FORCES| 1 -5.73440344E-02 2.95274914E-02 -1.50988167E-02 6.62433792E-02
471+
if len(parts) >= 5 and parts[1].isdigit():
472+
force.append(parts[2:5])
473+
else:
474+
# CP2K 2023 format
475+
if " Atom Kind " in ii:
476+
force_flag = True
477+
force_idx = idx
478+
if force_flag:
479+
if idx > force_idx:
480+
if "SUM OF ATOMIC FORCES" in ii:
481+
force_flag = False
482+
else:
483+
force.append(ii.split()[3:6])
443484
# add reading stress tensor
444485
if "STRESS TENSOR [GPa" in ii:
445486
stress_flag = True
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
DBCSR| Multiplication driver XSMM
2+
DBCSR| Multrec recursion limit 512
3+
4+
**** **** ****** ** PROGRAM STARTED AT 2025-01-15 10:30:00.000
5+
***** ** *** *** ** PROGRAM STARTED ON test-machine
6+
** **** ****** PROGRAM STARTED BY test
7+
***** ** ** ** ** PROGRAM PROCESS ID 12345
8+
**** ** ******* ** PROGRAM STARTED IN /test
9+
10+
CP2K| version string: CP2K version 2025.1
11+
CP2K| source code revision number: git:abc123
12+
13+
GLOBAL| Force Environment number 1
14+
GLOBAL| Run type ENERGY_FORCE
15+
GLOBAL| Global print level MEDIUM
16+
GLOBAL| Total number of message passing processes 1
17+
18+
MEMORY| system memory details [Kb]
19+
20+
CELL| Vector a [angstrom]: 5.000 0.000 0.000 |a| = 5.000
21+
CELL| Vector b [angstrom]: 0.000 5.000 0.000 |b| = 5.000
22+
CELL| Vector c [angstrom]: 0.000 0.000 5.000 |c| = 5.000
23+
24+
ATOMIC KIND INFORMATION
25+
26+
1. Atomic kind: H Number of atoms: 2
27+
28+
MOLECULE KIND INFORMATION
29+
30+
TOTAL NUMBERS AND MAXIMUM NUMBERS
31+
32+
Total number of - Atomic kinds: 1
33+
- Atoms: 2
34+
35+
36+
SCF WAVEFUNCTION OPTIMIZATION
37+
38+
Step Update method Time Convergence Total energy Change
39+
------------------------------------------------------------------------------
40+
41+
1 OT DIIS 0.80E-01 0.1 0.00000001 -7.3641902645877 -7.36E+00
42+
43+
*** SCF run converged in 1 steps ***
44+
45+
46+
*******************************************************************************
47+
MODULE QUICKSTEP: ATOMIC COORDINATES IN angstrom
48+
49+
Atom Kind Element X Y Z Z(eff) Mass
50+
51+
1 1 H 1 0.000000 0.000000 0.000000 1.00 1.0079
52+
2 1 H 1 0.760000 0.000000 0.000000 1.00 1.0079
53+
54+
ENERGY| Total FORCE_EVAL ( QS ) energy [hartree] -7.364190264587725
55+
56+
FORCES| Atomic forces [hartree/bohr]
57+
58+
FORCES| Atom x y z |f|
59+
60+
FORCES| 1 -5.73440344E-02 2.95274914E-02 -1.50988167E-02 6.62433792E-02
61+
FORCES| 2 7.92269287E-02 3.84670665E-02 -3.41478833E-02 9.44600412E-02
62+
63+
STRESS TENSOR [GPa]
64+
65+
X Y Z
66+
X 0.12345678 0.00000000 0.00000000
67+
Y 0.00000000 0.12345678 0.00000000
68+
Z 0.00000000 0.00000000 0.12345678
69+
70+
-------------------------------------------------------------------------------
71+
- -
72+
- T I M I N G -
73+
-------------------------------------------------------------------------------
74+
75+
**** **** ****** ** PROGRAM ENDED AT 2025-01-15 10:30:05.000
200 Bytes
Binary file not shown.
176 Bytes
Binary file not shown.
136 Bytes
Binary file not shown.
176 Bytes
Binary file not shown.
200 Bytes
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
0
2+
0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
H

tests/test_cp2k_2025_output.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import tempfile
5+
import unittest
6+
7+
from comp_sys import CompLabeledSys
8+
from context import dpdata
9+
10+
11+
class TestCp2k2025Output(unittest.TestCase, CompLabeledSys):
12+
"""Test CP2K 2025 output format parsing."""
13+
14+
def setUp(self):
15+
self.system_1 = dpdata.LabeledSystem(
16+
"cp2k/cp2k_2025_output/cp2k_2025_output", fmt="cp2k/output"
17+
)
18+
self.system_2 = dpdata.LabeledSystem(
19+
"cp2k/cp2k_2025_output/deepmd", fmt="deepmd/npy"
20+
)
21+
self.places = 6
22+
self.e_places = 6
23+
self.f_places = 6
24+
self.v_places = 4
25+
26+
def test_energy_extraction(self):
27+
"""Test that energy is correctly extracted from CP2K 2025 format."""
28+
# Energy should be -7.364190264587725 hartree
29+
# Using dpdata's conversion factor: -200.3898256786414 eV
30+
expected_energy = -200.3898256786414
31+
self.assertAlmostEqual(
32+
self.system_1.data["energies"][0], expected_energy, places=5
33+
)
34+
35+
def test_forces_extraction(self):
36+
"""Test that forces are correctly extracted from CP2K 2025 format."""
37+
# Forces should be converted from hartree/bohr to eV/angstrom
38+
self.assertEqual(self.system_1.data["forces"].shape, (1, 2, 3))
39+
# Check first atom force x-component
40+
self.assertAlmostEqual(
41+
self.system_1.data["forces"][0][0][0], -2.94874881, places=5
42+
)
43+
44+
45+
class TestCp2k2023FormatStillWorks(unittest.TestCase, CompLabeledSys):
46+
"""Test that CP2K 2023 format still works (regression test)."""
47+
48+
def setUp(self):
49+
self.system_1 = dpdata.LabeledSystem(
50+
"cp2k/cp2k_normal_output/cp2k_output", fmt="cp2k/output"
51+
)
52+
self.system_2 = dpdata.LabeledSystem(
53+
"cp2k/cp2k_normal_output/deepmd", fmt="deepmd/npy"
54+
)
55+
self.places = 6
56+
self.e_places = 6
57+
self.f_places = 6
58+
self.v_places = 4
59+
60+
61+
class TestCp2k2025EdgeCases(unittest.TestCase):
62+
"""Test edge cases for CP2K 2025 format parsing to improve coverage."""
63+
64+
def create_cp2k_output_2025(self, energy_line=None, forces_lines=None):
65+
"""Create a minimal CP2K 2025 output file for testing."""
66+
with tempfile.NamedTemporaryFile(mode="w", suffix=".out", delete=False) as f:
67+
# Header required for parsing
68+
f.write(
69+
" DBCSR| Multiplication driver XSMM\n"
70+
)
71+
f.write("\n")
72+
f.write(
73+
" **** **** ****** ** PROGRAM STARTED AT 2025-01-15 10:30:00.000\n"
74+
)
75+
f.write(
76+
" CP2K| version string: CP2K version 2025.1\n"
77+
)
78+
f.write("\n")
79+
f.write(
80+
" CELL| Vector a [angstrom]: 5.000 0.000 0.000 |a| = 5.000\n"
81+
)
82+
f.write(
83+
" CELL| Vector b [angstrom]: 0.000 5.000 0.000 |b| = 5.000\n"
84+
)
85+
f.write(
86+
" CELL| Vector c [angstrom]: 0.000 0.000 5.000 |c| = 5.000\n"
87+
)
88+
f.write("\n")
89+
f.write(" ATOMIC KIND INFORMATION\n")
90+
f.write("\n")
91+
f.write(
92+
" 1. Atomic kind: H Number of atoms: 2\n"
93+
)
94+
f.write("\n")
95+
f.write(" SCF WAVEFUNCTION OPTIMIZATION\n")
96+
f.write("\n")
97+
f.write(" *** SCF run converged in 1 steps ***\n")
98+
f.write("\n")
99+
f.write(" MODULE QUICKSTEP: ATOMIC COORDINATES IN angstrom\n")
100+
f.write("\n")
101+
f.write(
102+
" Atom Kind Element X Y Z Z(eff) Mass\n"
103+
)
104+
f.write("\n")
105+
f.write(
106+
" 1 1 H 1 0.000000 0.000000 0.000000 1.00 1.0079\n"
107+
)
108+
f.write(
109+
" 2 1 H 1 0.760000 0.000000 0.000000 1.00 1.0079\n"
110+
)
111+
f.write("\n")
112+
113+
# Energy line - use provided or default
114+
if energy_line is not None:
115+
f.write(energy_line + "\n")
116+
else:
117+
f.write(
118+
" ENERGY| Total FORCE_EVAL ( QS ) energy [hartree] -7.364190264587725\n"
119+
)
120+
121+
f.write("\n")
122+
f.write(" FORCES| Atomic forces [hartree/bohr]\n")
123+
f.write("\n")
124+
f.write(" FORCES| Atom x y z |f|\n")
125+
126+
# Forces lines - use provided or default
127+
if forces_lines is not None:
128+
for line in forces_lines:
129+
f.write(line + "\n")
130+
else:
131+
f.write(
132+
" FORCES| 1 -5.73440344E-02 2.95274914E-02 -1.50988167E-02 6.62433792E-02\n"
133+
)
134+
f.write(
135+
" FORCES| 2 7.92269287E-02 3.84670665E-02 -3.41478833E-02 9.44600412E-02\n"
136+
)
137+
138+
f.write("\n")
139+
f.write(" STRESS TENSOR [GPa]\n")
140+
f.write("\n")
141+
f.write(" X Y Z\n")
142+
f.write(" X 0.12345678 0.00000000 0.00000000\n")
143+
f.write(" Y 0.00000000 0.12345678 0.00000000\n")
144+
f.write(" Z 0.00000000 0.00000000 0.12345678\n")
145+
f.write("\n")
146+
f.write(
147+
" **** **** ****** ** PROGRAM ENDED AT 2025-01-15 10:30:05.000\n"
148+
)
149+
150+
return f.name
151+
152+
def test_cp2k2025_format_with_labeled_system(self):
153+
"""Test CP2K 2025 format using LabeledSystem (integration test for coverage)."""
154+
fname = self.create_cp2k_output_2025()
155+
try:
156+
system = dpdata.LabeledSystem(fname, fmt="cp2k/output")
157+
self.assertIsNotNone(system.data["energies"])
158+
self.assertIsNotNone(system.data["forces"])
159+
self.assertEqual(system.data["forces"].shape[1], 2)
160+
finally:
161+
os.unlink(fname)
162+
163+
def test_cp2k2025_energy_parsing_with_extra_whitespace(self):
164+
"""Test energy parsing with extra whitespace around value (coverage for parsing robustness)."""
165+
fname = self.create_cp2k_output_2025(
166+
energy_line=" ENERGY| Total FORCE_EVAL ( QS ) energy [hartree] -7.364190264587725 "
167+
)
168+
try:
169+
system = dpdata.LabeledSystem(fname, fmt="cp2k/output")
170+
self.assertIsNotNone(system.data["energies"])
171+
self.assertAlmostEqual(
172+
system.data["energies"][0], -200.3898256786414, places=5
173+
)
174+
self.assertEqual(system.data["forces"].shape[1], 2)
175+
finally:
176+
os.unlink(fname)
177+
178+
def test_cp2k2025_force_parsing_with_header_lines(self):
179+
"""Test that FORCES| header lines are correctly skipped (coverage for filtering)."""
180+
fname = self.create_cp2k_output_2025(
181+
forces_lines=[
182+
" FORCES| Atom x y z |f|", # Should be skipped - contains "Atom x y z"
183+
" FORCES| 1 -5.73440344E-02 2.95274914E-02 -1.50988167E-02 6.62433792E-02",
184+
" FORCES| 2 7.92269287E-02 3.84670665E-02 -3.41478833E-02 9.44600412E-02",
185+
]
186+
)
187+
try:
188+
system = dpdata.LabeledSystem(fname, fmt="cp2k/output")
189+
self.assertEqual(system.data["forces"].shape[1], 2)
190+
self.assertAlmostEqual(
191+
system.data["forces"][0][0][0], -2.94874881, places=5
192+
)
193+
finally:
194+
os.unlink(fname)
195+
196+
def test_cp2k2025_force_parsing_with_atomic_forces_line(self):
197+
"""Test that 'Atomic forces' line is correctly skipped (coverage for filtering)."""
198+
fname = self.create_cp2k_output_2025(
199+
forces_lines=[
200+
" FORCES| Atomic forces [hartree/bohr]", # Should be skipped
201+
" FORCES| 1 -5.73440344E-02 2.95274914E-02 -1.50988167E-02 6.62433792E-02",
202+
" FORCES| 2 7.92269287E-02 3.84670665E-02 -3.41478833E-02 9.44600412E-02",
203+
]
204+
)
205+
try:
206+
system = dpdata.LabeledSystem(fname, fmt="cp2k/output")
207+
self.assertEqual(system.data["forces"].shape[1], 2)
208+
self.assertAlmostEqual(
209+
system.data["forces"][0][0][0], -2.94874881, places=5
210+
)
211+
finally:
212+
os.unlink(fname)
213+
214+
215+
if __name__ == "__main__":
216+
unittest.main()

0 commit comments

Comments
 (0)