Skip to content

Commit 461e352

Browse files
authored
Ignore self interaction (#357)
* feat: add ignore parameter * move ignore to Fingerprint constructor * fix styling
1 parent 50e95a0 commit 461e352

10 files changed

Lines changed: 232 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11+
- Added `ignore` parameter in `Fingerprint` to ignore specific pairs of residues.
12+
Defaults to ignoring interactions of a residue with itself.
1113
- Support for `ImplicitHBAcceptor` and `ImplicitHBDonor` interactions to calculate
1214
hydrogen bond interactions with merely heavy atoms. This helps users to estimate
1315
the hydrogen bond interactions for structures from PDB database or AI-driven

docs/notebooks/advanced.ipynb

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,55 @@
560560
"You can then prepare your system and run the analysis as you normally would."
561561
]
562562
},
563+
{
564+
"cell_type": "markdown",
565+
"metadata": {},
566+
"source": [
567+
"### Ignoring specific residue pairs\n",
568+
"\n",
569+
"By default, ProLIF skips calculating interactions between a residue and itself, which typically happens when calculating interactions of a protein with itself:"
570+
]
571+
},
572+
{
573+
"cell_type": "code",
574+
"execution_count": null,
575+
"metadata": {},
576+
"outputs": [],
577+
"source": [
578+
"fp = plf.Fingerprint([\"HBAcceptor\", \"HBDonor\"])\n",
579+
"fp.run_from_iterable([protein_mol], protein_mol)\n",
580+
"fp.to_dataframe()"
581+
]
582+
},
583+
{
584+
"cell_type": "markdown",
585+
"metadata": {},
586+
"source": [
587+
"You can define your own predicate function to decide which residue pairs should be ignored. This can be done through the `ignore` parameter of the `Fingerprint` constructor, which takes two {class}`~prolif.residue.Residue` objects as input and returns a boolean:"
588+
]
589+
},
590+
{
591+
"cell_type": "code",
592+
"execution_count": null,
593+
"metadata": {},
594+
"outputs": [],
595+
"source": [
596+
"from prolif.residue import Residue\n",
597+
"\n",
598+
"\n",
599+
"def ignore_sequence_neighbours(res1: Residue, res2: Residue) -> bool:\n",
600+
" \"\"\"Skips the pair if they are close in the amino-acid sequence.\"\"\"\n",
601+
" return (\n",
602+
" res1.resid.chain == res2.resid.chain\n",
603+
" and abs(res1.resid.number - res2.resid.number) <= 4\n",
604+
" )\n",
605+
"\n",
606+
"\n",
607+
"fp = plf.Fingerprint([\"HBAcceptor\", \"HBDonor\"], ignore=ignore_sequence_neighbours)\n",
608+
"fp.run_from_iterable([protein_mol], protein_mol)\n",
609+
"fp.to_dataframe()"
610+
]
611+
},
563612
{
564613
"cell_type": "markdown",
565614
"metadata": {},
@@ -592,8 +641,8 @@
592641
"outputs": [],
593642
"source": [
594643
"frame_number = 0\n",
595-
"ligand_residue = \"UNL1\"\n",
596-
"protein_residue = \"VAL200.A\"\n",
644+
"ligand_residue = \"TYR40.A\"\n",
645+
"protein_residue = \"ASP352.B\"\n",
597646
"\n",
598647
"fp.ifp[frame_number][(ligand_residue, protein_residue)]"
599648
]

docs/source/modules/residues.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ Residues
99
.. autoclass:: ResidueId
1010
:members:
1111

12-
12+
Utilities
13+
---------
14+
15+
.. autofunction:: ignore_self_interactions
16+
1317
ResidueGroup
1418
------------
1519

prolif/fingerprint.py

Lines changed: 97 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,17 @@
2626
"""
2727

2828
import warnings
29-
from collections.abc import Iterable, Sequence, Sized
29+
from collections.abc import Callable, Iterable, Sequence, Sized
3030
from inspect import signature
31-
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast, overload
31+
from typing import (
32+
TYPE_CHECKING,
33+
Any,
34+
Literal,
35+
Optional,
36+
Union,
37+
cast,
38+
overload,
39+
)
3240

3341
import dill
3442
import numpy as np
@@ -53,6 +61,7 @@
5361
get_n_jobs,
5462
)
5563
from prolif.plotting.utils import IS_NOTEBOOK
64+
from prolif.residue import Residue, ignore_self_interactions
5665
from prolif.utils import (
5766
get_residues_near_ligand,
5867
to_bitvectors,
@@ -68,7 +77,7 @@
6877

6978
from prolif.plotting.complex3d import Complex3D
7079
from prolif.plotting.network import LigNetwork
71-
from prolif.residue import Residue, ResidueId
80+
from prolif.residue import ResidueId
7281
from prolif.typeshed import (
7382
IFPData,
7483
IFPResults,
@@ -131,6 +140,10 @@ class Fingerprint:
131140
implicit_hydrogens : bool
132141
Whether to use interactions compatible with implicit hydrogens instead of the
133142
default explicit hydrogen-based ones.
143+
ignore : Callable[[Residue, Residue], bool]
144+
Predicate function that returns ``True`` if the two residues should be
145+
skipped when calculating interactions. Default is to ignore interactions
146+
between the same residue (see :func:`~prolif.residue.ignore_self_interactions`).
134147
135148
Attributes
136149
----------
@@ -245,7 +258,7 @@ class for more information.
245258
246259
.. versionchanged:: 2.2.0
247260
Added ``implicit_hydrogens`` for easily switching to implicit-hydrogen
248-
interactions.
261+
interactions. Added ``ignore`` parameter.
249262
"""
250263

251264
def __init__(
@@ -256,6 +269,7 @@ def __init__(
256269
vicinity_cutoff: float = 6.0,
257270
use_segid: bool | None = None,
258271
implicit_hydrogens: bool = False,
272+
ignore: Callable[[Residue, Residue], bool] = ignore_self_interactions,
259273
) -> None:
260274
if interactions is None:
261275
interactions = DEFAULT_INTERACTIONS
@@ -286,6 +300,7 @@ def __init__(
286300
interactions = temp_interactions
287301

288302
self.count = count
303+
self.ignore = ignore
289304
self._set_interactions(interactions, parameters)
290305
self.vicinity_cutoff = vicinity_cutoff
291306
self.parameters = parameters
@@ -541,6 +556,8 @@ def generate(
541556
)
542557
for prot_key in prot_residues: # type:ignore[union-attr]
543558
pres = prot[prot_key]
559+
if self.ignore(lres, pres):
560+
continue
544561
key = (lresid, pres.resid)
545562
interactions = get_interactions(lres, pres)
546563
if any(interactions):
@@ -561,91 +578,91 @@ def run(
561578
) -> "Fingerprint":
562579
"""Generates the fingerprint on a trajectory for a ligand and a protein
563580
564-
Parameters
565-
----------
566-
traj : MDAnalysis.coordinates.base.ProtoReader or MDAnalysis.coordinates.base.FrameIteratorSliced or MDAnalysis.coordinates.base.FrameIteratorIndices or MDAnalysis.coordinates.timestep.Timestep
567-
Iterate over this Universe trajectory or sliced trajectory object
568-
to extract the frames used for the fingerprint extraction
569-
lig : MDAnalysis.core.groups.AtomGroup
570-
An MDAnalysis AtomGroup for the ligand
571-
prot : MDAnalysis.core.groups.AtomGroup
572-
An MDAnalysis AtomGroup for the protein (with multiple residues)
573-
residues : list or "all" or None
574-
A list of protein residues (:class:`str`, :class:`int` or
575-
:class:`~prolif.residue.ResidueId`) to take into account for
576-
the fingerprint extraction. If ``"all"``, all residues will be
577-
used. If ``None``, at each frame the
578-
:func:`~prolif.utils.get_residues_near_ligand` function is used to
579-
automatically use protein residues that are distant of 6.0 Å or
580-
less from each ligand residue.
581-
converter_kwargs : tuple[dict, dict], optional
582-
Tuple of kwargs passed to the underlying
583-
:class:`~MDAnalysis.converters.RDKit.RDKitConverter` from MDAnalysis: the
584-
first for the ligand, and the second for the protein
585-
progress : bool
586-
Display a :class:`~tqdm.std.tqdm` progressbar while running the calculation
587-
n_jobs : int or None
588-
Number of processes to run in parallel. If ``n_jobs=1``, the analysis
589-
will run in serial. If ``n_jobs=None``, see
590-
:func:`~prolif.parallel.get_n_jobs` for the default behavior.
591-
parallel_strategy : {"chunk", "queue"}, optional
592-
Strategy for parallel execution:
593-
594-
- ``"chunk"``: Split trajectory into chunks and distribute to workers.
595-
Each worker pickles the full MDAnalysis objects once per chunk.
596-
Scales better for small trajectories.
597-
- ``"queue"``: Main thread converts frames to Molecules and enqueues
598-
them for workers. Avoids repeated MDAnalysis pickling overhead.
599-
Better when pickling is expensive, e.g. large trajectories with many
600-
atoms.
601-
- ``None``: See :func:`~prolif.parallel.get_mda_parallel_strategy` for
602-
the default behavior.
603-
604-
Raises
581+
Parameters
582+
----------
583+
traj : MDAnalysis.coordinates.base.ProtoReader or MDAnalysis.coordinates.base.FrameIteratorSliced or MDAnalysis.coordinates.base.FrameIteratorIndices or MDAnalysis.coordinates.timestep.Timestep
584+
Iterate over this Universe trajectory or sliced trajectory object
585+
to extract the frames used for the fingerprint extraction
586+
lig : MDAnalysis.core.groups.AtomGroup
587+
An MDAnalysis AtomGroup for the ligand
588+
prot : MDAnalysis.core.groups.AtomGroup
589+
An MDAnalysis AtomGroup for the protein (with multiple residues)
590+
residues : list or "all" or None
591+
A list of protein residues (:class:`str`, :class:`int` or
592+
:class:`~prolif.residue.ResidueId`) to take into account for
593+
the fingerprint extraction. If ``"all"``, all residues will be
594+
used. If ``None``, at each frame the
595+
:func:`~prolif.utils.get_residues_near_ligand` function is used to
596+
automatically use protein residues that are distant of 6.0 Å or
597+
less from each ligand residue.
598+
converter_kwargs : tuple[dict, dict], optional
599+
Tuple of kwargs passed to the underlying
600+
:class:`~MDAnalysis.converters.RDKit.RDKitConverter` from MDAnalysis: the
601+
first for the ligand, and the second for the protein
602+
progress : bool
603+
Display a :class:`~tqdm.std.tqdm` progressbar while running the calculation
604+
n_jobs : int or None
605+
Number of processes to run in parallel. If ``n_jobs=1``, the analysis
606+
will run in serial. If ``n_jobs=None``, see
607+
:func:`~prolif.parallel.get_n_jobs` for the default behavior.
608+
parallel_strategy : {"chunk", "queue"}, optional
609+
Strategy for parallel execution:
610+
611+
- ``"chunk"``: Split trajectory into chunks and distribute to workers.
612+
Each worker pickles the full MDAnalysis objects once per chunk.
613+
Scales better for small trajectories.
614+
- ``"queue"``: Main thread converts frames to Molecules and enqueues
615+
them for workers. Avoids repeated MDAnalysis pickling overhead.
616+
Better when pickling is expensive, e.g. large trajectories with many
617+
atoms.
618+
- ``None``: See :func:`~prolif.parallel.get_mda_parallel_strategy` for
619+
the default behavior.
620+
621+
Raises
605622
------
606-
ValueError
607-
If ``n_jobs <= 0``
623+
ValueError
624+
If ``n_jobs <= 0``
608625
609-
Returns
610-
-------
611-
prolif.fingerprint.Fingerprint
612-
The Fingerprint instance that generated the fingerprint
626+
Returns
627+
-------
628+
prolif.fingerprint.Fingerprint
629+
The Fingerprint instance that generated the fingerprint
613630
614-
Example
615-
-------
616-
::
631+
Example
632+
-------
633+
::
617634
618-
>>> u = mda.Universe("top.pdb", "traj.nc")
619-
>>> lig = u.select_atoms("resname LIG")
620-
>>> prot = u.select_atoms("protein")
621-
>>> fp = prolif.Fingerprint().run(u.trajectory[:10], lig, prot)
635+
>>> u = mda.Universe("top.pdb", "traj.nc")
636+
>>> lig = u.select_atoms("resname LIG")
637+
>>> prot = u.select_atoms("protein")
638+
>>> fp = prolif.Fingerprint().run(u.trajectory[:10], lig, prot)
622639
623-
.. seealso::
640+
.. seealso::
624641
625-
- :meth:`Fingerprint.generate` to generate the fingerprint between
626-
two single structures.
627-
- :meth:`Fingerprint.run_from_iterable` to generate the fingerprint
628-
between a protein and a collection of ligands.
642+
- :meth:`Fingerprint.generate` to generate the fingerprint between
643+
two single structures.
644+
- :meth:`Fingerprint.run_from_iterable` to generate the fingerprint
645+
between a protein and a collection of ligands.
629646
630-
.. versionchanged:: 0.3.2
631-
Moved the ``return_atoms`` parameter from the ``run`` method to the
632-
dataframe conversion code
647+
.. versionchanged:: 0.3.2
648+
Moved the ``return_atoms`` parameter from the ``run`` method to the
649+
dataframe conversion code
633650
634-
.. versionchanged:: 1.0.0
635-
Added support for multiprocessing
651+
.. versionchanged:: 1.0.0
652+
Added support for multiprocessing
636653
637-
.. versionchanged:: 1.1.0
638-
Added support for passing kwargs to the RDKitConverter through
639-
the ``converter_kwargs`` parameter
654+
.. versionchanged:: 1.1.0
655+
Added support for passing kwargs to the RDKitConverter through
656+
the ``converter_kwargs`` parameter
640657
641-
.. versionchanged:: 2.0.0
642-
Changed the format of the :attr:`~Fingerprint.ifp` attribute to be a
643-
dictionary containing more complete interaction metadata instead of just
644-
atom indices.
658+
.. versionchanged:: 2.0.0
659+
Changed the format of the :attr:`~Fingerprint.ifp` attribute to be a
660+
dictionary containing more complete interaction metadata instead of just
661+
atom indices.
645662
646-
.. versionchanged:: 2.1.0
647-
Added ``use_segid``, ``parallel_strategy`` parameter and changed the
648-
default behavior of ``n_jobs=None``.
663+
.. versionchanged:: 2.1.0
664+
Added ``use_segid``, ``parallel_strategy`` parameter and changed the
665+
default behavior of ``n_jobs=None``.
649666
650667
""" # noqa: E501
651668
if converter_kwargs is not None and len(converter_kwargs) != 2:
@@ -997,7 +1014,7 @@ def _run_bridged_analysis(
9971014
""" # noqa: E501
9981015
self.ifp = cast("IFPResults", getattr(self, "ifp", {}))
9991016
for interaction in self.bridged_interactions.values():
1000-
interaction.setup(ifp_store=self.ifp, **kwargs)
1017+
interaction.setup(ifp_store=self.ifp, ignore=self.ignore, **kwargs)
10011018
interaction.run(traj, lig, prot)
10021019
return self
10031020

@@ -1016,7 +1033,7 @@ def _run_iter_bridged_analysis(
10161033
"""
10171034
self.ifp = getattr(self, "ifp", {})
10181035
for interaction in self.bridged_interactions.values():
1019-
interaction.setup(ifp_store=self.ifp, **kwargs)
1036+
interaction.setup(ifp_store=self.ifp, ignore=self.ignore, **kwargs)
10201037
interaction.run_from_iterable(lig_iterable, prot_mol)
10211038
return self
10221039

prolif/interactions/water_bridge.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def setup(self, ifp_store: Optional["IFPResults"] = None, **kwargs: Any) -> None
110110
self.residues = self.kwargs.pop("residues", None)
111111
self.converter_kwargs = self.kwargs.pop("converter_kwargs", ({}, {}))
112112
self.water_fp.use_segid = self.kwargs.pop("use_segid", False)
113+
self.water_fp.ignore = self.kwargs.pop("ignore", self.water_fp.ignore)
113114

114115
def run(
115116
self,

prolif/molecule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def from_mda(
157157
158158
.. versionchanged:: 2.1.0
159159
Added `use_segid`.
160-
"""
160+
""" # noqa: E501
161161
ag = obj.select_atoms(selection) if selection else obj.atoms
162162
if ag.n_atoms == 0:
163163
raise mda.SelectionError("AtomGroup is empty, please check your selection")

0 commit comments

Comments
 (0)