Skip to content

Commit

Permalink
Tool to compute all simple cycles using the new path graph implementa…
Browse files Browse the repository at this point in the history
…tions.

Signed-off-by: Egon Willighagen <egonw@users.sourceforge.net>
  • Loading branch information
johnmay authored and egonw committed Aug 1, 2013
1 parent f1d7d7d commit 29b1335
Show file tree
Hide file tree
Showing 2 changed files with 334 additions and 0 deletions.
188 changes: 188 additions & 0 deletions src/main/org/openscience/cdk/graph/AllCycles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package org.openscience.cdk.graph;

import org.openscience.cdk.annotations.TestClass;
import org.openscience.cdk.annotations.TestMethod;

import java.util.ArrayList;
import java.util.List;

/**
* Compute all simple cycles (rings) in a graph. Generally speaking one does not
* need all the cycles and tractable subsets offer good alternatives.
*
* <ul> <li>EdgeShortCycles - the smallest cycle through each edge</li>
* <li>{@link RelevantCycles} - union of all minimum cycle bases - unique but
* may be exponential</li> <li>{@link EssentialCycles} - intersection of all
* minimum cycle bases </li> <li>{@link MinimumCycleBasis} - a minimum cycles
* basis, may not be unique. Often used interchangeable with the term SSSR.</li>
* </ul>
*
*
* For maximum performance the algorithm should be run only on ring systems (a
* biconnected component with at least one tri-connected vertex). An example of
* this is shown below:
*
* <blockquote><pre>
* // convert the molecule to adjacency list - may be redundant in future
* IAtomContainer m =...;
* int[][] g = GraphUtil.toAdjList(m);
*
* // efficient computation/partitioning of the ring systems
* RingSearch rs = new RingSearch(m, g);
*
* // isolated cycles don't need to be run
* rs.isolated();
*
* // process fused systems separately
* for (int[] fused : rs.fused()) {
* // given the fused subgraph, max cycle size is
* // the number of vertices
* AllCycles ac = new AllCycles(subgraph(g, fused),
* fused.length,
* maxDegree);
* // cyclic walks
* int[][] paths = ac.paths();
* }
* </pre></blockquote>
*
* @author John May
* @cdk.module core
* @see RegularPathGraph
* @see JumboPathGraph
* @see org.openscience.cdk.ringsearch.RingSearch
* @see GraphUtil
* @see <a href="http://efficientbits.blogspot.co.uk/2013/06/allringsfinder-sport-edition.html">Performance
* Analysis (Blog Post)</a>
*/
@TestClass("org.openscience.cdk.graph.AllCyclesTest")
public final class AllCycles {

/** All simple cycles. */
private final List<int[]> cycles = new ArrayList<int[]>();

/** Indicates whether the perception completed. */
private final boolean completed;

/**
* Compute all simple cycles up to given <i>maxCycleSize</i> in the provided
* <i>graph</i>. In some graphs the topology makes it impracticable to
* compute all the simple. To avoid running forever on these molecules the
* <i>maxDegree</i> provides an escape clause. The value doesn't quantify
* how many cycles we get. The percentage of molecules in PubChem Compound
* (Dec '12) which would successfully complete for a given degree are listed
* below.
*
* <table> <tr><th>Percent</th><th>Max Degree</th></tr>
* <tr><td>99%</td><td>9</td></tr> <tr><td>99.95%</td><td>72</td></tr>
* <tr><td>99.96%</td><td>84</td></tr> <tr><td>99.97%</td><td>126</td></tr>
* <tr><td>99.98%</td><td>216</td></tr> <tr><td>99.99%</td><td>684</td></tr>
* </table>
*
* @param graph adjacency list representation of a graph
* @param maxCycleSize the maximum cycle size to perceive
* @param maxDegree escape clause to stop the algorithm running forever
*/
public AllCycles(final int[][] graph,
final int maxCycleSize,
final int maxDegree) {

// get the order in which we remove vertices, the rank tells us
// the index in the ordered array of each vertex
int[] rank = rank(graph);
int[] vertices = verticesInOrder(rank);

PathGraph pGraph =
graph.length < 64
? new RegularPathGraph(graph, rank, maxCycleSize)
: new JumboPathGraph(graph, rank, maxCycleSize);

// perceive the cycles by removing the vertices in order
int removed = 0;
for (final int v : vertices) {

if (pGraph.degree(v) > maxDegree)
break; // or could throw exception...

pGraph.remove(v, cycles);
removed++;
}

completed = removed == graph.length;
}

/**
* Using the pre-computed rank, get the vertices in order.
*
* @param rank see {@link #rank(int[][])}
* @return vertices in order
*/
@TestMethod("verticesInOrder")
static int[] verticesInOrder(final int[] rank) {
int[] vs = new int[rank.length];
for (int v = 0; v < rank.length; v++)
vs[rank[v]] = v;
return vs;
}

/**
* Compute a rank for each vertex. This rank is based on the degree and
* indicates the position each vertex would be in a sorted array.
*
* @param g a graph in adjacent list representation
* @return array indicating the rank of each vertex.
*/
@TestMethod("rank")
static int[] rank(final int[][] g) {
final int ord = g.length;

final int[] count = new int[ord + 1];
final int[] rank = new int[ord];

// frequency of each degree
for (int v = 0; v < ord; v++)
count[g[v].length + 1]++;
// cumulated counts
for (int i = 0; count[i] < ord; i++)
count[i + 1] += count[i];
// store sorted position of each vertex
for (int v = 0; v < ord; v++)
rank[v] = count[g[v].length]++;

return rank;
}

/**
* The paths describing all simple cycles in the given graph. The path stats
* and ends vertex.
*
* @return 2d array of paths
*/
@TestMethod("k4Paths,k5Paths")
public int[][] paths() {
int[][] paths = new int[cycles.size()][];
for (int i = 0; i < cycles.size(); i++)
paths[i] = cycles.get(i).clone();
return paths;
}

/**
* Cardinality of the set.
*
* @return number of cycles
*/
@TestMethod("k4Size,k5Size,k6Size,k7Size")
public int size() {
return cycles.size();
}

/**
* Did the cycle perception complete - if not the molecule was considered
* impractical and computation was aborted.
*
* @return algorithm completed
*/
@TestMethod("completed,impractical")
public boolean completed() {
return completed;
}
}
146 changes: 146 additions & 0 deletions src/test/org/openscience/cdk/graph/AllCyclesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package org.openscience.cdk.graph;

import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.openscience.cdk.graph.RegularPathGraphTest.completeGraphOfSize;

/**
* @author John May
* @cdk.module test-core
*/
public class AllCyclesTest {

@Test
public void rank() throws Exception {
// given vertices based on degree
int[][] g = new int[][]{
{0, 0, 0, 0}, // 5th
{0, 0, 0, 0}, // 6th
{0, 0, 0}, // 4th
{0}, // 2nd
{0, 0}, // 3rd
{} // 1st
};
assertThat(AllCycles.rank(g),
is(new int[]{4, 5, 3, 1, 2, 0}));
}

@Test
public void verticesInOrder() {
int[] vertices = AllCycles.verticesInOrder(new int[]{4, 3, 1, 2, 0});
assertThat(vertices, is(new int[]{4, 2, 3, 1, 0}));
}

@Test public void completed() {
AllCycles ac = new AllCycles(completeGraphOfSize(4),
4,
100);
assertTrue(ac.completed());
assertThat(ac.size(), is(7));
}

@Test(timeout = 50)
public void impractical() {
// k12 - ouch
AllCycles ac = new AllCycles(completeGraphOfSize(12),
12,
100);
assertFalse(ac.completed());
}

@Test
public void k4Paths() {
AllCycles ac = new AllCycles(completeGraphOfSize(4),
4,
1000);
assertThat(ac.paths(), is(new int[][]{{2, 1, 0, 2},
{3, 1, 0, 3},
{3, 2, 0, 3},
{3, 2, 1, 3},
{3, 2, 1, 0, 3},
{3, 2, 0, 1, 3},
{3, 0, 2, 1, 3}}));

}

@Test
public void k5Paths() {
AllCycles ac = new AllCycles(completeGraphOfSize(5),
5,
1000);
assertThat(ac.paths(), is(new int[][]{{2, 1, 0, 2},
{3, 1, 0, 3},
{4, 1, 0, 4},
{3, 2, 0, 3},
{3, 2, 1, 3},
{3, 2, 1, 0, 3},
{3, 2, 0, 1, 3},
{4, 2, 0, 4},
{4, 2, 1, 4},
{4, 2, 1, 0, 4},
{4, 2, 0, 1, 4},
{3, 0, 2, 1, 3},
{4, 0, 2, 1, 4},
{4, 3, 0, 4},
{4, 3, 1, 4},
{4, 3, 1, 0, 4},
{4, 3, 0, 1, 4},
{4, 3, 2, 4},
{4, 3, 2, 0, 4},
{4, 3, 2, 1, 4},
{4, 3, 2, 1, 0, 4},
{4, 3, 2, 0, 1, 4},
{4, 3, 0, 2, 4},
{4, 3, 1, 2, 4},
{4, 3, 0, 1, 2, 4},
{4, 3, 1, 0, 2, 4},
{4, 3, 0, 2, 1, 4},
{4, 3, 1, 2, 0, 4},
{4, 0, 3, 1, 4},
{4, 0, 3, 2, 4},
{4, 0, 3, 2, 1, 4},
{4, 0, 3, 1, 2, 4},
{4, 1, 3, 2, 4},
{4, 1, 3, 2, 0, 4},
{4, 1, 3, 0, 2, 4},
{4, 0, 1, 3, 2, 4},
{4, 1, 0, 3, 2, 4}}));
}


@Test
public void k4Size() {
AllCycles ac = new AllCycles(completeGraphOfSize(4),
4,
1000);
assertThat(ac.size(), is(7));
}

@Test
public void k5Size() {
AllCycles ac = new AllCycles(completeGraphOfSize(5),
5,
1000);
assertThat(ac.size(), is(37));
}

@Test
public void k6Size() {
AllCycles ac = new AllCycles(completeGraphOfSize(6),
6,
1000);
assertThat(ac.size(), is(197));
}

@Test
public void k7Size() {
AllCycles ac = new AllCycles(completeGraphOfSize(7),
7,
1000);
assertThat(ac.size(), is(1172));
}
}

0 comments on commit 29b1335

Please sign in to comment.