diff --git a/src/main/org/openscience/cdk/hash/AbstractHashGenerator.java b/src/main/org/openscience/cdk/hash/AbstractHashGenerator.java new file mode 100644 index 00000000000..7202db1aee3 --- /dev/null +++ b/src/main/org/openscience/cdk/hash/AbstractHashGenerator.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2013. John May + * + * 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 U + */ + +package org.openscience.cdk.hash; + +import org.openscience.cdk.annotations.TestClass; +import org.openscience.cdk.annotations.TestMethod; +import org.openscience.cdk.interfaces.IAtomContainer; +import org.openscience.cdk.interfaces.IBond; + +import java.util.Arrays; + +/** + * An abstract hash function providing several utility methods to be used by + * other hashing functions. + * + * @author John May + * @cdk.module hash + */ +@TestClass("org.openscience.cdk.hash.AbstractHashGeneratorTest") +class AbstractHashGenerator { + + /* pseudorandom number generator */ + private final Pseudorandom pseudorandom; + + /** + * Construct an abstract hash function providing the pseudorandom number + * generator. + * + * @param pseudorandom a pseudorandom number generator + * @throws NullPointerException the pseudorandom number generator was null + */ + @TestMethod("testConstruction_Null") + public AbstractHashGenerator(Pseudorandom pseudorandom) { + if (pseudorandom == null) + throw new NullPointerException("null pseduorandom number generator provided"); + this.pseudorandom = pseudorandom; + } + + /** + * Create a copy of the array of long values. + * + * @param src original values + * @return copy of the original values + * @see Arrays#copyOf(long[], int) + */ + @TestMethod("testCopy") + static long[] copy(long[] src) { + return Arrays.copyOf(src, src.length); + } + + /** + * Copy the values from the source (src) array to the destination (dest). + * + * @param src source values + * @param dest destination of the source copy + * @see System#arraycopy(Object, int, Object, int, int); + */ + @TestMethod("testCopy_SrcDest") + static void copy(long[] src, long[] dest) { + System.arraycopy(src, 0, dest, 0, dest.length); + } + + /** + * Generate the next random number. + * + * @param seed a {@literal long} value to seed a pseudorandom number + * generator + * @return next pseudorandom number + */ + @TestMethod("testRotate") long rotate(long seed) { + return pseudorandom.next(seed); + } + + /** + * Rotate a value, n times. The rotation uses a pseudorandom + * number generator to sequentially generate values seed on the previous + * value. + * + * @param value the {@literal long} value to rotate + * @param n the number of times to rotate the value + * @return the {@literal long} value rotated the specified number of times + */ + @TestMethod("testRotate_N") long rotate(long value, int n) { + while (n-- > 0) + value = pseudorandom.next(value); + return value; + } + + /** + * Returns the value of the lowest three bits. This value is between 0 and 7 + * inclusive. + * + * @param value a {@literal long} value + * @return the {@literal int} value of the lowest three bits. + */ + @TestMethod("testLowestThreeBits") + static int lowestThreeBits(long value) { + return (int) (value & 0x7); + } + + /** + * Distribute the provided value across the set of {@literal long} values. + * + * @param value a {@literal long} value to distribute + * @return the {@literal long} value distributed a set amount + */ + @TestMethod("testDistribute") long distribute(long value) { + // rotate 1-8 times + return rotate(value, 1 + lowestThreeBits(value)); + } + + /** + * Convert an IAtomContainer to an adjacency list. + * + * @param container the container to convert + * @return adjacency list representation + */ + @TestMethod("testToAdjList") + static int[][] toAdjList(IAtomContainer container) { + + if (container == null) + throw new IllegalArgumentException("atom container was null"); + + int n = container.getAtomCount(); + + int[][] graph = new int[n][16]; + int[] degree = new int[n]; + + for (IBond bond : container.bonds()) { + + int v = container.getAtomNumber(bond.getAtom(0)); + int w = container.getAtomNumber(bond.getAtom(1)); + + if (v < 0 || w < 0) + throw new IllegalArgumentException("bond at index " + container.getBondNumber(bond) + + " contained an atom not pressent in molecule"); + + graph[v][degree[v]++] = w; + graph[w][degree[w]++] = v; + + // if the vertex degree of v or w reaches capacity, double the size + if (degree[v] == graph[v].length) + graph[v] = Arrays.copyOf(graph[v], degree[v] * 2); + if (degree[w] == graph[w].length) + graph[w] = Arrays.copyOf(graph[w], degree[w] * 2); + } + + for (int v = 0; v < n; v++) { + graph[v] = Arrays.copyOf(graph[v], degree[v]); + } + + return graph; + + } +} diff --git a/src/test/org/openscience/cdk/hash/AbstractHashGeneratorTest.java b/src/test/org/openscience/cdk/hash/AbstractHashGeneratorTest.java new file mode 100644 index 00000000000..7d48292c593 --- /dev/null +++ b/src/test/org/openscience/cdk/hash/AbstractHashGeneratorTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2013. John May + * + * 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 U + */ + +package org.openscience.cdk.hash; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author John May + * @cdk.module test-hash + */ +public class AbstractHashGeneratorTest { + + @Test(expected = NullPointerException.class) + public void testConstruction_Null() { + new AbstractHashGenerator(null); + } + + @Test + public void testCopy() throws Exception { + long[] x = new long[]{2, 1, 3, 2}; + long[] y = AbstractHashGenerator.copy(x); + assertThat(x, is(y)); + assertThat(x, not(sameInstance(y))); + } + + @Test + public void testCopy_SrcDest() throws Exception { + long[] x = new long[]{42, 23, 1, 72}; + long[] y = new long[4]; + AbstractHashGenerator.copy(x, y); + assertThat(x, is(y)); + assertThat(x, not(sameInstance(y))); + } + + @Test + public void testRotate() throws Exception { + Pseudorandom pseudorandom = mock(Pseudorandom.class); + AbstractHashGenerator f = new AbstractHashGenerator(pseudorandom); + f.rotate(5L); + verify(pseudorandom, times(1)).next(5L); + } + + @Test + public void testRotate_N() throws Exception { + Pseudorandom pseudorandom = mock(Pseudorandom.class); + AbstractHashGenerator f = new AbstractHashGenerator(pseudorandom); + f.rotate(0, 5); // note 0 doesn't rotate... + verify(pseudorandom, times(5)).next(anyLong()); + } + + @Test + public void testLowestThreeBits() throws Exception { + assertThat(AbstractHashGenerator.lowestThreeBits(0L), is(0)); + assertThat(AbstractHashGenerator.lowestThreeBits(1L), is(1)); + assertThat(AbstractHashGenerator.lowestThreeBits(2L), is(2)); + assertThat(AbstractHashGenerator.lowestThreeBits(3L), is(3)); + assertThat(AbstractHashGenerator.lowestThreeBits(4L), is(4)); + assertThat(AbstractHashGenerator.lowestThreeBits(5L), is(5)); + assertThat(AbstractHashGenerator.lowestThreeBits(6L), is(6)); + assertThat(AbstractHashGenerator.lowestThreeBits(7L), is(7)); + + // check we don't exceed 7 + assertThat(AbstractHashGenerator.lowestThreeBits(8L), is(0)); + assertThat(AbstractHashGenerator.lowestThreeBits(9L), is(1)); + assertThat(AbstractHashGenerator.lowestThreeBits(10L), is(2)); + assertThat(AbstractHashGenerator.lowestThreeBits(11L), is(3)); + assertThat(AbstractHashGenerator.lowestThreeBits(12L), is(4)); + assertThat(AbstractHashGenerator.lowestThreeBits(13L), is(5)); + assertThat(AbstractHashGenerator.lowestThreeBits(14L), is(6)); + assertThat(AbstractHashGenerator.lowestThreeBits(15L), is(7)); + assertThat(AbstractHashGenerator.lowestThreeBits(16L), is(0)); + + // max/min numbers + assertThat(AbstractHashGenerator.lowestThreeBits(Long.MAX_VALUE), is(7)); + assertThat(AbstractHashGenerator.lowestThreeBits(Long.MIN_VALUE), is(0)); + } + + @Test + public void testDistribute_AtLeastOnce() throws Exception { + Pseudorandom pseudorandom = mock(Pseudorandom.class); + AbstractHashGenerator f = new AbstractHashGenerator(pseudorandom); + long x = f.distribute(8L); // lowest 3 bits = 0, make sure we rotate 1 + verify(pseudorandom, times(1)).next(anyLong()); + assertThat(x, is(not(8L))); + } + + @Test + public void testDistribute() throws Exception { + Pseudorandom pseudorandom = mock(Pseudorandom.class); + AbstractHashGenerator f = new AbstractHashGenerator(pseudorandom); + long x = f.distribute(5L); // lowest 3 bits = 5, rotate 6 times + verify(pseudorandom, times(6)).next(anyLong()); + assertThat(x, is(not(5L))); + } + + @Test + public void testToAdjList() { + // already tests in ShortestPaths... this method be moved once all + // pending patches are merged + } +} diff --git a/src/test/org/openscience/cdk/modulesuites/MhashTests.java b/src/test/org/openscience/cdk/modulesuites/MhashTests.java index 82597ca4c75..997f6e2c8da 100644 --- a/src/test/org/openscience/cdk/modulesuites/MhashTests.java +++ b/src/test/org/openscience/cdk/modulesuites/MhashTests.java @@ -21,6 +21,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; +import org.openscience.cdk.hash.AbstractHashGeneratorTest; import org.openscience.cdk.hash.XorshiftTest; import org.openscience.cdk.hash.seed.BasicAtomEncoderTest; import org.openscience.cdk.hash.seed.ConjugatedAtomEncoderTest; @@ -34,7 +35,8 @@ @SuiteClasses(value = { XorshiftTest.class, BasicAtomEncoderTest.class, - ConjugatedAtomEncoderTest.class + ConjugatedAtomEncoderTest.class, + AbstractHashGeneratorTest.class, }) public class MhashTests { }