Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interactions iterator #210

Merged
merged 2 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 in {"A", "B"}
assert data.interaction in {"Hydrophobic", "VdWContact"}
assert "distance" in data.metadata
for data in ifp.interactions():
assert isinstance(data, InteractionData)
Loading