diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6dea77172..ec1373c40 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,9 +56,13 @@ jobs: coverage report --omit='*/bin/pytest' - if: ${{ matrix.WITH_CODECOV }} - name: Report code coverage - run: | - codecov + name: Upload coverage codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + fail_ci_if_error: false + verbose: true lint: runs-on: ubuntu-latest diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 72e8c9db4..f5dc54266 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -60,7 +60,7 @@ jobs: - if: ${{ matrix.WITH_CODECOV }} name: Upload coverage codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/.gitignore b/.gitignore index fae21f56a..25761930a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ htmlcov doc/build doc/source/api doc/source/.doctrees + +.idea/ diff --git a/bin/martinize2 b/bin/martinize2 index f6e4f8d3b..fb269959d 100755 --- a/bin/martinize2 +++ b/bin/martinize2 @@ -634,7 +634,9 @@ def entry(): help="Mutate a residue. Desired mutation is " "specified as, e.g. A-PHE45:ALA. The format is " "-:. Elements " - "of the specification can be omitted as required.", + "of the specification can be omitted as required." + "e.g. PHE45:ALA will mutate all PHE with resid 45 to ALA, " + "A-PHE:ALA will mutate all PHE on chain A to ALA", ) prot_group.add_argument( "-modify", @@ -646,7 +648,9 @@ def entry(): "modification is specified as, e.g. A-ASP45:ASP0. " "The format is -:." " Elements of the specification can be omitted as " - "required.", + "required. e.g. ASP45:ASP0 will modify all ASP with " + "resid 45 to ASP0, A-ASP:ASP0 will modify all ASP on " + "chain A to ASP0", ) prot_group.add_argument( "-nter", diff --git a/vermouth/data/quotes.txt b/vermouth/data/quotes.txt index 92ac7f39f..42cd87498 100644 --- a/vermouth/data/quotes.txt +++ b/vermouth/data/quotes.txt @@ -35,3 +35,5 @@ Happiness is a dry martini and a good woman... or a bad woman. -- George Burns A classical Martini is made without Vermouth, although it is better with. -- Peter C Kroon A classical Martini is made with up to 2 sizes of olives, although newer variants can contain up to three sizes of olives. -- Peter C Kroon + +So I said 'I must get out of these wet clothes, and into a dry Martini' -- Homer Simpson diff --git a/vermouth/processors/annotate_mut_mod.py b/vermouth/processors/annotate_mut_mod.py index 52bdee045..8994d56c9 100644 --- a/vermouth/processors/annotate_mut_mod.py +++ b/vermouth/processors/annotate_mut_mod.py @@ -176,8 +176,41 @@ def _format_resname(res): out += res.get('insertion_code', '') return out +def _resiter(mod, residue_graph, resspec, library, key, molecule): + """ + Iterate over residues to find a specific modification + + Parameters + ---------- + mod: str + The modification to apply, eg N-ter, C-ter + residue_graph: networkx.Graph + A graph with one node per residue. + resspec: dict + Attributes that must be present in the residue node. 'resname' is + treated specially as described above. + library: dict + dictionary of modifications/mutations from the force field + key: str + from associations + molecule: networkx.Graph + """ + mod_found = False + for res_idx in residue_graph: + if residue_matches(resspec, residue_graph, res_idx): + mod_found = True + if mod != 'none' and mod not in library: + raise NameError('{} is not known as a {} for ' + 'force field {}' + ''.format(mod, key, molecule.force_field.name)) + res = residue_graph.nodes[res_idx] + LOGGER.debug('Annotating {} with {} {}', + _format_resname(res), key, mod) + for node_idx in res['graph']: + molecule.nodes[node_idx][key] = molecule.nodes[node_idx].get(key, []) + [mod] + return mod_found -def annotate_modifications(molecule, modifications, mutations): +def annotate_modifications(molecule, modifications, mutations, resspec_counts): """ Annotate nodes in molecule with the desired modifications and mutations @@ -194,6 +227,12 @@ def annotate_modifications(molecule, modifications, mutations): the attributes a residue has to fulfill. It can contain the elements 'chain', 'resname' and 'resid'. The second element is the mutation that should be applied. + resspec_counts: list[dict] + List modified in place containing information about whether a + modification/mutation has been applied successfully. If the target is + found, the dictionary has one entry, {'success': True}. If not, + 'success' is False and there are additional items to indicate information + about the failure. Raises ------ @@ -210,25 +249,23 @@ def annotate_modifications(molecule, modifications, mutations): (mutations, 'mutation', molecule.force_field.blocks)] residue_graph = make_residue_graph(molecule) + # Get the name of the chain in the molecule that we're looking at + residue = {key: residue_graph.nodes[0].get(key) + for key in 'chain resid resname insertion_code'.split()} + chain = residue['chain'] + extra = False for mutmod, key, library in associations: for resspec, mod in mutmod: - mod_found = False - for res_idx in residue_graph: - if residue_matches(resspec, residue_graph, res_idx): - mod_found = True - if mod != 'none' and mod not in library: - raise NameError('{} is not known as a {} for ' - 'force field {}' - ''.format(mod, key, molecule.force_field.name)) - res = residue_graph.nodes[res_idx] - LOGGER.debug('Annotating {} with {} {}', - _format_resname(res), key, mod) - for node_idx in res['graph']: - molecule.nodes[node_idx][key] = molecule.nodes[node_idx].get(key, []) + [mod] - if mod_found == False: - LOGGER.warning('Mutation "{}" not found. ' - 'Check target resid!' - ''.format(_format_resname(resspec))) + mod_found = _resiter(mod, residue_graph, resspec, library, key, molecule) + if not mod_found: + #if no mod found, return that there's a problem + resspec_counts.append({'success': False, + 'mutmod': _format_resname(resspec), + 'post': mod,}) + extra = True + #return that everything's fine by default + if not extra: + resspec_counts.append({'success': True}) class AnnotateMutMod(Processor): """ @@ -245,6 +282,7 @@ class AnnotateMutMod(Processor): :func:`annotate_modifications` """ def __init__(self, modifications=None, mutations=None): + self.resspec_counts = [] if not modifications: modifications = [] if not mutations: @@ -257,5 +295,11 @@ def __init__(self, modifications=None, mutations=None): self.mutations.append((parse_residue_spec(resspec), val)) def run_molecule(self, molecule): - annotate_modifications(molecule, self.modifications, self.mutations) + annotate_modifications(molecule, self.modifications, self.mutations, self.resspec_counts) return molecule + def run_system(self, system): + super().run_system(system) + _exit = sum([i['success'] for i in self.resspec_counts]) + if _exit == 0: + LOGGER.warning('Residue specified by "{}" for mutation "{}" not found', + self.resspec_counts[0]['mutmod'], self.resspec_counts[0]['post']) diff --git a/vermouth/tests/test_annotate_mut_mod.py b/vermouth/tests/test_annotate_mut_mod.py index 3608258be..2b37f575a 100644 --- a/vermouth/tests/test_annotate_mut_mod.py +++ b/vermouth/tests/test_annotate_mut_mod.py @@ -17,6 +17,7 @@ import networkx as nx import pytest +from vermouth.system import System from vermouth.molecule import Molecule from vermouth.forcefield import ForceField from vermouth.processors.annotate_mut_mod import ( @@ -149,7 +150,7 @@ def test_subdict(dict1, dict2, expected): ) ]) def test_annotate_modifications(example_mol, modifications, mutations, expected_mod, expected_mut): - annotate_modifications(example_mol, modifications, mutations) + annotate_modifications(example_mol, modifications, mutations,[]) for node_idx, mods in expected_mod.items(): assert _subdict(mods, example_mol.nodes[node_idx]) for node_idx, mods in expected_mut.items(): @@ -166,7 +167,7 @@ def test_single_residue_mol(): mol.add_edges_from([(0, 1)]) modification = [({'resname': 'A', 'resid': 2}, 'C-ter'),] - annotate_modifications(mol, modification, []) + annotate_modifications(mol, modification, [],[]) assert mol.nodes[0] == {'modification': ['C-ter'], 'resname': 'A', 'resid': 2, 'chain': 'A'} assert mol.nodes[1] == {'modification': ['C-ter'], 'resname': 'A', 'resid': 2, 'chain': 'A'} @@ -178,7 +179,7 @@ def test_single_residue_mol(): ]) def test_annotate_modifications_error(example_mol, modifications, mutations): with pytest.raises(NameError): - annotate_modifications(example_mol, modifications, mutations) + annotate_modifications(example_mol, modifications, mutations,[]) def test_unknown_terminus_match(): @@ -292,7 +293,7 @@ def test_nter_cter_modifications(node_data, edge_data, expected): modification = [({'resname': 'cter'}, 'C-ter'), ({'resname': 'nter'}, 'N-ter')] - annotate_modifications(mol, modification, []) + annotate_modifications(mol, modification, [],[]) found = {} for node_idx in mol: @@ -302,38 +303,159 @@ def test_nter_cter_modifications(node_data, edge_data, expected): assert found == expected -@pytest.mark.parametrize('node_data, edge_data, expected', [ +@pytest.mark.parametrize('node_data, edge_data, mutation, expected', [ ( [ - {'resname': 'GLY', 'resid': 1}, - {'resname': 'ALA', 'resid': 2}, - {'resname': 'ALA', 'resid': 3} + {'chain': 'A', 'resname': 'GLY', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3} ], [(0, 1), (1, 2)], + [({'resname': 'GLY', 'resid': 1, 'chain': 'A'}, 'MET')], False ), ( [ - {'resname': 'ALA', 'resid': 1}, - {'resname': 'ALA', 'resid': 2}, - {'resname': 'ALA', 'resid': 3} + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3} ], [(0, 1), (1, 2)], + [({'resname': 'GLY', 'resid': 1, 'chain': 'A'}, 'MET')], + True + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2)], + [({'resname': 'GLY', 'resid': 1}, 'MET')], + True + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2)], + [({'resname': 'ALA', 'resid': 1}, 'MET')], + False + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ALA', 'resid': 1}, + {'chain': 'B', 'resname': 'ALA', 'resid': 2}, + {'chain': 'B', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'resname': 'ALA', 'chain': 'A'}, 'GLY')], + False + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ALA', 'resid': 1}, + {'chain': 'B', 'resname': 'ALA', 'resid': 2}, + {'chain': 'B', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'resname': 'GLY', 'resid': 1}, 'ALA')], + True + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ALA', 'resid': 1}, + {'chain': 'B', 'resname': 'ALA', 'resid': 2}, + {'chain': 'B', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'resname': 'GLY', 'resid': 1, 'chain': 'A'}, 'ALA')], True - )]) -def test_mod_resid_not_correct(caplog, node_data, edge_data, expected): + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ALA', 'resid': 1}, + {'chain': 'B', 'resname': 'ALA', 'resid': 2}, + {'chain': 'B', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'resname': 'ALA', 'resid': 1}, 'GLY')], + False + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ALA', 'resid': 1}, + {'chain': 'B', 'resname': 'ALA', 'resid': 2}, + {'chain': 'B', 'resname': 'ALA', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'resname': 'GLY', 'chain': 'A'}, 'ALA')], + True + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ASN', 'resid': 1}, + {'chain': 'B', 'resname': 'ASN', 'resid': 2}, + {'chain': 'B', 'resname': 'ASN', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'resid': 1, 'resname': 'ASN'}, 'ALA')], + False + ), + ( + [ + {'chain': 'A', 'resname': 'ALA', 'resid': 1}, + {'chain': 'A', 'resname': 'ALA', 'resid': 2}, + {'chain': 'A', 'resname': 'ALA', 'resid': 3}, + {'chain': 'B', 'resname': 'ASN', 'resid': 1}, + {'chain': 'B', 'resname': 'ASN', 'resid': 2}, + {'chain': 'B', 'resname': 'ASN', 'resid': 3} + ], + [(0, 1), (1, 2), (3, 4), (4, 5)], + [({'chain':'B', 'resname': 'ASN'}, 'ALA')], + False + ) +]) +def test_mod_resid_not_correct(caplog, node_data, edge_data, mutation, expected): """ Tests that the modification is found in the expected residue. """ + system = System(force_field=ForceField(FF_UNIVERSAL_TEST)) mol = Molecule(force_field=ForceField(FF_UNIVERSAL_TEST)) mol.add_nodes_from(enumerate(node_data)) mol.add_edges_from(edge_data) - mutation = [({'resname': 'GLY', 'resid': 1}, 'MET')] - + + mols = nx.connected_components(mol) + for nodes in mols: + system.add_molecule(mol.subgraph(nodes)) + + processor = AnnotateMutMod() + processor.mutations = mutation # Resspecs are already "parsed" + caplog.clear() - annotate_modifications(mol, [], mutation) - - if expected: - assert '"GLY1" not found.' in str(caplog.records[0].getMessage()) + processor.run_system(system) + + if expected: + assert any(rec.levelname == 'WARNING' for rec in caplog.records) else: assert caplog.records == []