Skip to content

Commit

Permalink
add angles debugger
Browse files Browse the repository at this point in the history
  • Loading branch information
cbouy committed Oct 26, 2024
1 parent c8c2132 commit ce45fe4
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 59 deletions.
132 changes: 103 additions & 29 deletions prolif/debugger.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from abc import abstractmethod
from collections import defaultdict
from dataclasses import dataclass
from inspect import Parameter, signature
from typing import Generic, Literal, Optional, Self, Type, TypeVar
from typing import Generic, Literal, Optional, Self, Type, TypeVar, Union

from rdkit import Chem

from prolif.fingerprint import Fingerprint
from prolif.interactions.base import Interaction
from prolif.interactions.interactions import PiStacking
from prolif.residue import Residue
from prolif.types import Geometry, Pattern, Specs, is_geometry, is_pattern
from prolif.types import Angles, Geometry, Pattern, Specs, is_geometry, is_pattern

T = TypeVar("T")

Expand Down Expand Up @@ -83,15 +84,15 @@ def debug(
target=self.target,
explanation=(
f"{self.target} does not match {self.interaction} parameter"
f" {self.parameter!r}={smarts}"
f" {self.parameter}={smarts!r}"
),
)


@dataclass
class DebugDistance(DebugAction):
distance: float
cls: Type[Interaction]
distance: float
padding: float

@classmethod
Expand Down Expand Up @@ -124,16 +125,71 @@ def debug(
value=self.distance,
explanation=(
f"Could not find {self.interaction} interaction for parameter"
f" {self.parameter!r}={padded_distance}"
f" {self.parameter}={padded_distance}"
),
)
return None


@dataclass
class DebugAngles(DebugAction):
cls: Type[Interaction]
angles: Angles
padding: Angles

@classmethod
def from_spec(
cls, parameter: str, interaction: Interaction, spec: Geometry, padding: Angles
) -> Self:
icls = interaction.__class__
itype = icls.__name__
attr = spec.attr or parameter
angles: Angles = getattr(interaction, attr)
return cls(
interaction=itype,
parameter=parameter,
attr=attr,
angles=angles,
cls=icls,
padding=padding,
)

def debug(
self, ligand_residue: Residue, protein_residue: Residue
) -> Optional[DebugResult]:
padded_angles = (
self.angles[0] - self.padding[0],
self.angles[1] + self.padding[1],
)
interaction = self.cls(**{self.parameter: padded_angles})
metadata = next(interaction.detect(ligand_residue, protein_residue), None)
if metadata is None:
return DebugResult(
interaction=self.interaction,
parameter=self.parameter,
value=self.angles,
explanation=(
f"Could not find {self.interaction} interaction for parameter"
f" {self.parameter}={padded_angles}"
),
)
return None


class InteractionDebugger:
def __init__(self, fp: Fingerprint, distance_padding: float = 0.5) -> None:
def __init__(
self,
fp: Fingerprint,
distance_padding: float = 0.5,
angles_padding: Union[float, Angles] = 5,
) -> None:
self.fp = fp
self.distance_padding = distance_padding
self.angles_padding = (
(angles_padding, angles_padding)
if isinstance(angles_padding, float)
else angles_padding
)

interactions: dict[str, Interaction] = {}
for itype in fp.interactions:
Expand Down Expand Up @@ -175,33 +231,51 @@ def debug(
protein_residue: Residue,
interaction: Optional[str] = None,
) -> list[DebugResult]:
actions = self.gather_actions(interaction)
return [
result
for action in actions
if (result := action.debug(ligand_residue, protein_residue)) is not None
]

def gather_actions(self, interaction_name: Optional[str] = None):
results = []
for actions in self.gather_actions(interaction).values():
for action in actions:
if (
result := action.debug(ligand_residue, protein_residue)
) is not None:
results.append(result)
break
return results

def gather_actions(
self, interaction_name: Optional[str] = None
) -> dict[str, list[DebugAction]]:
actions: dict[str, list[DebugAction]] = defaultdict(list)

if interaction_name is None:
actions: list[DebugAction] = []
for itype in self.interactions:
actions.extend(self.gather_actions(itype))
return actions
for interaction_name in self.interactions:
acts = next(iter(self.gather_actions(interaction_name).values()))
actions[interaction_name].extend(acts)
return dict(actions)

interaction = self.interactions[interaction_name]
specs = self.specs[interaction_name]
actions: list[DebugAction] = []
for parameter, spec in specs.items():
if is_pattern(spec):
actions.extend(DebugPattern.from_spec(parameter, interaction, spec))
elif is_geometry(spec) and spec.type == "distance":
actions.append(
DebugDistance.from_spec(
parameter,
interaction,
spec,
padding=self.distance_padding,
)
actions[interaction_name].extend(
DebugPattern.from_spec(parameter, interaction, spec)
)
return actions
elif is_geometry(spec):
if spec.type == "distance":
actions[interaction_name].append(
DebugDistance.from_spec(
parameter,
interaction,
spec,
padding=self.distance_padding,
)
)
else:
actions[interaction_name].append(
DebugAngles.from_spec(
parameter,
interaction,
spec,
padding=self.angles_padding,
)
)
return dict(actions)
34 changes: 24 additions & 10 deletions prolif/interactions/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
SingleAngle,
)
from prolif.interactions.constants import VDW_PRESETS, VDWRADII # noqa
from prolif.types import Geometry, Pattern
from prolif.types import Angles, Geometry, Pattern
from prolif.utils import angle_between_limits, get_centroid, get_ring_normal_vector

__all__ = [
Expand Down Expand Up @@ -102,10 +102,14 @@ class HBAcceptor(SingleAngle):

def __init__(
self,
acceptor="[#7&!$([nX3])&!$([NX3]-*=[O,N,P,S])&!$([NX3]-[a])&!$([Nv4&+1]),O&!$([OX2](C)C=O)&!$(O(~a)~a)&!$(O=N-*)&!$([O-]-N=O),o+0,F&$(F-[#6])&!$(F-[#6][F,Cl,Br,I])]",
donor="[$([O,S;+0]),$([N;v3,v4&+1]),n+0]-[H]",
distance=3.5,
DHA_angle=(130, 180),
acceptor: Annotated[str, Pattern(ligand="lig_pattern")] = (
"[#7&!$([nX3])&!$([NX3]-*=[O,N,P,S])&!$([NX3]-[a])&!$([Nv4&+1]),O&!$([OX2](C)C=O)&!$(O(~a)~a)&!$(O=N-*)&!$([O-]-N=O),o+0,F&$(F-[#6])&!$(F-[#6][F,Cl,Br,I])]"
),
donor: Annotated[
str, Pattern(protein="prot_pattern")
] = "[$([O,S;+0]),$([N;v3,v4&+1]),n+0]-[H]",
distance: Annotated[float, Geometry("distance")] = 3.5,
DHA_angle: Annotated[Angles, Geometry("angles", attr="angle")] = (130, 180),
):
super().__init__(
lig_pattern=acceptor,
Expand Down Expand Up @@ -156,11 +160,21 @@ class XBAcceptor(DoubleAngle):

def __init__(
self,
acceptor="[#7,#8,P,S,Se,Te,a;!+{1-}]!#[*]",
donor="[#6,#7,Si,F,Cl,Br,I]-[Cl,Br,I,At]",
distance=3.5,
AXD_angle=(130, 180),
XAR_angle=(80, 140),
acceptor: Annotated[
str, Pattern(ligand="lig_pattern")
] = "[#7,#8,P,S,Se,Te,a;!+{1-}]!#[*]",
donor: Annotated[
str, Pattern(protein="prot_pattern")
] = "[#6,#7,Si,F,Cl,Br,I]-[Cl,Br,I,At]",
distance: Annotated[float, Geometry("distance")] = 3.5,
AXD_angle: Annotated[Angles, Geometry("angles", attr="L1P2P1_angle")] = (
130,
180,
),
XAR_angle: Annotated[Angles, Geometry("angles", attr="L2L1P2_angle")] = (
80,
140,
),
):
super().__init__(
lig_pattern=acceptor,
Expand Down
1 change: 0 additions & 1 deletion prolif/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class Pattern(Specs):
class Geometry(Specs):
type: Literal["distance", "angles"]
attr: Optional[str] = None
metadata: Optional[str] = None


def is_pattern(spec: Specs) -> TypeGuard[Pattern]:
Expand Down
63 changes: 44 additions & 19 deletions tests/test_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from MDAnalysis.transformations import rotateby, translate
from rdkit import Chem, RDLogger

import prolif
import prolif as plf
from prolif.debugger import DebugResult
from prolif.fingerprint import Fingerprint
from prolif.interactions import VdWContact
from prolif.interactions.base import _INTERACTIONS, Interaction, get_mapindex
from prolif.interactions.constants import VDW_PRESETS
from prolif.molecule import Molecule

# disable rdkit warnings
lg = RDLogger.logger()
Expand All @@ -17,7 +19,7 @@

@pytest.fixture(scope="module")
def benzene_universe():
benzene = mda.Universe(prolif.datafiles.datapath / "benzene.mol2")
benzene = mda.Universe(plf.datafiles.datapath / "benzene.mol2")
elements = mda.topology.guessers.guess_types(benzene.atoms.names)
benzene.add_TopologyAttr("elements", elements)
benzene.segments.segids = np.array(["U1"], dtype=object)
Expand All @@ -35,12 +37,12 @@ def interaction_instances():


@pytest.fixture(scope="session")
def any_mol(request):
def any_mol(request) -> Molecule:
return request.getfixturevalue(request.param)


@pytest.fixture(scope="session")
def any_other_mol(request):
def any_other_mol(request) -> Molecule:
return request.getfixturevalue(request.param)


Expand Down Expand Up @@ -129,24 +131,47 @@ def test_interaction(
assert next(interaction(any_mol[0], any_other_mol[0]), False) is expected

@pytest.mark.parametrize(
("any_mol", "any_other_mol"),
("any_mol", "any_other_mol", "fp_kwargs", "expected"),
[
("benzene", "ftf"),
(
"benzene",
"cation",
{"interactions": ["Hydrophobic"]},
["hydrophobic"],
),
(
"benzene",
"ftf",
{
"interactions": ["Hydrophobic"],
"parameters": {"Hydrophobic": {"distance": 0}},
},
["distance"],
),
(
"xb_donor",
"xb_acceptor_false_axd",
{"interactions": ["XBDonor"]},
["AXD_angle"],
),
],
indirect=["any_mol", "any_other_mol"],
)
def test_debugger(self, any_mol, any_other_mol):
def test_debugger(
self,
any_mol: Molecule,
any_other_mol: Molecule,
fp_kwargs: dict,
expected: list[DebugResult],
) -> None:
from prolif.debugger import InteractionDebugger

fp = Fingerprint(
["Hydrophobic"],
parameters={
"Hydrophobic": {"distance": 0},
},
)
debugger = InteractionDebugger(fp, distance_padding=1.0)
fp = Fingerprint(**fp_kwargs)
debugger = InteractionDebugger(fp)
results = debugger.debug(any_mol, any_other_mol)
results
assert len(results) == len(expected)
for r, e in zip(results, expected):
assert r.parameter == e

def test_warning_supersede(self):
old = id(_INTERACTIONS["Hydrophobic"])
Expand All @@ -160,8 +185,8 @@ def detect(self):
assert old != new
# fix dummy Hydrophobic class being reused in later unrelated tests

class Hydrophobic(prolif.interactions.Hydrophobic):
__doc__ = prolif.interactions.Hydrophobic.__doc__
class Hydrophobic(plf.interactions.Hydrophobic):
__doc__ = plf.interactions.Hydrophobic.__doc__

def test_error_no_detect(self):
with pytest.raises(
Expand Down Expand Up @@ -358,8 +383,8 @@ def create_rings(benzene_universe, xyz, rotation):
rotz = rotateby(rotation[2], [0, 0, 1], ag=r2.atoms)
r2.trajectory.add_transformations(tr, rotx, roty, rotz)
return (
prolif.Molecule.from_mda(benzene_universe)[0],
prolif.Molecule.from_mda(r2)[0],
Molecule.from_mda(benzene_universe)[0],
Molecule.from_mda(r2)[0],
)

def test_edgetoface_phe331(self, ligand_mol, protein_mol):
Expand Down

0 comments on commit ce45fe4

Please sign in to comment.