diff --git a/pyedr/pyedr/pyedr.py b/pyedr/pyedr/pyedr.py index eb4749b..6a8ee68 100644 --- a/pyedr/pyedr/pyedr.py +++ b/pyedr/pyedr/pyedr.py @@ -193,9 +193,9 @@ def do_eheader(self): fr.nblock = data.unpack_int() assert fr.nblock >= 0 if ndisre != 0: - if file_version >= 4: - raise ValueError("Distance restraint blocks " - "in old style in new style file") + # Distance restraint blocks of old style in new style file + # ndisre is only read from file for older than 4 versions + assert file_version < 4 fr.nblock += 1 # we now know what these should be, # or we've already bailed out because @@ -261,10 +261,10 @@ def do_enx(self): frametime = 0 try: self.do_eheader() - except ValueError: + except ValueError as e: print("Last energy frame read {} time {:8.3f}".format(framenr - 1, frametime)) - raise RuntimeError() + raise RuntimeError("Failed reading header") from e framenr += 1 frametime = fr.t @@ -347,12 +347,11 @@ def convert_full_sums(self): ener_prev[i].eav = eav_all nsum_prev = nstep_all elif fr.nsum > 0: - if fr.nsum != nstep_all: - warnings.warn('WARNING: something is wrong with the ' - 'energy sums, will not use exact averages') - nsum_prev = 0 - else: - nsum_prev = nstep_all + # Conversion is only done for version 1 files + # For those files, fr.nsum is always set + # to nstep_all while parsing the header + assert fr.nsum == nstep_all + nsum_prev = nstep_all # Copy all sums to ener_prev for i in range(fr.nre): ener_prev[i].esum = fr.ener[i].esum @@ -451,12 +450,13 @@ def ndo_double(data, n): def ndo_int64(data, n): """mimic of gmx_fio_ndo_int64 in gromacs""" - return [data.unpack_huge() for i in range(n)] + return [data.unpack_hyper() for i in range(n)] def ndo_char(data, n): """mimic of gmx_fio_ndo_char in gromacs""" - return [data.unpack_char() for i in range(n)] + # Note: chars are encoded as int(32) + return [data.unpack_int() for i in range(n)] def ndo_string(data, n): diff --git a/pyedr/pyedr/tests/data/mocks/edr_mock.py b/pyedr/pyedr/tests/data/mocks/edr_mock.py new file mode 100644 index 0000000..e9208be --- /dev/null +++ b/pyedr/pyedr/tests/data/mocks/edr_mock.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Helper script to generate mock EDR files with manipulated values. This allows +# setting some values in EDR files in such a way that certain conditions can be +# triggered while parsing. Note that not all parameter combinations are +# sensible and produce valid EDR files. + +import xdrlib +from dataclasses import dataclass + +MAGIC = -55555 # File magic number +ENX_VERSION = 4 # File version +assert ENX_VERSION >= 1 +NSUM = 0 # Value of fr.nsum +# Column names with associated units (units are ignored for version 1) +# These values are parsed in do_enxnms and stored as fr.nms +NMS = [("DUMMY1", "UNIT1"), ("DUMMY2", "UNIT2")] +NRE = len(NMS) # Derive number of columns from NMS +FIRST_REAL = -1.0 # Value of first_real_to_check in do_eheader +FRAME_MAGIC = -7777777 # Frame magic number +NSTEPS = 3 # Number of steps to write +# Time step between frames +# Frame times are simply set to step*DT +DT = 0.5 +NDISRE = 0 # Number of distance restraints (can be > 0 for versions < 4) +E_SIZE = 0 # Value to write as fr.e_size + + +@dataclass +class Block: + bid: int # block id + sub: [] # list of sub blocks + + +@dataclass +class SubBlock: + nr: int # number of values stored in sub block + btype: int # type of stored values + + +# Option 1: Write no other data blocks + +BLOCKS = [] + +# Option 2: Write blocks of every possible sub-block type + +# BLOCKS = [] +# for t in range(6): +# bid = t + 3 # generate some arbitrary block id +# sub_nr1 = 7 - t # and nr of values in the sub block +# sub_nr2 = 6 - t +# BLOCKS.append(Block(bid, [SubBlock(sub_nr1, t), SubBlock(sub_nr2, t)])) + +# Option 3: Write a block with an invalid sub-block type (> 5) + +# BLOCKS = [Block(7, [SubBlock(2, 0), SubBlock(1, 1_000_000_000)])] + +# Option 4: Write some additional distance restraints +# and optionally some more blocks +# Version must be set to < 4 +# Note from pyedr: blocks in old version files always have 1 subblock +# that consists of reals + +# NDISRE = 2 +# BLOCKS = [] +# for i in range(3): +# BLOCKS.append(Block(i, [SubBlock(4, 1)])) +# ENX_VERSION = 3 + +NBLOCK = len(BLOCKS) + +p = xdrlib.Packer() + +# do_enxnms +if ENX_VERSION == 1: + p.pack_int(NRE) +else: + p.pack_int(MAGIC) + p.pack_int(ENX_VERSION) + p.pack_int(NRE) +for nm, u in NMS: + p.pack_string(nm.encode("ascii")) + if ENX_VERSION >= 2: + p.pack_string(u.encode("ascii")) + +for step in range(NSTEPS): + t = step * DT # Just set some value for fr.t + # do_enx + # -> do_eheader + if ENX_VERSION == 1: + p.pack_float(t) + p.pack_int(step) + else: + p.pack_float(FIRST_REAL) + p.pack_int(FRAME_MAGIC) + p.pack_int(ENX_VERSION) + p.pack_double(t) + p.pack_hyper(step) + p.pack_int(NSUM) + if ENX_VERSION >= 3: + p.pack_hyper(NSTEPS) + if ENX_VERSION >= 5: + p.pack_double(DT) + p.pack_int(NRE) + p.pack_int(NDISRE) + p.pack_int(NBLOCK) + if NDISRE != 0: + assert ENX_VERSION < 4 + + frame_blocks = BLOCKS.copy() + startb = 0 + if NDISRE > 0: + enxDISRE = 3 # Some constant defined by Gromacs + # Sub-block type is 1 = float + frame_blocks.insert( + 0, Block(enxDISRE, [SubBlock(NDISRE, 1), SubBlock(NDISRE, 1)]) + ) + startb += 1 + for b in range(startb, len(frame_blocks)): + if ENX_VERSION < 4: + # Old versions have only one sub block of reals (here 1 = float) + assert len(frame_blocks[b].sub) == 1 + assert frame_blocks[b].sub[0].btype == 1 + p.pack_int(frame_blocks[b].sub[0].nr) + else: + p.pack_int(frame_blocks[b].bid) + p.pack_int(len(frame_blocks[b].sub)) + for sub in frame_blocks[b].sub: + p.pack_int(sub.btype) + p.pack_int(sub.nr) + + p.pack_int(E_SIZE) + p.pack_int(0) # dummy + p.pack_int(0) # dummy + # <- do_eheader + for i in range(NRE): + # Just generate some arbitrary value for the energy + # Depends on step and column number + p.pack_float(step * 100 + i) # e + if ENX_VERSION == 1 or NSUM > 0: + p.pack_float(0.0) # eav + p.pack_float(0.0) # esum + if ENX_VERSION == 1: + p.pack_float(0.0) # dummy + + for b in range(len(frame_blocks)): + for sub in frame_blocks[b].sub: + for n in range(sub.nr): + if sub.btype == 0: + p.pack_int(0) + elif sub.btype == 1: + p.pack_float(0.0) + elif sub.btype == 2: + p.pack_double(0.0) + elif sub.btype == 3: + p.pack_hyper(0) + elif sub.btype == 4: + # GMX casts the char to an u8 and writes this as an u32 + p.pack_int(0) + elif sub.btype == 5: + p.pack_string("ABC".encode("ascii")) + else: + print("WARNING: Unknown sub-block type") + p.pack_int(0) + + +with open("dump.edr", "wb") as f: + f.write(p.get_buffer()) diff --git a/pyedr/pyedr/tests/data/mocks/v1_nre2_esum0.edr b/pyedr/pyedr/tests/data/mocks/v1_nre2_esum0.edr new file mode 100644 index 0000000..9b1a088 Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v1_nre2_esum0.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v1_step_negative.edr b/pyedr/pyedr/tests/data/mocks/v1_step_negative.edr new file mode 100644 index 0000000..f31c997 Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v1_step_negative.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v3_ndisre2_blocks.edr b/pyedr/pyedr/tests/data/mocks/v3_ndisre2_blocks.edr new file mode 100644 index 0000000..c64413b Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v3_ndisre2_blocks.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v4_all_block_types.edr b/pyedr/pyedr/tests/data/mocks/v4_all_block_types.edr new file mode 100644 index 0000000..7059568 Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v4_all_block_types.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v4_first_real_v1.edr b/pyedr/pyedr/tests/data/mocks/v4_first_real_v1.edr new file mode 100644 index 0000000..1b5ba1c Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v4_first_real_v1.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v4_invalid_block_type.edr b/pyedr/pyedr/tests/data/mocks/v4_invalid_block_type.edr new file mode 100644 index 0000000..68ad7cf Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v4_invalid_block_type.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v4_invalid_file_magic.edr b/pyedr/pyedr/tests/data/mocks/v4_invalid_file_magic.edr new file mode 100644 index 0000000..26cfe8f Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v4_invalid_file_magic.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v4_invalid_frame_magic.edr b/pyedr/pyedr/tests/data/mocks/v4_invalid_frame_magic.edr new file mode 100644 index 0000000..041e42c Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v4_invalid_frame_magic.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v4_large_version_frame.edr b/pyedr/pyedr/tests/data/mocks/v4_large_version_frame.edr new file mode 100644 index 0000000..2b24c29 Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v4_large_version_frame.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v5_step_negative.edr b/pyedr/pyedr/tests/data/mocks/v5_step_negative.edr new file mode 100644 index 0000000..24e9cd2 Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v5_step_negative.edr differ diff --git a/pyedr/pyedr/tests/data/mocks/v_large.edr b/pyedr/pyedr/tests/data/mocks/v_large.edr new file mode 100644 index 0000000..0ed8434 Binary files /dev/null and b/pyedr/pyedr/tests/data/mocks/v_large.edr differ diff --git a/pyedr/pyedr/tests/datafiles.py b/pyedr/pyedr/tests/datafiles.py index e28d17f..439009c 100644 --- a/pyedr/pyedr/tests/datafiles.py +++ b/pyedr/pyedr/tests/datafiles.py @@ -107,3 +107,33 @@ (EDR_V4_DOUBLE, EDR_V4_DOUBLE_XVG, EDR_V4_DOUBLE_UNITS, 4), (Path(EDR), EDR_XVG, EDR_UNITS, 5), ] + +EDR_MOCK_V1_ESUM0 = resource_filename(__name__, 'data/mocks/v1_nre2_esum0.edr') +EDR_MOCK_V5_STEP_NEGATIVE = resource_filename( + __name__, 'data/mocks/v5_step_negative.edr' +) +EDR_MOCK_V1_STEP_NEGATIVE = resource_filename( + __name__, 'data/mocks/v1_step_negative.edr' +) +EDR_MOCK_V_LARGE = resource_filename(__name__, 'data/mocks/v_large.edr') +EDR_MOCK_V4_LARGE_VERSION_FRAME = resource_filename( + __name__, 'data/mocks/v4_large_version_frame.edr' +) +EDR_MOCK_V4_FIRST_REAL_V1 = resource_filename( + __name__, 'data/mocks/v4_first_real_v1.edr' +) +EDR_MOCK_V4_INVALID_FILE_MAGIC = resource_filename( + __name__, 'data/mocks/v4_invalid_file_magic.edr' +) +EDR_MOCK_V4_INVALID_FRAME_MAGIC = resource_filename( + __name__, 'data/mocks/v4_invalid_frame_magic.edr' +) +EDR_MOCK_V4_INVALID_BLOCK_TYPE = resource_filename( + __name__, 'data/mocks/v4_invalid_block_type.edr' +) +EDR_MOCK_V4_ALL_BLOCK_TYPES = resource_filename( + __name__, 'data/mocks/v4_all_block_types.edr' +) +EDR_MOCK_V3_NDISRE2_BLOCKS = resource_filename( + __name__, 'data/mocks/v3_ndisre2_blocks.edr' +) diff --git a/pyedr/pyedr/tests/test_mocks.py b/pyedr/pyedr/tests/test_mocks.py new file mode 100644 index 0000000..e155d4f --- /dev/null +++ b/pyedr/pyedr/tests/test_mocks.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- + +""" +Tests for pyedr +""" +import pytest +from numpy.testing import assert_allclose + +import pyedr +from pyedr.tests.datafiles import ( + EDR_MOCK_V1_ESUM0, + EDR_MOCK_V5_STEP_NEGATIVE, + EDR_MOCK_V1_STEP_NEGATIVE, + EDR_MOCK_V_LARGE, + EDR_MOCK_V4_LARGE_VERSION_FRAME, + EDR_MOCK_V4_FIRST_REAL_V1, + EDR_MOCK_V4_INVALID_FILE_MAGIC, + EDR_MOCK_V4_INVALID_FRAME_MAGIC, + EDR_MOCK_V4_INVALID_BLOCK_TYPE, + EDR_MOCK_V4_ALL_BLOCK_TYPES, + EDR_MOCK_V3_NDISRE2_BLOCKS, +) + + +def assert_dict(d): + assert len(d) == 3 + assert_allclose(d["Time"], [0.0, 0.5, 1.0]) + assert_allclose(d["DUMMY1"], [0.0, 100.0, 200.0]) + assert_allclose(d["DUMMY2"], [1.0, 101.0, 201.0]) + + +def test_esum_zero_v1(): + with pytest.warns( + UserWarning, + match=f"enx file_version 1, " + f"implementation version {pyedr.ENX_VERSION}", + ): + d = pyedr.edr_to_dict(EDR_MOCK_V1_ESUM0) + assert_dict(d) + + +def test_step_negative_v5(): + with pytest.raises(ValueError, match="Something went wrong"): + pyedr.edr_to_dict(EDR_MOCK_V5_STEP_NEGATIVE) + + +def test_step_negative_v1(): + with pytest.warns( + UserWarning, + match=f"enx file_version 1, " + f"implementation version {pyedr.ENX_VERSION}", + ): + with pytest.raises(RuntimeError, match="Failed reading header") as e: + pyedr.edr_to_dict(EDR_MOCK_V1_STEP_NEGATIVE) + assert ( + isinstance(e.value.__cause__, ValueError) + and str(e.value.__cause__) + == "edr file with negative step number or " + "unreasonable time (and without version number)." + ) + + +def test_large_invalid_version(): + with pytest.raises( + ValueError, + match="Reading file version 1000000000 with " + f"version {pyedr.ENX_VERSION} implementation", + ): + pyedr.edr_to_dict(EDR_MOCK_V_LARGE) + + +def test_large_invalid_version_frame_v4(): + with pytest.warns( + UserWarning, + match=f"enx file_version 4, " + f"implementation version {pyedr.ENX_VERSION}", + ): + with pytest.raises(RuntimeError, match="Failed reading header") as e: + pyedr.edr_to_dict(EDR_MOCK_V4_LARGE_VERSION_FRAME) + assert ( + isinstance(e.value.__cause__, ValueError) + and str(e.value.__cause__) + == "Reading file version 1000000000 with " + f"version {pyedr.ENX_VERSION} implementation" + ) + + +def test_step_negative_v4(): + with pytest.warns( + UserWarning, + match=f"enx file_version 4, " + f"implementation version {pyedr.ENX_VERSION}", + ): + with pytest.raises(RuntimeError, match="Failed reading header") as e: + pyedr.edr_to_dict(EDR_MOCK_V4_FIRST_REAL_V1) + assert ( + isinstance(e.value.__cause__, ValueError) + and str(e.value.__cause__) + == "Expected file version 1, found version 4" + ) + + +def test_invalid_file_magic_v4(): + with pytest.raises( + ValueError, + match="Energy names magic number mismatch, " + "this is not a GROMACS edr file", + ): + pyedr.edr_to_dict(EDR_MOCK_V4_INVALID_FILE_MAGIC) + + +def test_invalid_frame_magic_v4(): + with pytest.warns( + UserWarning, + match=f"enx file_version 4, " + f"implementation version {pyedr.ENX_VERSION}", + ): + with pytest.raises(RuntimeError, match="Failed reading header") as e: + pyedr.edr_to_dict(EDR_MOCK_V4_INVALID_FRAME_MAGIC) + assert ( + isinstance(e.value.__cause__, ValueError) + and str(e.value.__cause__) + == "Energy header magic number mismatch, " + "this is not a GROMACS edr file" + ) + + +def test_block_type_v4(): + with pytest.warns( + UserWarning, + match=f"enx file_version 4, " + f"implementation version {pyedr.ENX_VERSION}", + ): + with pytest.raises( + ValueError, + match="Reading unknown block data type: " + "this file is corrupted or from the future", + ): + pyedr.edr_to_dict(EDR_MOCK_V4_INVALID_BLOCK_TYPE) + + +def test_all_block_types_v4(): + with pytest.warns( + UserWarning, + match=f"enx file_version 4, " + f"implementation version {pyedr.ENX_VERSION}", + ): + d = pyedr.edr_to_dict(EDR_MOCK_V4_ALL_BLOCK_TYPES) + assert_dict(d) + + +def test_ndisre2_blocks_v3(): + with pytest.warns( + UserWarning, + match=f"enx file_version 3, " + f"implementation version {pyedr.ENX_VERSION}", + ): + d = pyedr.edr_to_dict(EDR_MOCK_V3_NDISRE2_BLOCKS) + assert_dict(d)