Source code for hbat.core.interactions

"""
Molecular interaction classes for HBAT analysis.

This module defines the data structures for representing different types of
molecular interactions including hydrogen bonds, halogen bonds, π interactions,
and cooperativity chains.
"""

import math
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional, Union

from .np_vector import NPVec3D
from .structure import Atom


[docs] class MolecularInteraction(ABC): """Base class for all molecular interactions. This abstract base class defines the unified interface for all types of molecular interactions analyzed by HBAT, including hydrogen bonds, halogen bonds, and π interactions. All interactions have the following core components: - Donor: The electron/proton donor (atom or virtual atom) - Acceptor: The electron/proton acceptor (atom or virtual atom) - Interaction: The mediating atom/point (e.g., hydrogen, π center) - Geometry: Distances and angles defining the interaction - Bonding: The interaction atom must be bonded to the donor atom **Bonding Requirements:** - For H-bonds: Hydrogen must be covalently bonded to the donor - For X-bonds: Halogen is covalently bonded to donor carbon - For X-H...π interactions: Hydrogen must be covalently bonded to the donor - For π-π stacking (future): No bonding requirement - uses centroid distances """
[docs] @abstractmethod def get_donor(self) -> Union[Atom, NPVec3D]: """Get the donor atom or virtual atom. :returns: The donor atom or virtual atom position :rtype: Union[Atom, NPVec3D] """ pass
[docs] @abstractmethod def get_acceptor(self) -> Union[Atom, NPVec3D]: """Get the acceptor atom or virtual atom. :returns: The acceptor atom or virtual atom position :rtype: Union[Atom, NPVec3D] """ pass
[docs] @abstractmethod def get_interaction(self) -> Union[Atom, NPVec3D]: """Get the interaction mediating atom or point. :returns: The mediating atom (e.g., hydrogen) or virtual point (e.g., π center) :rtype: Union[Atom, NPVec3D] """ pass
[docs] @abstractmethod def get_donor_residue(self) -> str: """Get the donor residue identifier. :returns: String identifier for the donor residue :rtype: str """ pass
[docs] @abstractmethod def get_acceptor_residue(self) -> str: """Get the acceptor residue identifier. :returns: String identifier for the acceptor residue :rtype: str """ pass
[docs] @abstractmethod def get_interaction_type(self) -> str: """Get the interaction type. :returns: String identifier for the interaction type :rtype: str """ pass
[docs] @abstractmethod def get_donor_interaction_distance(self) -> float: """Get the donor to interaction distance. :returns: Distance from donor to interaction point in Angstroms :rtype: float """ pass
[docs] @abstractmethod def get_donor_acceptor_distance(self) -> float: """Get the donor to acceptor distance. :returns: Distance from donor to acceptor in Angstroms :rtype: float """ pass
[docs] @abstractmethod def get_donor_interaction_acceptor_angle(self) -> float: """Get the donor-interaction-acceptor angle. :returns: Angle in radians :rtype: float """ pass
[docs] @abstractmethod def is_donor_interaction_bonded(self) -> bool: """Check if the interaction atom is bonded to the donor atom. This is a fundamental requirement for most molecular interactions (except π-π stacking which will be implemented separately). :returns: True if donor and interaction atom are bonded :rtype: bool """ pass
# Legacy and convenience properties @property def donor(self) -> Union[Atom, NPVec3D]: """Property accessor for donor.""" return self.get_donor() @property def acceptor(self) -> Union[Atom, NPVec3D]: """Property accessor for acceptor.""" return self.get_acceptor() @property def interaction(self) -> Union[Atom, NPVec3D]: """Property accessor for interaction.""" return self.get_interaction() @property def donor_residue(self) -> str: """Property accessor for donor residue.""" return self.get_donor_residue() @property def acceptor_residue(self) -> str: """Property accessor for acceptor residue.""" return self.get_acceptor_residue() @property def interaction_type(self) -> str: """Property accessor for interaction type.""" return self.get_interaction_type() @property def donor_interaction_distance(self) -> float: """Property accessor for donor-interaction distance.""" return self.get_donor_interaction_distance() @property def donor_acceptor_distance(self) -> float: """Property accessor for donor-acceptor distance.""" return self.get_donor_acceptor_distance() @property def donor_interaction_acceptor_angle(self) -> float: """Property accessor for donor-interaction-acceptor angle.""" return self.get_donor_interaction_acceptor_angle() # Legacy compatibility methods
[docs] def get_donor_atom(self) -> Optional[Atom]: """Get the donor atom if it's an Atom instance. :returns: The donor atom if it's an Atom, None otherwise :rtype: Optional[Atom] """ donor = self.get_donor() return donor if isinstance(donor, Atom) else None
[docs] def get_acceptor_atom(self) -> Optional[Atom]: """Get the acceptor atom if it's an Atom instance. :returns: The acceptor atom if it's an Atom, None otherwise :rtype: Optional[Atom] """ acceptor = self.get_acceptor() return acceptor if isinstance(acceptor, Atom) else None
@property def distance(self) -> float: """Legacy property for interaction distance. :returns: Donor-interaction distance for backward compatibility :rtype: float """ return self.get_donor_interaction_distance() @property def angle(self) -> float: """Legacy property for interaction angle. :returns: Donor-interaction-acceptor angle for backward compatibility :rtype: float """ return self.get_donor_interaction_acceptor_angle()
[docs] class HydrogenBond(MolecularInteraction): """Represents a hydrogen bond interaction. This class stores all information about a detected hydrogen bond, including the participating atoms, geometric parameters, and classification information. :param _donor: The hydrogen bond donor atom :type _donor: Atom :param hydrogen: The hydrogen atom in the bond :type hydrogen: Atom :param _acceptor: The hydrogen bond acceptor atom :type _acceptor: Atom :param distance: H...A distance in Angstroms :type distance: float :param angle: D-H...A angle in radians :type angle: float :param _donor_acceptor_distance: D...A distance in Angstroms :type _donor_acceptor_distance: float :param bond_type: Classification of the hydrogen bond type :type bond_type: str :param _donor_residue: Identifier for donor residue :type _donor_residue: str :param _acceptor_residue: Identifier for acceptor residue :type _acceptor_residue: str """
[docs] def __init__( self, _donor: Atom, hydrogen: Atom, _acceptor: Atom, distance: float, angle: float, _donor_acceptor_distance: float, bond_type: str, _donor_residue: str, _acceptor_residue: str, ): """Initialize a HydrogenBond object. :param _donor: The hydrogen bond donor atom :type _donor: Atom :param hydrogen: The hydrogen atom in the bond :type hydrogen: Atom :param _acceptor: The hydrogen bond acceptor atom :type _acceptor: Atom :param distance: H...A distance in Angstroms :type distance: float :param angle: D-H...A angle in radians :type angle: float :param _donor_acceptor_distance: D...A distance in Angstroms :type _donor_acceptor_distance: float :param bond_type: Classification of the hydrogen bond type :type bond_type: str :param _donor_residue: Identifier for donor residue :type _donor_residue: str :param _acceptor_residue: Identifier for acceptor residue :type _acceptor_residue: str """ self._donor = _donor self.hydrogen = hydrogen self._acceptor = _acceptor self._distance = distance self._angle = angle self._donor_acceptor_distance = _donor_acceptor_distance self.bond_type = bond_type self._donor_residue = _donor_residue self._acceptor_residue = _acceptor_residue # Generate donor-acceptor property description self._donor_acceptor_properties = self._generate_donor_acceptor_description()
# Backward compatibility properties @property def distance(self) -> float: return self._distance @property def angle(self) -> float: return self._angle @property def donor(self) -> Atom: """Property accessor for donor atom.""" return self._donor @property def acceptor(self) -> Atom: """Property accessor for acceptor atom.""" return self._acceptor # MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: return self._donor
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: return self._acceptor
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: return self.hydrogen
[docs] def get_donor_residue(self) -> str: return self._donor_residue
[docs] def get_acceptor_residue(self) -> str: return self._acceptor_residue
[docs] def get_interaction_type(self) -> str: return "H-Bond"
[docs] def get_donor_interaction_distance(self) -> float: """Distance from donor to hydrogen.""" return float(self._donor.coords.distance_to(self.hydrogen.coords))
[docs] def get_donor_acceptor_distance(self) -> float: """Distance from donor to acceptor.""" return self._donor_acceptor_distance
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """D-H...A angle.""" return self._angle
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if hydrogen is bonded to donor. For hydrogen bonds, the hydrogen must be covalently bonded to the donor atom. This method assumes the bond has been validated during creation. :returns: True (assumes validation was done during creation) :rtype: bool """ # In practice, this should be validated during object creation # by checking bond lists in the analyzer return True # Assuming validation was done during creation
def _generate_donor_acceptor_description(self) -> str: """Generate donor-acceptor property description string. Describes the hydrogen bond in terms of: - Donor properties: residue type, backbone/sidechain, aromatic - Acceptor properties: residue type, backbone/sidechain, aromatic Format: "donor_props-acceptor_props" (e.g., "PBS-PS", "DS-LN") :returns: Property description string :rtype: str """ # Get donor properties donor_residue_type = getattr(self._donor, "residue_type", "L") donor_backbone_sidechain = getattr(self._donor, "backbone_sidechain", "S") donor_aromatic = getattr(self._donor, "aromatic", "N") # Get acceptor properties acceptor_residue_type = getattr(self._acceptor, "residue_type", "L") acceptor_backbone_sidechain = getattr(self._acceptor, "backbone_sidechain", "S") acceptor_aromatic = getattr(self._acceptor, "aromatic", "N") # Build property strings donor_props = f"{donor_residue_type}{donor_backbone_sidechain}{donor_aromatic}" acceptor_props = ( f"{acceptor_residue_type}{acceptor_backbone_sidechain}{acceptor_aromatic}" ) return f"{donor_props}-{acceptor_props}" @property def donor_acceptor_properties(self) -> str: """Get the donor-acceptor property description. :returns: Property description string :rtype: str """ return self._donor_acceptor_properties
[docs] def get_backbone_sidechain_interaction(self) -> str: """Get simplified backbone/sidechain interaction description. :returns: Interaction type (B-B, B-S, S-B, S-S) :rtype: str """ donor_bs = getattr(self._donor, "backbone_sidechain", "S") acceptor_bs = getattr(self._acceptor, "backbone_sidechain", "S") return f"{donor_bs}-{acceptor_bs}"
def __str__(self) -> str: return ( f"H-Bond: {self.donor_residue}({self._donor.name}) - " f"H - {self.acceptor_residue}({self._acceptor.name}) " f"[{self.distance:.2f}Å, {math.degrees(self.angle):.1f}°] " f"[{self.get_backbone_sidechain_interaction()}] [{self.donor_acceptor_properties}]" )
[docs] class HalogenBond(MolecularInteraction): """Represents a halogen bond interaction. This class stores information about a detected halogen bond, where a halogen atom (Cl, Br, I) acts as an electrophilic center interacting with nucleophilic acceptors. HBAT uses updated default parameters with a 150° angle cutoff for improved detection of biologically relevant halogen bonds. :param halogen: The halogen atom (F, Cl, Br, I) :type halogen: Atom :param _acceptor: The electron donor/acceptor atom :type _acceptor: Atom :param distance: X...A distance in Angstroms :type distance: float :param angle: C-X...A angle in radians (default cutoff: 150°) :type angle: float :param bond_type: Classification of the halogen bond type :type bond_type: str :param _halogen_residue: Identifier for halogen-containing residue :type _halogen_residue: str :param _acceptor_residue: Identifier for acceptor residue :type _acceptor_residue: str :param _donor: The donor atom (typically carbon) bonded to the halogen :type _donor: Atom """
[docs] def __init__( self, halogen: Atom, _acceptor: Atom, distance: float, angle: float, bond_type: str, _halogen_residue: str, _acceptor_residue: str, _donor: Atom, ): """Initialize a HalogenBond object. :param halogen: The halogen atom (F, Cl, Br, I) :type halogen: Atom :param _acceptor: The electron donor/acceptor atom :type _acceptor: Atom :param distance: X...A distance in Angstroms :type distance: float :param angle: C-X...A angle in radians :type angle: float :param bond_type: Classification of the halogen bond type :type bond_type: str :param _halogen_residue: Identifier for halogen-containing residue :type _halogen_residue: str :param _acceptor_residue: Identifier for acceptor residue :type _acceptor_residue: str :param _donor: The donor atom (typically carbon) bonded to the halogen :type _donor: Atom """ self.halogen = halogen self._acceptor = _acceptor self._distance = distance self._angle = angle self.bond_type = bond_type self._halogen_residue = _halogen_residue self._acceptor_residue = _acceptor_residue self._donor = _donor # Generate donor-acceptor property description self._donor_acceptor_properties = self._generate_donor_acceptor_description()
# Backward compatibility properties @property def distance(self) -> float: return self._distance @property def angle(self) -> float: return self._angle @property def halogen_residue(self) -> str: """Legacy property for halogen residue.""" return self._halogen_residue @property def donor(self) -> Atom: """Property accessor for donor atom (halogen).""" return self.halogen @property def donor_atom(self) -> Atom: """Property accessor for donor atom (carbon bonded to halogen).""" return self._donor @property def acceptor(self) -> Atom: """Property accessor for acceptor atom.""" return self._acceptor # MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: return self.halogen # Halogen acts as electron acceptor (Lewis acid)
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: return self._acceptor
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: return self.halogen # Halogen is both donor and interaction point
[docs] def get_donor_residue(self) -> str: return self._halogen_residue
[docs] def get_acceptor_residue(self) -> str: return self._acceptor_residue
[docs] def get_interaction_type(self) -> str: return "X-Bond"
[docs] def get_donor_interaction_distance(self) -> float: """Distance from donor to interaction point (0 for halogen bonds).""" return 0.0 # Halogen is both donor and interaction point
[docs] def get_donor_acceptor_distance(self) -> float: """Distance from halogen to acceptor.""" return self._distance
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """C-X...A angle.""" return self._angle
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if halogen is bonded to donor carbon. For halogen bonds, the halogen atom must be covalently bonded to a carbon atom. The halogen serves as both the donor and interaction point. :returns: True (assumes validation was done during creation) :rtype: bool """ # In practice, this should be validated during object creation # by ensuring the halogen is bonded to carbon return True # Assuming validation was done during creation
def _generate_donor_acceptor_description(self) -> str: """Generate donor-acceptor property description string. Describes the halogen bond in terms of: - Donor properties: residue type, backbone/sidechain, aromatic (halogen donor) - Acceptor properties: residue type, backbone/sidechain, aromatic Format: "donor_props-acceptor_props" (e.g., "PSN-LBN", "LSN-PSA") :returns: Property description string :rtype: str """ # Get halogen (donor) properties donor_residue_type = getattr(self.halogen, "residue_type", "L") donor_backbone_sidechain = getattr(self.halogen, "backbone_sidechain", "S") donor_aromatic = getattr(self.halogen, "aromatic", "N") # Get acceptor properties acceptor_residue_type = getattr(self._acceptor, "residue_type", "L") acceptor_backbone_sidechain = getattr(self._acceptor, "backbone_sidechain", "S") acceptor_aromatic = getattr(self._acceptor, "aromatic", "N") # Build property strings donor_props = f"{donor_residue_type}{donor_backbone_sidechain}{donor_aromatic}" acceptor_props = ( f"{acceptor_residue_type}{acceptor_backbone_sidechain}{acceptor_aromatic}" ) return f"{donor_props}-{acceptor_props}" @property def donor_acceptor_properties(self) -> str: """Get the donor-acceptor property description. :returns: Property description string :rtype: str """ return self._donor_acceptor_properties
[docs] def get_backbone_sidechain_interaction(self) -> str: """Get simplified backbone/sidechain interaction description. :returns: Interaction type (B-B, B-S, S-B, S-S) :rtype: str """ donor_bs = getattr(self.halogen, "backbone_sidechain", "S") acceptor_bs = getattr(self._acceptor, "backbone_sidechain", "S") return f"{donor_bs}-{acceptor_bs}"
def __str__(self) -> str: return ( f"X-Bond: {self._halogen_residue}({self._donor.name}-{self.halogen.name}) - " f"{self._acceptor_residue}({self._acceptor.name}) " f"[{self.distance:.2f}Å, {math.degrees(self.angle):.1f}°] " f"[{self.get_backbone_sidechain_interaction()}] [{self.donor_acceptor_properties}]" )
[docs] class PiInteraction(MolecularInteraction): """Represents a D-X...π interaction. This class stores information about a detected D-X...π interaction, where a donor atom with an interaction atom (H, F, Cl, Br, I) interacts with an aromatic π system. Supports multiple subtypes: - C-H...π, N-H...π, O-H...π, S-H...π (hydrogen-π interactions) - C-Cl...π, C-Br...π, C-I...π (halogen-π interactions) :param _donor: The donor atom (C, N, O, S) :type _donor: Atom :param hydrogen: The interaction atom (H, F, Cl, Br, I) - name kept for backward compatibility :type hydrogen: Atom :param pi_center: Center of the aromatic π system :type pi_center: NPVec3D :param distance: X...π distance in Angstroms :type distance: float :param angle: D-X...π angle in radians :type angle: float :param _donor_residue: Identifier for donor residue :type _donor_residue: str :param _pi_residue: Identifier for π-containing residue :type _pi_residue: str """
[docs] def __init__( self, _donor: Atom, hydrogen: Atom, pi_center: NPVec3D, distance: float, angle: float, _donor_residue: str, _pi_residue: str, ): """Initialize a PiInteraction object. :param _donor: The donor atom (C, N, O, S) :type _donor: Atom :param hydrogen: The interaction atom (H, F, Cl, Br, I) - name kept for backward compatibility :type hydrogen: Atom :param pi_center: Center of the aromatic π system :type pi_center: NPVec3D :param distance: X...π distance in Angstroms :type distance: float :param angle: D-X...π angle in radians :type angle: float :param _donor_residue: Identifier for donor residue :type _donor_residue: str :param _pi_residue: Identifier for π-containing residue :type _pi_residue: str """ self._donor = _donor self.hydrogen = hydrogen self.pi_center = pi_center self._distance = distance self._angle = angle self._donor_residue = _donor_residue self._pi_residue = _pi_residue # Generate donor-acceptor property description self._donor_acceptor_properties = self._generate_donor_acceptor_description()
# Backward compatibility properties @property def distance(self) -> float: return self._distance @property def angle(self) -> float: return self._angle @property def pi_residue(self) -> str: """Legacy property for π residue.""" return self._pi_residue @property def donor(self) -> Atom: """Property accessor for donor atom.""" return self._donor # MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: return self._donor
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: return self.pi_center # π center is the acceptor
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: return self.hydrogen
[docs] def get_donor_residue(self) -> str: return self._donor_residue
[docs] def get_acceptor_residue(self) -> str: return self._pi_residue
[docs] def get_interaction_type(self) -> str: return "π–Inter"
[docs] def get_donor_interaction_distance(self) -> float: """Distance from donor to interaction atom.""" return float(self._donor.coords.distance_to(self.hydrogen.coords))
[docs] def get_donor_acceptor_distance(self) -> float: """Distance from donor to π center.""" return float(self._donor.coords.distance_to(self.pi_center))
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """D-H...π angle.""" return self._angle
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if hydrogen is bonded to donor. For X-H...π interactions, the hydrogen must be covalently bonded to the donor atom. :returns: True (assumes validation was done during creation) :rtype: bool """ # In practice, this should be validated during object creation # by checking bond lists in the analyzer return True # Assuming validation was done during creation
def _generate_donor_acceptor_description(self) -> str: """Generate donor-acceptor property description string. Describes the π interaction in terms of: - Donor properties: residue type, backbone/sidechain, aromatic - Acceptor properties: residue type, backbone/sidechain, aromatic (always aromatic for π) Format: "donor_props-acceptor_props" (e.g., "PSN-PSA") :returns: Property description string :rtype: str """ # Get donor properties donor_residue_type = getattr(self._donor, "residue_type", "L") donor_backbone_sidechain = getattr(self._donor, "backbone_sidechain", "S") donor_aromatic = getattr(self._donor, "aromatic", "N") # For π interactions, we need to determine acceptor properties from the π residue # Since we don't have the actual π atoms, we'll use the residue info from ..constants.pdb_constants import ( DNA_RESIDUES, PROTEIN_RESIDUES, RNA_RESIDUES, ) pi_res_name = ( self._pi_residue.split("_")[0] if "_" in self._pi_residue else self._pi_residue.split(":")[0] ) if pi_res_name in PROTEIN_RESIDUES: acceptor_residue_type = "P" elif pi_res_name in DNA_RESIDUES: acceptor_residue_type = "D" elif pi_res_name in RNA_RESIDUES: acceptor_residue_type = "R" else: acceptor_residue_type = "L" # π system atoms are always sidechain and aromatic acceptor_backbone_sidechain = "S" acceptor_aromatic = "A" # Build property strings donor_props = f"{donor_residue_type}{donor_backbone_sidechain}{donor_aromatic}" acceptor_props = ( f"{acceptor_residue_type}{acceptor_backbone_sidechain}{acceptor_aromatic}" ) return f"{donor_props}-{acceptor_props}" @property def donor_acceptor_properties(self) -> str: """Get the donor-acceptor property description. :returns: Property description string :rtype: str """ return self._donor_acceptor_properties
[docs] def get_backbone_sidechain_interaction(self) -> str: """Get simplified backbone/sidechain interaction description. :returns: Interaction type (B-S, S-S, etc.) :rtype: str """ donor_bs = getattr(self._donor, "backbone_sidechain", "S") # π systems are always sidechain acceptor_bs = "S" return f"{donor_bs}-{acceptor_bs}"
[docs] def get_interaction_type_display(self) -> str: """Get the interaction type for display purposes. Generates display strings for different π interaction subtypes: **Hydrogen-π interactions:** - "C-H...π" for carbon-hydrogen to π system - "N-H...π" for nitrogen-hydrogen to π system - "O-H...π" for oxygen-hydrogen to π system - "S-H...π" for sulfur-hydrogen to π system **Halogen-π interactions:** - "C-Cl...π" for carbon-chlorine to π system - "C-Br...π" for carbon-bromine to π system - "C-I...π" for carbon-iodine to π system :returns: Display format showing donor-interaction...π pattern :rtype: str """ donor_element = self._donor.element interaction_element = ( self.hydrogen.element ) # Still named hydrogen for backward compatibility return f"{donor_element}-{interaction_element}...π"
def __str__(self) -> str: interaction_type = self.get_interaction_type_display() return ( f"π-Int: {self._donor_residue}({self._donor.name}) - {interaction_type} - " f"{self._pi_residue} [{self.distance:.2f}Å, {math.degrees(self.angle):.1f}°] " f"[{self.get_backbone_sidechain_interaction()}] [{self.donor_acceptor_properties}]" )
[docs] class PiPiInteraction(MolecularInteraction): """Represents a π-π stacking interaction between aromatic rings. This class stores information about detected π-π interactions, which are important for protein stability, molecular recognition, and drug binding. Interactions are classified as parallel, T-shaped, or offset based on the angle between ring planes and the lateral displacement. :param ring1_atoms: Atoms in the first aromatic ring :type ring1_atoms: List[Atom] :param ring2_atoms: Atoms in the second aromatic ring :type ring2_atoms: List[Atom] :param ring1_center: Centroid of the first ring :type ring1_center: NPVec3D :param ring2_center: Centroid of the second ring :type ring2_center: NPVec3D :param distance: Centroid-to-centroid distance in Angstroms :type distance: float :param plane_angle: Angle between ring planes in degrees :type plane_angle: float :param offset: Lateral displacement in Angstroms (for parallel stacking) :type offset: float :param stacking_type: Classification ("parallel", "T-shaped", or "offset") :type stacking_type: str :param ring1_type: Type of first ring (e.g., PHE, TYR, TRP, HIS) :type ring1_type: str :param ring2_type: Type of second ring (e.g., PHE, TYR, TRP, HIS) :type ring2_type: str :param ring1_residue: Identifier for first ring's residue :type ring1_residue: str :param ring2_residue: Identifier for second ring's residue :type ring2_residue: str """
[docs] def __init__( self, ring1_atoms: List[Atom], ring2_atoms: List[Atom], ring1_center: NPVec3D, ring2_center: NPVec3D, distance: float, plane_angle: float, offset: float, stacking_type: str, ring1_type: str, ring2_type: str, ring1_residue: str, ring2_residue: str, ): """Initialize a PiPiInteraction object. :param ring1_atoms: Atoms in the first aromatic ring :type ring1_atoms: List[Atom] :param ring2_atoms: Atoms in the second aromatic ring :type ring2_atoms: List[Atom] :param ring1_center: Centroid of the first ring :type ring1_center: NPVec3D :param ring2_center: Centroid of the second ring :type ring2_center: NPVec3D :param distance: Centroid-to-centroid distance in Angstroms :type distance: float :param plane_angle: Angle between ring planes in degrees :type plane_angle: float :param offset: Lateral displacement in Angstroms :type offset: float :param stacking_type: Classification ("parallel", "T-shaped", or "offset") :type stacking_type: str :param ring1_type: Type of first ring (e.g., PHE, TYR, TRP, HIS) :type ring1_type: str :param ring2_type: Type of second ring (e.g., PHE, TYR, TRP, HIS) :type ring2_type: str :param ring1_residue: Identifier for first ring's residue :type ring1_residue: str :param ring2_residue: Identifier for second ring's residue :type ring2_residue: str """ self.ring1_atoms = ring1_atoms self.ring2_atoms = ring2_atoms self.ring1_center = ring1_center self.ring2_center = ring2_center self._distance = distance self.plane_angle = plane_angle self.offset = offset self.stacking_type = stacking_type self.ring1_type = ring1_type self.ring2_type = ring2_type self.ring1_residue = ring1_residue self.ring2_residue = ring2_residue # Ensure ring centers are NPVec3D objects (for compatibility with tests passing numpy arrays) if not isinstance(ring1_center, NPVec3D): ring1_center = NPVec3D(ring1_center[0], ring1_center[1], ring1_center[2]) if not isinstance(ring2_center, NPVec3D): ring2_center = NPVec3D(ring2_center[0], ring2_center[1], ring2_center[2]) # Calculate midpoint for interaction representation self.midpoint = NPVec3D( (ring1_center.x + ring2_center.x) / 2, (ring1_center.y + ring2_center.y) / 2, (ring1_center.z + ring2_center.z) / 2, ) # Determine if interaction is between different residues self.is_between_residues = ring1_residue != ring2_residue
# Backward compatibility properties @property def distance(self) -> float: """Centroid-to-centroid distance.""" return self._distance @property def angle(self) -> float: """Angle between ring planes in radians (for consistency with other interactions).""" return math.radians(self.plane_angle) @property def interaction_classification(self) -> str: """Get the stacking classification (for consistency with other interaction types). :returns: The stacking type ("parallel", "T-shaped", or "offset") :rtype: str """ return self.stacking_type # MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: """Get the first ring centroid (arbitrarily designated as donor). :returns: Centroid of the first aromatic ring :rtype: NPVec3D """ return self.ring1_center
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: """Get the second ring centroid (arbitrarily designated as acceptor). :returns: Centroid of the second aromatic ring :rtype: NPVec3D """ return self.ring2_center
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: """Get the interaction point (midpoint between centroids). :returns: Midpoint between the two ring centroids :rtype: NPVec3D """ return self.midpoint
[docs] def get_donor_residue(self) -> str: """Get the first ring's residue identifier. :returns: Residue identifier for the first ring :rtype: str """ return self.ring1_residue
[docs] def get_acceptor_residue(self) -> str: """Get the second ring's residue identifier. :returns: Residue identifier for the second ring :rtype: str """ return self.ring2_residue
[docs] def get_interaction_type(self) -> str: """Get the interaction type identifier. :returns: "Pi-Pi" as the interaction type :rtype: str """ return "Pi-Pi"
[docs] def get_stacking_type(self) -> str: """Get the specific stacking geometry classification. :returns: "parallel", "T-shaped", or "offset" :rtype: str """ return self.stacking_type
[docs] def get_donor_interaction_distance(self) -> float: """Distance from first ring centroid to midpoint. :returns: Half of the centroid-to-centroid distance :rtype: float """ return self._distance / 2
[docs] def get_donor_acceptor_distance(self) -> float: """Distance between ring centroids. :returns: Centroid-to-centroid distance :rtype: float """ return self._distance
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """Angle between ring planes in radians. For π-π interactions, this represents the dihedral angle between the two aromatic ring planes. :returns: Angle between planes in radians :rtype: float """ return math.radians(self.plane_angle)
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if bonding requirement is satisfied. π-π interactions are non-covalent and don't require bonding between the interacting rings. :returns: False (no bonding requirement for π-π stacking) :rtype: bool """ return False
[docs] def get_ring_atoms(self, ring_num: int) -> List[Atom]: """Get atoms of a specific ring. :param ring_num: Ring number (1 or 2) :type ring_num: int :returns: List of atoms in the specified ring :rtype: List[Atom] """ if ring_num == 1: return self.ring1_atoms elif ring_num == 2: return self.ring2_atoms else: raise ValueError("Ring number must be 1 or 2")
[docs] def __str__(self) -> str: """String representation of the π-π interaction. :returns: Human-readable description of the interaction :rtype: str """ return ( f"π-π {self.stacking_type}: {self.ring1_residue}({self.ring1_type}) - " f"{self.ring2_residue}({self.ring2_type}) " f"[{self._distance:.2f}Å, {self.plane_angle:.1f}°, offset: {self.offset:.2f}Å]" )
[docs] class CarbonylInteraction(MolecularInteraction): """Represents a carbonyl-carbonyl n→π* interaction between C=O groups. This class stores information about detected n→π* interactions between carbonyl groups, which are important for protein stability and secondary structure formation. The interaction follows the Bürgi-Dunitz trajectory where the donor oxygen approaches the acceptor carbon. :param donor_carbon: C atom of the donor C=O group :type donor_carbon: Atom :param donor_oxygen: O atom of the donor C=O group :type donor_oxygen: Atom :param acceptor_carbon: C atom of the acceptor C=O group :type acceptor_carbon: Atom :param acceptor_oxygen: O atom of the acceptor C=O group :type acceptor_oxygen: Atom :param distance: O···C distance in Angstroms :type distance: float :param burgi_dunitz_angle: O···C=O angle in degrees (typically 95-125°) :type burgi_dunitz_angle: float :param is_backbone: Whether both carbonyls are from backbone amides :type is_backbone: bool :param donor_residue: Identifier for donor residue :type donor_residue: str :param acceptor_residue: Identifier for acceptor residue :type acceptor_residue: str """
[docs] def __init__( self, donor_carbon: Atom, donor_oxygen: Atom, acceptor_carbon: Atom, acceptor_oxygen: Atom, distance: float, burgi_dunitz_angle: float, is_backbone: bool, donor_residue: str, acceptor_residue: str, ): """Initialize a CarbonylInteraction object. :param donor_carbon: C atom of the donor C=O group :type donor_carbon: Atom :param donor_oxygen: O atom of the donor C=O group :type donor_oxygen: Atom :param acceptor_carbon: C atom of the acceptor C=O group :type acceptor_carbon: Atom :param acceptor_oxygen: O atom of the acceptor C=O group :type acceptor_oxygen: Atom :param distance: O···C distance in Angstroms :type distance: float :param burgi_dunitz_angle: O···C=O angle in degrees :type burgi_dunitz_angle: float :param is_backbone: Whether both carbonyls are from backbone amides :type is_backbone: bool :param donor_residue: Identifier for donor residue :type donor_residue: str :param acceptor_residue: Identifier for acceptor residue :type acceptor_residue: str """ self.donor_carbon = donor_carbon self.donor_oxygen = donor_oxygen self.acceptor_carbon = acceptor_carbon self.acceptor_oxygen = acceptor_oxygen self._distance = distance self.burgi_dunitz_angle = burgi_dunitz_angle self.is_backbone = is_backbone self._donor_residue = donor_residue self._acceptor_residue = acceptor_residue # Generate interaction classification self.interaction_classification = self._generate_interaction_classification() # Determine if interaction is between different residues self.is_between_residues = donor_residue != acceptor_residue
# Backward compatibility properties @property def distance(self) -> float: """O···C distance in Angstroms.""" return self._distance @property def angle(self) -> float: """Bürgi-Dunitz angle in radians (for consistency with other interactions).""" return math.radians(self.burgi_dunitz_angle) @property def carbonyl_type(self) -> str: """Get the carbonyl interaction type (for GUI compatibility).""" return self.interaction_classification # MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: """Get the donor oxygen atom. The donor oxygen contributes its lone pair electrons to the interaction. :returns: Donor oxygen atom :rtype: Atom """ return self.donor_oxygen
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: """Get the acceptor carbon atom. The acceptor carbon receives electron density in the n→π* interaction. :returns: Acceptor carbon atom :rtype: Atom """ return self.acceptor_carbon
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: """Get the interaction point (donor oxygen). For carbonyl interactions, the donor oxygen is both the electron donor and the interaction point. :returns: Donor oxygen atom :rtype: Atom """ return self.donor_oxygen
[docs] def get_donor_residue(self) -> str: """Get the donor residue identifier. :returns: Residue identifier containing the donor carbonyl :rtype: str """ return self._donor_residue
[docs] def get_acceptor_residue(self) -> str: """Get the acceptor residue identifier. :returns: Residue identifier containing the acceptor carbonyl :rtype: str """ return self._acceptor_residue
[docs] def get_interaction_type(self) -> str: """Get the interaction type identifier. :returns: "Carbonyl-Carbonyl" as the interaction type :rtype: str """ return "Carbonyl-Carbonyl"
[docs] def is_backbone_interaction(self) -> bool: """Check if this is a backbone-backbone interaction. :returns: True if both carbonyls are from backbone amides :rtype: bool """ return self.is_backbone
[docs] def get_donor_interaction_distance(self) -> float: """Distance from donor carbon to donor oxygen. :returns: C=O bond length (approximately 1.2-1.3 Å) :rtype: float """ return float(self.donor_carbon.coords.distance_to(self.donor_oxygen.coords))
[docs] def get_donor_acceptor_distance(self) -> float: """Distance from donor oxygen to acceptor carbon. :returns: O···C distance in Angstroms :rtype: float """ return self._distance
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """Bürgi-Dunitz angle in radians. This is the O···C=O angle that defines the trajectory of approach for the n→π* interaction. :returns: Bürgi-Dunitz angle in radians :rtype: float """ return math.radians(self.burgi_dunitz_angle)
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if the donor oxygen is bonded to the donor carbon. For carbonyl interactions, the donor oxygen must be covalently bonded to the donor carbon in a C=O group. :returns: True (oxygen is bonded to carbon in C=O) :rtype: bool """ return True
[docs] def get_carbonyl_atoms(self, carbonyl_type: str) -> tuple: """Get atoms of a specific carbonyl group. :param carbonyl_type: "donor" or "acceptor" :type carbonyl_type: str :returns: Tuple of (carbon, oxygen) atoms :rtype: tuple """ if carbonyl_type == "donor": return (self.donor_carbon, self.donor_oxygen) elif carbonyl_type == "acceptor": return (self.acceptor_carbon, self.acceptor_oxygen) else: raise ValueError("Carbonyl type must be 'donor' or 'acceptor'")
def _generate_interaction_classification(self) -> str: """Generate interaction classification based on backbone/sidechain location. :returns: Classification string (e.g., "backbone-backbone", "sidechain-backbone") :rtype: str """ if self.is_backbone: return "backbone-backbone" # Check if donor or acceptor are backbone by atom names donor_is_backbone = ( hasattr(self.donor_carbon, "name") and self.donor_carbon.name == "C" ) acceptor_is_backbone = ( hasattr(self.acceptor_carbon, "name") and self.acceptor_carbon.name == "C" ) if donor_is_backbone and acceptor_is_backbone: return "backbone-backbone" elif donor_is_backbone and not acceptor_is_backbone: return "backbone-sidechain" elif not donor_is_backbone and acceptor_is_backbone: return "sidechain-backbone" else: return "sidechain-sidechain"
[docs] def __str__(self) -> str: """String representation of the carbonyl interaction. :returns: Human-readable description of the interaction :rtype: str """ return ( f"C=O···C=O {self.interaction_classification}: " f"{self.donor_residue}(O) - {self.acceptor_residue}(C) " f"[{self._distance:.2f}Å, {self.burgi_dunitz_angle:.1f}°]" )
[docs] class NPiInteraction(MolecularInteraction): """Represents a general n→π* interaction between lone pairs and π systems. This class stores information about detected n→π* interactions where lone pair electrons from atoms (O, N, S) interact with aromatic π systems. These interactions are important in molecular recognition, enzyme active sites, and protein-ligand binding. :param lone_pair_atom: Donor atom with lone pair electrons (O, N, S) :type lone_pair_atom: Atom :param pi_center: Center of the π system :type pi_center: NPVec3D :param pi_atoms: Atoms constituting the π system :type pi_atoms: List[Atom] :param distance: Lone pair to π center distance in Angstroms :type distance: float :param angle_to_plane: Angle to π plane normal in degrees :type angle_to_plane: float :param subtype: Interaction subtype classification :type subtype: str :param donor_residue: Identifier for residue containing lone pair :type donor_residue: str :param acceptor_residue: Identifier for residue containing π system :type acceptor_residue: str """
[docs] def __init__( self, lone_pair_atom: Atom, pi_center: NPVec3D, pi_atoms: List[Atom], distance: float, angle_to_plane: float, subtype: str, donor_residue: str, acceptor_residue: str, ): """Initialize an NPiInteraction object. :param lone_pair_atom: Donor atom with lone pair electrons (O, N, S) :type lone_pair_atom: Atom :param pi_center: Center of the π system :type pi_center: NPVec3D :param pi_atoms: Atoms constituting the π system :type pi_atoms: List[Atom] :param distance: Lone pair to π center distance in Angstroms :type distance: float :param angle_to_plane: Angle to π plane normal in degrees :type angle_to_plane: float :param subtype: Interaction subtype classification :type subtype: str :param donor_residue: Identifier for residue containing lone pair :type donor_residue: str :param acceptor_residue: Identifier for residue containing π system :type acceptor_residue: str """ self.lone_pair_atom = lone_pair_atom self.pi_center = pi_center self.pi_atoms = pi_atoms self._distance = distance self.angle_to_plane = angle_to_plane self.subtype = subtype self._donor_residue = donor_residue self._acceptor_residue = acceptor_residue # Generate interaction properties self.donor_element = lone_pair_atom.element.upper() self.pi_system_type = self._classify_pi_system() # Determine if interaction is between different residues self.is_between_residues = donor_residue != acceptor_residue
# Backward compatibility properties @property def distance(self) -> float: """Lone pair to π center distance in Angstroms.""" return self._distance @property def angle(self) -> float: """Angle to π plane in radians (for consistency with other interactions).""" return math.radians(self.angle_to_plane) @property def interaction_classification(self) -> str: """Get the interaction subtype classification (for consistency with other interaction types). :returns: The subtype classification :rtype: str """ return self.subtype # MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: """Get the lone pair donor atom. The lone pair atom contributes electron density to the π system. :returns: Lone pair donor atom :rtype: Atom """ return self.lone_pair_atom
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: """Get the π system center. The π system center represents the electron-deficient acceptor. :returns: π system centroid :rtype: NPVec3D """ return self.pi_center
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: """Get the interaction point (lone pair atom). For n→π* interactions, the lone pair atom is the interaction point that donates electron density to the π system. :returns: Lone pair donor atom :rtype: Atom """ return self.lone_pair_atom
[docs] def get_donor_residue(self) -> str: """Get the donor residue identifier. :returns: Residue identifier containing the lone pair donor :rtype: str """ return self._donor_residue
[docs] def get_acceptor_residue(self) -> str: """Get the acceptor residue identifier. :returns: Residue identifier containing the π system :rtype: str """ return self._acceptor_residue
[docs] def get_interaction_type(self) -> str: """Get the interaction type identifier. :returns: "n-Pi" as the interaction type :rtype: str """ return "n-Pi"
[docs] def get_subtype(self) -> str: """Get the specific n→π* interaction subtype. :returns: Subtype classification (e.g., "carbonyl-aromatic", "amine-aromatic") :rtype: str """ return self.subtype
[docs] def get_donor_element(self) -> str: """Get the donor atom element. :returns: Element symbol of the lone pair donor (O, N, or S) :rtype: str """ return self.donor_element
[docs] def get_donor_interaction_distance(self) -> float: """Distance from lone pair atom to π center (same as total distance). For n→π* interactions, there's no intermediate atom, so this is the same as the donor-acceptor distance. :returns: Lone pair to π center distance :rtype: float """ return self._distance
[docs] def get_donor_acceptor_distance(self) -> float: """Distance from lone pair donor to π system center. :returns: Lone pair to π center distance in Angstroms :rtype: float """ return self._distance
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """Angle to π plane normal in radians. This represents the angle between the lone pair vector and the normal to the π system plane. :returns: Angle to π plane normal in radians :rtype: float """ return math.radians(self.angle_to_plane)
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if bonding requirement is satisfied. n→π* interactions are direct interactions between the lone pair and π system, so no intermediate bonding is required. :returns: False (no bonding requirement for n→π* interactions) :rtype: bool """ return False
[docs] def get_pi_atoms(self) -> List[Atom]: """Get atoms constituting the π system. :returns: List of atoms in the π system :rtype: List[Atom] """ return self.pi_atoms
[docs] def is_carbonyl_donor(self) -> bool: """Check if the donor is a carbonyl oxygen. :returns: True if donor is carbonyl oxygen :rtype: bool """ return "carbonyl" in self.subtype.lower()
[docs] def is_amine_donor(self) -> bool: """Check if the donor is an amine nitrogen. :returns: True if donor is amine nitrogen :rtype: bool """ return "amine" in self.subtype.lower() or "amino" in self.subtype.lower()
[docs] def is_sulfur_donor(self) -> bool: """Check if the donor is a sulfur atom. :returns: True if donor is sulfur atom :rtype: bool """ return self.donor_element == "S" or "sulfur" in self.subtype.lower()
def _classify_pi_system(self) -> str: """Classify the π system type based on constituent atoms. :returns: π system type (e.g., "aromatic", "nucleobase", "indole") :rtype: str """ if not self.pi_atoms: return "unknown" # Get residue type from first π atom first_atom = self.pi_atoms[0] if hasattr(first_atom, "residue_name"): res_name = first_atom.residue_name.upper() # Classify based on residue if res_name in ["PHE"]: return "phenyl" elif res_name in ["TYR"]: return "phenol" elif res_name in ["TRP"]: return "indole" elif res_name in ["HIS"]: return "imidazole" elif res_name in ["A", "G", "C", "T", "U"]: return "nucleobase" else: return "aromatic" return "aromatic" def _classify_donor_subtype(self) -> str: """Classify the donor atom subtype. :returns: Donor subtype (e.g., "carbonyl-O", "amine-N", "thiol-S") :rtype: str """ element = self.donor_element if element == "O": # Check if carbonyl oxygen by looking at bonded carbon if hasattr(self.lone_pair_atom, "bonds"): for bonded_atom in self.lone_pair_atom.bonds: if bonded_atom.element.upper() == "C": # Simple heuristic: if O-C distance is short, likely carbonyl distance = self.lone_pair_atom.coords.distance_to( bonded_atom.coords ) if distance < 1.35: # Typical C=O bond length return "carbonyl-O" return "hydroxyl-O" elif element == "N": return "amine-N" elif element == "S": return "thiol-S" else: return f"{element.lower()}-lone_pair"
[docs] def __str__(self) -> str: """String representation of the n→π* interaction. :returns: Human-readable description of the interaction :rtype: str """ return ( f"n→π* {self.subtype}: {self.donor_residue}({self.donor_element}) - " f"{self.acceptor_residue}(π) " f"[{self._distance:.2f}Å, {self.angle_to_plane:.1f}°]" )
[docs] class CooperativityChain(MolecularInteraction): """Represents a chain of cooperative molecular interactions. This class represents a series of linked molecular interactions where the acceptor of one interaction acts as the donor of the next, creating cooperative effects. :param interactions: List of interactions in the chain :type interactions: List[Union[HydrogenBond, HalogenBond, PiInteraction]] :param chain_length: Number of interactions in the chain :type chain_length: int :param chain_type: Description of the interaction types in the chain :type chain_type: str """
[docs] def __init__( self, interactions: List[Union[HydrogenBond, HalogenBond, PiInteraction]], chain_length: int, chain_type: str, ): """Initialize a CooperativityChain object. :param interactions: List of interactions in the chain :type interactions: List[Union[HydrogenBond, HalogenBond, PiInteraction]] :param chain_length: Number of interactions in the chain :type chain_length: int :param chain_type: Description of the interaction types in the chain :type chain_type: str """ self.interactions = interactions self.chain_length = chain_length self.chain_type = chain_type # e.g., "H-Bond -> X-Bond -> π-Int"
# MolecularInteraction interface implementation
[docs] def get_donor(self) -> Union[Atom, NPVec3D]: """Get the donor of the first interaction in the chain.""" if self.interactions: return self.interactions[0].get_donor() return NPVec3D(0, 0, 0) # Return a default NPVec3D instead of None
[docs] def get_acceptor(self) -> Union[Atom, NPVec3D]: """Get the acceptor of the last interaction in the chain.""" if self.interactions: return self.interactions[-1].get_acceptor() return NPVec3D(0, 0, 0) # Return a default NPVec3D instead of None
[docs] def get_interaction(self) -> Union[Atom, NPVec3D]: """Get the center point of the chain (middle interaction point).""" if not self.interactions: return NPVec3D(0, 0, 0) # Return a default NPVec3D instead of None mid_idx = len(self.interactions) // 2 return self.interactions[mid_idx].get_interaction()
[docs] def get_donor_residue(self) -> str: """Get the donor residue of the first interaction.""" return ( self.interactions[0].get_donor_residue() if self.interactions else "Unknown" )
[docs] def get_acceptor_residue(self) -> str: """Get the acceptor residue of the last interaction.""" return ( self.interactions[-1].get_acceptor_residue() if self.interactions else "Unknown" )
[docs] def get_interaction_type(self) -> str: return "cooperativity_chain"
[docs] def get_donor_interaction_distance(self) -> float: """Get the distance from chain start to middle interaction.""" if not self.interactions: return 0.0 first_donor = self.interactions[0].get_donor() mid_idx = len(self.interactions) // 2 mid_interaction = self.interactions[mid_idx].get_interaction() if isinstance(first_donor, Atom) and isinstance(mid_interaction, Atom): return float(first_donor.coords.distance_to(mid_interaction.coords)) elif isinstance(first_donor, Atom) and isinstance(mid_interaction, NPVec3D): return float(first_donor.coords.distance_to(mid_interaction)) return 0.0
[docs] def get_donor_acceptor_distance(self) -> float: """Get the distance from chain start to end.""" if not self.interactions: return 0.0 first_donor = self.interactions[0].get_donor() last_acceptor = self.interactions[-1].get_acceptor() if isinstance(first_donor, Atom) and isinstance(last_acceptor, Atom): return float(first_donor.coords.distance_to(last_acceptor.coords)) elif isinstance(first_donor, Atom) and isinstance(last_acceptor, NPVec3D): return float(first_donor.coords.distance_to(last_acceptor)) return 0.0
[docs] def get_donor_interaction_acceptor_angle(self) -> float: """Get the angle across the chain (donor-middle-acceptor).""" if len(self.interactions) < 2: return 0.0 first_donor = self.interactions[0].get_donor() mid_idx = len(self.interactions) // 2 mid_interaction = self.interactions[mid_idx].get_interaction() last_acceptor = self.interactions[-1].get_acceptor() # Calculate angle between first donor, middle interaction, and last acceptor if ( isinstance(first_donor, Atom) and isinstance(last_acceptor, (Atom, NPVec3D)) and isinstance(mid_interaction, (Atom, NPVec3D)) ): donor_pos = first_donor.coords mid_pos = ( mid_interaction.coords if isinstance(mid_interaction, Atom) else mid_interaction ) acceptor_pos = ( last_acceptor.coords if isinstance(last_acceptor, Atom) else last_acceptor ) # Calculate vectors vec1 = NPVec3D( donor_pos.x - mid_pos.x, donor_pos.y - mid_pos.y, donor_pos.z - mid_pos.z, ) vec2 = NPVec3D( acceptor_pos.x - mid_pos.x, acceptor_pos.y - mid_pos.y, acceptor_pos.z - mid_pos.z, ) # Calculate angle dot_product = vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z mag1 = math.sqrt(vec1.x**2 + vec1.y**2 + vec1.z**2) mag2 = math.sqrt(vec2.x**2 + vec2.y**2 + vec2.z**2) if mag1 > 0 and mag2 > 0: cos_angle = dot_product / (mag1 * mag2) cos_angle = max(-1.0, min(1.0, cos_angle)) # Clamp to valid range return math.acos(cos_angle) return 0.0
[docs] def is_donor_interaction_bonded(self) -> bool: """Check if interactions in the chain satisfy bonding requirements. For cooperativity chains, each individual interaction must satisfy its own bonding requirements. :returns: True if all interactions in chain are properly bonded :rtype: bool """ # Check that all interactions in the chain satisfy bonding requirements return all( interaction.is_donor_interaction_bonded() for interaction in self.interactions )
def __str__(self) -> str: if not self.interactions: return "Empty chain" chain_str = [] for i, interaction in enumerate(self.interactions): if i == 0: # First interaction: show donor -> acceptor donor_res = interaction.get_donor_residue() donor_atom = interaction.get_donor_atom() donor_name = donor_atom.name if donor_atom else "?" chain_str.append(f"{donor_res}({donor_name})") acceptor_res = interaction.get_acceptor_residue() acceptor_atom = interaction.get_acceptor_atom() if acceptor_atom: acceptor_name = acceptor_atom.name acceptor_str = f"{acceptor_res}({acceptor_name})" else: acceptor_str = acceptor_res # For π interactions interaction_symbol = self._get_interaction_symbol( interaction.get_interaction_type() ) chain_str.append( f" {interaction_symbol} {acceptor_str} [{interaction.get_donor_interaction_acceptor_angle()*180/3.14159:.1f}°]" ) return f"Potential Cooperative Chain[{self.chain_length}]: " + "".join( chain_str ) def _get_interaction_symbol(self, interaction_type: str) -> str: """Get display symbol for interaction type.""" symbols = { "H-Bond": "->", "X-Bond": "=X=>", "π–Inter": "~π~>", } return symbols.get(interaction_type, "->")