/* $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;
}
}