Repository: marrink-lab/vermouth-martinize

Issue:
Increase usability by removing unrecognized atoms
Currently, if a PTM is not recognized martinize turns into a mushroom. This is not great. Instead, all atoms that are not recognized should be removed and warning(s) issued. This should either be done by CanonicalizeModifications, or a separate processor. In the second case, CanonicalizeModifications should mark all unknown atoms.

Similarly, the error message when RepairGraph can't find a reference block for a specific residue name should be better (IIRC). It's probably undesirable to remove complete unrecognized residues though.

Discussion:

Patch:
diff --git a/vermouth/processors/canonicalize_modifications.py b/vermouth/processors/canonicalize_modifications.py
index 2679131..33a9722 100644
--- a/vermouth/processors/canonicalize_modifications.py
+++ b/vermouth/processors/canonicalize_modifications.py
@@ -276,14 +276,18 @@ def fix_ptm(molecule):
         try:
             identified = identify_ptms(residue, res_ptms, options)
         except KeyError:
-            LOGGER.exception('Could not identify the modifications for'
-                             ' residues {}, involving atoms {}',
-                             ['{resname}{resid}'.format(**molecule.nodes[resid_to_idxs[resid][0]])
-                              for resid in sorted(set(resids))],
-                             ['{atomid}-{atomname}'.format(**molecule.nodes[idx])
-                              for idxs in res_ptms for idx in idxs[0]],
-                             type='unknown-input')
-            raise
+            LOGGER.warning('Could not identify the modifications for'
+                           ' residues {}, involving atoms {}',
+                           ['{resname}{resid}'.format(**molecule.nodes[resid_to_idxs[resid][0]])
+                            for resid in sorted(set(resids))],
+                           ['{atomid}-{atomname}'.format(**molecule.nodes[idx])
+                            for idxs in res_ptms for idx in idxs[0]],
+                           type='unknown-input')
+            for idxs in res_ptms:
+                for idx in idxs[0]:
+                    molecule.remove_node(idx)
+            continue
+
         # Why this mess? There can be multiple PTMs for a single (set of)
         # residue(s); and a single PTM can span multiple residues.
         LOGGER.info("Identified the modifications {} on residues {}",


Test patch:
diff --git a/vermouth/tests/test_repair_graph.py b/vermouth/tests/test_repair_graph.py
index e902d91..ece4438 100644
--- a/vermouth/tests/test_repair_graph.py
+++ b/vermouth/tests/test_repair_graph.py
@@ -19,7 +19,6 @@ Test graph reparation and related operations.
 import copy
 import logging
 
-import networkx as nx
 import pytest
 import vermouth
 from vermouth.molecule import Link
@@ -268,14 +267,15 @@ def test_renaming(renamed_graph):
 
 
 
-@pytest.mark.parametrize('resid,mutations,modifications,atomnames',[
+@pytest.mark.parametrize('resid,mutations,modifications,atomnames', [
     (1, ['ALA'], [], 'O C CA HA N HN CB HB1 HB2 HB3'),  # The glutamate chain and N-ter are removed
     (1, [], ['N-ter'], 'O C CA HA N H HN CB HB1 HB2 CG HG1 HG2 CD OE1 OE2'),  # HE1 got removed
     (2, ['ALA'], ['N-ter', 'C-ter'], 'O OXT C CA HA N H HN CB HB1 HB2 HB3'),
     (2, ['GLU'], [], 'O C CA HA N HN CB HB1 HB2 CG HG1 HG2 CD OE1 OE2'),  # Added glutamate sidechain
     (5, ['GLY'], ['none'], 'N CA C O HN HA1 HA2'),  # Remove O2 from C-ter mod
 ])
-def test_repair_graph_with_mutation_modification(system_mod, resid, mutations, modifications, atomnames):
+def test_repair_graph_with_mutation_modification(system_mod, resid, mutations,
+                                                 modifications, atomnames):
     mol = system_mod.molecules[0]
     # Let's mutate res1 to ALA
     for node_idx in mol:
@@ -296,11 +296,13 @@ def test_repair_graph_with_mutation_modification(system_mod, resid, mutations, m
     assert resid1_atomnames == set(atomnames.split())
 
 
-@pytest.mark.parametrize('resid,mutations,modifications',[
+@pytest.mark.parametrize('resid,mutations,modifications', [
     (2, [], ['GLU-H']),  # The glutamate chain and N-ter are removed
     (2, ['ALA', 'LEU'], [])
 ])
-def test_repair_graph_with_mutation_modification_error(system_mod, caplog, resid, mutations, modifications):
+def test_repair_graph_with_mutation_modification_error(system_mod, caplog,
+                                                       resid, mutations,
+                                                       modifications):
     mol = system_mod.molecules[0]
     # Let's mutate res1 to ALA
     for node_idx in mol:
@@ -313,3 +315,54 @@ def test_repair_graph_with_mutation_modification_error(system_mod, caplog, resid
         assert not caplog.records
         mol = vermouth.RepairGraph().run_molecule(mol)
         assert len(caplog.records) == 1
+
+
+@pytest.mark.parametrize('known_mod_names', [
+    [],
+    ['C-ter'],
+    ['C-ter', 'N-ter'],
+    ['GLU-H', 'N-ter'],
+])
+def test_unknown_mods_removed(caplog, repaired_graph, known_mod_names):
+    """
+    Tests that atoms that are part of modifications, but are not recognized, get
+    removed from the graph by CanonicalizeModifications
+    """
+    caplog.set_level(logging.WARNING)
+    ff = copy.copy(repaired_graph.force_field)
+    for mod_name in known_mod_names:
+        assert mod_name in ff.modifications  # Purely defensive
+
+    removed_mods = []
+    for name, mod in dict(ff.modifications).items():
+        if name not in known_mod_names:
+            del ff.modifications[name]
+            removed_mods.append(mod)
+
+    repaired_graph.force_field = ff
+    mol = repaired_graph.molecules[0]
+
+    assert not caplog.records
+    assert len(mol) == 46
+    vermouth.CanonicalizeModifications().run_system(repaired_graph)
+
+    assert caplog.records
+
+    for record in caplog.records:
+        assert record.levelname == 'WARNING'
+
+    assert len(mol) < 46
+    atomnames = dict(mol.nodes(data='atomname')).values()
+    for mod in removed_mods:
+        for node_key in mod.nodes:
+            node = mod.nodes[node_key]
+            if node['PTM_atom']:
+                assert node['atomname'] not in atomnames
+
+    for node_key in mol.nodes:
+        node = mol.nodes[node_key]
+        if node.get('PTM_atom'):
+            contained_by = [mod for mod in ff.modifications.values()
+                            if node.get('expected', node['atomname']) in
+                               dict(mod.nodes(data='atomname')).values()]
+            assert len(contained_by) == 1