/* $Revision$ $Author$ $Date$ * * Copyright (C) 2002-2007 Christoph Steinbeck <steinbeck@users.sf.net> * 2009 Mark Rijnbeek <mark_rynbeek@users.sf.net> * * 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. * All we ask is that proper credit is given for our work, which includes * - but is not limited to - adding the above copyright notice to the beginning * of your source code files, and to any copyright notice that you may distribute * with programs based on this work. * * 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.ringsearch; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.openscience.cdk.annotations.TestClass; import org.openscience.cdk.annotations.TestMethod; import org.openscience.cdk.exception.CDKException; import org.openscience.cdk.graph.ConnectivityChecker; import org.openscience.cdk.graph.SpanningTree; import org.openscience.cdk.interfaces.IAtom; import org.openscience.cdk.interfaces.IAtomContainer; import org.openscience.cdk.interfaces.IBond; import org.openscience.cdk.interfaces.IMolecule; import org.openscience.cdk.interfaces.IRing; import org.openscience.cdk.interfaces.IRingSet; import org.openscience.cdk.tools.ILoggingTool; import org.openscience.cdk.tools.LoggingToolFactory; /** * Finds the Set of all Rings. This is an implementation of the algorithm * published in {@cdk.cite HAN96}. Some of the comments refer to pseudo code * fragments listed in this article. The concept is that a regular molecular * graph is converted into a path graph first, i.e. a graph where the edges are * actually paths, i.e. can list several nodes that are implicitly connecting * the two nodes between the path is formed. The paths that join one endnode * are step by step fused and the joined nodes deleted from the pathgraph. What * remains is a graph of paths that have the same start and endpoint and are * thus rings. * * <p><b>WARNING</b>: This class has now a timeout of 5 seconds, after which it aborts * its ringsearch. The timeout value can be customized by the setTimeout() * method of this class. * <br>Also, by using the optional argument "maxRingSize" timeouts can possibly be avoided * because recursion depth will be limited accordingly. * Example: given a complex atom container and a maxRingSize of six, the find method * will return all rings only of size six or smaller. * * @author steinbeck * @cdk.created 2002-06-23 * @cdk.module standard * @cdk.githash */ @TestClass("org.openscience.cdk.ringsearch.AllRingsFinderTest") public class AllRingsFinder { private ILoggingTool logger=null; public boolean debug = false; private long timeout = 5000; private long startTime; /* * used for storing the original atomContainer for * reference purposes (printing) */ IAtomContainer originalAc = null; List<Path> newPaths = new ArrayList<Path>(); List<Path> potentialRings = new ArrayList<Path>(); List<Path> removePaths = new ArrayList<Path>(); /** * Constructor for the AllRingsFinder. * * @param logging true=logging will be done (slower), false = no logging. */ public AllRingsFinder(boolean logging){ if(logging) logger = LoggingToolFactory.createLoggingTool(AllRingsFinder.class); } /** * Constructor for the AllRingsFinder with logging. */ public AllRingsFinder(){ this(true); } /** * Returns a ringset containing all rings in the given AtomContainer * Calls {@link #findAllRings(IAtomContainer, Integer)} with max ring size argument set to null (=unlimited ring sizes) * *@param atomContainer The AtomContainer to be searched for rings *@return A RingSet with all rings in the AtomContainer *@exception CDKException An exception thrown if something goes wrong or if the timeout limit is reached */ @TestMethod("testFindAllRings_IAtomContainer,testBondsWithinRing") public IRingSet findAllRings(IAtomContainer atomContainer) throws CDKException { return findAllRings(atomContainer, null); } /** * Returns a ringset containing all rings up to a provided maximum size in a given AtomContainer * *@param atomContainer The AtomContainer to be searched for rings *@param maxRingSize Maximum ring size to consider. Provides a possible breakout from recursion for complex compounds. *@return A RingSet with all rings in the AtomContainer *@exception CDKException An exception thrown if something goes wrong or if the timeout limit is reached */ public IRingSet findAllRings(IAtomContainer atomContainer, Integer maxRingSize) throws CDKException { startTime = System.currentTimeMillis(); SpanningTree spanningTree = new SpanningTree(atomContainer); IAtomContainer ringSystems = spanningTree.getCyclicFragmentsContainer(); Iterator separateRingSystem = ConnectivityChecker.partitionIntoMolecules(ringSystems).molecules().iterator(); IRingSet resultSet = atomContainer.getBuilder().newRingSet(); while (separateRingSystem.hasNext()) { resultSet.add(findAllRingsInIsolatedRingSystem((IMolecule)separateRingSystem.next(), maxRingSize)); } return resultSet; } /** * Fings the set of all rings in a molecule * Calls {@link #findAllRingsInIsolatedRingSystem(IAtomContainer,Integer)} with max ring size argument set to null (=unlimited ring sizes) * *@param atomContainer the molecule to be searched for rings *@return a RingSet containing the rings in molecule *@exception CDKException An exception thrown if something goes wrong or if the timeout limit is reached */ public IRingSet findAllRingsInIsolatedRingSystem(IAtomContainer atomContainer) throws CDKException { return findAllRingsInIsolatedRingSystem(atomContainer, null); } /** *Finds the set of all rings in a molecule * *@param atomContainer the molecule to be searched for rings *@param maxRingSize Maximum ring size to consider. Provides a possible breakout from recursion for complex compounds. *@return a RingSet containing the rings in molecule *@exception CDKException An exception thrown if something goes wrong or if the timeout limit is reached */ public IRingSet findAllRingsInIsolatedRingSystem(IAtomContainer atomContainer, Integer maxRingSize) throws CDKException { if (startTime == 0) { startTime = System.currentTimeMillis(); } List<Path> paths = new ArrayList<Path>(); IRingSet ringSet = atomContainer.getBuilder().newRingSet(); IAtomContainer ac = atomContainer.getBuilder().newAtomContainer(); originalAc = atomContainer; ac.add(atomContainer); doSearch(ac, paths, ringSet, maxRingSize); return ringSet; } /** *@param ac The AtomContainer to be searched *@param paths A vectoring storing all the paths *@param ringSet A ringset to be extended while we search *@exception CDKException An exception thrown if something goes wrong or if the timeout limit is reached */ private void doSearch(IAtomContainer ac, List<Path> paths, IRingSet ringSet, Integer maxPathLen) throws CDKException { IAtom atom; /* * First we convert the molecular graph into a a path graph by * creating a set of two membered paths from all the bonds in the molecule */ initPathGraph(ac, paths); if(logger!=null){ logger.debug("BondCount: ", ac.getBondCount()); logger.debug("PathCount: ", paths.size()); } do { atom = selectAtom(ac); if (atom != null) { remove(atom, ac, paths, ringSet, maxPathLen); } } while (paths.size() > 0 && atom != null); if(logger!=null){ logger.debug("paths.size(): ", paths.size()); logger.debug("ringSet.size(): ", ringSet.getAtomContainerCount()); } } /** * Removes an atom from the AtomContainer under certain conditions. * See {@cdk.cite HAN96} for details * * *@param atom The atom to be removed *@param ac The AtomContainer to work on *@param paths The paths to manipulate *@param rings The ringset to be extended *@param maxPathLen Max path length = max ring size detected = max recursion depth *@exception CDKException Thrown if something goes wrong or if the timeout is exceeded */ private void remove(IAtom atom, IAtomContainer ac, List<Path> paths, IRingSet rings, Integer maxPathLen) throws CDKException { Path path1; Path path2; Path union; int intersectionSize; newPaths.clear(); removePaths.clear(); potentialRings.clear(); if(logger!=null) logger.debug("*** Removing atom " + originalAc.getAtomNumber(atom) + " ***"); for (int i = 0; i < paths.size(); i++) { path1 = paths.get(i); if (path1.firstElement() == atom || path1.lastElement() == atom) { for (int j = i + 1; j < paths.size(); j++) { //logger.debug("."); path2 = paths.get(j); if (path2.firstElement() == atom || path2.lastElement() == atom) { intersectionSize = path1.getIntersectionSize(path2); if (intersectionSize < 3) { if(logger!=null){ logger.debug("Joining " + path1.toString(originalAc) + " and " + path2.toString(originalAc)); } union = Path.join(path1, path2, atom); if (intersectionSize == 1) { newPaths.add(union); } else { if (maxPathLen == null || union.size() <= (maxPathLen+1)) { potentialRings.add(union); } } //logger.debug("Intersection Size: " + intersectionSize); if(logger!=null){ logger.debug("Union: ", union.toString(originalAc)); } /* * Now we know that path1 and * path2 share the Atom atom. */ removePaths.add(path1); removePaths.add(path2); } } if (timeout > 0) checkTimeout(); } } } for (Path removePath : removePaths) { paths.remove(removePath); } for (Path newPath : newPaths) { if (maxPathLen == null || newPath.size() <= (maxPathLen+1)) { paths.add(newPath); } } detectRings(potentialRings, rings, originalAc); ac.removeAtomAndConnectedElectronContainers(atom); if(logger!=null) logger.debug("\n" + paths.size() + " paths and " + ac.getAtomCount() + " atoms left."); } /** * Checks the paths if a ring has been found * *@param paths The paths to check for rings *@param ringSet The ringset to add the detected rings to *@param ac The AtomContainer with the original structure */ private void detectRings(List<Path> paths, IRingSet ringSet, IAtomContainer ac) { IRing ring; int bondNum; IAtom a1, a2 = null; for (Path path : paths) { if (path.size() > 3 && path.lastElement() == path.firstElement()) { if(logger!=null) logger.debug("Removing path " + path.toString(originalAc) + " which is a ring."); path.removeElementAt(0); ring = ac.getBuilder().newRing(); for (int g = 0; g < path.size() - 1; g++) { a1 = (IAtom) path.elementAt(g); a2 = (IAtom) path.elementAt(g + 1); ring.addAtom(a1); bondNum = ac.getBondNumber(a1, a2); //logger.debug("bondNum " + bondNum); ring.addBond(ac.getBond(bondNum)); } ring.addAtom(a2); a1 = (IAtom) path.elementAt(0); a2 = (IAtom) path.elementAt(path.size() - 1); ring.addAtom(a1); bondNum = ac.getBondNumber(a1, a2); //logger.debug("bondNum " + bondNum); ring.addBond(ac.getBond(bondNum)); /* * The following code had a problem when two atom in the ring * found are connected the in orignal graph but do not belong * to this particular ring. IBond[] bonds = ac.getBonds(); for (int g = 0; g < bonds.length; g++) { bond = bonds[g]; if (ring.contains(bond.getAtom(0)) && ring.contains(bond.getAtom(1))) { ring.addBond(bond); } }*/ ringSet.addAtomContainer(ring); } } } /** * Initialized the path graph * See {@cdk.cite HAN96} for details * *@param ac The AtomContainer with the original structure *@param paths The paths to initialize */ private void initPathGraph(IAtomContainer ac, List<Path> paths) { Path path; Iterator bonds = ac.bonds().iterator(); while (bonds.hasNext()) { IBond bond = (IBond) bonds.next(); path = new Path(bond.getAtom(0), bond.getAtom(1)); paths.add(path); if(logger!=null) logger.debug("initPathGraph: " + path.toString(originalAc)); } } /** * Selects an optimal atom for removal * See {@cdk.cite HAN96} for details * *@param ac The AtomContainer to search *@return The selected Atom */ private IAtom selectAtom(IAtomContainer ac) { int minDegree = 999; // :-) int degree; IAtom minAtom = null; IAtom atom; for (int f = 0; f < ac.getAtomCount(); f++) { atom = ac.getAtom(f); degree = ac.getConnectedBondsCount(atom); if (degree < minDegree) { minAtom = atom; minDegree = degree; } } return minAtom; } /** * Checks if the timeout has been reached and throws an * exception if so. This is used to prevent this AllRingsFinder * to run for ages in certain rare cases with ring systems of * large size or special topology. * *@exception CDKException The exception thrown in case of hitting the timeout */ @TestMethod("testCheckTimeout") public void checkTimeout() throws CDKException { if (startTime == 0) return; long time = System.currentTimeMillis(); if (time - startTime > timeout) { throw new CDKException("Timeout for AllringsFinder exceeded"); } } /** * Sets the timeout value in milliseconds of the AllRingsFinder object * This is used to prevent this AllRingsFinder * to run for ages in certain rare cases with ring systems of * large size or special topology * *@param timeout The new timeout value * @return a reference to the instance this method was called for */ @TestMethod("testSetTimeout_long") public AllRingsFinder setTimeout(long timeout) { this.timeout = timeout; return this; } /** * Gets the timeout values in milliseconds of the AllRingsFinder object * *@return The timeout value */ @TestMethod("testGetTimeout") public long getTimeout() { return timeout; } }