Identifying Acceptor and Donor Atoms๏ƒ

Problem โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€“lโ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”

You want to identify acceptor and donor atoms in a molecule.

Table 1. Example of Lipinski definitions (acceptor on the left, donor on the right)๏ƒ
../_images/lipinski-acceptor-example.svg ../_images/lipinski-donor-example.svg

Ingredients๏ƒ

Difficulty Level๏ƒ

๐ŸŒถ๏ธ

Solution๏ƒ

While the MolProp TK provides the functions to count the number of acceptor and donor atoms in the molecules, it does not identify which atoms are counted.

Lipinski [Lipinski-1997] provides a simple way to identify them. A Lipinski acceptor is either an oxygen or a nitrogen:

class IsLipinskiAcceptor(oechem.OEUnaryAtomPred):
    """
    Predicate class to identify Lipinski acceptor atoms.

    See Also
    --------
    * num_lipinsky_acceptors_
    * :ref:`section_cheminfo_acceptor_donor` section

    """

    def __call__(self, atom: oechem.OEAtomBase) -> bool:
        """Evaluate the atom."""
        return atom.GetAtomicNum() in [oechem.OEElemNo_O, oechem.OEElemNo_N]

A Lipinski donor is either an oxygen or a nitrogen with at least one hydrogen.

class IsLipinskiDonor(oechem.OEUnaryAtomPred):
    """
    Predicate class to identify Lipinski donor atoms.

    See Also
    --------
    * num_lipinsky_donors_
    * :ref:`section_cheminfo_acceptor_donor` section

    """

    def __call__(self, atom: oechem.OEAtomBase) -> bool:
        """Evaluate the atom."""
        if atom.GetAtomicNum() not in [oechem.OEElemNo_O, oechem.OEElemNo_N]:
            return False
        return has_hydrogen(atom)

Where has_hydrogen is defined as the following to handle both implicit and explicit hydrogens:

def has_hydrogen(atom: oechem.OEAtomBase) -> bool:
    """Return true if the atom has implicit or explicit hydrogens."""
    if atom.GetImplicitHCount() > 0:
        return True
    return any(neigh.IsHydrogen() for neigh in atom.GetAtoms())

After implementing the definitions as predicates we can use them in any APIs that takes atom predicates.

See Examples

def print_lipinsky_donors(mol: oechem.OEMolBase) -> None:
    """Print Lipinsky donor atoms to console."""
    oechem.OETriposAtomNames(mol)
    console = rich.console.Console()
    console.print(f"Acceptor atoms in molecule '{oechem.OEMolToSmiles(mol)}'")
    for atom in mol.GetAtoms(IsLipinskiDonor()):
        console.print(atom.GetName())

For more information see Predicate Functors chapter of the OEChem TK manual.

Discussion๏ƒ

Another possible implementation is based on Daylight SMARTS definitions.

This following predicate only identifies hydrogen bond acceptor atoms in carbonyl and nitroso functional groups:

class IsHBondAcceptorCarbonylNitroso(oechem.OEUnaryAtomPred):
    """Predicate class to identify atoms in [#6,#7;R0]=[#8]."""

    def __call__(self, atom: oechem.OEAtomBase) -> bool:
        """Evaluate the atom."""
        if not atom.IsOxygen():
            return False
        if atom.GetHvyDegree() != 1:
            return False

        for bond in atom.GetBonds():
            if bond.GetOrder() != 2:  # noqa: PLR2004
                return False
        return all(
            neigh.GetAtomicNum in [oechem.OEElemNo_O, oechem.OEElemNo_N]
            for neigh in atom.GetAtoms()
        )

The following predicate identifies hydrogen bond donor as either a nitrogen, oxygen or fluorine atom with at least one hydrogen:

class IsHBondDonor(oechem.OEUnaryAtomPred):
    """Predicate class to identify atoms in [!H0;#7,#8,#9]."""

    def __call__(self, atom: oechem.OEAtomBase) -> bool:
        """Evaluate the atom."""
        if atom.GetAtomicNum() not in [
            oechem.OEElemNo_O,
            oechem.OEElemNo_N,
            oechem.OEElemNo_F,
        ]:
            return False
        return has_hydrogen(atom)

The following predicate identifies hydrogen bond donor as a non-negatively charged hetero-atom with at least one hydrogen:

class IsHBondDonorInclusive(oechem.OEUnaryAtomPred):
    """Predicate class to identify atoms in [!$([#6,H0,-,-2,-3])]."""

    def __call__(self, atom: oechem.OEAtomBase) -> bool:
        """Evaluate the atom."""
        if atom.GetAtomicNum() in [oechem.OEElemNo_C, oechem.OEElemNo_H]:
            return False
        if atom.GetFormalCharge() != 0:
            return False

        return has_hydrogen(atom)

Also if a SMARTS pattern exists, it is very easy to create a predicate using the OEMatchAtom built-in predicate:

def print_custom_donors(mol: oechem.OEMolBase) -> None:
    """Print donor atoms to console."""
    oechem.OETriposAtomNames(mol)
    console = rich.console.Console()
    donor_pred = oechem.OEMatchAtom("[!H0;#7,#8,#9]")
    console.print(f"Donor atoms in molecule '{oechem.OEMolToSmiles(mol)}'")
    for atom in mol.GetAtoms(donor_pred):
        console.print(atom.GetName())

For comparison, the simplified SMARTS definitions of acceptor and donor atoms in OpenEyeโ€™s ROCS application are the following:

  • donor: [$([#7,#8,#15,#16]);H]

  • acceptor: [#8&!$(\*~N~[OD1]),#7&H0;!$([D4]);!$([D3]-\*=,:[$([#7,#8,#15,#16])])]

Please note that the built-in Mills and Dean [MillsDean-1996] definitions used in ROCS are significantly more precise and elaborate.

Figure 1. Example of color definition in ROCS

../_images/lipitor-query.svg

See also in OEChem TK manual๏ƒ

Theory

API

See also in OEMolProp TK manual๏ƒ

See also๏ƒ

Theory