From 607f448646e10579fd3a3b6a8fb07b2066a8a3f9 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 12:04:27 +0000 Subject: [PATCH 01/21] fix #216: angle parameters are not being read from topology file --- tests/fileformats/top/test_TOP.py | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/fileformats/top/test_TOP.py diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py new file mode 100644 index 00000000..b9eaed3e --- /dev/null +++ b/tests/fileformats/top/test_TOP.py @@ -0,0 +1,50 @@ +import gromacs as gmx +from unittest.mock import patch, mock_open +import enum + +class AnglesFuncT(enum.Enum): + Angle = 1 + G96_angle = 2 + cross_bond_bond = 3 + cross_bond_angle = 4 + urey_bradley = 5 + quartic_angle = 6 + # there is no funct = 7 + tabulated_angle = 8 + linear_angle = 9 + restricted_bending_potential = 10 + + +angles_types = { + AnglesFuncT.Angle: {"theta0", } +} + +def create_topology_data() -> str: + """ + https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html#tab-topfile2 + """ + return """ +[ moleculetype ] +; Name nrexcl +Example 3 + +[ atoms ] +; nr type resnr residue atom cgnr charge mass typeB cha +; residue 1 ASN rtp ASN q +1.0 + 1 NH3 1 ASN N 1 -0.3 14.007 ; qtot -0.3 + 2 HC 1 ASN H1 2 0.33 1.008 ; qtot 0.03 + 3 HC 1 ASN H2 3 0.33 1.008 ; qtot 0.36 + +[ angles ] +; ai aj ak funct c0 c1 c2 c3 + 2 1 3 5 + +[ molecules ] +; Compound #mols +Example 1 +""" + +@patch("builtins.open", mock_open(read_data=create_topology_data())) +def test_angles(): + topol = gmx.fileformats.top.TOP("topol.top") + topol.molecules[0].angles[0].gromacs From 6d66b8f91791bb5b26e7bbbe0852b6a996daba8b Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 14:47:03 +0000 Subject: [PATCH 02/21] run black --- tests/fileformats/top/test_TOP.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index b9eaed3e..2caf2ab3 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -2,6 +2,7 @@ from unittest.mock import patch, mock_open import enum + class AnglesFuncT(enum.Enum): Angle = 1 G96_angle = 2 @@ -13,12 +14,15 @@ class AnglesFuncT(enum.Enum): tabulated_angle = 8 linear_angle = 9 restricted_bending_potential = 10 - + angles_types = { - AnglesFuncT.Angle: {"theta0", } + AnglesFuncT.Angle: { + "theta0", + } } + def create_topology_data() -> str: """ https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html#tab-topfile2 @@ -44,6 +48,7 @@ def create_topology_data() -> str: Example 1 """ + @patch("builtins.open", mock_open(read_data=create_topology_data())) def test_angles(): topol = gmx.fileformats.top.TOP("topol.top") From ff3dbea599ac43eff292b961317c6b74a2fa06e5 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 18:40:22 +0000 Subject: [PATCH 03/21] refactored heavily to support all angle types --- gromacs/fileformats/blocks.py | 29 +++++ gromacs/fileformats/top.py | 185 ++++++++++++++---------------- tests/fileformats/top/test_TOP.py | 60 ++++++---- 3 files changed, 152 insertions(+), 122 deletions(-) diff --git a/gromacs/fileformats/blocks.py b/gromacs/fileformats/blocks.py index c52ba1f8..2aaab426 100644 --- a/gromacs/fileformats/blocks.py +++ b/gromacs/fileformats/blocks.py @@ -55,6 +55,7 @@ """ +import enum import logging @@ -441,6 +442,34 @@ def __eq__(self, other): ) +class AngleFunctionType(enum.IntEnum): + HARMONIC = 1 + G96_ANGLE = 2 + CROSS_BOND_BOND = 3 + CROSS_BOND_ANGLE = 4 + UREY_BRADLEY = 5 + QUARTIC_ANGLE = 6 + # There's no function type 7 as per the given code comments + TABULATED_ANGLE = 8 + LINEAR_ANGLE = 9 + RESTRICTED_BENDING = 10 + + @property + def num_params(self): + # Define the number of parameters expected for each function type + return { + self.HARMONIC: 2, + self.G96_ANGLE: 2, + self.CROSS_BOND_BOND: 3, + self.CROSS_BOND_ANGLE: 4, + self.UREY_BRADLEY: 4, + self.QUARTIC_ANGLE: 6, + self.TABULATED_ANGLE: 2, # This might be different depending on your implementation + self.LINEAR_ANGLE: 2, + self.RESTRICTED_BENDING: 2, + }[self] + + class AngleType(Param): def __init__(self, format): super(AngleType, self).__init__(format) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 7fd0f879..58f88508 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -471,7 +471,7 @@ def _add_info(sys_or_mol, section, container): else: raise NotImplementedError - elif curr_sec in ("angletypes", "angles"): + elif curr_sec in {"angletypes", "angles"}: """ section #at fu #param ---------------------------------- @@ -483,110 +483,99 @@ def _add_info(sys_or_mol, section, container): angles 3 6 6 angles 3 8 ?? """ - + print(fields) ai, aj, ak = fields[:3] - fu = int(fields[3]) - assert fu in (1, 2, 3, 4, 5, 6, 8) # no 7 + ai, aj, ak = list(map(int, [ai, aj, ak])) - if fu not in (1, 2, 5): - raise NotImplementedError( - "function {0:d} is not yet supported".format(fu) + fu = blocks.AngleFunctionType(int(fields[3])) + + if len(fields[4:]) != fu.num_params: + raise ValueError( + f"Expected {fu.num_params} parameters for function type {fu}, got {len(fields[4:])}" ) ang = blocks.AngleType("gromacs") - if fu == 1: - if curr_sec == "angletypes": - ang.atype1 = ai - ang.atype2 = aj - ang.atype3 = ak - - tetha0, ktetha = list(map(float, fields[4:6])) - ang.gromacs = { - "param": { - "ktetha": ktetha, - "tetha0": tetha0, - "kub": None, - "s0": None, - }, - "func": fu, - } - - self.angletypes.append(ang) - _add_info(self, curr_sec, self.angletypes) - - elif curr_sec == "angles": - ai, aj, ak = list(map(int, [ai, aj, ak])) - ang.atom1 = mol.atoms[ai - 1] - ang.atom2 = mol.atoms[aj - 1] - ang.atom3 = mol.atoms[ak - 1] - ang.gromacs["func"] = fu - - mol.angles.append(ang) - _add_info(mol, curr_sec, mol.angles) - - else: - raise ValueError - - elif fu == 2: - if curr_sec == "angletypes": - raise NotImplementedError() - - elif curr_sec == "angles": - ai, aj, ak = list(map(int, [ai, aj, ak])) - ang.atom1 = mol.atoms[ai - 1] - ang.atom2 = mol.atoms[aj - 1] - ang.atom3 = mol.atoms[ak - 1] - ang.gromacs["func"] = fu - - tetha0, ktetha = list(map(float, fields[4:6])) - ang.gromacs = { - "param": { - "ktetha": ktetha, - "tetha0": tetha0, - "kub": None, - "s0": None, - }, - "func": fu, - } - - mol.angles.append(ang) - _add_info(mol, curr_sec, mol.angles) - - elif fu == 5: - if curr_sec == "angletypes": - ang.atype1 = ai - ang.atype2 = aj - ang.atype3 = ak - tetha0, ktetha, s0, kub = list(map(float, fields[4:8])) - - ang.gromacs = { - "param": { - "ktetha": ktetha, - "tetha0": tetha0, - "kub": kub, - "s0": s0, - }, - "func": fu, - } - - self.angletypes.append(ang) - _add_info(self, curr_sec, self.angletypes) - - elif curr_sec == "angles": - ai, aj, ak = list(map(int, [ai, aj, ak])) - ang.atom1 = mol.atoms[ai - 1] - ang.atom2 = mol.atoms[aj - 1] - ang.atom3 = mol.atoms[ak - 1] - ang.gromacs["func"] = fu - - mol.angles.append(ang) - _add_info(mol, curr_sec, mol.angles) - - else: - raise ValueError + ang.gromacs = {"func": fu} + + if curr_sec == "angletypes": + ang.atype1 = ai + ang.atype2 = aj + ang.atype3 = ak + elif curr_sec == "angles": + ang.atom1 = mol.atoms[ai - 1] + ang.atom2 = mol.atoms[aj - 1] + ang.atom3 = mol.atoms[ak - 1] + + # Parse parameters based on the function type + params = list(map(float, fields[4:])) + + # Handle parameters based on function types + if fu in { + blocks.AngleFunctionType.HARMONIC, + blocks.AngleFunctionType.G96_ANGLE, + }: + ang.gromacs["params"] = { + "tetha0": params[0], + "ktetha": params[1], + } + elif fu == blocks.AngleFunctionType.CROSS_BOND_BOND: + ang.gromacs["params"] = { + "r1e": params[0], + "r2e": params[1], + "krrprime": params[2], + } + elif fu == blocks.AngleFunctionType.CROSS_BOND_ANGLE: + ang.gromacs["params"] = { + "r1e": params[0], + "r2eprime": params[1], + "r3e": params[2], + "krtheta": params[3], + } + elif fu == blocks.AngleFunctionType.UREY_BRADLEY: + ang.gromacs["params"] = { + "tetha0": params[0], + "ktetha": params[1], + "r13": params[2], + "kub": params[3], + } + elif fu == blocks.AngleFunctionType.QUARTIC_ANGLE: + ang.gromacs["params"] = { + "tetha0": params[0], + "C1": params[1], + "C2": params[2], + "C3": params[3], + "C4": params[4], + "C5": params[5], + } + elif fu == blocks.AngleFunctionType.TABULATED_ANGLE: + ang.gromacs["params"] = { + "table_number": params[0], + "k": params[1], + } # Assuming 'table number' is a parameter here + elif fu == blocks.AngleFunctionType.LINEAR_ANGLE: + ang.gromacs["params"] = { + "a0": params[0], + "klin": params[1], + } + elif fu == blocks.AngleFunctionType.RESTRICTED_BENDING: + ang.gromacs["params"] = { + "tetha0": params[0], + "ktheta": params[1], + } + else: + raise NotImplementedError( + f"Function type {fu} is not implemented" + ) + # Add the angle to the appropriate list and call _add_info + if curr_sec == "angletypes": + self.angletypes.append(ang) + _add_info(self, curr_sec, self.angletypes) + elif curr_sec == "angles": + mol.angles.append(ang) + _add_info(mol, curr_sec, mol.angles) else: - raise NotImplementedError + raise ValueError("Unknown section while parsing angles") elif curr_sec in ("dihedraltypes", "dihedrals"): """ diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index 2caf2ab3..73b72082 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -1,33 +1,40 @@ import gromacs as gmx from unittest.mock import patch, mock_open -import enum +import random +import pytest +from gromacs.fileformats import blocks -class AnglesFuncT(enum.Enum): - Angle = 1 - G96_angle = 2 - cross_bond_bond = 3 - cross_bond_angle = 4 - urey_bradley = 5 - quartic_angle = 6 - # there is no funct = 7 - tabulated_angle = 8 - linear_angle = 9 - restricted_bending_potential = 10 +def generate_topology_line(func_type: blocks.AngleFunctionType) -> str: + # Generates random parameters for the different function types + def random_params(num) -> list[float]: + return [random.uniform(0.0, 100.0) for _ in range(num)] - -angles_types = { - AnglesFuncT.Angle: { - "theta0", + # Define the pattern for each function type + patterns = { + blocks.AngleFunctionType.HARMONIC: "{:5d}{:5d}{:5d} 1{:10.4f}{:10.4f}", + blocks.AngleFunctionType.G96_ANGLE: "{:5d}{:5d}{:5d} 2{:10.4f}{:10.4f}", + blocks.AngleFunctionType.CROSS_BOND_BOND: "{:5d}{:5d}{:5d} 3{:10.4f}{:10.4f}{:10.4f}", + blocks.AngleFunctionType.CROSS_BOND_ANGLE: "{:5d}{:5d}{:5d} 4{:10.4f}{:10.4f}{:10.4f}{:10.4f}", + blocks.AngleFunctionType.UREY_BRADLEY: "{:5d}{:5d}{:5d} 5{:10.4f}{:10.4f}{:10.4f}{:10.4f}", + blocks.AngleFunctionType.QUARTIC_ANGLE: "{:5d}{:5d}{:5d} 6{:10.4f}{:10.4f}{:10.4f}{:10.4f}{:10.4f}{:10.4f}", + blocks.AngleFunctionType.TABULATED_ANGLE: "{:5d}{:5d}{:5d} 8{:10.4f}{:10.4f}", + blocks.AngleFunctionType.LINEAR_ANGLE: "{:5d}{:5d}{:5d} 9{:10.4f}{:10.4f}", + blocks.AngleFunctionType.RESTRICTED_BENDING: "{:5d}{:5d}{:5d} 10{:10.4f}{:10.4f}", } -} + + atoms = [1, 2, 3] + params = random_params(func_type.num_params) + line = patterns[func_type].format(*atoms, *params) + return line -def create_topology_data() -> str: +def create_topology_data(func_type: blocks.AngleFunctionType) -> str: """ https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html#tab-topfile2 """ - return """ + line = generate_topology_line(func_type) + return f""" [ moleculetype ] ; Name nrexcl Example 3 @@ -41,7 +48,7 @@ def create_topology_data() -> str: [ angles ] ; ai aj ak funct c0 c1 c2 c3 - 2 1 3 5 +{line} [ molecules ] ; Compound #mols @@ -49,7 +56,12 @@ def create_topology_data() -> str: """ -@patch("builtins.open", mock_open(read_data=create_topology_data())) -def test_angles(): - topol = gmx.fileformats.top.TOP("topol.top") - topol.molecules[0].angles[0].gromacs +@pytest.mark.parametrize("func_type", blocks.AngleFunctionType) +def test_angles(func_type: blocks.AngleFunctionType): + with patch( + "builtins.open", mock_open(read_data=create_topology_data(func_type)) + ) as mock: + topol = gmx.fileformats.top.TOP("topol.top") + [molecule] = topol.molecules + [angle] = molecule.angles + assert len(angle.gromacs["params"].keys()) == func_type.num_params From 03de6f6ccb552655adc38a3eac41ce29f9a6ca49 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 18:51:04 +0000 Subject: [PATCH 04/21] update top.py --- gromacs/fileformats/top.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 58f88508..d2042d5d 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -485,7 +485,6 @@ def _add_info(sys_or_mol, section, container): """ print(fields) ai, aj, ak = fields[:3] - ai, aj, ak = list(map(int, [ai, aj, ak])) fu = blocks.AngleFunctionType(int(fields[3])) @@ -502,6 +501,7 @@ def _add_info(sys_or_mol, section, container): ang.atype2 = aj ang.atype3 = ak elif curr_sec == "angles": + ai, aj, ak = list(map(int, [ai, aj, ak])) ang.atom1 = mol.atoms[ai - 1] ang.atom2 = mol.atoms[aj - 1] ang.atom3 = mol.atoms[ak - 1] From 882c35272200664aa9430d405322568dd53efd81 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 18:56:41 +0000 Subject: [PATCH 05/21] fix another bug in top.py --- gromacs/fileformats/top.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index d2042d5d..d7a2e019 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -483,12 +483,11 @@ def _add_info(sys_or_mol, section, container): angles 3 6 6 angles 3 8 ?? """ - print(fields) ai, aj, ak = fields[:3] fu = blocks.AngleFunctionType(int(fields[3])) - if len(fields[4:]) != fu.num_params: + if len(fields[4:]) != 0 and len(fields[4:]) != fu.num_params: raise ValueError( f"Expected {fu.num_params} parameters for function type {fu}, got {len(fields[4:])}" ) From 8093e066ea0f55a58c0b1abd0990d2cd41fc4c77 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 19:01:00 +0000 Subject: [PATCH 06/21] catch more corner-cases --- gromacs/fileformats/top.py | 113 +++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index d7a2e019..3920667a 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -509,62 +509,63 @@ def _add_info(sys_or_mol, section, container): params = list(map(float, fields[4:])) # Handle parameters based on function types - if fu in { - blocks.AngleFunctionType.HARMONIC, - blocks.AngleFunctionType.G96_ANGLE, - }: - ang.gromacs["params"] = { - "tetha0": params[0], - "ktetha": params[1], - } - elif fu == blocks.AngleFunctionType.CROSS_BOND_BOND: - ang.gromacs["params"] = { - "r1e": params[0], - "r2e": params[1], - "krrprime": params[2], - } - elif fu == blocks.AngleFunctionType.CROSS_BOND_ANGLE: - ang.gromacs["params"] = { - "r1e": params[0], - "r2eprime": params[1], - "r3e": params[2], - "krtheta": params[3], - } - elif fu == blocks.AngleFunctionType.UREY_BRADLEY: - ang.gromacs["params"] = { - "tetha0": params[0], - "ktetha": params[1], - "r13": params[2], - "kub": params[3], - } - elif fu == blocks.AngleFunctionType.QUARTIC_ANGLE: - ang.gromacs["params"] = { - "tetha0": params[0], - "C1": params[1], - "C2": params[2], - "C3": params[3], - "C4": params[4], - "C5": params[5], - } - elif fu == blocks.AngleFunctionType.TABULATED_ANGLE: - ang.gromacs["params"] = { - "table_number": params[0], - "k": params[1], - } # Assuming 'table number' is a parameter here - elif fu == blocks.AngleFunctionType.LINEAR_ANGLE: - ang.gromacs["params"] = { - "a0": params[0], - "klin": params[1], - } - elif fu == blocks.AngleFunctionType.RESTRICTED_BENDING: - ang.gromacs["params"] = { - "tetha0": params[0], - "ktheta": params[1], - } - else: - raise NotImplementedError( - f"Function type {fu} is not implemented" - ) + if fu.num_params == len(params): + if fu in { + blocks.AngleFunctionType.HARMONIC, + blocks.AngleFunctionType.G96_ANGLE, + }: + ang.gromacs["params"] = { + "tetha0": params[0], + "ktetha": params[1], + } + elif fu == blocks.AngleFunctionType.CROSS_BOND_BOND: + ang.gromacs["params"] = { + "r1e": params[0], + "r2e": params[1], + "krrprime": params[2], + } + elif fu == blocks.AngleFunctionType.CROSS_BOND_ANGLE: + ang.gromacs["params"] = { + "r1e": params[0], + "r2eprime": params[1], + "r3e": params[2], + "krtheta": params[3], + } + elif fu == blocks.AngleFunctionType.UREY_BRADLEY: + ang.gromacs["params"] = { + "tetha0": params[0], + "ktetha": params[1], + "r13": params[2], + "kub": params[3], + } + elif fu == blocks.AngleFunctionType.QUARTIC_ANGLE: + ang.gromacs["params"] = { + "tetha0": params[0], + "C1": params[1], + "C2": params[2], + "C3": params[3], + "C4": params[4], + "C5": params[5], + } + elif fu == blocks.AngleFunctionType.TABULATED_ANGLE: + ang.gromacs["params"] = { + "table_number": params[0], + "k": params[1], + } # Assuming 'table number' is a parameter here + elif fu == blocks.AngleFunctionType.LINEAR_ANGLE: + ang.gromacs["params"] = { + "a0": params[0], + "klin": params[1], + } + elif fu == blocks.AngleFunctionType.RESTRICTED_BENDING: + ang.gromacs["params"] = { + "tetha0": params[0], + "ktheta": params[1], + } + else: + raise NotImplementedError( + f"Function type {fu} is not implemented" + ) # Add the angle to the appropriate list and call _add_info if curr_sec == "angletypes": From be30949e6a27312d7b46c6a8dde3103a6198853f Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 19:04:12 +0000 Subject: [PATCH 07/21] found some latent bug or a typo? --- gromacs/fileformats/top.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 3920667a..8c3a763a 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -1218,10 +1218,10 @@ def _make_angletypes(self, m): at3 = ang.atype3 ang.convert("gromacs") - ktetha = ang.gromacs["param"]["ktetha"] - tetha0 = ang.gromacs["param"]["tetha0"] - kub = ang.gromacs["param"]["kub"] - s0 = ang.gromacs["param"]["s0"] + ktetha = ang.gromacs["params"]["ktetha"] + tetha0 = ang.gromacs["params"]["tetha0"] + kub = ang.gromacs["params"]["kub"] + s0 = ang.gromacs["params"]["s0"] fu = ang.gromacs["func"] From 52c4aff093c34a05e6523d285cdb73308ea68b95 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 19:06:55 +0000 Subject: [PATCH 08/21] update top.py to fix an UB problem --- gromacs/fileformats/top.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 8c3a763a..687f4ea6 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -535,7 +535,7 @@ def _add_info(sys_or_mol, section, container): ang.gromacs["params"] = { "tetha0": params[0], "ktetha": params[1], - "r13": params[2], + "s0": params[2], "kub": params[3], } elif fu == blocks.AngleFunctionType.QUARTIC_ANGLE: From 9404601252f57b8ccabbd6fd49b8a2c2ec30c024 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Sun, 5 Nov 2023 19:12:23 +0000 Subject: [PATCH 09/21] everything is 'param' singular --- gromacs/fileformats/top.py | 24 ++++++++++++------------ tests/fileformats/top/test_TOP.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 687f4ea6..8a962646 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -514,32 +514,32 @@ def _add_info(sys_or_mol, section, container): blocks.AngleFunctionType.HARMONIC, blocks.AngleFunctionType.G96_ANGLE, }: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "tetha0": params[0], "ktetha": params[1], } elif fu == blocks.AngleFunctionType.CROSS_BOND_BOND: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "r1e": params[0], "r2e": params[1], "krrprime": params[2], } elif fu == blocks.AngleFunctionType.CROSS_BOND_ANGLE: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "r1e": params[0], "r2eprime": params[1], "r3e": params[2], "krtheta": params[3], } elif fu == blocks.AngleFunctionType.UREY_BRADLEY: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "tetha0": params[0], "ktetha": params[1], "s0": params[2], "kub": params[3], } elif fu == blocks.AngleFunctionType.QUARTIC_ANGLE: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "tetha0": params[0], "C1": params[1], "C2": params[2], @@ -548,17 +548,17 @@ def _add_info(sys_or_mol, section, container): "C5": params[5], } elif fu == blocks.AngleFunctionType.TABULATED_ANGLE: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "table_number": params[0], "k": params[1], } # Assuming 'table number' is a parameter here elif fu == blocks.AngleFunctionType.LINEAR_ANGLE: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "a0": params[0], "klin": params[1], } elif fu == blocks.AngleFunctionType.RESTRICTED_BENDING: - ang.gromacs["params"] = { + ang.gromacs["param"] = { "tetha0": params[0], "ktheta": params[1], } @@ -1218,10 +1218,10 @@ def _make_angletypes(self, m): at3 = ang.atype3 ang.convert("gromacs") - ktetha = ang.gromacs["params"]["ktetha"] - tetha0 = ang.gromacs["params"]["tetha0"] - kub = ang.gromacs["params"]["kub"] - s0 = ang.gromacs["params"]["s0"] + ktetha = ang.gromacs["param"]["ktetha"] + tetha0 = ang.gromacs["param"]["tetha0"] + kub = ang.gromacs["param"]["kub"] + s0 = ang.gromacs["param"]["s0"] fu = ang.gromacs["func"] diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index 73b72082..a8adee71 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -64,4 +64,4 @@ def test_angles(func_type: blocks.AngleFunctionType): topol = gmx.fileformats.top.TOP("topol.top") [molecule] = topol.molecules [angle] = molecule.angles - assert len(angle.gromacs["params"].keys()) == func_type.num_params + assert len(angle.gromacs["param"].keys()) == func_type.num_params From a2e0ea4945e1b7564dbab576efef96fbfea220c5 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Tue, 7 Nov 2023 21:16:13 +0000 Subject: [PATCH 10/21] fixing tests --- gromacs/fileformats/top.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 8a962646..7b7812ec 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -517,6 +517,8 @@ def _add_info(sys_or_mol, section, container): ang.gromacs["param"] = { "tetha0": params[0], "ktetha": params[1], + "kub": None, # FIXME(jandom) + "s0": None, # FIXME(jandom) } elif fu == blocks.AngleFunctionType.CROSS_BOND_BOND: ang.gromacs["param"] = { @@ -1399,7 +1401,12 @@ def _make_angles(self, m): result = [] for ang in m.angles: fu = ang.gromacs["func"] - if ang.gromacs["param"]["ktetha"] and ang.gromacs["param"]["tetha0"]: + has_params = ( + "param" in ang.gromacs + and ang.gromacs["param"]["ktetha"] + and ang.gromacs["param"]["tetha0"] + ) + if has_params: ktetha, tetha0 = ( ang.gromacs["param"]["ktetha"], ang.gromacs["param"]["tetha0"], From 15828f42ce83d677b25560ac6cceb5ccb18693f2 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Tue, 7 Nov 2023 21:30:32 +0000 Subject: [PATCH 11/21] let's make this sane --- gromacs/fileformats/top.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index 7b7812ec..f9235fde 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -517,8 +517,6 @@ def _add_info(sys_or_mol, section, container): ang.gromacs["param"] = { "tetha0": params[0], "ktetha": params[1], - "kub": None, # FIXME(jandom) - "s0": None, # FIXME(jandom) } elif fu == blocks.AngleFunctionType.CROSS_BOND_BOND: ang.gromacs["param"] = { @@ -1222,8 +1220,8 @@ def _make_angletypes(self, m): ktetha = ang.gromacs["param"]["ktetha"] tetha0 = ang.gromacs["param"]["tetha0"] - kub = ang.gromacs["param"]["kub"] - s0 = ang.gromacs["param"]["s0"] + kub = ang.gromacs["param"].get("kub") + s0 = ang.gromacs["param"].get("s0") fu = ang.gromacs["func"] From 33ba32710b250af990a024b4c3435921428332bf Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:05:09 +0000 Subject: [PATCH 12/21] compatibility with python2.7 --- gromacs/fileformats/blocks.py | 4 ++-- gromacs/fileformats/top.py | 4 +++- tests/fileformats/top/test_TOP.py | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/gromacs/fileformats/blocks.py b/gromacs/fileformats/blocks.py index 2aaab426..d3122211 100644 --- a/gromacs/fileformats/blocks.py +++ b/gromacs/fileformats/blocks.py @@ -55,8 +55,8 @@ """ -import enum import logging +from aenum import IntEnum class System(object): @@ -442,7 +442,7 @@ def __eq__(self, other): ) -class AngleFunctionType(enum.IntEnum): +class AngleFunctionType(IntEnum): HARMONIC = 1 G96_ANGLE = 2 CROSS_BOND_BOND = 3 diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index f9235fde..dbd7a86b 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -489,7 +489,9 @@ def _add_info(sys_or_mol, section, container): if len(fields[4:]) != 0 and len(fields[4:]) != fu.num_params: raise ValueError( - f"Expected {fu.num_params} parameters for function type {fu}, got {len(fields[4:])}" + "Expected {num_params} parameters for function type {fu}, got {len(fields[4:])}".format( + num_params=fu.num_params, fu=fu + ) ) ang = blocks.AngleType("gromacs") diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index a8adee71..c997adec 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -5,9 +5,9 @@ from gromacs.fileformats import blocks -def generate_topology_line(func_type: blocks.AngleFunctionType) -> str: +def generate_topology_line(func_type: blocks.AngleFunctionType): # Generates random parameters for the different function types - def random_params(num) -> list[float]: + def random_params(num): return [random.uniform(0.0, 100.0) for _ in range(num)] # Define the pattern for each function type @@ -29,12 +29,12 @@ def random_params(num) -> list[float]: return line -def create_topology_data(func_type: blocks.AngleFunctionType) -> str: +def create_topology_data(func_type: blocks.AngleFunctionType): """ https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html#tab-topfile2 """ line = generate_topology_line(func_type) - return f""" + return """ [ moleculetype ] ; Name nrexcl Example 3 @@ -53,7 +53,9 @@ def create_topology_data(func_type: blocks.AngleFunctionType) -> str: [ molecules ] ; Compound #mols Example 1 -""" +""".format( + line=line + ) @pytest.mark.parametrize("func_type", blocks.AngleFunctionType) From 28264d2a3094b5b12858f58d37409848aba03fca Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:07:31 +0000 Subject: [PATCH 13/21] missing deps --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e3b90cf6..8fa4a3f7 100644 --- a/setup.py +++ b/setup.py @@ -69,10 +69,11 @@ ], }, install_requires=[ + "aenum", + "matplotlib", + "numkit", # numerical helpers "numpy>=1.0", "six", # towards py 3 compatibility - "numkit", # numerical helpers - "matplotlib", ], tests_require=["pytest", "pandas>=0.17"], zip_safe=True, From ba5a24346ab97911774dd61fb519e8ab3e560276 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:13:51 +0000 Subject: [PATCH 14/21] a new dep --- ci/conda-envs/test_env.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/conda-envs/test_env.yaml b/ci/conda-envs/test_env.yaml index 28698900..abef6b9a 100644 --- a/ci/conda-envs/test_env.yaml +++ b/ci/conda-envs/test_env.yaml @@ -8,3 +8,4 @@ dependencies: - matplotlib - pandas>=0.17 - numkit>=1.0 +- aenum From d77feb2aeed8ce5f9a88d7328c7e7a214022097f Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:17:04 +0000 Subject: [PATCH 15/21] update top.py --- gromacs/fileformats/top.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gromacs/fileformats/top.py b/gromacs/fileformats/top.py index dbd7a86b..36e45e96 100644 --- a/gromacs/fileformats/top.py +++ b/gromacs/fileformats/top.py @@ -566,7 +566,7 @@ def _add_info(sys_or_mol, section, container): } else: raise NotImplementedError( - f"Function type {fu} is not implemented" + "Function type {fu} is not implemented".forma(fu=fu) ) # Add the angle to the appropriate list and call _add_info From d6e4ebd6782e7ae19d72011b07b403d39d4b2a7b Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:19:56 +0000 Subject: [PATCH 16/21] removing typing --- tests/fileformats/top/test_TOP.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index c997adec..8668aaf4 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -5,7 +5,7 @@ from gromacs.fileformats import blocks -def generate_topology_line(func_type: blocks.AngleFunctionType): +def generate_topology_line(func_type): # Generates random parameters for the different function types def random_params(num): return [random.uniform(0.0, 100.0) for _ in range(num)] @@ -29,7 +29,7 @@ def random_params(num): return line -def create_topology_data(func_type: blocks.AngleFunctionType): +def create_topology_data(func_type): """ https://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html#tab-topfile2 """ @@ -59,7 +59,7 @@ def create_topology_data(func_type: blocks.AngleFunctionType): @pytest.mark.parametrize("func_type", blocks.AngleFunctionType) -def test_angles(func_type: blocks.AngleFunctionType): +def test_angles(func_type): with patch( "builtins.open", mock_open(read_data=create_topology_data(func_type)) ) as mock: From 17da06339b16d2aa93819047c631f46038604ae5 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:23:25 +0000 Subject: [PATCH 17/21] more python 2.7 compatibility --- tests/fileformats/top/test_TOP.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index 8668aaf4..42f0afa0 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -25,7 +25,7 @@ def random_params(num): atoms = [1, 2, 3] params = random_params(func_type.num_params) - line = patterns[func_type].format(*atoms, *params) + line = patterns[func_type].format(*(atoms + params)) return line From 47137195cfcbb01d44b62968ff11443457d1cd30 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:26:33 +0000 Subject: [PATCH 18/21] remove aenum --- ci/conda-envs/test_env.yaml | 1 - gromacs/fileformats/blocks.py | 2 +- setup.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ci/conda-envs/test_env.yaml b/ci/conda-envs/test_env.yaml index abef6b9a..28698900 100644 --- a/ci/conda-envs/test_env.yaml +++ b/ci/conda-envs/test_env.yaml @@ -8,4 +8,3 @@ dependencies: - matplotlib - pandas>=0.17 - numkit>=1.0 -- aenum diff --git a/gromacs/fileformats/blocks.py b/gromacs/fileformats/blocks.py index d3122211..d9a5042b 100644 --- a/gromacs/fileformats/blocks.py +++ b/gromacs/fileformats/blocks.py @@ -56,7 +56,7 @@ """ import logging -from aenum import IntEnum +from enum import IntEnum class System(object): diff --git a/setup.py b/setup.py index 4c66d861..a597758e 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,6 @@ ], }, install_requires=[ - "aenum", "matplotlib", "numkit", # numerical helpers "numpy>=1.0", From 3e9eb2ebd52dbf7b50971255b004f0df1521f809 Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:30:05 +0000 Subject: [PATCH 19/21] update CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 2b685dcd..767b4aee 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ orbeckst * removed support for legacy Python (<= 3.7) (#259) +* fix bug in gromacs TOP reader (#261) 2023-09-16 0.8.5 From 3ad892b029594476a54f0c0d95cbc741d93e68ac Mon Sep 17 00:00:00 2001 From: Jan Domanski Date: Wed, 8 Nov 2023 15:34:33 +0000 Subject: [PATCH 20/21] move to pytest.MonkeyPatch --- tests/fileformats/top/test_TOP.py | 38 +++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/fileformats/top/test_TOP.py b/tests/fileformats/top/test_TOP.py index 42f0afa0..cb28a0d6 100644 --- a/tests/fileformats/top/test_TOP.py +++ b/tests/fileformats/top/test_TOP.py @@ -1,10 +1,23 @@ import gromacs as gmx -from unittest.mock import patch, mock_open +import io import random import pytest from gromacs.fileformats import blocks +# This class simulates a file object +class MockFile(io.StringIO): + def __init__(self, text): + super(MockFile, self).__init__(text) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def generate_topology_line(func_type): # Generates random parameters for the different function types def random_params(num): @@ -59,11 +72,18 @@ def create_topology_data(func_type): @pytest.mark.parametrize("func_type", blocks.AngleFunctionType) -def test_angles(func_type): - with patch( - "builtins.open", mock_open(read_data=create_topology_data(func_type)) - ) as mock: - topol = gmx.fileformats.top.TOP("topol.top") - [molecule] = topol.molecules - [angle] = molecule.angles - assert len(angle.gromacs["param"].keys()) == func_type.num_params +def test_angles(func_type, monkeypatch): + # Create a custom function that will replace 'open' + def mock_open(*args, **kwargs): + if args[0] == "topol.top": + return MockFile(create_topology_data(func_type)) + else: + return open(*args, **kwargs) + + # Use monkeypatch to replace 'open' with 'mock_open' + monkeypatch.setattr("builtins.open", mock_open) + + topol = gmx.fileformats.top.TOP("topol.top") + [molecule] = topol.molecules + [angle] = molecule.angles + assert len(angle.gromacs["param"].keys()) == func_type.num_params From 6664d771b651864a488de45406b98ca4a97d2c20 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Wed, 8 Nov 2023 22:05:32 -0500 Subject: [PATCH 21/21] update CHANGES --- CHANGES | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 767b4aee..abb880dd 100644 --- a/CHANGES +++ b/CHANGES @@ -3,10 +3,11 @@ ============================== 2023-xx-xx 0.9.0 -orbeckst +orbeckst, jandom * removed support for legacy Python (<= 3.7) (#259) -* fix bug in gromacs TOP reader (#261) +* fixed GROMACS TOP reader not reading angle parameters from topology + file (#261) 2023-09-16 0.8.5