/* $RCSfile$ * $Author$ * $Date$ * $Revision$ * * Copyright (C) 2004-2008 Rajarshi Guha <rajarshi.guha@gmail.com> * * Contact: cdk-devel@lists.sourceforge.net * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2.1 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.openscience.cdk.pharmacophore; import nu.xom.*; import org.openscience.cdk.CDKConstants; import org.openscience.cdk.annotations.TestClass; import org.openscience.cdk.annotations.TestMethod; import org.openscience.cdk.exception.CDKException; import org.openscience.cdk.interfaces.IAtom; import org.openscience.cdk.interfaces.IBond; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * Provides some utility methods for pharmacophore handling. * * @author Rajarshi Guha * @cdk.module pcore * @cdk.githash * @cdk.keyword pharmacophore * @cdk.keyword 3D isomorphism * @cdk.builddepends xom-1.1.jar * @cdk.depends xom-1.1.jar */ @TestClass("org.openscience.cdk.pharmacophore.PharmacophoreUtilityTest") public class PharmacophoreUtils { /** * Read in a set of pharmacophore definitions to create pharmacophore queries. * <p/> * Pharmacophore queries can be saved in an XML format which is described XXX. The * file can contain multiple definitions. This method will process all the definitions * and return a list fo {@link org.openscience.cdk.pharmacophore.PharmacophoreQuery} objects which can be used with * the {@link PharmacophoreMatcher} class. * <p/> * The current schema for the document allows one to specify angle and distance * constraints. Currently the CDK does not support angle constraints, so they are * ignored. * <p/> * The schema also specifies a <i>units</i> attribute for a given constraint. The * current reader ignores this and assumes that all distances are in Angstroms. * <p/> * Finally, if there is a description associated with a pharmacophore definition, it is * available as the <i>"description"</i> property of the {@link org.openscience.cdk.pharmacophore.PharmacophoreQuery} object. * <p/> * Example usage is * <pre> * List<PharmacophoreQuery> defs = readPharmacophoreDefinitions("mydefs.xml"); * System.out.println("Number of definitions = "+defs.size()); * for (int i = 0; i < defs.size(); i++) { * System.out.println("Desc: "+defs.get(i).getProperty("description"); * } * </pre> * * @param filename The file to read the definitions from * @return A list of {@link org.openscience.cdk.pharmacophore.PharmacophoreQuery} objects * @throws CDKException if there is an error in the format * @throws IOException if there is an error in opening the file * @see org.openscience.cdk.pharmacophore.PharmacophoreQueryAtom * @see org.openscience.cdk.pharmacophore.PharmacophoreQueryBond * @see org.openscience.cdk.pharmacophore.PharmacophoreQuery * @see org.openscience.cdk.pharmacophore.PharmacophoreMatcher */ @TestMethod("testReadPcoreDef, testInvalidPcoreXML") public static List<PharmacophoreQuery> readPharmacophoreDefinitions(String filename) throws CDKException, IOException { Builder parser = new Builder(); Document doc; try { doc = parser.build(filename); } catch (ParsingException e) { throw new CDKException("Invalid pharmacophore definition file"); } return getdefs(doc); } /** * Read in a set of pharmacophore definitions to create pharmacophore queries. * <p/> * Pharmacophore queries can be saved in an XML format which is described XXX. The * file can contain multiple definitions. This method will process all the definitions * and return a list of {@link org.openscience.cdk.pharmacophore.PharmacophoreQuery} objects which can be used with * the {@link PharmacophoreMatcher} class. * <p/> * The current schema for the document allows one to specify angle and distance * constraints. * <p/> * The schema also specifies a <i>units</i> attribute for a given constraint. The * current reader ignores this and assumes that all distances are in Angstroms and * angles are in degrees. * <p/> * Finally, if there is a description associated with a pharmacophore definition, it is * available as the <i>"description"</i> property of the {@link org.openscience.cdk.pharmacophore.PharmacophoreQuery} object. * <p/> * Example usage is * <pre> * List<PharmacophoreQuery> defs = readPharmacophoreDefinitions"mydefs.xml"); * System.out.println("Number of definitions = "+defs.size()); * for (int i = 0; i < defs.size(); i++) { * System.out.println("Desc: "+defs.get(i).getProperty("description"); * } * </pre> * * @param ins The stream to read the definitions from * @return A list of {@link org.openscience.cdk.pharmacophore.PharmacophoreQuery} objects * @throws CDKException if there is an error in the format * @throws IOException if there is an error in opening the file * @see org.openscience.cdk.pharmacophore.PharmacophoreQueryAtom * @see org.openscience.cdk.pharmacophore.PharmacophoreQueryBond * @see org.openscience.cdk.pharmacophore.PharmacophoreMatcher * @see org.openscience.cdk.pharmacophore.PharmacophoreQuery */ @TestMethod("testReadPcoreDef, testReadPcoreAngleDef, testInvalidPcoreXML") public static List<PharmacophoreQuery> readPharmacophoreDefinitions(InputStream ins) throws IOException, CDKException { Builder parser = new Builder(); Document doc; try { doc = parser.build(ins); } catch (ParsingException e) { throw new CDKException("Invalid pharmacophore definition file"); } return getdefs(doc); } /** * Write out one or more pharmacophore queries in the CDK XML format. * * @param query The pharmacophore queries * @param out The OutputStream to write to * @throws IOException if there is a problem writing the XML document */ @TestMethod("testPCoreWrite") public static void writePharmacophoreDefinition(PharmacophoreQuery query, OutputStream out) throws IOException { writePharmacophoreDefinition(new PharmacophoreQuery[]{query}, out); } /** * Write out one or more pharmacophore queries in the CDK XML format. * * @param queries The pharmacophore queries * @param out The OutputStream to write to * @throws IOException if there is a problem writing the XML document */ @TestMethod("testPCoreWrite") public static void writePharmacophoreDefinition(List<PharmacophoreQuery> queries, OutputStream out) throws IOException { writePharmacophoreDefinition(queries.toArray(new PharmacophoreQuery[]{}), out); } /** * Write out one or more pharmacophore queries in the CDK XML format. * * @param queries The pharmacophore queries * @param out The OutputStream to write to * @throws IOException if there is a problem writing the XML document */ @TestMethod("testPCoreWrite") public static void writePharmacophoreDefinition(PharmacophoreQuery[] queries, OutputStream out) throws IOException { Element root = new Element("pharmacophoreContainer"); root.addAttribute(new Attribute("version", "1.0")); for (PharmacophoreQuery query : queries) { Element pcore = new Element("pharmacophore"); Object description = query.getProperty("description"); if (description != null) pcore.addAttribute(new Attribute("description", (String) description)); Object name = query.getProperty(CDKConstants.TITLE); if (name != null) pcore.addAttribute(new Attribute("name", (String) name)); // we add the pcore groups for this query as local to the group for (IAtom atom : query.atoms()) { Element group = new Element("group"); group.addAttribute(new Attribute("id", atom.getSymbol())); group.appendChild(((PharmacophoreQueryAtom) atom).getSmarts()); pcore.appendChild(group); } // now add the constraints for (IBond bond : query.bonds()) { Element elem = null; if (bond instanceof PharmacophoreQueryBond) { PharmacophoreQueryBond dbond = (PharmacophoreQueryBond) bond; elem = new Element("distanceConstraint"); elem.addAttribute(new Attribute("lower", String.valueOf(dbond.getLower()))); elem.addAttribute(new Attribute("upper", String.valueOf(dbond.getUpper()))); elem.addAttribute(new Attribute("units", "A")); } else if (bond instanceof PharmacophoreQueryAngleBond) { PharmacophoreQueryAngleBond dbond = (PharmacophoreQueryAngleBond) bond; elem = new Element("angleConstraint"); elem.addAttribute(new Attribute("lower", String.valueOf(dbond.getLower()))); elem.addAttribute(new Attribute("upper", String.valueOf(dbond.getUpper()))); elem.addAttribute(new Attribute("units", "degrees")); } // now add the group associated with this constraint for (IAtom iAtom : bond.atoms()) { PharmacophoreQueryAtom atom = (PharmacophoreQueryAtom) iAtom; Element gelem = new Element("groupRef"); gelem.addAttribute(new Attribute("id", atom.getSymbol())); if (elem != null) { elem.appendChild(gelem); } } pcore.appendChild(elem); } root.appendChild(pcore); } Document doc = new Document(root); Serializer serializer = new Serializer(out, "ISO-8859-1"); serializer.setIndent(4); serializer.setMaxLength(128); serializer.write(doc); } private static List<PharmacophoreQuery> getdefs(Document doc) throws CDKException { Element root = doc.getRootElement(); // ltes get the children of the container // these will be either group or pharmacophore elems List<PharmacophoreQuery> ret = new ArrayList<PharmacophoreQuery>(); // get global group defs HashMap<String, String> groups = getGroupDefinitions(root); //now get the pcore defs Elements children = root.getChildElements(); for (int i = 0; i < children.size(); i++) { Element e = children.get(i); if (e.getQualifiedName().equals("pharmacophore")) ret.add(processPharmacophoreElement(e, groups)); } return ret; } /* find all <group> elements that are directly under the supplied element so this wont recurse through sub elements that may contain group elements */ private static HashMap<String, String> getGroupDefinitions(Element e) { HashMap<String, String> groups = new HashMap<String, String>(); Elements children = e.getChildElements(); for (int i = 0; i < children.size(); i++) { Element child = children.get(i); if (child.getQualifiedName().equals("group")) { String id = child.getAttributeValue("id").trim(); String smarts = child.getValue().trim(); groups.put(id, smarts); } } return groups; } /* process a single pcore definition */ private static PharmacophoreQuery processPharmacophoreElement(Element e, HashMap<String, String> global) throws CDKException { PharmacophoreQuery ret = new PharmacophoreQuery(); ret.setProperty("description", e.getAttributeValue("description")); ret.setProperty(CDKConstants.TITLE, e.getAttributeValue("name")); // first get any local group definitions HashMap<String, String> local = getGroupDefinitions(e); // now lets look at the constraints Elements children = e.getChildElements(); for (int i = 0; i < children.size(); i++) { Element child = children.get(i); if (child.getQualifiedName().equals("distanceConstraint")) { processDistanceConstraint(child, local, global, ret); } else if (child.getQualifiedName().equals("angleConstraint")) { processAngleConstraint(child, local, global, ret); } } return ret; } private static void processDistanceConstraint(Element child, HashMap<String, String> local, HashMap<String, String> global, PharmacophoreQuery ret) throws CDKException { double lower; String tmp = child.getAttributeValue("lower"); if (tmp == null) throw new CDKException("Must have a 'lower' attribute"); else lower = Double.parseDouble(tmp); // we may not have an upper bound specified double upper; tmp = child.getAttributeValue("upper"); if (tmp != null) upper = Double.parseDouble(tmp); else upper = lower; // now get the two groups for this distance Elements grouprefs = child.getChildElements(); if (grouprefs.size() != 2) throw new CDKException("A distance constraint can only refer to 2 groups."); String id1 = grouprefs.get(0).getAttributeValue("id"); String id2 = grouprefs.get(1).getAttributeValue("id"); // see if it's a local def, else get it from the global list String smarts1, smarts2; if (local.containsKey(id1)) smarts1 = local.get(id1); else if (global.containsKey(id1)) smarts1 = global.get(id1); else throw new CDKException("Referring to a non-existant group definition"); if (local.containsKey(id2)) smarts2 = local.get(id2); else if (global.containsKey(id2)) smarts2 = global.get(id2); else throw new CDKException("Referring to a non-existant group definition"); // now see if we already have a correpsondiong pcore atom // else create a new atom if (!containsPatom(ret, id1)) { PharmacophoreQueryAtom pqa = new PharmacophoreQueryAtom(id1, smarts1); ret.addAtom(pqa); } if (!containsPatom(ret, id2)) { PharmacophoreQueryAtom pqa = new PharmacophoreQueryAtom(id2, smarts2); ret.addAtom(pqa); } // now add the constraint as a bond IAtom a1 = null, a2 = null; for (IAtom queryAtom : ret.atoms()) { if (queryAtom.getSymbol().equals(id1)) a1 = queryAtom; if (queryAtom.getSymbol().equals(id2)) a2 = queryAtom; } ret.addBond(new PharmacophoreQueryBond((PharmacophoreQueryAtom) a1, (PharmacophoreQueryAtom) a2, lower, upper)); } private static void processAngleConstraint(Element child, HashMap<String, String> local, HashMap<String, String> global, PharmacophoreQuery ret) throws CDKException { double lower; String tmp = child.getAttributeValue("lower"); if (tmp == null) throw new CDKException("Must have a 'lower' attribute"); else lower = Double.parseDouble(tmp); // we may not have an upper bound specified double upper; tmp = child.getAttributeValue("upper"); if (tmp != null) upper = Double.parseDouble(tmp); else upper = lower; // now get the three groups for this distance Elements grouprefs = child.getChildElements(); if (grouprefs.size() != 3) throw new CDKException("An angle constraint can only refer to 3 groups."); String id1 = grouprefs.get(0).getAttributeValue("id"); String id2 = grouprefs.get(1).getAttributeValue("id"); String id3 = grouprefs.get(2).getAttributeValue("id"); // see if it's a local def, else get it from the global list String smarts1, smarts2, smarts3; if (local.containsKey(id1)) smarts1 = local.get(id1); else if (global.containsKey(id1)) smarts1 = global.get(id1); else throw new CDKException("Referring to a non-existant group definition"); if (local.containsKey(id2)) smarts2 = local.get(id2); else if (global.containsKey(id2)) smarts2 = global.get(id2); else throw new CDKException("Referring to a non-existant group definition"); if (local.containsKey(id3)) smarts3 = local.get(id3); else if (global.containsKey(id3)) smarts3 = global.get(id3); else throw new CDKException("Referring to a non-existant group definition"); // now see if we already have a correpsondiong pcore atom // else create a new atom if (!containsPatom(ret, id1)) { PharmacophoreQueryAtom pqa = new PharmacophoreQueryAtom(id1, smarts1); ret.addAtom(pqa); } if (!containsPatom(ret, id2)) { PharmacophoreQueryAtom pqa = new PharmacophoreQueryAtom(id2, smarts2); ret.addAtom(pqa); } if (!containsPatom(ret, id3)) { PharmacophoreQueryAtom pqa = new PharmacophoreQueryAtom(id3, smarts3); ret.addAtom(pqa); } // now add the constraint as a bond IAtom a1 = null, a2 = null; IAtom a3 = null; for (IAtom queryAtom : ret.atoms()) { if (queryAtom.getSymbol().equals(id1)) a1 = queryAtom; if (queryAtom.getSymbol().equals(id2)) a2 = queryAtom; if (queryAtom.getSymbol().equals(id3)) a3 = queryAtom; } ret.addBond(new PharmacophoreQueryAngleBond( (PharmacophoreQueryAtom) a1, (PharmacophoreQueryAtom) a2, (PharmacophoreQueryAtom) a3, lower, upper)); } private static boolean containsPatom(PharmacophoreQuery q, String id) { for (IAtom queryAtom : q.atoms()) { if (queryAtom.getSymbol().equals(id)) return true; } return false; } }