Skip to content

Commit

Permalink
add interactions iterator
Browse files Browse the repository at this point in the history
  • Loading branch information
cbouy committed Jul 1, 2024
1 parent 9be3cea commit 9172025
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `IFP.interactions()` iterator that yields all interaction data for a given frame in
a single flat structure. This makes iterating over the `fp.ifp` results a bit
easier / less nested.
- `Complex3D` and `fp.plot_3d` now have access to `only_interacting` and
`remove_hydrogens` parameters to control which residues and hydrogen atoms are
displayed. Non-polar hydrogen atoms that aren't involved in interactions are now
Expand Down
58 changes: 57 additions & 1 deletion docs/notebooks/advanced.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,62 @@
"source": [
"You can then prepare your system and run the analysis as you normally would."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Accessing results\n",
"\n",
"Once the fingerprint analysis has been run, there are multiple ways to access the data. The most convenient one showcased in the tutorials is through a pandas DataFrame, however this only shows the residues involved in each interaction."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fp.to_dataframe()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The complete data is stored on the `ifp` attribute of the fingerprint object as a dictionary indexed by residues:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"frame_number = 0\n",
"ligand_residue = \"UNL1\"\n",
"protein_residue = \"VAL200.A\"\n",
"\n",
"fp.ifp[frame_number][(ligand_residue, protein_residue)]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To make it easier to work with this deeply nested data structure, the results can also be accessed in a flatter structure like so:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for interaction_data in fp.ifp[frame_number].interactions():\n",
" print(interaction_data)\n",
" break"
]
}
],
"metadata": {
Expand All @@ -399,7 +455,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.13"
"version": "3.11.6"
},
"orig_nbformat": 4
},
Expand Down
24 changes: 24 additions & 0 deletions prolif/ifp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
"""

from collections import UserDict
from typing import Iterator, NamedTuple

from prolif.residue import ResidueId


class InteractionData(NamedTuple):
ligand: ResidueId
protein: ResidueId
interaction: str
metadata: dict


class IFP(UserDict):
"""Mapping between residue pairs and interaction fingerprint.
Expand Down Expand Up @@ -67,3 +75,19 @@ def __getitem__(self, key):
"either ResidueId or residue string. If you need to filter the IFP, a "
"single ResidueId or residue string can also be used.",
)

def interactions(self) -> Iterator[InteractionData]:
"""Yields all interactions data as an :class:`InteractionData` namedtuple.
.. versionadded:: 2.1.0
"""
for (ligand_resid, protein_resid), ifp_dict in self.data.items():
for int_name, metadata_tuple in ifp_dict.items():
for metadata in metadata_tuple:
yield InteractionData(
ligand=ligand_resid,
protein=protein_resid,
interaction=int_name,
metadata=metadata,
)
20 changes: 16 additions & 4 deletions tests/test_ifp.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import pytest

from prolif.fingerprint import Fingerprint
from prolif.ifp import IFP, InteractionData
from prolif.residue import ResidueId


@pytest.fixture(scope="session")
def ifp(u, ligand_ag, protein_ag):
def ifp(u, ligand_ag, protein_ag) -> IFP:
fp = Fingerprint(["Hydrophobic", "VdWContact"])
fp.run(u.trajectory[0:1], ligand_ag, protein_ag)
return fp.ifp[0]


def test_ifp_indexing(ifp):
def test_ifp_indexing(ifp: IFP) -> None:
lig_id, prot_id = "LIG1.G", "LEU126.A"
metadata1 = ifp[(ResidueId.from_string(lig_id), ResidueId.from_string(prot_id))]
metadata2 = ifp[(lig_id, prot_id)]
assert metadata1 is metadata2


def test_ifp_filtering(ifp):
def test_ifp_filtering(ifp: IFP) -> None:
lig_id, prot_id = "LIG1.G", "LEU126.A"
assert ifp[lig_id] == ifp
assert (
Expand All @@ -27,6 +28,17 @@ def test_ifp_filtering(ifp):
)


def test_wrong_key(ifp):
def test_wrong_key(ifp: IFP) -> None:
with pytest.raises(KeyError, match="does not correspond to a valid IFP key"):
ifp[0]


def test_interaction_data_iteration(ifp: IFP) -> None:
data = next(ifp.interactions())
assert isinstance(data, InteractionData)
assert data.ligand == ResidueId("LIG", 1, "G")
assert data.protein.chain == "A"
assert data.interaction in {"Hydrophobic", "VdWContact"}
assert "distance" in data.metadata
for data in ifp.interactions():
assert isinstance(data, InteractionData)

0 comments on commit 9172025

Please sign in to comment.