11from __future__ import annotations
22
3+ import io
34import unittest
5+ import warnings
46
57import numpy as np
68from comp_sys import CompLabeledSys , IsPBC
79from context import dpdata
810
11+ from dpdata .formats .vasp .outcar import _get_frames_lower
912from dpdata .utils import uniq_atom_names
1013
1114
@@ -21,6 +24,83 @@ def setUp(self):
2124 self .v_places = 4
2225
2326
27+ class TestVaspOUTCARIncompleteForceTable (unittest .TestCase ):
28+ """Regression tests for force tables cut short before all atom rows."""
29+
30+ @staticmethod
31+ def _block (force_rows , * , include_header = False , energy = - 1.0 ):
32+ lines = []
33+ if include_header :
34+ lines .extend (
35+ [
36+ " TITEL = PAW_PBE H 15Jun2001" ,
37+ " NELM = 60; maximum number of electronic SC steps" ,
38+ " ions per type = 2" ,
39+ ]
40+ )
41+ lines .extend (
42+ [
43+ " VOLUME and BASIS-vectors are now :" ,
44+ " filler" ,
45+ " filler" ,
46+ " filler" ,
47+ " filler" ,
48+ " 1.0 0.0 0.0" ,
49+ " 0.0 1.0 0.0" ,
50+ " 0.0 0.0 1.0" ,
51+ " POSITION TOTAL-FORCE (eV/Angst)" ,
52+ " -----------------------------------------------------------------------------------" ,
53+ * force_rows ,
54+ f" free energy TOTEN = { energy :.6f} eV" ,
55+ ]
56+ )
57+ return "\n " .join (lines ) + "\n "
58+
59+ def test_non_numeric_record_before_all_atoms_skips_incomplete_frame (self ):
60+ # The first frame is valid, but the second table reaches its energy
61+ # record after only one of the two expected atoms. Older tests used
62+ # complete tables, so converting that non-numeric record was never
63+ # exercised.
64+ valid_rows = ["0 0 0 1 2 3" , "1 1 1 4 5 6" ]
65+ incomplete_rows = ["2 2 2 7 8 9" ]
66+ contents = self ._block (valid_rows , include_header = True ) + self ._block (
67+ incomplete_rows , energy = - 2.0
68+ )
69+
70+ with warnings .catch_warnings (record = True ) as caught :
71+ warnings .simplefilter ("always" )
72+ frames = _get_frames_lower (io .StringIO (contents ), "OUTCAR" )
73+
74+ self .assertEqual (frames [3 ].shape , (1 , 3 , 3 ))
75+ self .assertEqual (frames [4 ].shape , (1 , 2 , 3 ))
76+ self .assertEqual (frames [6 ].shape , (1 , 2 , 3 ))
77+ self .assertTrue (
78+ any ("incomplete labels in frame 2" in str (item .message ) for item in caught )
79+ )
80+
81+ def test_truncated_table_does_not_index_past_block (self ):
82+ # A file truncated immediately after its first atom previously raised
83+ # IndexError while the parser blindly indexed all ``ntot`` rows.
84+ valid_rows = ["0 0 0 1 2 3" , "1 1 1 4 5 6" ]
85+ truncated_block = "\n " .join (
86+ [
87+ " POSITION TOTAL-FORCE (eV/Angst)" ,
88+ " -----------------------------------------------------------------------------------" ,
89+ "0 0 0 1 2 3" ,
90+ ]
91+ )
92+ contents = self ._block (valid_rows , include_header = True ) + truncated_block
93+
94+ with warnings .catch_warnings (record = True ) as caught :
95+ warnings .simplefilter ("always" )
96+ frames = _get_frames_lower (io .StringIO (contents ), "OUTCAR" )
97+
98+ self .assertEqual (frames [4 ].shape , (1 , 2 , 3 ))
99+ self .assertTrue (
100+ any ("expected 2 atom rows, found 1" in str (item .message ) for item in caught )
101+ )
102+
103+
24104class TestVaspOUTCARTypeMap (unittest .TestCase , CompLabeledSys , IsPBC ):
25105 def setUp (self ):
26106 sys0 = dpdata .LabeledSystem ("poscars/OUTCAR.ch4.unconverged" , fmt = "vasp/outcar" )
0 commit comments