Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

fluent API for building hash generators

  • Loading branch information...
commit 7a40b0cd54746949ddf812fecc387457ec697535 1 parent f230075
@johnmay authored
View
236 src/main/org/openscience/cdk/hash/HashGeneratorMaker.java
@@ -0,0 +1,236 @@
+package org.openscience.cdk.hash;
@egonw
egonw added a note

Please add the missing copyright header.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+import org.openscience.cdk.annotations.TestClass;
+import org.openscience.cdk.annotations.TestMethod;
+import org.openscience.cdk.hash.seed.AtomEncoder;
+import org.openscience.cdk.hash.seed.BasicAtomEncoder;
+import org.openscience.cdk.hash.seed.ConjugatedAtomEncoder;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.ATOMIC_NUMBER;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.FORMAL_CHARGE;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.FREE_RADICALS;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.MASS_NUMBER;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.ORBITAL_HYBRIDIZATION;
+
+/**
+ * Fluent API for creating hash generators. The maker is first configured with
+ * one or more attributes. Once fully configured the generator is made by
+ * invoking {@link #atomic()}, {@link #molecular()} or {@link #ensemble()}. The
+ * order of the built-in configuration methods does not matter however when
+ * specifying custom encoders with {@link #encode(AtomEncoder)} the order they
+ * are added is the order they will be used. Therefore one can expect different
+ * hash codes if there is a change in the order they are specified.
+ *
+ * <h4>Examples</h4>
+ * <blockquote><pre>
+ * // simple
+ * MoleculeHashGenerator generator = new HashGeneratorMaker().depth(16)
+ * .elemental()
+ * .molecular();
+ *
+ * // fast
+ * MoleculeHashGenerator generator = new HashGeneratorMaker().depth(8)
+ * .elemental()
+ * .isotopic()
+ * .charged()
+ * .orbital()
+ * .molecular();
+ * // comprehensive
+ * MoleculeHashGenerator generator = new HashGeneratorMaker().depth(32)
+ * .elemental()
+ * .isotopic()
+ * .charged()
+ * .chiral()
+ * .perturbed()
+ * .molecular();
+ * </pre></blockquote>
+ *
+ * @author John May
+ * @cdk.module hash
+ */
+@TestClass("org.openscience.cdk.hash.HashGeneratorMakerTest")
+public class HashGeneratorMaker {
+
+ /* no default depth */
+ private int depth = -1;
+
+ /* ordered list of custom encoders */
+ private List<AtomEncoder> customEncoders = new ArrayList<AtomEncoder>();
+
+ /* ordered set of basic encoders */
+ private EnumSet<BasicAtomEncoder> encoderSet = EnumSet
+ .noneOf(BasicAtomEncoder.class);
+
+ /**
+ * Specify the depth of the hash generator. Larger values discriminate more
+ * molecules.
+ *
+ * @param depth how deep should the generator hash
+ * @return reference for fluent API
+ * @throws IllegalArgumentException if the depth was less then zero
+ */
+ @TestMethod("testInvalidDepth,testDepth")
+ public HashGeneratorMaker depth(int depth) {
+ if (depth < 0)
+ throw new IllegalArgumentException("depth must not be less than 0");
+ this.depth = depth;
+ return this;
+ }
+
+ /**
+ * Discriminate elements.
+ *
+ * @return fluent API reference (self)
+ * @see BasicAtomEncoder#ATOMIC_NUMBER
+ */
+ @TestMethod("testElemental")
+ public HashGeneratorMaker elemental() {
+ encoderSet.add(ATOMIC_NUMBER);
+ return this;
+ }
+
+ /**
+ * Discriminate isotopes.
+ *
+ * @return fluent API reference (self)
+ * @see BasicAtomEncoder#MASS_NUMBER
+ */
+ @TestMethod("testIsotopic")
+ public HashGeneratorMaker isotopic() {
+ encoderSet.add(MASS_NUMBER);
+ return this;
+ }
+
+ /**
+ * Discriminate protonation states.
+ *
+ * @return fluent API reference (self)
+ * @see BasicAtomEncoder#FORMAL_CHARGE
+ */
+ @TestMethod("testCharged")
+ public HashGeneratorMaker charged() {
+ encoderSet.add(FORMAL_CHARGE);
+ return this;
+ }
+
+ /**
+ * Discriminate atomic orbitals.
+ *
+ * @return fluent API reference (self)
+ * @see BasicAtomEncoder#ORBITAL_HYBRIDIZATION
+ */
+ @TestMethod("testOrbital")
+ public HashGeneratorMaker orbital() {
+ encoderSet.add(ORBITAL_HYBRIDIZATION);
+ return this;
+ }
+
+ /**
+ * Discriminate free radicals.
+ *
+ * @return fluent API reference (self)
+ * @see BasicAtomEncoder#FREE_RADICALS
+ */
+ @TestMethod("testRadical")
+ public HashGeneratorMaker radical() {
+ encoderSet.add(FREE_RADICALS);
+ return this;
+ }
+
+ /**
+ * Discriminate chiral centers.
+ *
+ * @return fluent API reference (self)
+ * @throws UnsupportedOperationException not yet implemented
+ */
+ @TestMethod("testChiral")
+ public HashGeneratorMaker chiral() {
+ throw new UnsupportedOperationException("not yet supported");
+ }
+
+ /**
+ * Discriminate symmetrical atoms experiencing uniform environments.
+ *
+ * @return fluent API reference (self)
+ * @throws UnsupportedOperationException not yet implemented
+ */
+ @TestMethod("testPerturbed")
+ public HashGeneratorMaker perturbed() {
+ throw new UnsupportedOperationException("not yet supported");
+ }
+
+ /**
+ * Add a custom encoder to the hash generator which will be built. Although
+ * not enforced, the encoder should be stateless. A message to standard
+ * error is printed if the encoder has any fields.
+ *
+ * @param encoder an atom encoder
+ * @return fluent API reference (self)
+ * @throws NullPointerException no encoder provided
+ */
+ @TestMethod("testEncode_Null,testEncode")
+ public HashGeneratorMaker encode(AtomEncoder encoder) {
+ if (encoder == null)
+ throw new NullPointerException("no encoder provided");
+ if (encoder.getClass().getDeclaredFields().length > 0)
+ System.err
+ .println("AtomEncoder had fields but should be stateless");
+ customEncoders.add(encoder);
+ return this;
+ }
+
+ /**
+ * Given the current configuration create an {@link EnsembleHashGenerator}.
+ *
+ * @return instance of the generator
+ * @throws IllegalArgumentException no depth or encoders were configured
+ */
+ @TestMethod("testEnsemble")
+ public EnsembleHashGenerator ensemble() {
+ throw new UnsupportedOperationException("not yet supported");
+ }
+
+
+ /**
+ * Given the current configuration create an {@link MoleculeHashGenerator}.
+ *
+ * @return instance of the generator
+ * @throws IllegalArgumentException no depth or encoders were configured
+ */
+ @TestMethod("testMolecular")
+ public MoleculeHashGenerator molecular() {
+ return new BasicMoleculeHashGenerator(atomic());
+ }
+
+ /**
+ * Given the current configuration create an {@link AtomHashGenerator}.
+ *
+ * @return instance of the generator
+ * @throws IllegalArgumentException no depth or encoders were configured
+ */
+ @TestMethod("testAtomic,testNoDepth")
+ public AtomHashGenerator atomic() {
+
+ if (depth < 0)
+ throw new IllegalArgumentException("no depth specified, use .depth(int)");
+
+ List<AtomEncoder> encoders = new ArrayList<AtomEncoder>();
+
+ // set is ordered
+ for (AtomEncoder encoder : encoderSet) {
+ encoders.add(encoder);
+ }
+ encoders.addAll(this.customEncoders);
+
+ AtomEncoder encoder = new ConjugatedAtomEncoder(encoders);
+
+ return new BasicAtomHashGenerator(new SeedGenerator(encoder),
+ new Xorshift(),
+ depth);
+ }
+
+}
View
237 src/test/org/openscience/cdk/hash/HashGeneratorMakerTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2013. John May <jwmay@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 U
+ */
+
+package org.openscience.cdk.hash;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openscience.cdk.hash.seed.AtomEncoder;
+import org.openscience.cdk.hash.seed.ConjugatedAtomEncoder;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.ATOMIC_NUMBER;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.FORMAL_CHARGE;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.FREE_RADICALS;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.MASS_NUMBER;
+import static org.openscience.cdk.hash.seed.BasicAtomEncoder.ORBITAL_HYBRIDIZATION;
+
+/**
+ * @author John May
+ * @cdk.module test-hash
+ */
+public class HashGeneratorMakerTest {
+
+
+ @Test public void testElemental() {
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(0)
+ .elemental()
+ .atomic();
+ List<AtomEncoder> encoders = getEncoders((BasicAtomHashGenerator) generator);
+ Assert.assertThat(encoders.size(), is(1));
+ Assert.assertThat(encoders.get(0), is((AtomEncoder) ATOMIC_NUMBER));
+ }
+
+ @Test public void testIsotopic() {
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(0)
+ .isotopic()
+ .atomic();
+ List<AtomEncoder> encoders = getEncoders((BasicAtomHashGenerator) generator);
+ Assert.assertThat(encoders.size(), is(1));
+ Assert.assertThat(encoders.get(0), is((AtomEncoder) MASS_NUMBER));
+ }
+
+ @Test public void testCharged() {
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(0)
+ .charged()
+ .atomic();
+ List<AtomEncoder> encoders = getEncoders((BasicAtomHashGenerator) generator);
+ Assert.assertThat(encoders.size(), is(1));
+ Assert.assertThat(encoders.get(0), is((AtomEncoder) FORMAL_CHARGE));
+ }
+
+ @Test public void testRadical() {
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(0)
+ .radical()
+ .atomic();
+ List<AtomEncoder> encoders = getEncoders((BasicAtomHashGenerator) generator);
+ Assert.assertThat(encoders.size(), is(1));
+ Assert.assertThat(encoders.get(0), is((AtomEncoder) FREE_RADICALS));
+ }
+
+ @Test public void testOrbital() {
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(0)
+ .orbital()
+ .atomic();
+ List<AtomEncoder> encoders = getEncoders((BasicAtomHashGenerator) generator);
+ Assert.assertThat(encoders.size(), is(1));
+ Assert.assertThat(encoders.get(0), is((AtomEncoder) ORBITAL_HYBRIDIZATION));
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testChiral() {
+ new HashGeneratorMaker().depth(0)
+ .chiral()
+ .atomic();
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testPerturbed() {
+ new HashGeneratorMaker().depth(0)
+ .perturbed()
+ .atomic();
+ }
+
+ @Test
+ public void testOrdering() {
+ AtomHashGenerator g1 = new HashGeneratorMaker().depth(0)
+ .elemental()
+ .isotopic()
+ .charged()
+ .atomic();
+ AtomHashGenerator g2 = new HashGeneratorMaker().depth(0)
+ .isotopic()
+ .charged()
+ .elemental()
+ .atomic();
+ assertThat(getEncoders(g1).size(), is(3));
+ assertThat(getEncoders(g1), is(getEncoders(g2)));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testEncode_Null(){
+ new HashGeneratorMaker().encode(null);
+ }
+
+ @Test public void testEncode() {
+ AtomEncoder e1 = mock(AtomEncoder.class);
+ AtomEncoder e2 = mock(AtomEncoder.class);
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(0)
+ .encode(e1)
+ .encode(e2)
+ .atomic();
+ List<AtomEncoder> encoders = getEncoders((BasicAtomHashGenerator) generator);
+ assertThat(encoders.size(), is(2));
+ assertThat(encoders.get(0), is(e1));
+ assertThat(encoders.get(1), is(e2));
+
+ generator = new HashGeneratorMaker().depth(0)
+ .encode(e2)
+ .encode(e1)
+ .atomic();
+ encoders = getEncoders((BasicAtomHashGenerator) generator);
+ assertThat(encoders.size(), is(2));
+ assertThat(encoders.get(0), is(e2));
+ assertThat(encoders.get(1), is(e1));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNoDepth() {
+ new HashGeneratorMaker().atomic();
+ }
+
+ @Test public void testAtomic() {
+ assertNotNull(new HashGeneratorMaker().depth(0).elemental().atomic());
+ }
+
+ @Test public void testMolecular() {
+ assertNotNull(new HashGeneratorMaker().depth(0).elemental()
+ .molecular());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testEnsemble() {
+ new HashGeneratorMaker().depth(0).elemental().ensemble();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testMissingEncoders() {
+ new HashGeneratorMaker().depth(0).atomic();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidDepth() {
+ new HashGeneratorMaker().depth(-1);
+ }
+
+ @Test
+ public void testDepth() throws NoSuchFieldException,
+ IllegalAccessException {
+ AtomHashGenerator generator = new HashGeneratorMaker().depth(5)
+ .elemental()
+ .atomic();
+ Field depthField = generator.getClass().getDeclaredField("depth");
+ depthField.setAccessible(true);
+ int value = depthField.getInt(generator);
+ assertThat(value, is(5));
+ }
+
+
+ /**
+ * Extract the AtomEncoders using reflection
+ *
+ * @param generator
+ * @return
+ */
+ public static List<AtomEncoder> getEncoders(AtomHashGenerator generator) {
+ try {
+ Field field = generator.getClass()
+ .getDeclaredField("seedGenerator");
+ field.setAccessible(true);
+ Object o1 = field.get(generator);
+ if (o1 instanceof SeedGenerator) {
+ SeedGenerator seedGenerator = (SeedGenerator) o1;
+ Field f2 = seedGenerator.getClass().getDeclaredField("encoder");
+ f2.setAccessible(true);
+ Object o2 = f2.get(seedGenerator);
+ return getEncoders((ConjugatedAtomEncoder) o2);
+ }
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ return Collections.emptyList();
+ }
+
+ public static List<AtomEncoder> getEncoders(ConjugatedAtomEncoder conjugated) {
+ try {
+ Field field = conjugated.getClass().getDeclaredField("encoders");
+ field.setAccessible(true);
+ return (List<AtomEncoder>) field.get(conjugated);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ return Collections.emptyList();
+ }
+
+}
View
4 src/test/org/openscience/cdk/modulesuites/MhashTests.java
@@ -26,6 +26,7 @@
import org.openscience.cdk.hash.BasicAtomHashGeneratorTest;
import org.openscience.cdk.hash.BasicMoleculeHashGenerator;
import org.openscience.cdk.hash.BasicMoleculeHashGeneratorTest;
+import org.openscience.cdk.hash.HashGeneratorMakerTest;
import org.openscience.cdk.hash.XorshiftTest;
import org.openscience.cdk.hash.seed.BasicAtomEncoderTest;
import org.openscience.cdk.hash.seed.ConjugatedAtomEncoderTest;
@@ -43,7 +44,8 @@
ConjugatedAtomEncoderTest.class,
AbstractHashGeneratorTest.class,
BasicAtomHashGeneratorTest.class,
- BasicMoleculeHashGeneratorTest.class
+ BasicMoleculeHashGeneratorTest.class,
+ HashGeneratorMakerTest.class
})
public class MhashTests {
}
@egonw

Please add the missing copyright header.

Please sign in to comment.
Something went wrong with that request. Please try again.