Source code for openpharmacophore.io.pharmacophore_writer

from collections import namedtuple
from copy import deepcopy
import datetime
import json
import xml.etree.ElementTree as ET
import pyunitwizard as puw
import numpy as np


# MOL2 Files

def _pad_coordinate_with_zeros(coord):
    """ Pad a number with zeros to the right.

        Parameters
        ----------
        coord: float
            A number

        Returns
        -------
        str
            A string with the coordinate padded with zeros to the right
    """
    # The final string will contain 6 characters in total including the point or 7
    # if the number is negative
    total_characters = 6
    coord_float = float(coord)
    coord_str = str(coord)

    if "." not in coord_str:
        coord_str = str(coord) + "."

    if coord_float < 0:
        # If the coordinate is negative the sign will add a character
        total_characters += 1
    return coord_str.ljust(total_characters, "0")


def _get_props_dict(pharmacophore):
    """
        Parameters
        ----------
        pharmacophore: Pharmacophore

        Returns
        -------
        dict[str, str]
    """
    props = deepcopy(pharmacophore.props)
    if pharmacophore.score is not None:
        props["score"] = str(pharmacophore.score)
    if pharmacophore.ref_mol is not None:
        props["ref_mol"] = str(pharmacophore.ref_mol)
    if pharmacophore.ref_struct is not None:
        props["ref_struct"] = str(pharmacophore.ref_struct)
    return props


def _mol2_pharmacophores(pharmacophores, save_props):
    """ Get necessary info to create a mol2 file to store pharmacophores.

        Parameters
        ----------
        pharmacophores : list[Pharmacophore]
            Nested list of pharmacophoric points were each entry represents a pharmacophore

        save_props : bool
            Whether to save the properties stored in the pharmacophore.

        Returns
        -------
        lines : list[str]
            List with the contents of a mol2 file.
    """
    pharmagist_element_name = {
        "aromatic ring": "AR ",
        "hydrophobicity": "HYD",
        "hb acceptor": "ACC",
        "hb donor": "DON",
        "positive charge": "CAT",
        "negative charge": "ANI",
    }

    pharmagist_element_specs = {
        "aromatic ring": "AR ",
        "hydrophobicity": "HYD",
        "hb acceptor": "HB ",
        "hb donor": "HB ",
        "positive charge": "CHG",
        "negative charge": "CHG",
    }

    lines = []
    for pharmacophore in pharmacophores:
        lines.extend(["@<TRIPOS>PHARMACOPHORE\n", "@<TRIPOS>POINTS\n"])
        index = 0
        for element in pharmacophore:
            try:
                feat_name = pharmagist_element_name[element.feature_name]
            except KeyError:
                continue
            element_inx = str(index + 1)
            line = element_inx.rjust(7)
            line += " " + feat_name

            center = np.around(puw.get_value(element.center, to_unit="angstroms"), 4)
            radius = np.around(puw.get_value(element.radius, to_unit="angstroms"), 4)

            x = _pad_coordinate_with_zeros(center[0]).rjust(16)
            y = _pad_coordinate_with_zeros(center[1]).rjust(10)
            z = _pad_coordinate_with_zeros(center[2]).rjust(10)
            radius_str = (_pad_coordinate_with_zeros(radius) + '\n').rjust(12)

            line += x + y + z + " "
            line += pharmagist_element_specs[element.feature_name].rjust(5)
            line += str(index).rjust(5)
            line += pharmagist_element_specs[element.feature_name].rjust(6)
            # line += "0.0000\n".rjust(12)
            line += radius_str

            lines.append(line)
            index += 1

        if save_props:
            props = _get_props_dict(pharmacophore)
            if len(props) > 0:
                lines.append("@<TRIPOS>PROPERTIES\n")
                for prop_name, value in props.items():
                    line = prop_name.rjust(len(prop_name) + 7) + " "
                    value = str(value)
                    line += (value + '\n').rjust(20 - len(prop_name))
                    lines.append(line)

    return lines


[docs]def save_mol2(pharmacophores, file_name, save_props=True): """ Save several pharmacophores to a mol2 file. Parameters ---------- pharmacophores : list[Pharmacophore] A list of pharmacophores file_name : str Name of the file that will be created. save_props : bool Whether to save the properties stored in the pharmacophore. """ contents = _mol2_pharmacophores(pharmacophores, save_props) with open(file_name, "w") as fp: fp.writelines(contents)
# PH4 (MOE) Files def _ph4_pharmacophore(pharmacophoric_points): """ Returns a string with the necessary data to create a MOE ph4 file. Parameters ---------- pharmacophoric_points : Pharmacophore List with the pharmacophoric points. Returns ------ ph4_str : str The pharmacophore string. """ oph_to_moe = { "aromatic ring": "Aro", "hydrophobicity": "Hyd", "hb acceptor": "Acc", "hb donor": "Don", "positive charge": "Cat", "negative charge": "Ani", } now = datetime.datetime.now() ph4_str = "#moe:ph4que" + " " + str(now.year) + "." + str(now.month) + "\n" ph4_str += "#pharmacophore 5 tag t value *\n" ph4_str += "scheme t Unified matchsize i 0 title t s $\n" ph4_str += f"#feature {len(pharmacophoric_points)} expr tt color ix x r y r z r r r ebits ix gbits ix\n" excluded_spheres = [] for element in pharmacophoric_points: if element.feature_name == "excluded volume": excluded_spheres.append(element) continue feat_name = oph_to_moe[element.feature_name] center = puw.get_value(element.center, to_unit="angstroms") radius = puw.get_value(element.radius, to_unit="angstroms") radius_str = f"{radius:.3f}" + " " ph4_str += feat_name + " df2f2 " for ii in range(3): coord = f"{(center[ii]):.3f}" + " " ph4_str += coord ph4_str += radius_str + "0 300 " if excluded_spheres: ph4_str += "\n#volumesphere 90 x r y r z r r r\n" for excluded in excluded_spheres: center = puw.get_value(excluded.center, to_unit="angstroms") radius = puw.get_value(excluded.radius, to_unit="angstroms") radius_str = f"{radius:.3f}" + " " for ii in range(3): coord = f"{(center[ii]):.3f}" + " " ph4_str += coord ph4_str += radius_str ph4_str += "\n#endpharmacophore" return ph4_str
[docs]def save_ph4(pharmacophore, file_name): """ Save a pharmacophore to a ph4 (MOE) file. Parameters ---------- pharmacophore : Pharmacophore A pharmacophore. file_name : str Name of the file that will be created. """ contents = _ph4_pharmacophore(pharmacophore) with open(file_name, "w") as fp: fp.write(contents)
# JSON files def _json_pharmacophore(pharmacophore): """ Returns a dictionary with the necessary data to construct a json pharmacophore file. Parameters ---------- pharmacophore : Pharmacophore Pharmacophoric points that will be used to construct the dictionary Returns ------- pharmacophore_dict : dict[str, Any] Dictionary with the necessary info to construct a .json pharmer file. """ pharmer_element_name = { "aromatic ring": "Aromatic", "hydrophobicity": "Hydrophobic", "hb acceptor": "HydrogenAcceptor", "hb donor": "HydrogenDonor", "included volume": "InclusionSphere", "excluded volume": "ExclusionSphere", "positive charge": "PositiveIon", "negative charge": "NegativeIon", } points = [] for element in pharmacophore: point_dict = {} temp_center = puw.get_value(element.center, to_unit='angstroms') point_dict["name"] = pharmer_element_name[element.feature_name] point_dict["svector"] = {} if element.has_direction: point_dict["hasvec"] = True point_dict["svector"]["x"] = element.direction[0] point_dict["svector"]["y"] = element.direction[1] point_dict["svector"]["z"] = element.direction[2] else: point_dict["hasvec"] = False point_dict["svector"]["x"] = 1. point_dict["svector"]["y"] = 0. point_dict["svector"]["z"] = 0. point_dict["x"] = temp_center[0] point_dict["y"] = temp_center[1] point_dict["z"] = temp_center[2] point_dict["radius"] = puw.get_value(element.radius, to_unit='angstroms') point_dict["enabled"] = True point_dict["vector_on"] = 0 point_dict["minsize"] = "" point_dict["maxsize"] = "" point_dict["selected"] = False points.append(point_dict) return {"points": points}
[docs]def save_json(pharmacophore, file_name): """ Save a pharmacophore to a JSON file. Parameters ---------- pharmacophore : Pharmacophore A pharmacophore. file_name : str Name of the file that will be created. """ contents = _json_pharmacophore(pharmacophore) with open(file_name, "w") as fp: json.dump(contents, fp)
# PML (LigandScout) Files def _set_coords_and_radius(tree_element, coords, radius, feat_id): """ Set the coords and radius of a tree element corresponding to a volume, plane or point tag. """ x, y, z = coords tree_element.set("featureId", feat_id) tree_element.set("optional", "false") tree_element.set("disabled", "false") tree_element.set("weight", "1.0") # Add position tag position = ET.SubElement(tree_element, "position") position.set("x3", x) position.set("y3", y) position.set("z3", z) position.set("tolerance", radius) def _ligandscout_xml_tree(pharmacophore): """ Returns a xml element tree necessary to create a ligandscout pharmacophore file. Parameters ---------- pharmacophore : openpharmacophore.Pharmacophore The pharmacophore that will be saved to a file. Returns ------- tree : xml.etree.ElementTree The element tree. """ Feature = namedtuple("Feature", ["name", "id"]) feature_mapper = { "aromatic ring": Feature("AR", "ai_"), "hydrophobicity": Feature("H", "hi_"), "hb acceptor": Feature("HBA", "ha_"), "hb donor": Feature("HBD", "hd_"), "excluded volume": Feature("exclusion", "ev_"), "positive charge": Feature("PI", "pi_"), "negative charge": Feature("NI", "ni_"), } tree = ET.ElementTree("tree") document = ET.Element("pharmacophore") document.set("name", "pharmacophore.pml") document.set("pharmacophoreType", "LIGAND_SCOUT") for ii, element in enumerate(pharmacophore): try: feat_name = feature_mapper[element.feature_name].name except KeyError: # skip features not supported by ligandscout continue coords = puw.get_value(element.center, to_unit="angstroms") x = f"{coords[0]:.3f}" y = f"{coords[1]:.3f}" z = f"{coords[2]:.3f}" radius_val = puw.get_value(element.radius, to_unit="angstroms") radius = f"{radius_val:.3f}" feat_id = feature_mapper[element.feature_name].id + str(ii + 1) is_point = (feat_name == "PI" or feat_name == "NI" or feat_name == "H" or feat_name == "HBD" or feat_name == "HBA") is_vector = element.has_direction and (feat_name == "HBD" or feat_name == "HBA") if is_vector: direction = coords - element.direction dir_x = f"{direction[0]:.3f}" dir_y = f"{direction[1]:.3f}" dir_z = f"{direction[2]:.3f}" vector = ET.SubElement(document, "vector") # Set vector attributes vector.set("name", feat_name) vector.set("featureId", feat_id) vector.set("pointsToLigand", "false") vector.set("hasSyntheticProjectedPoint", "false") vector.set("optional", "false") vector.set("disabled", "false") vector.set("weight", "1.0") # Add origin tag origin = ET.SubElement(vector, "origin") origin.set("x3", x) origin.set("y3", y) origin.set("z3", z) origin.set("tolerance", radius) # Add target tag target = ET.SubElement(vector, "target") target.set("x3", dir_x) target.set("y3", dir_y) target.set("z3", dir_z) target.set("tolerance", radius) elif is_point: point = ET.SubElement(document, "point") point.set("name", feat_name) _set_coords_and_radius(point, (x, y, z), radius, feat_id) elif feat_name == "AR": # TODO: ligandscout expects aromatic rings to have direction. # what should we do if an aromatic point doesn't have direction if not element.has_direction: continue direction = element.direction dir_x = f"{direction[0]:.3f}" dir_y = f"{direction[1]:.3f}" dir_z = f"{direction[2]:.3f}" plane = ET.SubElement(document, "plane") plane.set("name", feat_name) _set_coords_and_radius(plane, (x, y, z), radius, feat_id) # Add normal tag normal = ET.SubElement(plane, "normal") normal.set("x3", dir_x) normal.set("y3", dir_y) normal.set("z3", dir_z) normal.set("tolerance", radius) elif feat_name == "exclusion": volume = ET.SubElement(document, "volume") # Set volume attributes volume.set("type", "exclusion") _set_coords_and_radius(volume, (x, y, z), radius, feat_id) tree._setroot(document) return tree
[docs]def save_pml(pharmacophore, file_name): """ Save a pharmacophore to a pml (Ligand Scout) file. Parameters ---------- pharmacophore : Pharmacophore A pharmacophore. file_name : str Name of the file that will be created. """ xml_tree = _ligandscout_xml_tree(pharmacophore) xml_string = ET.tostring(xml_tree) with open(file_name, "wb") as fp: fp.write(xml_string)