diff options
author | Yohann Roussel <yroussel@google.com> | 2014-03-19 16:25:37 +0100 |
---|---|---|
committer | Yohann Roussel <yroussel@google.com> | 2014-03-20 15:13:33 +0100 |
commit | 4eceb95409e844fdc33c9c706e1dc307bfd40303 (patch) | |
tree | ee9f4f3fc79f757c79081c336bce4f1782c6ccd8 /watchmaker/examples | |
parent | 3d2402901b1a6462e2cf47a6fd09711f327961c3 (diff) | |
download | toolchain_jack-4eceb95409e844fdc33c9c706e1dc307bfd40303.zip toolchain_jack-4eceb95409e844fdc33c9c706e1dc307bfd40303.tar.gz toolchain_jack-4eceb95409e844fdc33c9c706e1dc307bfd40303.tar.bz2 |
Initial Jack import.
Change-Id: I953cf0a520195a7187d791b2885848ad0d5a9b43
Diffstat (limited to 'watchmaker/examples')
127 files changed, 12083 insertions, 0 deletions
diff --git a/watchmaker/examples/examples.iml b/watchmaker/examples/examples.iml new file mode 100644 index 0000000..6f94484 --- /dev/null +++ b/watchmaker/examples/examples.iml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_6" inherit-compiler-output="false"> + <output url="file://$MODULE_DIR$/target/classes" /> + <output-test url="file://$MODULE_DIR$/target/test-classes" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src/java/main" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/java/resources" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/java/test" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="swing" /> + <orderEntry type="module" module-name="framework" /> + <orderEntry type="library" name="Maven: org.uncommons.maths:uncommons-maths:1.2.2" level="project" /> + <orderEntry type="library" name="Maven: jfree:jcommon:1.0.12" level="project" /> + <orderEntry type="library" name="Maven: jfree:jfreechart:1.0.13" level="project" /> + <orderEntry type="library" name="Maven: com.google.collections:google-collections:1.0" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.testng:testng:6.2.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: junit:junit:3.8.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.beanshell:bsh:2.0b4" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: com.beust:jcommander:1.12" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.yaml:snakeyaml:1.6" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-swing:1.2.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-assert:1.2" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-util:1.1.3" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-reflect:1.2" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: net.jcip:jcip-annotations:1.0" level="project" /> + </component> +</module> + diff --git a/watchmaker/examples/nb-configuration.xml b/watchmaker/examples/nb-configuration.xml new file mode 100644 index 0000000..ae35717 --- /dev/null +++ b/watchmaker/examples/nb-configuration.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<project-shared-configuration>
+ <!-- +This file contains additional configuration written by modules in the NetBeans IDE. +The configuration is intended to be shared among all the users of project and +therefore it is assumed to be part of version control checkout. +Without this configuration present, some functionality in the IDE may be limited or fail altogether. +-->
+ <properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
+ <!-- +Properties that influence various parts of the IDE, especially code formatting and the like. +You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up. +That way multiple projects can share the same settings (useful for formatting rules for example). +Any value defined here will override the pom.xml file value but is only applicable to the current project. +-->
+ <org-netbeans-modules-editor-indent.CodeStyle.usedProfile>project</org-netbeans-modules-editor-indent.CodeStyle.usedProfile>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab>2</org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.tab-size>2</org-netbeans-modules-editor-indent.CodeStyle.project.tab-size>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width>2</org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs>false</org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width>100</org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap>words</org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.blankLinesAfterClassHeader>0</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.blankLinesAfterClassHeader>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantIfBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantIfBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantForBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantForBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceBeforeColon>false</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceBeforeColon>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTryResources>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTryResources>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTernaryOps>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTernaryOps>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaces-per-tab>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaces-per-tab>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssignOps>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssignOps>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodCallArgs>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodCallArgs>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapChainedMethodCalls>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapChainedMethodCalls>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapArrayInit>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapArrayInit>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapBinaryOps>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapBinaryOps>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.indent-shift-width>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.indent-shift-width>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeWhileOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeWhileOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.text-limit-width>100</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.text-limit-width>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.generateParagraphTagOnBlankLines>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.generateParagraphTagOnBlankLines>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantWhileBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantWhileBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.tab-size>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.tab-size>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.expand-tabs>false</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.expand-tabs>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantDoWhileBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantDoWhileBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList>
+ </properties>
+</project-shared-configuration>
diff --git a/watchmaker/examples/pom.xml b/watchmaker/examples/pom.xml new file mode 100644 index 0000000..773b7d2 --- /dev/null +++ b/watchmaker/examples/pom.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!--=========================================================================== + Copyright 2006-2010 Daniel W. Dyer + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ==========================================================================--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.uncommons.watchmaker</groupId> + <artifactId>watchmaker</artifactId> + <version>0.7.2</version> + </parent> + <artifactId>watchmaker-examples</artifactId> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>watchmaker-swing</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <version>6.2.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.easytesting</groupId> + <artifactId>fest-swing</artifactId> + <version>1.2.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <sourceDirectory>src/java/main</sourceDirectory> + <testSourceDirectory>src/java/test</testSourceDirectory> + <resources> + <resource> + <directory>src/java/resources</directory> + </resource> + </resources> + </build> +</project> diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/AbstractExampleApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/AbstractExampleApplet.java new file mode 100644 index 0000000..8bf600a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/AbstractExampleApplet.java @@ -0,0 +1,102 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.awt.Container; +import java.lang.reflect.InvocationTargetException; +import javax.swing.JApplet; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +/** + * Base class for examples that run as applets. + * @author Daniel Dyer + */ +public abstract class AbstractExampleApplet extends JApplet +{ + /** + * {@inheritDoc} + */ + @Override + public void init() + { + configure(this); + } + + + /** + * Configure the program to display its GUI in the specified container. + * @param container The container to place the GUI components in. + */ + private void configure(final Container container) + { + try + { + // Use invokeAndWait so that we can be sure that initialisation is complete + // before continuing. + SwingUtilities.invokeAndWait(new Runnable() + { + public void run() + { + try + { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception ex) + { + // This should never happen as we are installing a known look-and-feel. + System.err.println("Failed to load System look-and-feel."); + } + prepareGUI(container); + } + }); + } + catch (InterruptedException ex) + { + // Restore interrupt flag. + Thread.currentThread().interrupt(); + } + catch (InvocationTargetException ex) + { + ex.getCause().printStackTrace(); + JOptionPane.showMessageDialog(container, ex.getCause(), "Error Occurred", JOptionPane.ERROR_MESSAGE); + } + } + + + /** + * Implemented in sub-classes to initialise and layout the GUI. + * @param container The container that this method should add components to. + */ + protected abstract void prepareGUI(Container container); + + + /** + * Display this example program using a JFrame as the top-level GUI container (rather + * than running the example as an applet). + * @param title The text to use for the frame's title bar. + */ + protected void displayInFrame(String title) + { + JFrame frame = new JFrame(title); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + configure(frame); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/EvolutionLogger.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/EvolutionLogger.java new file mode 100644 index 0000000..5c47146 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/EvolutionLogger.java @@ -0,0 +1,39 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * Trivial evolution observer for displaying information at the end + * of each generation. + * @param <T> The type of entity being evolved. + * @author Daniel Dyer + */ +public class EvolutionLogger<T> implements IslandEvolutionObserver<T> +{ + public void populationUpdate(PopulationData<? extends T> data) + { + System.out.println("Generation " + data.getGenerationNumber() + ": " + data.getBestCandidateFitness()); + } + + + public void islandPopulationUpdate(int islandIndex, PopulationData<? extends T> populationData) + { + // Do nothing. + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/Launcher.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/Launcher.java new file mode 100644 index 0000000..b4b5b7e --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/Launcher.java @@ -0,0 +1,79 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.uncommons.util.reflection.ReflectionUtils; +import org.uncommons.watchmaker.examples.biomorphs.BiomorphApplet; +import org.uncommons.watchmaker.examples.bits.BitsExample; +import org.uncommons.watchmaker.examples.geneticprogramming.GeneticProgrammingExample; +import org.uncommons.watchmaker.examples.monalisa.MonaLisaApplet; +import org.uncommons.watchmaker.examples.strings.StringsExample; +import org.uncommons.watchmaker.examples.sudoku.SudokuApplet; +import org.uncommons.watchmaker.examples.travellingsalesman.TravellingSalesmanApplet; + +/** + * Launcher for Watchmaker example applications. + * @author Daniel Dyer + */ +public class Launcher +{ + private static final Map<String, Class<?>> EXAMPLES = new LinkedHashMap<String, Class<?>>(); + static + { + EXAMPLES.put("biomorphs", BiomorphApplet.class); + EXAMPLES.put("bits", BitsExample.class); + EXAMPLES.put("gp", GeneticProgrammingExample.class); + EXAMPLES.put("monalisa", MonaLisaApplet.class); + EXAMPLES.put("salesman", TravellingSalesmanApplet.class); + EXAMPLES.put("strings", StringsExample.class); + EXAMPLES.put("sudoku", SudokuApplet.class); + } + + + private Launcher() + { + // Prevents instantiation of launcher class. + } + + + /** + * Launch the specified example application from the command-line. + * @param args First item is the name of the example to run. Any subsequent arguments are passed + * on to the specific example. + */ + public static void main(String[] args) + { + Class<?> exampleClass = args.length > 0 ? EXAMPLES.get(args[0]) : null; + if (exampleClass == null) + { + System.err.println("First argument must be the name of an example, i.e. one of " + + Arrays.toString(EXAMPLES.keySet().toArray())); + System.exit(1); + } + + // All args except the first one should be passed to the example application. + String[] appArgs = new String[args.length - 1]; + System.arraycopy(args, 1, appArgs, 0, appArgs.length); + + // Invoke the main method for the selected example application. + Method main = ReflectionUtils.findKnownMethod(exampleClass, "main", String[].class); + ReflectionUtils.invokeUnchecked(main, exampleClass, new Object[]{appArgs}); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/Biomorph.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/Biomorph.java new file mode 100644 index 0000000..7fe6fd5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/Biomorph.java @@ -0,0 +1,160 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.Arrays; + +/** + * <p>Candidate representation for a biomorph. We could just as easily have + * used an array of integers or a bit string representation with Gray codes + * but for clarity we will use this more object-oriented representation that + * conveniently combines state and related logic.</p> + * + * <p>The Biomporph class encapsulates 9 genes as described by Dawkins in his + * "The Evolution of Evolvability" paper.</p> + * + * @author Daniel Dyer + */ +public final class Biomorph +{ + /** The total number of genes that make up a biomorph. */ + public static final int GENE_COUNT = 9; + /** The minimum permitted value for most genes. */ + public static final int GENE_MIN = -5; + /** The maximum permitted value for most genes. */ + public static final int GENE_MAX = 5; + /** The index of the gene that controls biomporph size. */ + public static final int LENGTH_GENE_INDEX = 8; + /** The minimum permitted value for the length gene. */ + public static final int LENGTH_GENE_MIN = 1; + /** The maximum permitted value for the length gene. */ + public static final int LENGTH_GENE_MAX = 7; + + private final int[] genes; + + private int[][] phenotype; + + + /** + * Creates a biomorph with the specified genes. + * @param genes A 9-element array. The final element must be a positive + * value. + */ + public Biomorph(int[] genes) + { + if (genes.length != GENE_COUNT) + { + throw new IllegalArgumentException("Biomorph must have " + GENE_COUNT + " genes."); + } + this.genes = genes.clone(); + } + + + /** + * Returns an array of integers that represent the graphical pattern + * determined by the biomorph's genes. + * @return A 2-dimensional array containing the 8-element dx and dy + * arrays required to draw the biomorph. + */ + public int[][] getPatternPhenotype() + { + if (phenotype == null) + { + // Decode the genes as per Dawkins' rules. + int[] dx = new int[GENE_COUNT - 1]; + dx[3] = genes[0]; + dx[4] = genes[1]; + dx[5] = genes[2]; + + dx[1] = -dx[3]; + dx[0] = -dx[4]; + dx[7] = -dx[5]; + + dx[2] = 0; + dx[6] = 0; + + int[] dy = new int[GENE_COUNT - 1]; + dy[2] = genes[3]; + dy[3] = genes[4]; + dy[4] = genes[5]; + dy[5] = genes[6]; + dy[6] = genes[7]; + + dy[0] = dy[4]; + dy[1] = dy[3]; + dy[7] = dy[5]; + + phenotype = new int[][]{dx, dy}; + } + return phenotype; + } + + + /** + * @return The value of the gene that controls the size of this biomorph. + */ + public int getLengthPhenotype() + { + return genes[LENGTH_GENE_INDEX]; + } + + + /** + * Returns the 9 genes that make up the biomorph's genotype. + * @return A 9-element array containing this biomorph's genes. + */ + public int[] getGenotype() + { + return genes.clone(); + } + + + /** + * Compares the genes of two Biomporph objects and returns true if they are + * identical. + * @param obj The object to compare with this one. + * @return True if the argument is a Biomoprh instance and the 2 + * biomorphs have the same genes, false otherwise. + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass()) + { + return false; + } + + Biomorph biomorph = (Biomorph) obj; + + return Arrays.equals(genes, biomorph.genes); + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. Biomorphs + * with identical genes return identical hash codes. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + return Arrays.hashCode(genes); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphApplet.java new file mode 100644 index 0000000..0766422 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphApplet.java @@ -0,0 +1,241 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SpringLayout; +import javax.swing.SwingUtilities; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.interactive.InteractiveSelection; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.framework.termination.GenerationCount; +import org.uncommons.watchmaker.swing.SwingConsole; + +/** + * Watchmaker Framework implementation of Dawkin's biomorph program. + * @author Daniel Dyer + */ +public class BiomorphApplet extends AbstractExampleApplet +{ + private Renderer<Biomorph, JComponent> renderer; + private SwingConsole console; + private JDialog selectionDialog; + private JPanel biomorphHolder; + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + renderer = new SwingBiomorphRenderer(); + console = new SwingConsole(5); + selectionDialog = new JDialog((JFrame) null, "Biomorph Selection", true); + biomorphHolder = new JPanel(new GridLayout(1, 1)); + + container.add(new ControlPanel(), BorderLayout.WEST); + container.add(biomorphHolder, BorderLayout.CENTER); + biomorphHolder.setBorder(BorderFactory.createTitledBorder("Last Evolved Biomorph")); + biomorphHolder.add(new JLabel("Nothing generated yet.", JLabel.CENTER)); + selectionDialog.add(console, BorderLayout.CENTER); + selectionDialog.setSize(800, 600); + selectionDialog.validate(); + } + + + /** + * Helper method to create a background task for running the interactive evolutionary + * algorithm. + * @param populationSize How big the population used by the created evolution engine + * should be. + * @param generationCount How many generations to use when the evolution engine is + * invoked. + * @param random If true use random mutation, otherwise use Dawkins mutation. + * @return A Swing task that will execute on a background thread and update + * the GUI when it is done. + */ + private SwingBackgroundTask<Biomorph> createTask(final int populationSize, + final int generationCount, + final boolean random) + { + return new SwingBackgroundTask<Biomorph>() + { + @Override + protected Biomorph performTask() + { + EvolutionaryOperator<Biomorph> mutation = random ? new RandomBiomorphMutation(new Probability(0.12d)) + : new DawkinsBiomorphMutation(); + InteractiveSelection<Biomorph> selection = new InteractiveSelection<Biomorph>(console, + renderer, + populationSize, + 1); + EvolutionEngine<Biomorph> engine = new GenerationalEvolutionEngine<Biomorph>(new BiomorphFactory(), + mutation, + selection, + new MersenneTwisterRNG()); + engine.addEvolutionObserver(new GenerationTracker()); + return engine.evolve(populationSize, + 0, + new GenerationCount(generationCount)); + } + + @Override + protected void postProcessing(Biomorph result) + { + selectionDialog.setVisible(false); + biomorphHolder.removeAll(); + biomorphHolder.add(renderer.render(result)); + biomorphHolder.revalidate(); + } + }; + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + */ + public static void main(String[] args) + { + new BiomorphApplet().displayInFrame("Watchmaker Framework - Biomporphs Example"); + } + + + /** + * Simple observer to update the dialog title every time the evolution advances + * to a new generation. + */ + private final class GenerationTracker implements EvolutionObserver<Biomorph> + { + public void populationUpdate(final PopulationData<? extends Biomorph> populationData) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + selectionDialog.setTitle("Biomorph Selection - Generation " + + (populationData.getGenerationNumber() + 1)); + } + }); + } + } + + + /** + * Panel for controlling the evolutionary algorithm parameters. + */ + private final class ControlPanel extends JPanel + { + private JSpinner populationSpinner; + private JSpinner generationsSpinner; + private JComboBox mutationCombo; + + ControlPanel() + { + super(new BorderLayout()); + add(createInputPanel(), BorderLayout.NORTH); + add(createButtonPanel(), BorderLayout.SOUTH); + setBorder(BorderFactory.createTitledBorder("Evolution Controls")); + } + + + private JComponent createInputPanel() + { + JPanel inputPanel = new JPanel(new SpringLayout()); + JLabel populationLabel = new JLabel("Population Size: "); + populationSpinner = new JSpinner(new SpinnerNumberModel(18, 2, 25, 1)); + populationSpinner.setEnabled(false); + populationLabel.setLabelFor(populationSpinner); + inputPanel.add(populationLabel); + inputPanel.add(populationSpinner); + JLabel generationsLabel = new JLabel("Number of Generations: "); + generationsSpinner = new JSpinner(new SpinnerNumberModel(20, 1, 100, 1)); + generationsLabel.setLabelFor(generationsSpinner); + inputPanel.add(generationsLabel); + inputPanel.add(generationsSpinner); + JLabel mutationLabel = new JLabel("Mutation Type: "); + mutationCombo = new JComboBox(new String[]{"Dawkins (Non-random)", "Random"}); + mutationCombo.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + if (mutationCombo.getSelectedIndex() == 0) + { + populationSpinner.setValue(18); + populationSpinner.setEnabled(false); + } + else + { + populationSpinner.setEnabled(true); + } + } + }); + inputPanel.add(mutationLabel); + inputPanel.add(mutationCombo); + + SpringUtilities.makeCompactGrid(inputPanel, 3, 2, 30, 6, 6, 6); + + return inputPanel; + } + + + private JComponent createButtonPanel() + { + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton startButton = new JButton("Start"); + startButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent actionEvent) + { + createTask((Integer) populationSpinner.getValue(), + (Integer) generationsSpinner.getValue(), + mutationCombo.getSelectedIndex() == 1).execute(); + selectionDialog.setVisible(true); + } + }); + buttonPanel.add(startButton); + return buttonPanel; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactory.java new file mode 100644 index 0000000..d081e40 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactory.java @@ -0,0 +1,44 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.Random; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Candidate factory for creating random biomorphs. + * @author Daniel Dyer + */ +public class BiomorphFactory extends AbstractCandidateFactory<Biomorph> +{ + /** + * Generates a random biomorph by providing a random value for each gene. + * @param rng The source of randomness used to generate the biomoprh. + * @return A randomly-generated biomorph. + */ + public Biomorph generateRandomCandidate(Random rng) + { + int[] genes = new int[Biomorph.GENE_COUNT]; + for (int i = 0; i < Biomorph.GENE_COUNT - 1; i++) + { + // First 8 genes have values between -5 and 5. + genes[i] = rng.nextInt(11) - 5; + } + // Last genes ha a value between 1 and 7. + genes[Biomorph.LENGTH_GENE_INDEX] = rng.nextInt(Biomorph.LENGTH_GENE_MAX) + 1; + return new Biomorph(genes); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutation.java new file mode 100644 index 0000000..f718a76 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutation.java @@ -0,0 +1,67 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Non-random mutation of a population of biomorphs. This ensures that each selected candidate + * is mutated differently. This is the mutation used by Dawkins in his original experiment. + * @author Daniel Dyer + */ +public class DawkinsBiomorphMutation implements EvolutionaryOperator<Biomorph> +{ + /** + * Mutate a population of biomorphs non-randomly, ensuring that each selected + * candidate is mutated differently. + * @param selectedCandidates {@inheritDoc} + * @param rng A source of randomness (not used since this mutation is non-random). + * @return {@inheritDoc} + */ + public List<Biomorph> apply(List<Biomorph> selectedCandidates, Random rng) + { + List<Biomorph> mutatedPopulation = new ArrayList<Biomorph>(selectedCandidates.size()); + int mutatedGene = 0; + int mutation = 1; + for (Biomorph b : selectedCandidates) + { + int[] genes = b.getGenotype(); + + mutation *= -1; // Alternate between incrementing and decrementing. + if (mutation == 1) // After gene has been both incremented and decremented, move to next one. + { + mutatedGene = (mutatedGene + 1) % Biomorph.GENE_COUNT; + } + genes[mutatedGene] += mutation; + int min = mutatedGene == Biomorph.LENGTH_GENE_INDEX ? Biomorph.LENGTH_GENE_MIN : Biomorph.GENE_MIN; + int max = mutatedGene == Biomorph.LENGTH_GENE_INDEX ? Biomorph.LENGTH_GENE_MAX : Biomorph.GENE_MAX; + if (genes[mutatedGene] > max) + { + genes[mutatedGene] = min; + } + else if (genes[mutatedGene] < min) + { + genes[mutatedGene] = max; + } + + mutatedPopulation.add(new Biomorph(genes)); + } + return mutatedPopulation; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutation.java new file mode 100644 index 0000000..a98cb55 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutation.java @@ -0,0 +1,98 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutation operator for biomorphs. Mutates each individual gene + * according to some mutation probability. + * @author Daniel Dyer + */ +public class RandomBiomorphMutation implements EvolutionaryOperator<Biomorph> +{ + private final Probability mutationProbability; + + /** + * @param mutationProbability The probability that a given gene + * is changed. + */ + public RandomBiomorphMutation(Probability mutationProbability) + { + this.mutationProbability = mutationProbability; + } + + + /** + * Randomly mutate each selected candidate. + * @param selectedCandidates {@inheritDoc} + * @param rng {@inheritDoc} + * @return {@inheritDoc} + */ + public List<Biomorph> apply(List<Biomorph> selectedCandidates, Random rng) + { + List<Biomorph> mutatedPopulation = new ArrayList<Biomorph>(selectedCandidates.size()); + for (Biomorph biomorph : selectedCandidates) + { + mutatedPopulation.add(mutateBiomorph(biomorph, rng)); + } + return mutatedPopulation; + } + + + /** + * Mutates a single biomorph. + * @param biomorph The biomorph to mutate. + * @param rng The source of randomness to use for mutation. + * @return A mutated version of the biomorph. + */ + private Biomorph mutateBiomorph(Biomorph biomorph, Random rng) + { + int[] genes = biomorph.getGenotype(); + assert genes.length == Biomorph.GENE_COUNT : "Biomorphs must have " + Biomorph.GENE_COUNT + " genes."; + for (int i = 0; i < Biomorph.GENE_COUNT - 1; i++) + { + if (mutationProbability.nextEvent(rng)) + { + boolean increase = rng.nextBoolean(); + genes[i] += (increase ? 1 : -1); + if (genes[i] > Biomorph.GENE_MAX) + { + genes[i] = Biomorph.GENE_MIN; + } + else if (genes[i] < Biomorph.GENE_MIN) + { + genes[i] = Biomorph.GENE_MAX; + } + } + } + boolean increase = rng.nextBoolean(); + genes[Biomorph.LENGTH_GENE_INDEX] += (increase ? 1 : -1); + if (genes[Biomorph.LENGTH_GENE_INDEX] > Biomorph.LENGTH_GENE_MAX) + { + genes[Biomorph.LENGTH_GENE_INDEX] = Biomorph.LENGTH_GENE_MIN; + } + else if (genes[Biomorph.LENGTH_GENE_INDEX] < Biomorph.LENGTH_GENE_MIN) + { + genes[Biomorph.LENGTH_GENE_INDEX] = Biomorph.LENGTH_GENE_MAX; + } + return new Biomorph(genes); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/SwingBiomorphRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/SwingBiomorphRenderer.java new file mode 100644 index 0000000..d6d8e82 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/SwingBiomorphRenderer.java @@ -0,0 +1,111 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import javax.swing.JComponent; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Renders Biomorphs as Swing components. + * @author Daniel Dyer + */ +public class SwingBiomorphRenderer implements Renderer<Biomorph, JComponent> +{ + /** + * Renders an evolved biomorph as a component that can be displayed + * in a Swing GUI. + * @param biomorph The biomorph to render. + * @return A component that displays a visual representation of the + * biomorph. + */ + public JComponent render(Biomorph biomorph) + { + return new BiomorphView(biomorph); + } + + + /** + * A Swing component that can display a visual representation of a + * biomorph. + */ + private static final class BiomorphView extends JComponent + { + private final Biomorph biomorph; + + BiomorphView(Biomorph biomorph) + { + this.biomorph = biomorph; + Dimension size = new Dimension(200, 200); + setMinimumSize(size); + setPreferredSize(size); + } + + + @Override + protected void paintComponent(Graphics graphics) + { + super.paintComponent(graphics); + if (graphics instanceof Graphics2D) + { + ((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + + int[][] pattern = biomorph.getPatternPhenotype(); + int depth = biomorph.getLengthPhenotype(); + + drawTree(graphics, + getSize().width / 2, + getSize().height / 2, + depth, + 2, // Initial direction should be 2 or 6 to ensure horizontal symmetry. + pattern[0], // dx + pattern[1]); // dy + } + + + /** + * Recursive method for drawing tree branches. + */ + private void drawTree(Graphics graphics, + int x, + int y, + int length, + int direction, + int[] dx, + int[] dy) + { + // Make sure direction wraps round in the range 0 - 7. + direction = (direction + 8) % 8; + + int x2 = x + length * dx[direction]; + int y2 = y + length * dy[direction]; + + graphics.drawLine(x, y, x2, y2); + + if (length > 0) + { + // Recursively draw the left and right branches of the tree. + drawTree(graphics, x2, y2, length - 1, direction - 1, dx, dy); + drawTree(graphics, x2, y2, length - 1, direction + 1, dx, dy); + } + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/package-info.java new file mode 100644 index 0000000..1f73010 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/package-info.java @@ -0,0 +1,21 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary simulation along the lines of the Biomporph program described + * by Richard Dawkins in his book, The Blind Watchmaker. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.biomorphs; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitStringEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitStringEvaluator.java new file mode 100644 index 0000000..3a6421c --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitStringEvaluator.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.bits; + +import java.util.List; +import org.uncommons.maths.binary.BitString; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * A fitness evaluator that simply counts the number of ones in a bit + * string. + * @see BitString + * @author Daniel Dyer + */ +public class BitStringEvaluator implements FitnessEvaluator<BitString> +{ + /** + * Calculates a fitness score for the candidate bit string. + * @param candidate The evolved bit string to evaluate. + * @param population {@inheritDoc} + * @return How many bits in the string are set to 1. + */ + public double getFitness(BitString candidate, + List<? extends BitString> population) + { + return candidate.countSetBits(); + } + + + /** + * Always returns true. A higher score indicates a fitter individual. + * @return True. + */ + public boolean isNatural() + { + return true; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitsExample.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitsExample.java new file mode 100644 index 0000000..4b6a1c8 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitsExample.java @@ -0,0 +1,67 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.bits; + +import java.util.ArrayList; +import java.util.List; +import org.uncommons.maths.binary.BitString; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.EvolutionLogger; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.factories.BitStringFactory; +import org.uncommons.watchmaker.framework.operators.BitStringCrossover; +import org.uncommons.watchmaker.framework.operators.BitStringMutation; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; + +/** + * An implementation of the first exercise (page 31) from the book An Introduction to + * Genetic Algorithms, by Melanie Mitchell. The algorithm evolves bit strings and the + * fitness function simply counts the number of ones in the bit string. The evolution + * should therefore converge on strings that consist only of ones. + * @author Daniel Dyer + */ +public class BitsExample +{ + private static final int BITS = 20; + + public static void main(String[] args) + { + evolveBits(BITS); + } + + + public static BitString evolveBits(int length) + { + List<EvolutionaryOperator<BitString>> operators = new ArrayList<EvolutionaryOperator<BitString>>(2); + operators.add(new BitStringCrossover(1, new Probability(0.7d))); + operators.add(new BitStringMutation(new Probability(0.01d))); + EvolutionaryOperator<BitString> pipeline = new EvolutionPipeline<BitString>(operators); + GenerationalEvolutionEngine<BitString> engine = new GenerationalEvolutionEngine<BitString>(new BitStringFactory(length), + pipeline, + new BitStringEvaluator(), + new RouletteWheelSelection(), + new MersenneTwisterRNG()); + engine.setSingleThreaded(true); // Performs better for very trivial fitness evaluations. + engine.addEvolutionObserver(new EvolutionLogger<BitString>()); + return engine.evolve(100, // 100 individuals in each generation. + 0, // Don't use elitism. + new TargetFitness(length, true)); // Continue until a perfect match is found. + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/package-info.java new file mode 100644 index 0000000..058458b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/package-info.java @@ -0,0 +1,23 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An implementation of the first exercise from the book An Introduction to Genetic + * Algorithms, by Melanie Mitchell. The algorithm evolves bit strings and the + * fitness function simply counts the number of ones in the bit string. The evolution + * should therefore converge on strings that consist only of ones. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.bits; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Addition.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Addition.java new file mode 100644 index 0000000..641510c --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Addition.java @@ -0,0 +1,79 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * Simple addition operator {@link Node}. + * @author Daniel Dyer + */ +public class Addition extends BinaryNode +{ + /** + * Creates a node that evaluates to the sum of the values of its two + * child nodes ({@literal left} and {@literal right}). + * @param left The first operand. + * @param right The second operand. + */ + public Addition(Node left, Node right) + { + super(left, right, '+'); + } + + + /** + * Evaluates the two sub-trees and returns the sum of these two values. + * @param programParameters Program parameters (ignored by the addition operator + * but may be used in evaluating the sub-trees). + * @return The sum of the values of both child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) + right.evaluate(programParameters); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // Adding zero is pointless, the expression can be reduced to its other argument. + if (simplifiedRight instanceof Constant && simplifiedRight.evaluate(NO_ARGS) == 0) + { + return simplifiedLeft; + } + else if (simplifiedLeft instanceof Constant && simplifiedLeft.evaluate(NO_ARGS) == 0) + { + return simplifiedRight; + } + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + else if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) + simplifiedRight.evaluate(NO_ARGS)); + } + else if (simplifiedLeft != left || simplifiedRight != right) + { + return new Addition(simplifiedLeft, simplifiedRight); + } + else + { + return this; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/BinaryNode.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/BinaryNode.java new file mode 100644 index 0000000..2273f74 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/BinaryNode.java @@ -0,0 +1,214 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.lang.reflect.Constructor; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.util.reflection.ReflectionUtils; + +/** + * Convenient base class for {@link Node}s that have two sub-trees. + * @author Daniel Dyer + */ +abstract class BinaryNode implements Node +{ + protected static final double[] NO_ARGS = new double[0]; + + /** The first argument to the binary function. */ + protected final Node left; + /** The second argument to the binary function. */ + protected final Node right; + + private final char symbol; + + + /** + * @param left The first argument to the binary function. + * @param right The second argument to the binary function. + * @param symbol A single character that indicates the type of function. + */ + protected BinaryNode(Node left, Node right, char symbol) + { + this.left = left; + this.right = right; + this.symbol = symbol; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return String.valueOf(symbol); + } + + + /** + * The arity of a binary node is two. + * @return 2 + */ + public int getArity() + { + return 2; + } + + + /** + * The depth of a binary node is the depth of its deepest sub-tree plus one. + * @return The depth of the tree rooted at this node. + */ + public int getDepth() + { + return 1 + Math.max(left.getDepth(), right.getDepth()); + } + + + /** + * The width of a binary node is the sum of the widths of its two sub-trees. + * @return The width of the tree rooted at this node. + */ + public int getWidth() + { + return left.getWidth() + right.getWidth(); + } + + + /** + * {@inheritDoc} + */ + public int countNodes() + { + return 1 + left.countNodes() + right.countNodes(); + } + + + /** + * {@inheritDoc} + */ + public Node getNode(int index) + { + if (index == 0) + { + return this; + } + int leftNodes = left.countNodes(); + if (index <= leftNodes) + { + return left.getNode(index - 1); + } + else + { + return right.getNode(index - leftNodes - 1); + } + } + + + /** + * {@inheritDoc} + */ + public Node getChild(int index) + { + switch (index) + { + case 0: return left; + case 1: return right; + default: throw new IndexOutOfBoundsException("Invalid child index: " + index); + } + } + + + /** + * {@inheritDoc} + */ + public Node replaceNode(int index, Node newNode) + { + if (index == 0) + { + return newNode; + } + + int leftNodes = left.countNodes(); + if (index <= leftNodes) + { + return newInstance(left.replaceNode(index - 1, newNode), right); + } + else + { + return newInstance(left, right.replaceNode(index - leftNodes - 1, newNode)); + } + } + + + + /** + * {@inheritDoc} + */ + public String print() + { + StringBuilder buffer = new StringBuilder("("); + buffer.append(left.print()); + buffer.append(' '); + buffer.append(symbol); + buffer.append(' '); + buffer.append(right.print()); + buffer.append(')'); + return buffer.toString(); + } + + + /** + * {@inheritDoc} + */ + public Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory) + { + if (mutationProbability.nextEvent(rng)) + { + return treeFactory.generateRandomCandidate(rng); + } + else + { + Node newLeft = left.mutate(rng, mutationProbability, treeFactory); + Node newRight = right.mutate(rng, mutationProbability, treeFactory); + if (newLeft != left && newRight != right) + { + return newInstance(newLeft, newRight); + } + else + { + // Tree has not changed. + return this; + } + } + } + + + private Node newInstance(Node newLeft, Node newRight) + { + Constructor<? extends BinaryNode> constructor = ReflectionUtils.findKnownConstructor(this.getClass(), + Node.class, + Node.class); + return ReflectionUtils.invokeUnchecked(constructor, newLeft, newRight); + } + + + @Override + public String toString() + { + return print(); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Constant.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Constant.java new file mode 100644 index 0000000..2a07538 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Constant.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.text.DecimalFormat; + +/** + * A program node that evaluates to a constant value. + * @author Daniel Dyer + */ +public class Constant extends LeafNode +{ + private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("######0.##"); + + private final double constant; + private final String label; + + + /** + * Creates a constant-valued node. + * @param constant The value that this node will always evaluate to. + */ + public Constant(double constant) + { + this.constant = constant; + this.label = NUMBER_FORMAT.format(constant); + } + + + /** + * @param programParameters The parameters passed to the program (ignored by this node). + * @return The numeric value of this constant. + */ + public double evaluate(double[] programParameters) + { + return constant; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return label; + } + + + /** + * @return The String representation of this constant. + */ + public String print() + { + return String.valueOf(constant); + } + + + /** + * Two constants are equal if they have the same numeric value. + * @param other The object that this object is compared to. + * @return True if the constants are equivalent, false otherwise. + */ + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (other == null || getClass() != other.getClass()) + { + return false; + } + return Double.compare(((Constant) other).constant, constant) == 0; + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + long temp = constant != +0.0d ? Double.doubleToLongBits(constant) : 0L; + return (int) (temp ^ (temp >>> 32)); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExample.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExample.java new file mode 100644 index 0000000..c060d49 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExample.java @@ -0,0 +1,85 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.EvolutionLogger; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; + +/** + * Simple tree-based genetic programming application based on the first example + * in Chapter 11 of Toby Segaran's Programming Collective Intelligence. + * @author Daniel Dyer + */ +public class GeneticProgrammingExample +{ + // This data describes the problem. For each pair of inputs, the generated program + // should return the associated output. The goal of this appliction is to generalise + // the examples into an equation. + private static final Map<double[], Double> TEST_DATA = new HashMap<double[], Double>(); + static + { + TEST_DATA.put(new double[]{26, 35}, 829.0d); + TEST_DATA.put(new double[]{8, 24}, 141.0d); + TEST_DATA.put(new double[]{20, 1}, 467.0d); + TEST_DATA.put(new double[]{33, 11}, 1215.0d); + TEST_DATA.put(new double[]{37, 16}, 1517.0d); + } + + + public static void main(String[] args) + { + Node program = evolveProgram(TEST_DATA); + System.out.println(program.print()); + } + + + /** + * Evolve a function to fit the specified data. + * @param data A map from input values to expected output values. + * @return A program that generates the correct outputs for all specified + * sets of input. + */ + public static Node evolveProgram(Map<double[], Double> data) + { + TreeFactory factory = new TreeFactory(2, // Number of parameters passed into each program. + 4, // Maximum depth of generated trees. + Probability.EVENS, // Probability that a node is a function node. + new Probability(0.6d)); // Probability that other nodes are params rather than constants. + List<EvolutionaryOperator<Node>> operators = new ArrayList<EvolutionaryOperator<Node>>(3); + operators.add(new TreeMutation(factory, new Probability(0.4d))); + operators.add(new TreeCrossover()); + operators.add(new Simplification()); + TreeEvaluator evaluator = new TreeEvaluator(data); + EvolutionEngine<Node> engine = new GenerationalEvolutionEngine<Node>(factory, + new EvolutionPipeline<Node>(operators), + evaluator, + new RouletteWheelSelection(), + new MersenneTwisterRNG()); + engine.addEvolutionObserver(new EvolutionLogger<Node>()); + return engine.evolve(1000, 5, new TargetFitness(0d, evaluator.isNatural())); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElse.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElse.java new file mode 100644 index 0000000..54abc4c --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElse.java @@ -0,0 +1,268 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; + +/** + * Simple conditional program {@link Node}. + * @author Daniel Dyer + */ +public class IfThenElse implements Node +{ + private final Node condition; + private final Node then; + private final Node otherwise; + + /** + * @param condition If this node evaluates to a value greater than zero then + * the value of the {@literal then} node is returned. Otherwise the value of + * the {@literal otherwise} node is returned. + * @param then This node is evaluated if the {@literal condition} node has a + * value greater than zero. + * @param otherwise This node is evaluated if the {@literal condition} node has a + * value less than or equal to zero. + */ + public IfThenElse(Node condition, Node then, Node otherwise) + { + this.condition = condition; + this.then = then; + this.otherwise = otherwise; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return "if"; + } + + + /** + * The arity of a ternary node is three. + * @return 3 + */ + public int getArity() + { + return 3; + } + + + /** + * {@inheritDoc} + */ + public int getDepth() + { + return 1 + Math.max(condition.getDepth(), Math.max(then.getDepth(), otherwise.getDepth())); + } + + + /** + * {@inheritDoc} + */ + public int getWidth() + { + return condition.getWidth() + then.getWidth() + otherwise.getWidth(); + } + + + /** + * {@inheritDoc} + */ + public int countNodes() + { + return 1 + condition.countNodes() + then.countNodes() + otherwise.countNodes(); + } + + + /** + * {@inheritDoc} + */ + public Node getNode(int index) + { + if (index == 0) + { + return this; + } + int conditionNodes = condition.countNodes(); + if (index <= conditionNodes) + { + return condition.getNode(index - 1); + } + else + { + int thenNodes = then.countNodes(); + if (index <= conditionNodes + thenNodes) + { + return then.getNode(index - conditionNodes - 1); + } + else + { + return otherwise.getNode(index - conditionNodes - thenNodes - 1); + } + } + } + + + /** + * {@inheritDoc} + */ + public Node getChild(int index) + { + switch (index) + { + case 0: return condition; + case 1: return then; + case 2 : return otherwise; + default: throw new IndexOutOfBoundsException("Invalid child index: " + index); + } + } + + + + /** + * {@inheritDoc} + */ + public Node replaceNode(int index, Node newNode) + { + if (index == 0) + { + return newNode; + } + + int conditionNodes = condition.countNodes(); + if (index <= conditionNodes) + { + return new IfThenElse(condition.replaceNode(index - 1, newNode), then, otherwise); + } + else + { + int thenNodes = then.countNodes(); + if (index <= conditionNodes + thenNodes) + { + return new IfThenElse(condition, then.replaceNode(index - conditionNodes - 1, newNode), otherwise); + } + else + { + return new IfThenElse(condition, + then, + otherwise.replaceNode(index - conditionNodes - thenNodes - 1, newNode)); + } + } + } + + + + /** + * {@inheritDoc} + * Operates on three other nodes. The first is an expression to evaluate. + * Which of the other two nodes is evaluated and returned depends on whether + * this node evaluates to greater than zero or not. + * @param programParameters Program parameters (ignored by the conditional operator + * but may be used in evaluating child nodes). + */ + public double evaluate(double[] programParameters) + { + return condition.evaluate(programParameters) > 0 // If... + ? then.evaluate(programParameters) // Then... + : otherwise.evaluate(programParameters); // Else... + } + + + /** + * {@inheritDoc} + */ + public String print() + { + return "(" + condition.print() + " ? " + then.print() + " : " + otherwise.print() + ")"; + } + + + /** + * {@inheritDoc} + */ + public Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory) + { + if (mutationProbability.nextEvent(rng)) + { + return treeFactory.generateRandomCandidate(rng); + } + else + { + Node newCondition = condition.mutate(rng, mutationProbability, treeFactory); + Node newThen = then.mutate(rng, mutationProbability, treeFactory); + Node newOtherwise = otherwise.mutate(rng, mutationProbability, treeFactory); + if (newCondition != condition || newThen != then || newOtherwise != otherwise) + { + return new IfThenElse(newCondition, newThen, newOtherwise); + } + else + { + // Tree has not changed. + return this; + } + } + + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return print(); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedCondition = condition.simplify(); + + // If the condition is constant then the expression can be replaced by the branch that + // always gets evaluated. + if (simplifiedCondition instanceof Constant) + { + return simplifiedCondition.evaluate(null) > 0 ? then.simplify() : otherwise.simplify(); + } + else + { + Node simplifiedThen = then.simplify(); + Node simplifiedOtherwise = otherwise.simplify(); + // If both branches are identical, the condition is irrelevant. + if (simplifiedThen.equals(simplifiedOtherwise)) + { + return simplifiedThen; + } + // Only create a new node if something has actually changed, otherwise return the existing node. + if (simplifiedCondition != condition || simplifiedThen != then || simplifiedOtherwise != otherwise) + { + return new IfThenElse(simplifiedCondition, simplifiedThen, simplifiedOtherwise); + } + else + { + return this; + } + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IsGreater.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IsGreater.java new file mode 100644 index 0000000..bd23472 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IsGreater.java @@ -0,0 +1,78 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * A program {@link Node} that evaluates to a one if the value of its first + * argument is greater than the value of its second, or evaluates to zero otherwise. + * @author Daniel Dyer + */ +public class IsGreater extends BinaryNode +{ + /** + * Creates a node that evaluates to one if the value of the first child node + * is greater than the value of the second child node. Otherwise it evaluates + * to zero. + * @param left The first operand. + * @param right The second operand. + */ + public IsGreater(Node left, Node right) + { + super(left, right, '>'); + } + + + /** + * Returns a value of one if the value of the first node is greater than the value of + * the second node. Returns a value of zero otherwise. + * @param programParameters The parameters passed to this program (ignored by the + * IsGreater node but may be used in the evaluation of child nodes). + * @return One or zero depending on the relative values of the two child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) > right.evaluate(programParameters) ? 1 : 0; + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // If the two arguments are exactly equivalent, one cannot be greater than the other. + if (simplifiedLeft.equals(simplifiedRight)) + { + return new Constant(0); + } + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + else if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) > simplifiedRight.evaluate(NO_ARGS) ? 1 : 0); + } + else if (simplifiedLeft != left || simplifiedRight != right) + { + return new IsGreater(simplifiedLeft, simplifiedRight); + } + else + { + return this; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/LeafNode.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/LeafNode.java new file mode 100644 index 0000000..cf8718a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/LeafNode.java @@ -0,0 +1,127 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; + +/** + * Convenient base class for {@link Node}s that have no sub-trees. + * @author Daniel Dyer + */ +abstract class LeafNode implements Node +{ + /** + * The arity of a non-function node is always zero. + * @return 0 + */ + public int getArity() + { + return 0; + } + + + /** + * Leaf nodes always have a depth of 1 since they have no child nodes. + * @return 1 + */ + public int getDepth() + { + return 1; + } + + + /** + * Leaf nodes always have a width of 1 since they have no child nodes. + * @return 1 + */ + public int getWidth() + { + return 1; + } + + + /** + * {@inheritDoc} + */ + public int countNodes() + { + return 1; + } + + + public Node getNode(int index) + { + if (index != 0) + { + throw new IndexOutOfBoundsException("Invalid node index: " + index); + } + return this; + } + + + /** + * {@inheritDoc} + */ + public Node getChild(int index) + { + throw new IndexOutOfBoundsException("Leaf nodes have no children."); + } + + + public Node replaceNode(int index, Node newNode) + { + if (index != 0) + { + throw new IndexOutOfBoundsException("Invalid node index: " + index); + } + return newNode; + } + + + /** + * {@inheritDoc} + */ + public Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory) + { + if (mutationProbability.nextEvent(rng)) + { + return treeFactory.generateRandomCandidate(rng); + } + else + { + // Node is unchanged. + return this; + } + } + + + @Override + public String toString() + { + return print(); + } + + + /** + * Returns this node (leaf nodes cannot be simplified). + * @return This node, unmodified. + */ + public Node simplify() + { + return this; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Multiplication.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Multiplication.java new file mode 100644 index 0000000..bd5dd60 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Multiplication.java @@ -0,0 +1,90 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * Simple multiplication operator {@link Node}. + * @author Daniel Dyer + */ +public class Multiplication extends BinaryNode +{ + /** + * Creates a node that evaluates to the product of the values of its two + * child nodes ({@literal left} and {@literal right}). + * @param left The first operand. + * @param right The second operand. + */ + public Multiplication(Node left, Node right) + { + super(left, right, '*'); + } + + + /** + * Evaluates the two sub-trees and returns the product of these two values. + * @param programParameters Program parameters (ignored by the multiplication operator + * but may be used in evaluating the sub-trees). + * @return The sum of the values of both child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) * right.evaluate(programParameters); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) * simplifiedRight.evaluate(NO_ARGS)); + } + // Multiplying by one is pointless, the expression can be reduced to its other argument. + else if (simplifiedRight instanceof Constant) + { + double constant = simplifiedRight.evaluate(NO_ARGS); + if (constant == 1) + { + return simplifiedLeft; + } + else if (constant == 0) + { + return new Constant(0); + } + } + else if (simplifiedLeft instanceof Constant) + { + double constant = simplifiedLeft.evaluate(NO_ARGS); + if (constant == 1) + { + return simplifiedRight; + } + else if (constant == 0) + { + return new Constant(0); + } + } + return simplifiedLeft != left || simplifiedRight != right + ? new Multiplication(simplifiedLeft, simplifiedRight) + : this; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Node.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Node.java new file mode 100644 index 0000000..7df1685 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Node.java @@ -0,0 +1,120 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; + +/** + * Operations supported by the different types of nodes that make up + * genetic program trees. + * @author Daniel Dyer + */ +public interface Node +{ + /** + * Recursively evaluates the (sub-)tree represented by this node (including any + * child nodes) and returns a numeric value. + * @param programParameters Program parameters (possibly used by this node and/or + * in the evaluation of child nodes). + * @return The result of evaluating this node and all of its children. + */ + double evaluate(double[] programParameters); + + /** + * Recursively builds a string representation of the tree rooted at this node. + * @return A string representation of this tree. + */ + String print(); + + /** + * @return A short String that represents the function or value represented by this node. + */ + String getLabel(); + + /** + * If this is a function (non-leaf) node, how many arguments does it take? For + * leaf nodes the answer is zero. + * @return The arity of this function, or zero if this node is a leaf node. + * @see #countNodes() + */ + int getArity(); + + /** + * @return The number of levels of nodes that make up this tree. + * @see #getWidth() + */ + int getDepth(); + + /** + * Work out how wide (in nodes) this tree is. Used primarily for laying out a + * visual representation. A leaf node has a width of 1. A binary node's width + * is the sum of the widths of its sub-trees. + * @return The maximum width of this tree. + * @see #getDepth() + * @see #getArity() + */ + int getWidth(); + + /** + * @return The total number of nodes in this tree (recursively counts the nodes + * for each sub-node of this node). + * @see #getArity() + */ + int countNodes(); + + /** + * Retrieves a sub-node from this tree. + * @param index The index of a node. Index 0 is the root node. Nodes are numbered + * depth-first, right-to-left. + * @return The node at the specified position. + */ + Node getNode(int index); + + /** + * Retrieves a direct sub-node from this tree. + * @param index The index of a child node. Index 0 is the first child. Nodes are numbered + * right-to-left, grandchild nodes are not included. + * @return The node at the specified position. + */ + Node getChild(int index); + + /** + * Returns a new tree that is identical to this tree except with the specified node + * replaced. + * @param index The index of the node to replace. + * @param newNode The replacement node. + * @return A new tree with the node at the specified index replaced by + * {@code newNode}. + */ + Node replaceNode(int index, Node newNode); + + /** + * Helper method for the {@link TreeMutation} evolutionary operator. + * @param rng A source of randomness. + * @param mutationProbability The probability that a given node will be mutated. + * @param treeFactory A factory for creating the new sub-trees needed for mutation. + * @return The mutated node (or the same node if no mutation occurred). + */ + Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory); + + /** + * Reduce this program tree to its simplest equivalent form. + * @return A simplification of this program tree, or this program tree unodified if it + * cannot be simplified. + */ + Node simplify(); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Parameter.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Parameter.java new file mode 100644 index 0000000..575bd1d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Parameter.java @@ -0,0 +1,102 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * A program {@link Node} that simply returns the value of one of the + * program's parameters. + * @author Daniel Dyer + */ +public class Parameter extends LeafNode +{ + private final int parameterIndex; + + /** + * @param parameterIndex Which of the program's (zero-indexed) parameter + * values should be returned upon evaluation of this node. + */ + public Parameter(int parameterIndex) + { + this.parameterIndex = parameterIndex; + } + + + /** + * Returns the value of one of the program parameters. + * @param programParameters The parameters to this program. + * @return The program parameter at the index condigured for this node. + */ + public double evaluate(double[] programParameters) + { + if (parameterIndex >= programParameters.length) + { + throw new IllegalArgumentException("Invalid parameter index: " + parameterIndex); + } + return programParameters[parameterIndex]; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return "P" + parameterIndex; + } + + + /** + * {@inheritDoc} + * For a parameter node the String representation is simply "arg0", "arg1", etc. + * depending on which program parameter it refers to. + */ + public String print() + { + return "arg" + parameterIndex; + } + + + /** + * Two parameters are equal if they evaluate to the same program argument + * (i.e. they have the same parameter index). + * @param other The object that this object is compared to. + * @return True if the parameters are equivalent, false otherwise. + */ + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (other == null || getClass() != other.getClass()) + { + return false; + } + return parameterIndex == ((Parameter) other).parameterIndex; + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + return parameterIndex; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Simplification.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Simplification.java new file mode 100644 index 0000000..f44864b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Simplification.java @@ -0,0 +1,71 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Evolutionary operator for GP trees that reduces them to their simplest form. + * @author Daniel Dyer + */ +public class Simplification implements EvolutionaryOperator<Node> +{ + private final Probability probability; + + + /** + * Creates a simplification operator with a probability of 1 (i.e. all + * canidates will be simplified). + */ + public Simplification() + { + this(Probability.ONE); + } + + + /** + * Creates a simplfication operator that has the specified probability of being + * applied to any individual candidate. + * @param probability The probability that this operator will attempt to simplify + * any single expression. + */ + public Simplification(Probability probability) + { + this.probability = probability; + } + + + /** + * Simplify the expressions represented by the candidates. Each expression + * is simplified according to the configured probability. + * @param selectedCandidates The individuals to evolve. + * @param rng A source of randomness. + * @return The (possibly) simplified candidates. + */ + public List<Node> apply(List<Node> selectedCandidates, Random rng) + { + List<Node> evolved = new ArrayList<Node>(selectedCandidates.size()); + for (Node node : selectedCandidates) + { + evolved.add(probability.nextEvent(rng) ? node.simplify() : node); + } + return evolved; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Subtraction.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Subtraction.java new file mode 100644 index 0000000..97083af --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Subtraction.java @@ -0,0 +1,80 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * Simple subtraction operator {@link Node}. + * @author Daniel Dyer + */ +public class Subtraction extends BinaryNode +{ + /** + * Creates a node that evaluates the the value of {@literal left} + * minus the value of {@literal right}. + * @param left The first operand. + * @param right The second operand. + */ + public Subtraction(Node left, Node right) + { + super(left, right, '-'); + } + + + /** + * Evaluates the two sub-trees and returns the difference between these two values. + * @param programParameters Program parameters (ignored by the subtraction operator + * but may be used in evaluating the sub-trees). + * @return The difference between the values of the two child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) - right.evaluate(programParameters); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // If the two arguments are identical then the result will always be zero. + if (simplifiedLeft.equals(simplifiedRight)) + { + return new Constant(0); + } + // Subtracting zero is pointless, the expression can be reduced to its lefthand side. + else if (simplifiedRight instanceof Constant && simplifiedRight.evaluate(NO_ARGS) == 0) + { + return simplifiedLeft; + } + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + else if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) - simplifiedRight.evaluate(NO_ARGS)); + } + else if (simplifiedLeft != left || simplifiedRight != right) + { + return new Subtraction(simplifiedLeft, simplifiedRight); + } + else + { + return this; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRenderer.java new file mode 100644 index 0000000..fd16238 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRenderer.java @@ -0,0 +1,141 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import javax.swing.JComponent; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * A Swing renderer for genetic programming trees. + * @author Daniel Dyer + */ +public class SwingGPTreeRenderer implements Renderer<Node, JComponent> +{ + /** + * Renders a GP tree as a Swing component. + * @param tree The root node of the GP tree to render. + * @return A {@link JComponent} that displays a graphical representation of the tree. + */ + public JComponent render(Node tree) + { + return new GPTreeView(tree); + } + + + private static final class GPTreeView extends JComponent + { + // Allow 30 pixels for each node horizontally. + private static final int NODE_WIDTH = 30; + // Allow 50 pixels for each node vertically. + private static final int NODE_HEIGHT = 50; + private static final int CIRCLE_RADIUS = 9; + private static final int CIRCLE_DIAMETER = CIRCLE_RADIUS * 2; + + private final Node rootNode; + + + GPTreeView(Node rootNode) + { + this.rootNode = rootNode; + int minHeight = rootNode.getDepth() * NODE_HEIGHT; + int minWidth = rootNode.getWidth() * NODE_WIDTH; + Dimension size = new Dimension(minWidth, minHeight); + setMinimumSize(size); + setPreferredSize(size); + } + + + @Override + protected void paintComponent(Graphics graphics) + { + super.paintComponent(graphics); + if (graphics instanceof Graphics2D) + { + ((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + // Center the tree if the component is bigger than required. + int offset = (getSize().width - getMinimumSize().width) / 2; + drawNode(rootNode, offset, 0, graphics); + } + + + /** + * Recursively draw the specified node and its children. + * @param node The sub-tree to draw. + * @param x The left edge of the area in which this tree is drawn. + * @param y The top edge of the area in which this tree is drawn. + * @param graphics The target for drawing. + */ + private void drawNode(Node node, int x, int y, Graphics graphics) + { + int start = x + node.getWidth() * NODE_WIDTH / 2; + if (node instanceof Constant) + { + graphics.setColor(Color.YELLOW); + graphics.fillRoundRect(start - (NODE_WIDTH / 2 - 2), + y, + NODE_WIDTH - 4, + CIRCLE_DIAMETER, + CIRCLE_RADIUS, + CIRCLE_RADIUS); + graphics.setColor(Color.BLACK); + graphics.drawRoundRect(start - (NODE_WIDTH / 2 - 2), + y, + NODE_WIDTH - 4, + CIRCLE_DIAMETER, + CIRCLE_RADIUS, + CIRCLE_RADIUS); + } + else if (node instanceof Parameter) + { + graphics.setColor(Color.GREEN); + graphics.fillRect(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + graphics.setColor(Color.BLACK); + graphics.drawRect(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + } + else + { + graphics.setColor(Color.WHITE); + graphics.fillOval(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + graphics.setColor(Color.BLACK); + graphics.drawOval(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + } + FontMetrics metrics = graphics.getFontMetrics(); + int stringWidth = metrics.stringWidth(node.getLabel()); + graphics.drawString(node.getLabel(), + (int) Math.round(start - (double) stringWidth / 2), + (int) Math.round(y + CIRCLE_RADIUS + (double) metrics.getHeight() / 2 - metrics.getDescent())); + + int xOffset = x; + for (int i = 0; i < node.getArity(); i++) + { + Node child = node.getChild(i); + drawNode(child, xOffset, y + NODE_HEIGHT, graphics); + graphics.drawLine(start, + y + CIRCLE_DIAMETER, xOffset + (child.getWidth() * NODE_WIDTH / 2), + y + NODE_HEIGHT); + xOffset += child.getWidth() * NODE_WIDTH; + } + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossover.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossover.java new file mode 100644 index 0000000..8fe711e --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossover.java @@ -0,0 +1,72 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.operators.AbstractCrossover; + +/** + * Cross-over operator for the trees of {@link Node}s used in the genetic + * programming example application. + * @author Daniel Dyer + */ +public class TreeCrossover extends AbstractCrossover<Node> +{ + /** + * Creates a single-point cross-over operator. + */ + public TreeCrossover() + { + super(1); + } + + + /** + * Swaps randomly selected sub-trees between the two parents. + * @param parent1 The first parent. + * @param parent2 The second parent. + * @param numberOfCrossoverPoints The number of cross-overs to perform. + * @param rng A source of randomness. + * @return A list of two offspring, generated by swapping sub-trees + * between the two parents. + */ + @Override + protected List<Node> mate(Node parent1, + Node parent2, + int numberOfCrossoverPoints, + Random rng) + { + List<Node> offspring = new ArrayList<Node>(2); + Node offspring1 = parent1; + Node offspring2 = parent2; + + for (int i = 0; i < numberOfCrossoverPoints; i++) + { + int crossoverPoint1 = rng.nextInt(parent1.countNodes()); + Node subTree1 = parent1.getNode(crossoverPoint1); + int crossoverPoint2 = rng.nextInt(parent2.countNodes()); + Node subTree2 = parent2.getNode(crossoverPoint2); + offspring1 = parent1.replaceNode(crossoverPoint1, subTree2); + offspring2 = parent2.replaceNode(crossoverPoint2, subTree1); + } + + offspring.add(offspring1); + offspring.add(offspring2); + return offspring; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluator.java new file mode 100644 index 0000000..57e718b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluator.java @@ -0,0 +1,79 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.List; +import java.util.Map; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Fitness function for the genetic programming example application. + * An evolved program tree is tested against a table of inputs and associated + * expected outputs. If the evolved program correctly calculates the right answer + * for all sets of inputs then it has a fitness of zero. Otherwise, its fitness + * is an error value that indicates how accurate it was (the larger the combined + * error value, the less accurate the function is). + * @author Daniel Dyer + */ +public class TreeEvaluator implements FitnessEvaluator<Node> +{ + private final Map<double[], Double> data; + + + /** + * @param data Each row is consists of a set of inputs and an expected output (the + * last item in the row is the output). + */ + public TreeEvaluator(Map<double[], Double> data) + { + this.data = data; + } + + + /** + * If the evolved program correctly calculates the right answer + * for all sets of inputs then it has a fitness of zero. Otherwise, its fitness + * is an error value that indicates how accurate it was (the larger the combined + * error value, the less accurate the function is). + * The combined error value is calculated by summing the squares of each individual + * error (the difference between the expected output and the actual output). + * @param candidate The program tree to evaluate. + * @param population Ignored by this implementation. + * @return The fitness score for the specified candidate. + */ + public double getFitness(Node candidate, List<? extends Node> population) + { + double error = 0; + for (Map.Entry<double[], Double> entry : data.entrySet()) + { + double actualValue = candidate.evaluate(entry.getKey()); + double diff = actualValue - entry.getValue(); + error += (diff * diff); + } + return error; + } + + + /** + * This fitness evaluator is a minimising function. A fitness of zero + * indicates a perfect solution. + * @return false + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactory.java new file mode 100644 index 0000000..abb935d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactory.java @@ -0,0 +1,115 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * {@link org.uncommons.watchmaker.framework.CandidateFactory} for generating + * trees of {@link Node}s for the genetic programming example application. + * @author Daniel Dyer + */ +public class TreeFactory extends AbstractCandidateFactory<Node> +{ + // The number of program parameters that each program tree will be provided. + private final int parameterCount; + + // The maximum depth of a program tree. No function nodes will be created below + // this depth (branches will be terminated with parameters or constants). + private final int maximumDepth; + + // Probability that a created node is a function node rather + // than a value node. + private final Probability functionProbability; + + // Probability that a value (non-function) node is a parameter + // node rather than a constant node. + private final Probability parameterProbability; + + + /** + * @param parameterCount The number of program parameters that each + * generated program tree can will be provided when executed. + * @param maxDepth The maximum depth of generated trees. + * @param functionProbability The probability (between 0 and 1) that a + * randomly-generated node will be a function node rather than a value + * (parameter or constant) node. + * @param parameterProbability The probability that a randomly-generated + * non-function node will be a parameter node rather than a constant node. + */ + public TreeFactory(int parameterCount, + int maxDepth, + Probability functionProbability, + Probability parameterProbability) + { + if (parameterCount < 0) + { + throw new IllegalArgumentException("Parameter count must be greater than or equal to 0."); + } + if (maxDepth < 1) + { + throw new IllegalArgumentException("Max depth must be at least 1."); + } + + this.parameterCount = parameterCount; + this.maximumDepth = maxDepth; + this.functionProbability = functionProbability; + this.parameterProbability = parameterProbability; + } + + + /** + * {@inheritDoc} + */ + public Node generateRandomCandidate(Random rng) + { + return makeNode(rng, maximumDepth); + } + + + /** + * Recursively constructs a tree of Nodes, up to the specified maximum depth. + * @param rng The RNG used to random create nodes. + * @param maxDepth The maximum depth of the generated tree. + * @return A tree of nodes. + */ + private Node makeNode(Random rng, int maxDepth) + { + if (functionProbability.nextEvent(rng) && maxDepth > 1) + { + // Max depth for sub-trees is one less than max depth for this node. + int depth = maxDepth - 1; + switch (rng.nextInt(5)) + { + case 0: return new Addition(makeNode(rng, depth), makeNode(rng, depth)); + case 1: return new Subtraction(makeNode(rng, depth), makeNode(rng, depth)); + case 2: return new Multiplication(makeNode(rng, depth), makeNode(rng, depth)); + case 3: return new IfThenElse(makeNode(rng, depth), makeNode(rng, depth), makeNode(rng, depth)); + default: return new IsGreater(makeNode(rng, depth), makeNode(rng, depth)); + } + } + else if (parameterProbability.nextEvent(rng)) + { + return new Parameter(rng.nextInt(parameterCount)); + } + else + { + return new Constant(rng.nextInt(11)); + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutation.java new file mode 100644 index 0000000..75f64b5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutation.java @@ -0,0 +1,61 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutation operator for the trees of {@link Node}s used in the genetic + * programming example application. + * @author Daniel Dyer + */ +public class TreeMutation implements EvolutionaryOperator<Node> +{ + private final TreeFactory treeFactory; + + private final Probability mutationProbability; + + /** + * The tree mutation operator requires a {@link TreeFactory} because + * the process of mutation involves creating new sub-trees. The same + * TreeFactory that is used to create the initial population should be + * used. + * @param treeFactory Used to generate the new sub-trees required for mutation. + * @param mutationProbability The probability that any given node in a tree is + * mutated by this operator. + */ + public TreeMutation(TreeFactory treeFactory, + Probability mutationProbability) + { + this.treeFactory = treeFactory; + this.mutationProbability = mutationProbability; + } + + + public List<Node> apply(List<Node> selectedCandidates, Random rng) + { + List<Node> mutatedPopulation = new ArrayList<Node>(selectedCandidates.size()); + for (Node tree : selectedCandidates) + { + mutatedPopulation.add(tree.mutate(rng, mutationProbability, treeFactory)); + } + return mutatedPopulation; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/package-info.java new file mode 100644 index 0000000..2aa8b11 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/package-info.java @@ -0,0 +1,20 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * A simple example of tree-based genetic programming. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.geneticprogramming; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AbstractVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AbstractVertexMutation.java new file mode 100644 index 0000000..490ed92 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AbstractVertexMutation.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Base class for mutation operators that modify the points of polygons in an + * image. + * @author Daniel Dyer + */ +abstract class AbstractVertexMutation implements EvolutionaryOperator<ColouredPolygon> +{ + private final Dimension canvasSize; + private final NumberGenerator<Probability> mutationProbability; + + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the probability + * that a polygon's points will be mutated. + * @param canvasSize The size of the canvas. Used to constrain the positions of the points. + */ + protected AbstractVertexMutation(NumberGenerator<Probability> mutationProbability, + Dimension canvasSize) + { + this.mutationProbability = mutationProbability; + this.canvasSize = canvasSize; + } + + + /** + * @return The dimensions of the target image. + */ + protected Dimension getCanvasSize() + { + return canvasSize; + } + + + /** + * @return The {@link NumberGenerator} that provides the mutation probability. + */ + protected NumberGenerator<Probability> getMutationProbability() + { + return mutationProbability; + } + + + /** + * Applies the mutation to each polygon in the list provided according to the + * pre-configured mutation probability. If the probability is 0.1, approximately + * 10% of the individuals will be mutated. The actual mutation operation is + * defined in the sub-class implementation of the + * {@link #mutateVertices(java.util.List, java.util.Random)} method. + * @param polygons The list of polygons to be mutated. + * @param rng A source of randomness. + * @return The polygons after mutation. None, some or all will have been + * modified. + */ + public List<ColouredPolygon> apply(List<ColouredPolygon> polygons, Random rng) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(polygons.size()); + for (ColouredPolygon polygon : polygons) + { + List<Point> newVertices = mutateVertices(polygon.getVertices(), rng); + newPolygons.add(newVertices == polygon.getVertices() + ? polygon + : new ColouredPolygon(polygon.getColour(), newVertices)); + } + return newPolygons; + } + + + /** + * Implemented in sub-classes to perform the mutation of the vertices. + * @param vertices A list of the points that make up the polygon. + * @param rng A source of randomness. + * @return A mutated list of points. + */ + protected abstract List<Point> mutateVertices(List<Point> vertices, Random rng); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutation.java new file mode 100644 index 0000000..183a8b7 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutation.java @@ -0,0 +1,97 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Randomly mutates the polygons that make up an image by adding a polygon + * according to some probability. + * @author Daniel Dyer + */ +public class AddPolygonMutation implements EvolutionaryOperator<List<ColouredPolygon>> +{ + private final NumberGenerator<Probability> addPolygonProbability; + private final PolygonImageFactory factory; + private final int maxPolygons; + + + + /** + * @param addPolygonProbability A {@link NumberGenerator} that controls the probability + * that a polygon will be added. + * @param factory Used to create new polygons. + * @param maxPolygons The maximum number of polygons permitted in an image (must be at least 2). + */ + public AddPolygonMutation(NumberGenerator<Probability> addPolygonProbability, + PolygonImageFactory factory, + int maxPolygons) + { + if (maxPolygons < 2) + { + throw new IllegalArgumentException("Max polygons must be > 1."); + } + this.addPolygonProbability = addPolygonProbability; + this.factory = factory; + this.maxPolygons = maxPolygons; + } + + + /** + * @param addPolygonProbability The probability that a polygon will be removed. + * @param factory Used to create new polygons. + * @param maxPolygons The maximum number of polygons permitted in an image (must be at least 2). + */ + public AddPolygonMutation(Probability addPolygonProbability, + PolygonImageFactory factory, + int maxPolygons) + { + this(new ConstantGenerator<Probability>(addPolygonProbability), + factory, + maxPolygons); + } + + + public List<List<ColouredPolygon>> apply(List<List<ColouredPolygon>> selectedCandidates, Random rng) + { + List<List<ColouredPolygon>> mutatedCandidates = new ArrayList<List<ColouredPolygon>>(selectedCandidates.size()); + for (List<ColouredPolygon> candidate : selectedCandidates) + { + // A single polygon is added with the configured probability, unless + // we already have the maximum permitted number of polygons. + if (candidate.size() < maxPolygons && addPolygonProbability.nextValue().nextEvent(rng)) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(candidate); + newPolygons.add(rng.nextInt(newPolygons.size() + 1), + factory.createRandomPolygon(rng)); + mutatedCandidates.add(newPolygons); + } + else // Nothing changed. + { + mutatedCandidates.add(candidate); + } + } + return mutatedCandidates; + } + + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddVertexMutation.java new file mode 100644 index 0000000..decebdd --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddVertexMutation.java @@ -0,0 +1,84 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by adding a point, according to some probability. + * @author Daniel Dyer + */ +public class AddVertexMutation extends AbstractVertexMutation +{ + static final int MAX_VERTEX_COUNT = 10; + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the probability + * that a point will be added. + * @param canvasSize The size of the canvas. Used to constrain the positions of the points. + */ + public AddVertexMutation(Dimension canvasSize, + NumberGenerator<Probability> mutationProbability) + { + super(mutationProbability, canvasSize); + } + + + /** + * @param mutationProbability The probability that a point will be added. + * @param canvasSize The size of the canvas. Used to constrain the positions of the points. + */ + public AddVertexMutation(Dimension canvasSize, + Probability mutationProbability) + { + this(canvasSize, new ConstantGenerator<Probability>(mutationProbability)); + } + + + /** + * Mutates the list of vertices for a given polygon by adding a new random point. + * Whether or not a point is actually added is determined by the configured mutation probability. + * @param vertices A list of the points that make up the polygon. + * @param rng A source of randomness. + * @return A mutated list of points. + */ + @Override + protected List<Point> mutateVertices(List<Point> vertices, Random rng) + { + // A single point is added with the configured probability, unless + // we already have the maximum permitted number of points. + if (vertices.size() < MAX_VERTEX_COUNT && getMutationProbability().nextValue().nextEvent(rng)) + { + List<Point> newVertices = new ArrayList<Point>(vertices); + newVertices.add(rng.nextInt(newVertices.size()), + new Point(rng.nextInt(getCanvasSize().width), + rng.nextInt(getCanvasSize().height))); + return newVertices; + } + else // Nothing changed. + { + return vertices; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutation.java new file mode 100644 index 0000000..0549cc6 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutation.java @@ -0,0 +1,94 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.Maths; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by moving a point, according to some probability. + * @author Daniel Dyer + */ +public class AdjustVertexMutation extends AbstractVertexMutation +{ + private final NumberGenerator<? extends Number> changeAmount; + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the + * probability that a point will be moved. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + * @param changeAmount A {@link NumberGenerator} that controls the distance + * that points are moved (in pixels). Should generate both positive and + * negative values. + */ + public AdjustVertexMutation(Dimension canvasSize, + NumberGenerator<Probability> mutationProbability, + NumberGenerator<? extends Number> changeAmount) + { + super(mutationProbability, canvasSize); + this.changeAmount = changeAmount; + } + + + /** + * @param mutationProbability The probability that a point will be moved. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + * @param changeAmount A {@link NumberGenerator} that controls the distance + * that points are moved (in pixels). Should generate both positive and + * negative values. + */ + public AdjustVertexMutation(Dimension canvasSize, + Probability mutationProbability, + NumberGenerator<? extends Number> changeAmount) + { + this(canvasSize, new ConstantGenerator<Probability>(mutationProbability), changeAmount); + } + + + @Override + protected List<Point> mutateVertices(List<Point> vertices, Random rng) + { + // A single point is modified with the configured probability. + if (getMutationProbability().nextValue().nextEvent(rng)) + { + List<Point> newVertices = new ArrayList<Point>(vertices); + int xDelta = (int) Math.round(changeAmount.nextValue().doubleValue()); + int yDelta = (int) Math.round(changeAmount.nextValue().doubleValue()); + int index = rng.nextInt(newVertices.size()); + Point oldPoint = newVertices.get(index); + int newX = oldPoint.x + xDelta; + int newY = oldPoint.y + yDelta; + newX = Maths.restrictRange(newX, 0, getCanvasSize().width - 1); + newY = Maths.restrictRange(newY, 0, getCanvasSize().height - 1); + newVertices.set(index, new Point(newX, newY)); + return newVertices; + } + else // Nothing changed. + { + return vertices; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ColouredPolygon.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ColouredPolygon.java new file mode 100644 index 0000000..eb8c481 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ColouredPolygon.java @@ -0,0 +1,78 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Polygon; +import java.util.List; + +/** + * A coloured polygon consists of a set of vertices and a colour that is used to + * fill the polygon when it is rendered. + * @author Daniel Dyer + */ +public class ColouredPolygon +{ + private final Color colour; + private final List<Point> vertices; + private final Polygon polygon; + + + /** + * Creates a new filled polygon with the specified points and colour. + * @param colour The colour of this polygon. The alpha channel is used + * to specify the polygons translucency. + * @param vertices The points that define the polygon's outline. + */ + public ColouredPolygon(Color colour, List<Point> vertices) + { + this.colour = colour; + this.vertices = vertices; + this.polygon = new Polygon(); + for (Point point : vertices) + { + polygon.addPoint(point.x, point.y); + } + } + + + /** + * @return The colour that this polygon should be rendered. + */ + public Color getColour() + { + return colour; + } + + + /** + * @return A list of this polygon's vertices. + */ + public List<Point> getVertices() + { + return vertices; + } + + + /** + * @return The AWT shape used for rendering. + */ + public Polygon getPolygon() + { + return polygon; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MonaLisaApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MonaLisaApplet.java new file mode 100644 index 0000000..4a0db99 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MonaLisaApplet.java @@ -0,0 +1,249 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Random; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import org.uncommons.maths.random.Probability; +import org.uncommons.maths.random.XORShiftRNG; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.CachingFitnessEvaluator; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FitnessEvaluator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.TerminationCondition; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.framework.selection.TournamentSelection; +import org.uncommons.watchmaker.framework.termination.Stagnation; +import org.uncommons.watchmaker.swing.AbortControl; +import org.uncommons.watchmaker.swing.ProbabilityParameterControl; +import org.uncommons.watchmaker.swing.evolutionmonitor.EvolutionMonitor; + +/** + * This program is inspired by Roger Alsing's evolution of the Mona Lisa + * (http://rogeralsing.com/2008/12/07/genetic-programming-evolution-of-mona-lisa/). + * It attempts to find the combination of 50 translucent polygons that most closely + * resembles Leonardo da Vinci's Mona Lisa. + * @author Daniel Dyer + */ +public class MonaLisaApplet extends AbstractExampleApplet +{ + private static final String IMAGE_PATH = "org/uncommons/watchmaker/examples/monalisa/monalisa.jpg"; + + private ProbabilitiesPanel probabilitiesPanel; + private EvolutionMonitor<List<ColouredPolygon>> monitor; + private JButton startButton; + private AbortControl abort; + private JSpinner populationSpinner; + private JSpinner elitismSpinner; + private ProbabilityParameterControl selectionPressureControl; + private BufferedImage targetImage; + + + @Override + public void init() + { + try + { + URL imageURL = MonaLisaApplet.class.getClassLoader().getResource(IMAGE_PATH); + targetImage = ImageIO.read(imageURL); + super.init(); + } + catch (IOException ex) + { + ex.printStackTrace(); + JOptionPane.showMessageDialog(this, ex, "Failed to Load Image", JOptionPane.ERROR_MESSAGE); + + } + } + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + probabilitiesPanel = new ProbabilitiesPanel(); + probabilitiesPanel.setBorder(BorderFactory.createTitledBorder("Evolution Probabilities")); + JPanel controls = new JPanel(new BorderLayout()); + controls.add(createParametersPanel(), BorderLayout.NORTH); + controls.add(probabilitiesPanel, BorderLayout.SOUTH); + container.add(controls, BorderLayout.NORTH); + + Renderer<List<ColouredPolygon>, JComponent> renderer = new PolygonImageSwingRenderer(targetImage); + monitor = new EvolutionMonitor<List<ColouredPolygon>>(renderer, false); + container.add(monitor.getGUIComponent(), BorderLayout.CENTER); + } + + + private JComponent createParametersPanel() + { + Box parameters = Box.createHorizontalBox(); + parameters.add(Box.createHorizontalStrut(10)); + final JLabel populationLabel = new JLabel("Population Size: "); + parameters.add(populationLabel); + parameters.add(Box.createHorizontalStrut(10)); + populationSpinner = new JSpinner(new SpinnerNumberModel(10, 2, 1000, 1)); + populationSpinner.setMaximumSize(populationSpinner.getMinimumSize()); + parameters.add(populationSpinner); + parameters.add(Box.createHorizontalStrut(10)); + final JLabel elitismLabel = new JLabel("Elitism: "); + parameters.add(elitismLabel); + parameters.add(Box.createHorizontalStrut(10)); + elitismSpinner = new JSpinner(new SpinnerNumberModel(2, 1, 1000, 1)); + elitismSpinner.setMaximumSize(elitismSpinner.getMinimumSize()); + parameters.add(elitismSpinner); + parameters.add(Box.createHorizontalStrut(10)); + + parameters.add(new JLabel("Selection Pressure: ")); + parameters.add(Box.createHorizontalStrut(10)); + selectionPressureControl = new ProbabilityParameterControl(Probability.EVENS, + Probability.ONE, + 2, + new Probability(0.7)); + parameters.add(selectionPressureControl.getControl()); + parameters.add(Box.createHorizontalStrut(10)); + + startButton = new JButton("Start"); + abort = new AbortControl(); + startButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent ev) + { + abort.getControl().setEnabled(true); + populationLabel.setEnabled(false); + populationSpinner.setEnabled(false); + elitismLabel.setEnabled(false); + elitismSpinner.setEnabled(false); + startButton.setEnabled(false); + new EvolutionTask((Integer) populationSpinner.getValue(), + (Integer) elitismSpinner.getValue(), + abort.getTerminationCondition(), + new Stagnation(1000, false)).execute(); + } + }); + abort.getControl().setEnabled(false); + parameters.add(startButton); + parameters.add(abort.getControl()); + parameters.add(Box.createHorizontalStrut(10)); + + parameters.setBorder(BorderFactory.createTitledBorder("Parameters")); + return parameters; + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + * @throws IOException If there is a problem loading the target image. + */ + public static void main(String[] args) throws IOException + { + MonaLisaApplet gui = new MonaLisaApplet(); + // If a URL is specified as an argument, use that image. Otherwise use the default Mona Lisa picture. + URL imageURL = args.length > 0 + ? new URL(args[0]) + : MonaLisaApplet.class.getClassLoader().getResource(IMAGE_PATH); + gui.targetImage = ImageIO.read(imageURL); + gui.displayInFrame("Watchmaker Framework - Mona Lisa Example"); + } + + + /** + * The task that acutally performs the evolution. + */ + private class EvolutionTask extends SwingBackgroundTask<List<ColouredPolygon>> + { + private final int populationSize; + private final int eliteCount; + private final TerminationCondition[] terminationConditions; + + + EvolutionTask(int populationSize, int eliteCount, TerminationCondition... terminationConditions) + { + this.populationSize = populationSize; + this.eliteCount = eliteCount; + this.terminationConditions = terminationConditions; + } + + + @Override + protected List<ColouredPolygon> performTask() throws Exception + { + Dimension canvasSize = new Dimension(targetImage.getWidth(), targetImage.getHeight()); + + Random rng = new XORShiftRNG(); + FitnessEvaluator<List<ColouredPolygon>> evaluator + = new CachingFitnessEvaluator<List<ColouredPolygon>>(new PolygonImageEvaluator(targetImage)); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + EvolutionaryOperator<List<ColouredPolygon>> pipeline + = probabilitiesPanel.createEvolutionPipeline(factory, canvasSize, rng); + + SelectionStrategy<Object> selection = new TournamentSelection(selectionPressureControl.getNumberGenerator()); + EvolutionEngine<List<ColouredPolygon>> engine + = new GenerationalEvolutionEngine<List<ColouredPolygon>>(factory, + pipeline, + evaluator, + selection, + rng); + engine.addEvolutionObserver(monitor); + + return engine.evolve(populationSize, eliteCount, terminationConditions); + } + + + @Override + protected void postProcessing(List<ColouredPolygon> result) + { + abort.reset(); + abort.getControl().setEnabled(false); + populationSpinner.setEnabled(true); + elitismSpinner.setEnabled(true); + startButton.setEnabled(true); + } + + + @Override + protected void onError(Throwable throwable) + { + super.onError(throwable); + postProcessing(null); + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutation.java new file mode 100644 index 0000000..1b91f73 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutation.java @@ -0,0 +1,74 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Randomly mutates an image by swapping the z-order of two of its polygons + * according to some probability. + * @author Daniel Dyer + */ +public class MovePolygonMutation implements EvolutionaryOperator<List<ColouredPolygon>> +{ + private final NumberGenerator<Probability> movePolygonProbability; + + + /** + * @param movePolygonProbability A {@link NumberGenerator} that controls the probability + * that a polygon will be replaced. + */ + public MovePolygonMutation(NumberGenerator<Probability> movePolygonProbability) + { + this.movePolygonProbability = movePolygonProbability; + } + + + /** + * @param replacePolygonProbability The probability that a polygon will be replaced. + */ + public MovePolygonMutation(Probability replacePolygonProbability) + { + this(new ConstantGenerator<Probability>(replacePolygonProbability)); + } + + + public List<List<ColouredPolygon>> apply(List<List<ColouredPolygon>> selectedCandidates, Random rng) + { + List<List<ColouredPolygon>> mutatedCandidates = new ArrayList<List<ColouredPolygon>>(selectedCandidates.size()); + for (List<ColouredPolygon> candidate : selectedCandidates) + { + if (movePolygonProbability.nextValue().nextEvent(rng)) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(candidate); + ColouredPolygon polygon = newPolygons.remove(rng.nextInt(newPolygons.size())); + newPolygons.add(rng.nextInt(newPolygons.size()) + 1, polygon); + mutatedCandidates.add(newPolygons); + } + else // Nothing changed. + { + mutatedCandidates.add(candidate); + } + } + return mutatedCandidates; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutation.java new file mode 100644 index 0000000..0434646 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutation.java @@ -0,0 +1,113 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.Maths; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by changing their colour and/or either adding a point, removing a point or + * changing the position of a point. + * @author Daniel Dyer + */ +public class PolygonColourMutation implements EvolutionaryOperator<ColouredPolygon> +{ + private final NumberGenerator<Probability> mutationProbability; + private final NumberGenerator<Double> mutationAmount; + + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the + * probability that the colour will be modified. + * @param mutationAmount A {@link NumberGenerator} that controls the amount + * that the colour's components are adjusted by. + */ + public PolygonColourMutation(NumberGenerator<Probability> mutationProbability, + NumberGenerator<Double> mutationAmount) + { + this.mutationProbability = mutationProbability; + this.mutationAmount = mutationAmount; + } + + + /** + * @param mutationProbability The probability that the colour will be modified. + * @param mutationAmount A {@link NumberGenerator} that controls the amount + * that the colour's components are adjusted by. + */ + public PolygonColourMutation(Probability mutationProbability, + NumberGenerator<Double> mutationAmount) + { + this(new ConstantGenerator<Probability>(mutationProbability), mutationAmount); + } + + + public List<ColouredPolygon> apply(List<ColouredPolygon> polygons, Random rng) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(polygons.size()); + for (ColouredPolygon polygon : polygons) + { + Color newColour = mutateColour(polygon.getColour(), rng); + newPolygons.add(newColour == polygon.getColour() + ? polygon + : new ColouredPolygon(newColour, polygon.getVertices())); + } + return newPolygons; + } + + + /** + * Mutate the specified colour. + * @param colour The colour to mutate. + * @param rng A source of randomness. + * @return The (possibly) mutated colour. + */ + private Color mutateColour(Color colour, Random rng) + { + if (mutationProbability.nextValue().nextEvent(rng)) + { + return new Color(mutateColourComponent(colour.getRed()), + mutateColourComponent(colour.getGreen()), + mutateColourComponent(colour.getBlue()), + mutateColourComponent(colour.getAlpha())); + } + else + { + return colour; + } + } + + + /** + * Adjust a single component (red, green, blue or alpha) of a colour. + * @param component The value to mutate. + * @return The mutated component value. + */ + private int mutateColourComponent(int component) + { + int mutatedComponent = (int) Math.round(component + mutationAmount.nextValue()); + mutatedComponent = Maths.restrictRange(mutatedComponent, 0, 255); + return mutatedComponent; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluator.java new file mode 100644 index 0000000..acc3e1b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluator.java @@ -0,0 +1,158 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.util.List; +import org.uncommons.watchmaker.framework.FitnessEvaluator; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Compares the generated polygon-based images to the target bitmap. The polygon images + * are rendered the same size as the target image and then each pixel is compared. The + * fitness value is a combination of the differences for each pixel. Lower fitness is better. + * @author Daniel Dyer + */ +public class PolygonImageEvaluator implements FitnessEvaluator<List<ColouredPolygon>> +{ + // This field is marked as transient, even though the class is not Serializable, because + // Terracotta will respect the fact it is transient and not try to share it. + private final transient ThreadLocal<Renderer<List<ColouredPolygon>, BufferedImage>> threadLocalRenderer + = new ThreadLocal<Renderer<List<ColouredPolygon>, BufferedImage>>(); + + private final int width; + private final int height; + private final AffineTransform transform; + private final int[] targetPixels; + + + /** + * Creates an evaluator that assigns fitness scores to images based on how + * close they are to the specified target image. + * @param targetImage The image that all others are compared to. + */ + public PolygonImageEvaluator(BufferedImage targetImage) + { + // Scale the image down so that its smallest dimension is 100 pixels. For large images this drastically + // reduces the number of pixels that we need to check for fitness evaluation. + Raster targetImageData; + if (targetImage.getWidth() > 100 && targetImage.getHeight() > 100) + { + double ratio = 100.0d / (targetImage.getWidth() > targetImage.getHeight() ? targetImage.getHeight() : targetImage.getWidth()); + transform = AffineTransform.getScaleInstance(ratio, ratio); + AffineTransformOp transformOp = new AffineTransformOp(transform, + AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + targetImageData = convertImage(transformOp.filter(targetImage, null)).getData(); + } + else + { + targetImageData = convertImage(targetImage).getData(); + transform = null; + } + + this.width = targetImageData.getWidth(); + this.height = targetImageData.getHeight(); + int[] pixelArray = new int[targetImageData.getWidth() * targetImageData.getHeight()]; + targetPixels = (int[]) targetImageData.getDataElements(0, + 0, + targetImageData.getWidth(), + targetImageData.getHeight(), + pixelArray); + } + + + /** + * Make sure that the image is in the most efficient format for reading from. + * This avoids having to convert pixels every time we access them. + * @param image The image to convert. + * @return The image converted to INT_RGB format. + */ + private BufferedImage convertImage(BufferedImage image) + { + if (image.getType() == BufferedImage.TYPE_INT_RGB) + { + return image; + } + else + { + BufferedImage newImage = new BufferedImage(image.getWidth(), + image.getHeight(), + BufferedImage.TYPE_INT_RGB); + newImage.getGraphics().drawImage(image, 0, 0, null); + return newImage; + } + } + + + /** + * Render the polygons as an image and then do a pixel-by-pixel comparison + * against the target image. The fitness score is the total error. A lower + * score means a closer match. + * @param candidate The image to evaluate. + * @param population Not used. + * @return A number indicating how close the candidate image is to the target image + * (lower is better). + */ + public double getFitness(List<ColouredPolygon> candidate, + List<? extends List<ColouredPolygon>> population) + { + // Use one renderer per thread because they are not thread safe. + Renderer<List<ColouredPolygon>, BufferedImage> renderer = threadLocalRenderer.get(); + if (renderer == null) + { + renderer = new PolygonImageRenderer(new Dimension(width, height), + false, + transform); + threadLocalRenderer.set(renderer); + } + + BufferedImage candidateImage = renderer.render(candidate); + Raster candidateImageData = candidateImage.getData(); + + int[] candidatePixelValues = new int[targetPixels.length]; + candidatePixelValues = (int[]) candidateImageData.getDataElements(0, + 0, + candidateImageData.getWidth(), + candidateImageData.getHeight(), + candidatePixelValues); + double fitness = 0; + for (int i = 0; i < targetPixels.length; i++) + { + fitness += comparePixels(targetPixels[i], candidatePixelValues[i]); + } + + return fitness; + } + + + private double comparePixels(int p1, int p2) + { + int deltaR = ((p1 >>> 16) & 0xFF) - ((p2 >>> 16) & 0xFF); + int deltaG = ((p1 >>> 8) & 0xFF) - ((p2 >>> 8) & 0xFF); + int deltaB = (p1 & 0xFF) - (p2 & 0xFF); + return Math.sqrt(deltaR * deltaR + deltaG * deltaG + deltaB * deltaB); + } + + + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactory.java new file mode 100644 index 0000000..9613172 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactory.java @@ -0,0 +1,75 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Creates random polygon-based images. + * @author Daniel Dyer + */ +public class PolygonImageFactory extends AbstractCandidateFactory<List<ColouredPolygon>> +{ + /** + * Each image must have at least 2 polygons. + */ + static final int MINIMUM_POLYGON_COUNT = 2; + + /** + * Each polygon must have at least 3 points. + */ + static final int MINIMUM_VERTEX_COUNT = 3; + + private final Dimension canvasSize; + + /** + * @param canvasSize The size of the canvas on which the image will be rendered. + * All polygons must fit within its bounds. + */ + public PolygonImageFactory(Dimension canvasSize) + { + this.canvasSize = canvasSize; + } + + + public List<ColouredPolygon> generateRandomCandidate(Random rng) + { + List<ColouredPolygon> polygons = new ArrayList<ColouredPolygon>(MINIMUM_POLYGON_COUNT); + for (int i = 0; i < MINIMUM_POLYGON_COUNT; i++) + { + polygons.add(createRandomPolygon(rng)); + } + return polygons; + } + + + ColouredPolygon createRandomPolygon(Random rng) + { + List<Point> vertices = new ArrayList<Point>(MINIMUM_VERTEX_COUNT); + for (int j = 0; j < MINIMUM_VERTEX_COUNT; j++) + { + vertices.add(new Point(rng.nextInt(canvasSize.width), rng.nextInt(canvasSize.height))); + } + Color colour = new Color(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256), rng.nextInt(256)); + return new ColouredPolygon(colour, vertices); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageRenderer.java new file mode 100644 index 0000000..876c9f0 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageRenderer.java @@ -0,0 +1,91 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.util.List; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Renders a polygon-based image to a {@link BufferedImage}. For efficiency reasons, this + * renderer returns the same image object (with different data) for subsequent invocations. + * This means that invoking code should not expect returned images to be unaltered following + * subsequent invocations. It also means that a renderer is not thread-safe. + * @author Daniel Dyer + */ +public class PolygonImageRenderer implements Renderer<List<ColouredPolygon>, BufferedImage> +{ + private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); + + private final Dimension targetSize; + private final AffineTransform transform; + private final BufferedImage image; + private final Graphics2D graphics; + + + /** + * @param targetSize The size of the canvas on which the polygons will be rendered. + * @param antialias Whether or not to enable anti-aliasing for the rendered image. + * @param transform A transformation applied to the vertices of an image's polygons + * before drawing to the destination image. This transformation adjusts the image + * so that it fits on a canvas of the specified {@code targetSize}. + */ + public PolygonImageRenderer(Dimension targetSize, + boolean antialias, + AffineTransform transform) + { + this.targetSize = targetSize; + this.transform = transform; + this.image = new BufferedImage(targetSize.width, + targetSize.height, + BufferedImage.TYPE_INT_RGB); + this.graphics = image.createGraphics(); + if (antialias) + { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + } + + + /** + * Renders the specified polygons as an image. + * @param entity A collection of coloured polygons. + * @return An image object displaying the polygons. + */ + public BufferedImage render(List<ColouredPolygon> entity) + { + // Need to set the background before applying the transform. + graphics.setTransform(IDENTITY_TRANSFORM); + graphics.setColor(Color.GRAY); + graphics.fillRect(0, 0, targetSize.width, targetSize.height); + if (transform != null) + { + graphics.setTransform(transform); + } + for (ColouredPolygon polygon : entity) + { + graphics.setColor(polygon.getColour()); + graphics.fillPolygon(polygon.getPolygon()); + } + return image; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageSwingRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageSwingRenderer.java new file mode 100644 index 0000000..1ce7e05 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageSwingRenderer.java @@ -0,0 +1,143 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.util.List; +import javax.swing.JComponent; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Converts a {@link BufferedImage} to a {@link JComponent} that displays that image + * alongside a pre-specified target image. + * @author Daniel Dyer + */ +public class PolygonImageSwingRenderer implements Renderer<List<ColouredPolygon>, JComponent> +{ + private final BufferedImage targetImage; + private final Renderer<List<ColouredPolygon>, BufferedImage> delegate; + + + /** + * @param targetImage This image is displayed on the left side of the + * JComponent. + */ + public PolygonImageSwingRenderer(BufferedImage targetImage) + { + // Convert the target image into the most efficient format for rendering. + this.targetImage = new BufferedImage(targetImage.getWidth(), + targetImage.getHeight(), + BufferedImage.TYPE_INT_RGB); + this.targetImage.getGraphics().drawImage(targetImage, 0, 0, null); + this.targetImage.setAccelerationPriority(1); + + this.delegate = new PolygonImageRenderer(new Dimension(targetImage.getWidth(), + targetImage.getHeight()), + true, // Anti-alias. + null); + } + + + /** + * Renders the specified image as a JComponent with the image on the + * right and the pre-specified target image on the left. + * @param entity The image to render on the right side of the component. + * @return A Swing component that displays the rendered image. + */ + public JComponent render(List<ColouredPolygon> entity) + { + return new ImageComponent(entity); + } + + + /** + * Swing component for rendering a fixed size image. If the image is smaller + * than the component, it is centered. + */ + private final class ImageComponent extends JComponent + { + private static final int GAP = 10; + private static final int FOOTER = 20; + + private final List<ColouredPolygon> candidate; + private final Dimension minimumSize; + + + ImageComponent(List<ColouredPolygon> candidate) + { + this.candidate = candidate; + this.minimumSize = new Dimension(targetImage.getWidth() * 2 + GAP, + targetImage.getHeight() + FOOTER); + } + + @Override + public Dimension getPreferredSize() + { + return minimumSize; + } + + @Override + public Dimension getMinimumSize() + { + return minimumSize; + } + + + @Override + protected void paintComponent(Graphics graphics) + { + int x = Math.max(0, (getWidth() - minimumSize.width) / 2); + int y = Math.max(0, (getHeight() - minimumSize.height) / 2); + graphics.drawImage(targetImage, x, y, this); + BufferedImage candidateImage = delegate.render(candidate); + candidateImage.setAccelerationPriority(1); + Graphics clip = graphics.create(x + targetImage.getWidth() + GAP, + y, + candidateImage.getWidth(), + candidateImage.getHeight() + FOOTER); + clip.drawImage(candidateImage, 0, 0, this); + + clip.setColor(getForeground()); + String info = candidate.size() + " polygons, " + countVertices(candidate) + " vertices"; + FontMetrics fontMetrics = clip.getFontMetrics(); + int width = fontMetrics.stringWidth(info); + int height = Math.round(fontMetrics.getLineMetrics(info, clip).getHeight()); + clip.drawString(info, + candidateImage.getWidth() - width, + candidateImage.getHeight() + height); + } + + + /** + * Count the number of vertices in each polygon in the image and return + * the total. + * @param image The image to inspect. + * @return The total number of vertices in all polygons in the image. + */ + private int countVertices(List<ColouredPolygon> image) + { + int count = 0; + for (ColouredPolygon polygon : image) + { + count += polygon.getVertices().size(); + } + return count; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanel.java new file mode 100644 index 0000000..5267be3 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanel.java @@ -0,0 +1,173 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SpringLayout; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.GaussianGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.operators.ListCrossover; +import org.uncommons.watchmaker.framework.operators.ListOperator; +import org.uncommons.watchmaker.swing.ProbabilityParameterControl; + +/** + * Panel that displays controls for the Mona Lisa example program. These + * controls allow the evolution parameters to be tweaked. + * @author Daniel Dyer + */ +class ProbabilitiesPanel extends JPanel +{ + private static final Probability ONE_TENTH = new Probability(0.1d); + + private final ProbabilityParameterControl addPolygonControl; + private final ProbabilityParameterControl removePolygonControl; + private final ProbabilityParameterControl movePolygonControl; + private final ProbabilityParameterControl crossOverControl; + private final ProbabilityParameterControl addVertexControl; + private final ProbabilityParameterControl removeVertexControl; + private final ProbabilityParameterControl moveVertexControl; + private final ProbabilityParameterControl changeColourControl; + + + ProbabilitiesPanel() + { + super(new SpringLayout()); + addPolygonControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.02)); + add(new JLabel("Add Polygon: ")); + add(addPolygonControl.getControl()); + addPolygonControl.setDescription("For each IMAGE, the probability that a new " + + "randomly-generated polygon will be added."); + + addVertexControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Add Vertex: ")); + add(addVertexControl.getControl()); + addVertexControl.setDescription("For each POLYGON, the probability that a new " + + "randomly-generated vertex will be added."); + + removePolygonControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.02)); + add(new JLabel("Remove Polygon: ")); + add(removePolygonControl.getControl()); + removePolygonControl.setDescription("For each IMAGE, the probability that a " + + "randomly-selected polygon will be discarded."); + + removeVertexControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Remove Vertex: ")); + add(removeVertexControl.getControl()); + removeVertexControl.setDescription("For each POLYGON, the probability that a " + + "randomly-selected vertex will be discarded."); + + movePolygonControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Reorder Polygons: ")); + add(movePolygonControl.getControl()); + movePolygonControl.setDescription("For each IMAGE, the probability that the z-positions " + + "of two randomly-selected polygons will be swapped."); + + moveVertexControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.03)); + add(new JLabel("Move Vertex: ")); + add(moveVertexControl.getControl()); + moveVertexControl.setDescription("For each POLYGON, the probability that a randomly-selected " + + "vertex will be displaced."); + + crossOverControl = new ProbabilityParameterControl(Probability.ZERO, + Probability.ONE, + 2, + Probability.ONE); + add(new JLabel("Cross-over: ")); + add(crossOverControl.getControl()); + crossOverControl.setDescription("For each PAIR of parent IMAGES, the probability that " + + "2-point cross-over is applied."); + + changeColourControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Change Colour: ")); + add(changeColourControl.getControl()); + changeColourControl.setDescription("For each POLYGON, the probability that its colour will be mutated."); + + // Set component names for easy look-up from tests. + addPolygonControl.getControl().setName("AddPolygon"); + removePolygonControl.getControl().setName("RemovePolygon"); + movePolygonControl.getControl().setName("MovePolygon"); + crossOverControl.getControl().setName("Cross-over"); + addVertexControl.getControl().setName("AddVertex"); + removeVertexControl.getControl().setName("RemoveVertex"); + moveVertexControl.getControl().setName("MoveVertex"); + changeColourControl.getControl().setName("ChangeColour"); + + SpringUtilities.makeCompactGrid(this, 4, 4, 10, 0, 10, 0); + } + + + /** + * Construct the combination of evolutionary operators that will be used to evolve the + * polygon-based images. + * @param factory A source of polygons. + * @param canvasSize The size of the target image. + * @param rng A source of randomness. + * @return A complex evolutionary operator constructed from simpler operators. + */ + public EvolutionaryOperator<List<ColouredPolygon>> createEvolutionPipeline(PolygonImageFactory factory, + Dimension canvasSize, + Random rng) + { + List<EvolutionaryOperator<List<ColouredPolygon>>> operators + = new LinkedList<EvolutionaryOperator<List<ColouredPolygon>>>(); + operators.add(new ListCrossover<ColouredPolygon>(new ConstantGenerator<Integer>(2), + crossOverControl.getNumberGenerator())); + operators.add(new RemovePolygonMutation(removePolygonControl.getNumberGenerator())); + operators.add(new MovePolygonMutation(movePolygonControl.getNumberGenerator())); + operators.add(new ListOperator<ColouredPolygon>(new RemoveVertexMutation(canvasSize, + removeVertexControl.getNumberGenerator()))); + operators.add(new ListOperator<ColouredPolygon>(new AdjustVertexMutation(canvasSize, + moveVertexControl.getNumberGenerator(), + new GaussianGenerator(0, 3, rng)))); + operators.add(new ListOperator<ColouredPolygon>(new AddVertexMutation(canvasSize, + addVertexControl.getNumberGenerator()))); + operators.add(new ListOperator<ColouredPolygon>(new PolygonColourMutation(changeColourControl.getNumberGenerator(), + new GaussianGenerator(0, 20, rng)))); + operators.add(new AddPolygonMutation(addPolygonControl.getNumberGenerator(), factory, 50)); + return new EvolutionPipeline<List<ColouredPolygon>>(operators); + } + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutation.java new file mode 100644 index 0000000..d1d33bb --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutation.java @@ -0,0 +1,75 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Randomly mutates the polygons that make up an image by removing a polygon + * according to some probability. + * @author Daniel Dyer + */ +public class RemovePolygonMutation implements EvolutionaryOperator<List<ColouredPolygon>> +{ + private final NumberGenerator<Probability> removePolygonProbability; + + /** + * @param removePolygonProbability A {@link NumberGenerator} that controls the probability + * that a polygon will be removed. + */ + public RemovePolygonMutation(NumberGenerator<Probability> removePolygonProbability) + { + this.removePolygonProbability = removePolygonProbability; + } + + + /** + * @param removePolygonProbability The probability that a polygon will be removed. + */ + public RemovePolygonMutation(Probability removePolygonProbability) + { + this(new ConstantGenerator<Probability>(removePolygonProbability)); + } + + + public List<List<ColouredPolygon>> apply(List<List<ColouredPolygon>> selectedCandidates, Random rng) + { + List<List<ColouredPolygon>> mutatedCandidates = new ArrayList<List<ColouredPolygon>>(selectedCandidates.size()); + for (List<ColouredPolygon> candidate : selectedCandidates) + { + // A single polygon is removed with the configured probability, unless + // we already have the minimum permitted number of polygons. + if (candidate.size() > PolygonImageFactory.MINIMUM_POLYGON_COUNT + && removePolygonProbability.nextValue().nextEvent(rng)) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(candidate); + newPolygons.remove(rng.nextInt(newPolygons.size())); + mutatedCandidates.add(newPolygons); + } + else // Nothing changed. + { + mutatedCandidates.add(candidate); + } + } + return mutatedCandidates; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutation.java new file mode 100644 index 0000000..4ec530d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutation.java @@ -0,0 +1,76 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by removing a point, according to some probability. + * @author Daniel Dyer + */ +public class RemoveVertexMutation extends AbstractVertexMutation +{ + /** + * @param mutationProbability A {@link NumberGenerator} that controls the + * probability that a point will be removed. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + */ + public RemoveVertexMutation(Dimension canvasSize, + NumberGenerator<Probability> mutationProbability) + { + super(mutationProbability, canvasSize); + } + + + /** + * @param mutationProbability The probability that a point will be removed. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + */ + public RemoveVertexMutation(Dimension canvasSize, + Probability mutationProbability) + { + this(canvasSize, new ConstantGenerator<Probability>(mutationProbability)); + } + + + @Override + protected List<Point> mutateVertices(List<Point> vertices, Random rng) + { + // A single point is removed with the configured probability, unless + // we already have the minimum permitted number of points. + if (vertices.size() > PolygonImageFactory.MINIMUM_VERTEX_COUNT + && getMutationProbability().nextValue().nextEvent(rng)) + { + List<Point> newVertices = new ArrayList<Point>(vertices); + newVertices.remove(rng.nextInt(newVertices.size())); + return newVertices; + } + else // Nothing changed. + { + return vertices; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/package-info.java new file mode 100644 index 0000000..1602c91 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/package-info.java @@ -0,0 +1,23 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary art example application inspired by Roger Alsing's evolution of the + * Mona Lisa (http://rogeralsing.com/2008/12/07/genetic-programming-evolution-of-mona-lisa/). + * It attempts to find the combination of 50 translucent polygons that most closely + * resembles Leonardo da Vinci's Mona Lisa. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.monalisa; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringEvaluator.java new file mode 100644 index 0000000..ac56c9f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringEvaluator.java @@ -0,0 +1,73 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import java.util.List; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Evaluates strings and assigns a fitness score based on how many characters + * differ from the equivalent positions in a given target string. + * @author Daniel Dyer + */ +public class StringEvaluator implements FitnessEvaluator<String> +{ + private final String targetString; + + + /** + * Creates a {@link FitnessEvaluator} that calculates scores + * for Strings based on how close they are to a target String. + * @param targetString The target of the evolution. + */ + public StringEvaluator(String targetString) + { + this.targetString = targetString; + } + + + /** + * Assigns one "penalty point" for every character in the candidate + * string that differs from the corresponding position in the target + * string. + * @param candidate The evolved string to evaluate. + * @param population {@inheritDoc} + * @return The fitness score (how many characters are wrong) of the + * specified string. + */ + public double getFitness(String candidate, + List<? extends String> population) + { + int errors = 0; + for (int i = 0; i < candidate.length(); i++) + { + if (candidate.charAt(i) != targetString.charAt(i)) + { + ++errors; + } + } + return errors; + } + + + /** + * {@inheritDoc} + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringsExample.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringsExample.java new file mode 100644 index 0000000..d8425fa --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringsExample.java @@ -0,0 +1,119 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import java.util.ArrayList; +import java.util.List; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.factories.StringFactory; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.operators.StringCrossover; +import org.uncommons.watchmaker.framework.operators.StringMutation; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; + +/** + * Simple evolutionary algorithm that evolves a population of randomly-generated + * strings until at least one matches a specified target string. + * @author Daniel Dyer + */ +public final class StringsExample +{ + private static final char[] ALPHABET = new char[27]; + static + { + for (char c = 'A'; c <= 'Z'; c++) + { + ALPHABET[c - 'A'] = c; + } + ALPHABET[26] = ' '; + } + + + /** + * Entry point for the sample application. Any data specified on the + * command line is considered to be the target String. If no target is + * specified, a default of "HELLOW WORLD" is used instead. + * @param args The target String (as an array of words). + */ + public static void main(String[] args) + { + String target = args.length == 0 ? "HELLO WORLD" : convertArgs(args); + String result = evolveString(target); + System.out.println("Evolution result: " + result); + } + + + public static String evolveString(String target) + { + StringFactory factory = new StringFactory(ALPHABET, target.length()); + List<EvolutionaryOperator<String>> operators = new ArrayList<EvolutionaryOperator<String>>(2); + operators.add(new StringMutation(ALPHABET, new Probability(0.02d))); + operators.add(new StringCrossover()); + EvolutionaryOperator<String> pipeline = new EvolutionPipeline<String>(operators); + EvolutionEngine<String> engine = new GenerationalEvolutionEngine<String>(factory, + pipeline, + new StringEvaluator(target), + new RouletteWheelSelection(), + new MersenneTwisterRNG()); + engine.addEvolutionObserver(new EvolutionLogger()); + return engine.evolve(100, // 100 individuals in the population. + 5, // 5% elitism. + new TargetFitness(0, false)); + } + + + /** + * Converts an arguments array into a single String of words + * separated by spaces. + * @param args The command-line arguments. + * @return A single String made from the command line data. + */ + private static String convertArgs(String[] args) + { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < args.length; i++) + { + result.append(args[i]); + if (i < args.length - 1) + { + result.append(' '); + } + } + return result.toString().toUpperCase(); + } + + + /** + * Trivial evolution observer for displaying information at the end + * of each generation. + */ + private static class EvolutionLogger implements EvolutionObserver<String> + { + public void populationUpdate(PopulationData<? extends String> data) + { + System.out.printf("Generation %d: %s\n", + data.getGenerationNumber(), + data.getBestCandidate()); + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/package-info.java new file mode 100644 index 0000000..f5f4489 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/package-info.java @@ -0,0 +1,22 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * Simple example that demonstrates how to use the evolution framework. + * Evolves candidates of type String, attempting to find a particular + * target phrase. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.strings; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/Sudoku.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/Sudoku.java new file mode 100644 index 0000000..9319177 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/Sudoku.java @@ -0,0 +1,150 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +/** + * A potential solution for a Sudoku puzzle. + * @author Daniel Dyer + */ +public final class Sudoku +{ + /** The dimensions (in cells) of the puzzle square. */ + public static final int SIZE = 9; + private static final int MIN_VALUE = 1; + private static final int MAX_VALUE = SIZE; + + private final Cell[][] cells; + + + /** + * Creates a sudoku solution from a 2-dimensional array of cells. + * @param cells The cells that make-up this Sudoku grid. + */ + public Sudoku(Cell[][] cells) + { + if (cells.length != SIZE) + { + throw new IllegalArgumentException("Sudoku must have 9 rows."); + } + if (cells[0].length != SIZE) // Should really check all rows because the array may not be square. + { + throw new IllegalArgumentException("Sudoku must have 9 columns."); + } + this.cells = cells; + } + + + /** + * Queries the value of a particular cell. + * @param row The row index of the cell. + * @param column The column index of the cell. + * @return The value (1 - 9) of the specified cell. + */ + public int getValue(int row, int column) + { + return cells[row][column].getValue(); + } + + + /** + * Checks whether a particular cell is a 'given' or not. + * @param row The row index of the cell. + * @param column The column index of the cell. + * @return True if the value in the identified cell is fixed (a 'given' cell), + * false otherwise. + */ + public boolean isFixed(int row, int column) + { + return cells[row][column].isFixed(); + } + + + /** + * Returns an array of cells that make up a row. The array + * returned is a clone of the underlying data structure and + * therefore can be modified without affecting this object. + * @param row The index of the row to return. + * @return A row of cells from this Sudoku grid. + */ + public Cell[] getRow(int row) + { + return cells[row].clone(); + } + + + /** + * Renders the Sudoku grid as a multi-line String. + * @return The String representation of this grid. + */ + @Override + public String toString() + { + StringBuilder buffer = new StringBuilder(); + for (Cell[] row : cells) + { + for (Cell cell : row) + { + buffer.append(' '); + buffer.append(cell.getValue()); + } + buffer.append('\n'); + } + return buffer.toString(); + } + + + /** + * A single cell in a sudoku grid. Contains a value and may be fixed (i.e. it is + * one of the given cells at the start). + */ + public static final class Cell + { + private final int value; + private final boolean fixed; + + /** + * @param value The value (1 - 9) contained in this cell. + * @param fixed Whether or not this cell's value is fixed (a 'given'). + */ + public Cell(int value, boolean fixed) + { + if (value < MIN_VALUE || value > MAX_VALUE) + { + throw new IllegalArgumentException("Value must be between 1 and 9."); + } + this.value = value; + this.fixed = fixed; + } + + + /** + * @return The value contained in this cell. + */ + public int getValue() + { + return value; + } + + + /** + * @return True if this cell is a 'given', false otherwise. + */ + public boolean isFixed() + { + return fixed; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuApplet.java new file mode 100644 index 0000000..2a856e9 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuApplet.java @@ -0,0 +1,283 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SpringLayout; +import javax.swing.SwingUtilities; +import org.uncommons.maths.random.DiscreteUniformGenerator; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.PoissonGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.selection.TournamentSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; +import org.uncommons.watchmaker.swing.AbortControl; +import org.uncommons.watchmaker.swing.ProbabilityParameterControl; +import org.uncommons.watchmaker.swing.SwingEvolutionObserver; +import org.uncommons.watchmaker.swing.evolutionmonitor.StatusBar; + +/** + * An evolutionary Sudoku solver. + * @author Daniel Dyer + */ +public class SudokuApplet extends AbstractExampleApplet +{ + private static final String[] BLANK_PUZZLE = {".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + "........."}; + + private static final String[] EASY_PUZZLE = {"4.5...9.7", + ".2..9..6.", + "39.6.7.28", + "9..3.2..6", + "7..9.6..3", + "5..4.8..1", + "28.1.5.49", + ".7..3..8.", + "6.4...3.2"}; + + private static final String[] MEDIUM_PUZZLE = {"....3....", + ".....6293", + ".2.9.48..", + ".754...38", + "..46.71..", + "91...547.", + "..38.9.1.", + "1567.....", + "....1...."}; + + private static final String[] HARD_PUZZLE = {"...891...", + "....5.8..", + ".....6.2.", + "5....4..8", + "49....67.", + "8.13....5", + ".6..8..9.", + "..5.4.2.7", + "...1.3.8."}; + + private static final String[][] PUZZLES = {EASY_PUZZLE, + MEDIUM_PUZZLE, + HARD_PUZZLE, + BLANK_PUZZLE}; + + private SelectionStrategy<Object> selectionStrategy; + + private SudokuView sudokuView; + private JButton solveButton; + private JComboBox puzzleCombo; + private JSpinner populationSizeSpinner; + private AbortControl abortControl; + private StatusBar statusBar; + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + sudokuView = new SudokuView(); + container.add(createControls(), BorderLayout.NORTH); + container.add(sudokuView, BorderLayout.CENTER); + statusBar = new StatusBar(); + container.add(statusBar, BorderLayout.SOUTH); + sudokuView.setPuzzle(EASY_PUZZLE); + } + + + private JComponent createControls() + { + JPanel controls = new JPanel(new BorderLayout()); + JPanel innerPanel = new JPanel(new SpringLayout()); + innerPanel.add(new JLabel("Puzzle: ")); + puzzleCombo = new JComboBox(new String[]{"Easy Demo (38 givens)", + "Medium Demo (32 givens)", + "Hard Demo (28 givens)", + "Custom"}); + innerPanel.add(puzzleCombo); + puzzleCombo.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent ev) + { + sudokuView.setPuzzle(PUZZLES[puzzleCombo.getSelectedIndex()]); + } + }); + innerPanel.add(new JLabel("Selection Pressure: ")); + ProbabilityParameterControl selectionPressure = new ProbabilityParameterControl(Probability.EVENS, + Probability.ONE, + 2, + new Probability(0.85d)); + + selectionStrategy = new TournamentSelection(selectionPressure.getNumberGenerator()); + innerPanel.add(selectionPressure.getControl()); + innerPanel.add(new JLabel("Population Size: ")); + populationSizeSpinner = new JSpinner(new SpinnerNumberModel(500, 10, 50000, 1)); + innerPanel.add(populationSizeSpinner); + SpringUtilities.makeCompactGrid(innerPanel, 3, 2, 0, 6, 6, 6); + innerPanel.setBorder(BorderFactory.createTitledBorder("Configuration")); + controls.add(innerPanel, BorderLayout.CENTER); + controls.add(createButtonPanel(), BorderLayout.SOUTH); + return controls; + } + + + private JComponent createButtonPanel() + { + JPanel buttonPanel = new JPanel(new FlowLayout()); + solveButton = new JButton("Solve"); + solveButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent ev) + { + int populationSize = (Integer) populationSizeSpinner.getValue(); + puzzleCombo.setEnabled(false); + populationSizeSpinner.setEnabled(false); + solveButton.setEnabled(false); + abortControl.reset(); + createTask(sudokuView.getPuzzle(), + populationSize, + (int) Math.round(populationSize * 0.05)).execute(); // Elite count is 5%. + } + }); + + buttonPanel.add(solveButton); + abortControl = new AbortControl(); + buttonPanel.add(abortControl.getControl()); + abortControl.getControl().setEnabled(false); + return buttonPanel; + } + + + /** + * Helper method to create a background task for running the interactive evolutionary + * algorithm. + * @return A Swing task that will execute on a background thread and update + * the GUI when it is done. + */ + private SwingBackgroundTask<Sudoku> createTask(final String[] puzzle, + final int populationSize, + final int eliteCount) + { + return new SwingBackgroundTask<Sudoku>() + { + @Override + protected Sudoku performTask() + { + Random rng = new MersenneTwisterRNG(); + List<EvolutionaryOperator<Sudoku>> operators = new ArrayList<EvolutionaryOperator<Sudoku>>(2); + // Cross-over rows between parents (so offspring is x rows from parent1 and + // y rows from parent2). + operators.add(new SudokuVerticalCrossover()); + // Mutate the order of cells within individual rows. + operators.add(new SudokuRowMutation(new PoissonGenerator(2, rng), + new DiscreteUniformGenerator(1, 8, rng))); + + EvolutionaryOperator<Sudoku> pipeline = new EvolutionPipeline<Sudoku>(operators); + + EvolutionEngine<Sudoku> engine = new GenerationalEvolutionEngine<Sudoku>(new SudokuFactory(puzzle), + pipeline, + new SudokuEvaluator(), + selectionStrategy, + rng); + engine.addEvolutionObserver(new SwingEvolutionObserver<Sudoku>(new GridViewUpdater(), + 100, + TimeUnit.MILLISECONDS)); + engine.addEvolutionObserver(statusBar); + return engine.evolve(populationSize, + eliteCount, + new TargetFitness(0, false), // Continue until a perfect solution is found... + abortControl.getTerminationCondition()); // ...or the user aborts. + } + + + @Override + protected void postProcessing(Sudoku result) + { + puzzleCombo.setEnabled(true); + populationSizeSpinner.setEnabled(true); + solveButton.setEnabled(true); + abortControl.getControl().setEnabled(false); + } + }; + } + + + + /** + * Evolution observer for displaying information at the end of + * each generation. + */ + private class GridViewUpdater implements EvolutionObserver<Sudoku> + { + public void populationUpdate(final PopulationData<? extends Sudoku> data) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + sudokuView.setSolution(data.getBestCandidate()); + } + }); + } + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + */ + public static void main(String[] args) + { + new SudokuApplet().displayInFrame("Watchmaker Framework - Sudoku Example"); + } + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuCellRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuCellRenderer.java new file mode 100644 index 0000000..ebfa15a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuCellRenderer.java @@ -0,0 +1,162 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import javax.swing.table.DefaultTableCellRenderer; +import org.uncommons.swing.ConfigurableLineBorder; + +/** + * Renders Sudoku cells in a JTable. As well as displaying the cell value, + * grid lines are drawn to conform to normal Sudoku conventions and incorrect + * solutions have their invalid cells colour-coded. + * @author Daniel Dyer + */ +class SudokuCellRenderer extends DefaultTableCellRenderer +{ + private static final Font VARIABLE_CELL_FONT = new Font("Monospaced", Font.PLAIN, 32); + private static final Font FIXED_CELL_FONT = new Font("Monospaced", Font.BOLD, 34); + private static final Color VARIABLE_TEXT_COLOUR = Color.DARK_GRAY; + private static final Color FIXED_TEXT_COLOUR = Color.BLACK; + + private static final Color[] CONFLICT_COLOURS = {Color.WHITE, Color.YELLOW, Color.ORANGE, Color.RED}; + + private static final Border TOP_BORDER = new ConfigurableLineBorder(true, false, false, false, 1); + private static final Border BOTTOM_BORDER = new ConfigurableLineBorder(false, false, true, false, 1); + private static final Border LEFT_BORDER = new ConfigurableLineBorder(false, true, false, false, 1); + private static final Border RIGHT_BORDER = new ConfigurableLineBorder(false, false, false, true, 1); + private static final Border TOP_LEFT_BORDER = new ConfigurableLineBorder(true, true, false, false, 1); + private static final Border TOP_RIGHT_BORDER = new ConfigurableLineBorder(true, false, false, true, 1); + private static final Border BOTTOM_LEFT_BORDER = new ConfigurableLineBorder(false, true, true, false, 1); + private static final Border BOTTOM_RIGHT_BORDER = new ConfigurableLineBorder(false, false, true, true, 1); + + + SudokuCellRenderer() + { + setHorizontalAlignment(JLabel.CENTER); + } + + + @Override + public Component getTableCellRendererComponent(JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column) + { + Component component = super.getTableCellRendererComponent(table, + value, + false, + false, + row, + column); + SudokuTableModel model = (SudokuTableModel) table.getModel(); + Sudoku sudoku = model.getSudoku(); + if (sudoku != null) + { + if (sudoku.isFixed(row, column)) + { + component.setFont(FIXED_CELL_FONT); + component.setForeground(FIXED_TEXT_COLOUR); + } + else + { + component.setFont(VARIABLE_CELL_FONT); + component.setForeground(VARIABLE_TEXT_COLOUR); + } + + int conflicts = 0; + // Calculate conflicts in columns (there should be no conflicts in rows + // because of the constraints enforced by the evolutionary operators). + for (int i = 0; i < Sudoku.SIZE; i++) + { + if (i != row && model.getValueAt(i, column).equals(value)) + { + ++conflicts; + } + } + // Calculate conflicts in sub-grid. + int band = row / 3; + int bandStart = band * 3; + int stack = column / 3; + int stackStart = stack * 3; + for (int i = bandStart; i < bandStart + 3; i++) + { + for (int j = stackStart; j < stackStart + 3; j++) + { + if (i != row && j != column && model.getValueAt(i, j).equals(value)) + { + ++conflicts; + } + } + } + + // Color the cell based on how "wrong" it is. + component.setBackground(CONFLICT_COLOURS[Math.min(conflicts, CONFLICT_COLOURS.length - 1)]); + } + else + { + // If the model is in puzzle mode, then all non-null cells are 'givens'. + component.setFont(FIXED_CELL_FONT); + component.setForeground(FIXED_TEXT_COLOUR); + } + + ((JComponent) component).setBorder(getBorder(row, column)); + + + return component; + } + + + /** + * Get appropriate border for cell based on its position in the grid. + */ + private Border getBorder(int row, int column) + { + if (row % 3 == 2) + { + switch (column % 3) + { + case 2: return BOTTOM_RIGHT_BORDER; + case 0: return BOTTOM_LEFT_BORDER; + default: return BOTTOM_BORDER; + } + } + else if (row % 3 == 0) + { + switch (column % 3) + { + case 2: return TOP_RIGHT_BORDER; + case 0: return TOP_LEFT_BORDER; + default: return TOP_BORDER; + } + } + + switch (column % 3) + { + case 2: return RIGHT_BORDER; + case 0: return LEFT_BORDER; + default: return null; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluator.java new file mode 100644 index 0000000..ca44394 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluator.java @@ -0,0 +1,89 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * {@link org.uncommons.watchmaker.framework.FitnessEvaluator} for potential Sudoku + * solutions. Counts the number of duplicate values in rows, columns and sub-grids. + * The fitness score is the total number of duplicate values. Therefore, a fitness + * score of zero indicates a perfect solution. + * @author Daniel Dyer + */ +public class SudokuEvaluator implements FitnessEvaluator<Sudoku> +{ + /** + * The fitness score for a potential Sudoku solution is the number of + * cells that conflict with other cells in the grid (i.e. if there are + * two 7s in the same column, both of these cells are conflicting). A + * lower score indicates a fitter individual. + * @param candidate The Sudoku grid to evaluate. + * @param population {@inheritDoc} + * @return The fitness score for the specified individual. + */ + public double getFitness(Sudoku candidate, + List<? extends Sudoku> population) + { + // We can assume that there are no duplicates in any rows because + // the candidate factory and evolutionary operators that we use do + // not permit rows to contain duplicates. + + int fitness = 0; + + // Check columns for duplicates. + Set<Integer> values = new HashSet<Integer>(Sudoku.SIZE * 2); // Big enough to avoid re-hashing. + for (int column = 0; column < Sudoku.SIZE; column++) + { + for (int row = 0; row < Sudoku.SIZE; row++) + { + values.add(candidate.getValue(row, column)); + } + fitness += (Sudoku.SIZE - values.size()); + values.clear(); + } + + // Check sub-grids for duplicates. + for (int band = 0; band < Sudoku.SIZE; band += 3) + { + for (int stack = 0; stack < Sudoku.SIZE; stack += 3) + { + for (int row = band; row < band + 3; row++) + { + for (int column = stack; column < stack + 3; column++) + { + values.add(candidate.getValue(row, column)); + } + } + fitness += (Sudoku.SIZE - values.size()); + values.clear(); + } + } + return fitness; + } + + + /** + * @return false + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuFactory.java new file mode 100644 index 0000000..d96ed1a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuFactory.java @@ -0,0 +1,125 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Factory that generates potential Sudoku solutions from a list of "givens". + * The rows of the generated solutions will all be valid (i.e. no duplicate values) + * but there are no constraints on the columns or sub-grids (these will be refined + * by the evolutionary algorithm). + * @author Daniel Dyer + */ +public class SudokuFactory extends AbstractCandidateFactory<Sudoku> +{ + private static final List<Integer> VALUES = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); + + private final Sudoku.Cell[][] template; + private final List<List<Integer>> nonFixedValues = new ArrayList<List<Integer>>(Sudoku.SIZE); + + + /** + * Creates a factory for generating random candidate solutions + * for a specified Sudoku puzzle. + * @param pattern An array of Strings. Each element represents + * one row in the puzzle. Each character represents a single + * cell. Permitted characters are the digits '1' to '9' (each + * of which represents a fixed cell in the pattern) or the '.' + * character, which represents an empty cell. + * @throws IllegalArgumentException If {@literal pattern} does not + * consist of nine Strings with nine characters ('0' to '9', or '.') + * in each. + */ + public SudokuFactory(String... pattern) + { + if (pattern.length != Sudoku.SIZE) + { + throw new IllegalArgumentException("Sudoku layout must have " + Sudoku.SIZE + " rows."); + } + + this.template = new Sudoku.Cell[Sudoku.SIZE][Sudoku.SIZE]; + + // Keep track of which values in each row are not 'givens'. + for (int i = 0; i < Sudoku.SIZE; i++) + { + nonFixedValues.add(new ArrayList<Integer>(VALUES)); + } + + for (int i = 0; i < pattern.length; i++) + { + char[] rowPattern = pattern[i].toCharArray(); + if (rowPattern.length != Sudoku.SIZE) + { + throw new IllegalArgumentException("Sudoku layout must have " + Sudoku.SIZE + " cells in each row."); + } + for (int j = 0; j < rowPattern.length; j++) + { + char c = rowPattern[j]; + if (c >= '1' && c <= '9') // Cell is a 'given'. + { + int value = c - '0'; // Convert char to in that it represents.. + template[i][j] = new Sudoku.Cell(value, true); + List<Integer> rowValues = nonFixedValues.get(i); + int index = Collections.binarySearch(rowValues, value); + rowValues.remove(index); + } + else if (c != '.') + { + throw new IllegalArgumentException("Unexpected character at (" + i + ", " + j + "): " + c); + } + } + } + } + + + /** + * {@inheritDoc} + * The generated potential solution is guaranteed to have no + * duplicates in any row but could have duplicates in a column or sub-grid. + */ + public Sudoku generateRandomCandidate(Random rng) + { + // Clone the template as the basis for this grid. + Sudoku.Cell[][] rows = template.clone(); + for (int i = 0; i < rows.length; i++) + { + rows[i] = template[i].clone(); + } + + // Fill-in the non-fixed cells. + for (int i = 0; i < rows.length; i++) + { + List<Integer> rowValues = nonFixedValues.get(i); + Collections.shuffle(rowValues); + int index = 0; + for (int j = 0; j < rows[i].length; j++) + { + if (rows[i][j] == null) + { + rows[i][j] = new Sudoku.Cell(rowValues.get(index), false); + ++index; + } + } + } + return new Sudoku(rows); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutation.java new file mode 100644 index 0000000..029ae2b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutation.java @@ -0,0 +1,206 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutates rows in a potential Sudoku solution by manipulating the order + * of non-fixed cells in much the same way as the + * {@link org.uncommons.watchmaker.framework.operators.ListOrderMutation} + * operator does in the Travelling Salesman example. + * @author Daniel Dyer + */ +public class SudokuRowMutation implements EvolutionaryOperator<Sudoku> +{ + private final NumberGenerator<Integer> mutationCountVariable; + private final NumberGenerator<Integer> mutationAmountVariable; + + // These look-up tables keep track of which values are fixed in which columns + // and sub-grids. Because the values are fixed, they are the same for all + // potential solutions, so we cache the information here to minimise the amount + // of processing that needs to be done for each mutation. There is no need to + // worry about rows since the mutation ensures that rows are always valid. + private final boolean[][] columnFixedValues = new boolean[Sudoku.SIZE][Sudoku.SIZE]; + private final boolean[][] subGridFixedValues = new boolean[Sudoku.SIZE][Sudoku.SIZE]; + private boolean cached = false; + + /** + * Default is one mutation per candidate. + */ + public SudokuRowMutation() + { + this(1, 1); + } + + /** + * @param mutationCount The constant number of mutations + * to apply to each row in a Sudoku solution. + * @param mutationAmount The constant number of positions by + * which a list element will be displaced as a result of mutation. + */ + public SudokuRowMutation(int mutationCount, int mutationAmount) + { + this(new ConstantGenerator<Integer>(mutationCount), + new ConstantGenerator<Integer>(mutationAmount)); + if (mutationCount < 1) + { + throw new IllegalArgumentException("Mutation count must be at least 1."); + } + else if (mutationAmount < 1) + { + throw new IllegalArgumentException("Mutation amount must be at least 1."); + } + } + + + /** + * Typically the mutation count will be from a Poisson distribution. + * The mutation amount can be from any discrete probability distribution + * and can include negative values. + * @param mutationCount A random variable that provides a number + * of mutations that will be applied to each row in an individual. + * @param mutationAmount A random variable that provides a number + * of positions by which to displace an element when mutating. + */ + public SudokuRowMutation(NumberGenerator<Integer> mutationCount, + NumberGenerator<Integer> mutationAmount) + { + this.mutationCountVariable = mutationCount; + this.mutationAmountVariable = mutationAmount; + } + + + public List<Sudoku> apply(List<Sudoku> selectedCandidates, Random rng) + { + if (!cached) + { + buildCache(selectedCandidates.get(0)); + } + + List<Sudoku> mutatedCandidates = new ArrayList<Sudoku>(selectedCandidates.size()); + for (Sudoku sudoku : selectedCandidates) + { + mutatedCandidates.add(mutate(sudoku, rng)); + } + return mutatedCandidates; + } + + + private Sudoku mutate(Sudoku sudoku, Random rng) + { + Sudoku.Cell[][] newRows = new Sudoku.Cell[Sudoku.SIZE][]; + // Mutate each row in turn. + for (int i = 0; i < Sudoku.SIZE; i++) + { + newRows[i] = new Sudoku.Cell[Sudoku.SIZE]; + System.arraycopy(sudoku.getRow(i), 0, newRows[i], 0, Sudoku.SIZE); + } + + int mutationCount = Math.abs(mutationCountVariable.nextValue()); + while (mutationCount > 0) + { + int row = rng.nextInt(Sudoku.SIZE); + int fromIndex = rng.nextInt(Sudoku.SIZE); + int mutationAmount = mutationAmountVariable.nextValue(); + int toIndex = (fromIndex + mutationAmount) % Sudoku.SIZE; + + // Make sure we're not trying to mutate a 'given'. + if (!newRows[row][fromIndex].isFixed() + && !newRows[row][toIndex].isFixed() + // ...or trying to introduce a duplicate of a given value. + && (!isIntroducingFixedConflict(sudoku, row, fromIndex, toIndex) + || isRemovingFixedConflict(sudoku, row, fromIndex, toIndex))) + { + // Swap the randomly selected element with the one that is the + // specified displacement distance away. + Sudoku.Cell temp = newRows[row][fromIndex]; + newRows[row][fromIndex] = newRows[row][toIndex]; + newRows[row][toIndex] = temp; + --mutationCount; + } + } + + return new Sudoku(newRows); + } + + + private void buildCache(Sudoku sudoku) + { + for (int row = 0; row < Sudoku.SIZE; row++) + { + for (int column = 0; column < Sudoku.SIZE; column++) + { + if (sudoku.isFixed(row, column)) + { + columnFixedValues[column][sudoku.getValue(row, column) - 1] = true; + subGridFixedValues[convertToSubGrid(row, column)][sudoku.getValue(row, column) - 1] = true; + } + } + } + cached = true; + } + + + /** + * Checks whether the proposed mutation would introduce a duplicate of a fixed value + * into a column or sub-grid. + */ + private boolean isIntroducingFixedConflict(Sudoku sudoku, + int row, + int fromIndex, + int toIndex) + { + return columnFixedValues[fromIndex][sudoku.getValue(row, toIndex) - 1] + || columnFixedValues[toIndex][sudoku.getValue(row, fromIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, fromIndex)][sudoku.getValue(row, toIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, toIndex)][sudoku.getValue(row, fromIndex) - 1]; + } + + + /** + * Checks whether the proposed mutation would remove a duplicate of a fixed value + * from a column or sub-grid. + */ + private boolean isRemovingFixedConflict(Sudoku sudoku, + int row, + int fromIndex, + int toIndex) + { + return columnFixedValues[fromIndex][sudoku.getValue(row, fromIndex) - 1] + || columnFixedValues[toIndex][sudoku.getValue(row, toIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, fromIndex)][sudoku.getValue(row, fromIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, toIndex)][sudoku.getValue(row, toIndex) - 1]; + } + + + + /** + * Returns the index of the sub-grid that the specified cells belongs to. + * @return A number between 0 (top left) and 8 (bottom right). + */ + private int convertToSubGrid(int row, int column) + { + int band = row / 3; + int stack = column / 3; + return band * 3 + stack; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuTableModel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuTableModel.java new file mode 100644 index 0000000..0249f7f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuTableModel.java @@ -0,0 +1,156 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import javax.swing.table.AbstractTableModel; + +/** + * {@link javax.swing.table.TableModel} for displaying a Sudoku + * grid in a {@link javax.swing.JTable}. + * @author Daniel Dyer + */ +class SudokuTableModel extends AbstractTableModel +{ + // In puzzle mode, the user can edit the given cells and all other + // cells are blank. In solution mode, all cells have values and are + // uneditable. + private boolean puzzleMode = true; + + // Solution mode data. + private Sudoku sudoku; + + // Puzzle mode data. + private final Character[][] cells = new Character[Sudoku.SIZE][Sudoku.SIZE]; + + + /** + * Sets the Sudoku grid represented by this table model. + */ + public void setSudoku(Sudoku sudoku) + { + this.sudoku = sudoku; + puzzleMode = false; + fireTableRowsUpdated(0, getRowCount() - 1); + } + + + /** + * @return The Sudoku grid represented by this table model. + */ + public Sudoku getSudoku() + { + return sudoku; + } + + + public int getRowCount() + { + return Sudoku.SIZE; + } + + + public int getColumnCount() + { + return Sudoku.SIZE; + } + + + public Object getValueAt(int row, int column) + { + if (puzzleMode) + { + return cells[row][column]; + } + else + { + return sudoku == null ? null : sudoku.getValue(row, column); + } + } + + + @Override + public boolean isCellEditable(int row, int column) + { + return puzzleMode; + } + + + @Override + public void setValueAt(Object object, int row, int column) + { + Character value = (Character) object; + if (!(value == null || (value >= '1' && value <= '9'))) + { + throw new IllegalArgumentException("Invalid character: " + value); + } + cells[row][column] = value; + fireTableCellUpdated(row, column); + } + + + /** + * Sets all cells at once using the same patterns as supported by + * {@link SudokuFactory}. + * @param pattern A String representation of a Sudoku puzzle. Each element + * in the array represents a single row. There are 9 elements and each has 9 + * characters, one per cell. Number cells are represented by the characters + * '0' to '9' and blank cells are represented by dots. + */ + public void setPattern(String[] pattern) + { + if (pattern.length != Sudoku.SIZE) + { + throw new IllegalArgumentException("Pattern must have " + Sudoku.SIZE + " rows."); + } + for (int row = 0; row < pattern.length; row++) + { + String patternRow = pattern[row]; + if (patternRow.length() != Sudoku.SIZE) + { + throw new IllegalArgumentException("Row must have " + Sudoku.SIZE + " columns."); + } + for (int column = 0; column < patternRow.toCharArray().length; column++) + { + char c = patternRow.toCharArray()[column]; + cells[row][column] = c == '.' ? null : c; + } + } + sudoku = null; + puzzleMode = true; + fireTableRowsUpdated(0, getRowCount() - 1); + } + + + /** + * @return A Sudoku puzzle in the pattern format used by {@link SudokuFactory}. + */ + public String[] getPattern() + { + String[] pattern = new String[Sudoku.SIZE]; + for (int i = 0; i < cells.length; i++) + { + Character[] row = cells[i]; + StringBuilder rowString = new StringBuilder(); + for (Character c : row) + { + rowString.append(c == null ? '.' : c); + } + pattern[i] = rowString.toString(); + } + return pattern; + } + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossover.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossover.java new file mode 100644 index 0000000..e5c790f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossover.java @@ -0,0 +1,105 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.watchmaker.framework.operators.AbstractCrossover; + +/** + * Performs cross-over between Sudoku grids by re-combining rows from parents + * to form new offspring. Rows are copied intact, only columns are disrupted + * by this cross-over. + * @author Daniel Dyer + */ +public class SudokuVerticalCrossover extends AbstractCrossover<Sudoku> +{ + /** + * Single-point cross-over. + */ + public SudokuVerticalCrossover() + { + this(1); + } + + + /** + * Multiple-point cross-over (fixed number of points). + * @param crossoverPoints The fixed number of cross-overs applied to each + * pair of parents. + */ + public SudokuVerticalCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Multiple-point cross-over (variable number of points). + * @param crossoverPointsVariable Provides the (possibly variable) number of + * cross-overs applied to each pair of parents. + */ + public SudokuVerticalCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Applies cross-over to a pair of parents. Cross-over is performed vertically + * (each offspring consists of some rows from {@literal parent1} and some rows + * from {@literal parent2}). + * @param parent1 The first parent. + * @param parent2 The second parent. + * @param numberOfCrossoverPoints The number of cross-overs to perform. + * @param rng The RNG used to select the cross-over points. + * @return A list containing a pair of offspring. + */ + @Override + protected List<Sudoku> mate(Sudoku parent1, + Sudoku parent2, + int numberOfCrossoverPoints, + Random rng) + { + Sudoku.Cell[][] offspring1 = new Sudoku.Cell[Sudoku.SIZE][]; + Sudoku.Cell[][] offspring2 = new Sudoku.Cell[Sudoku.SIZE][]; + for (int i = 0; i < Sudoku.SIZE; i++) + { + offspring1[i] = parent1.getRow(i); + offspring2[i] = parent2.getRow(i); + } + + // Apply as many cross-overs as required. + Sudoku.Cell[][] temp = new Sudoku.Cell[Sudoku.SIZE][]; + for (int i = 0; i < numberOfCrossoverPoints; i++) + { + // Cross-over index is always greater than zero and less than + // the length of the parent so that we always pick a point that + // will result in a meaningful cross-over. + int crossoverIndex = (1 + rng.nextInt(Sudoku.SIZE - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + + List<Sudoku> result = new ArrayList<Sudoku>(2); + result.add(new Sudoku(offspring1)); + result.add(new Sudoku(offspring2)); + return result; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuView.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuView.java new file mode 100644 index 0000000..1f8f269 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuView.java @@ -0,0 +1,76 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.BorderLayout; +import java.awt.Color; +import javax.swing.BorderFactory; +import javax.swing.DefaultCellEditor; +import javax.swing.JComboBox; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +/** + * A component for displaying Sudoku puzzles and solutions. + * @author Daniel Dyer + */ +class SudokuView extends JPanel +{ + private final SudokuTableModel sudokuTableModel = new SudokuTableModel(); + + SudokuView() + { + super(new BorderLayout()); + JTable sudokuTable = new JTable(sudokuTableModel); + sudokuTable.setRowHeight(40); + sudokuTable.setGridColor(Color.GRAY); + sudokuTable.setShowGrid(true); + TableColumnModel columnModel = sudokuTable.getColumnModel(); + TableCellRenderer renderer = new SudokuCellRenderer(); + JComboBox valueCombo = new JComboBox(new Object[]{null, '1', '2', '3', '4', '5', '6', '7', '8', '9'}); + TableCellEditor editor = new DefaultCellEditor(valueCombo); + for (int i = 0; i < columnModel.getColumnCount(); i++) + { + TableColumn column = columnModel.getColumn(i); + column.setCellRenderer(renderer); + column.setCellEditor(editor); + } + add(sudokuTable, BorderLayout.CENTER); + setBorder(BorderFactory.createTitledBorder("Puzzle/Solution")); + } + + + public void setSolution(Sudoku sudoku) + { + sudokuTableModel.setSudoku(sudoku); + } + + + public void setPuzzle(String[] pattern) + { + sudokuTableModel.setPattern(pattern); + } + + + public String[] getPuzzle() + { + return sudokuTableModel.getPattern(); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/package-info.java new file mode 100644 index 0000000..bd69653 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/package-info.java @@ -0,0 +1,20 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary Sudoku solver. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.sudoku; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesman.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesman.java new file mode 100644 index 0000000..312e6d4 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesman.java @@ -0,0 +1,107 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import org.uncommons.maths.combinatorics.PermutationGenerator; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Naive brute-force solution to the travelling salesman problem. It would take about + * a day and a half to brute-force the 15-city travelling salesman problem on a home + * computer using this implementation. However, this is a not the best possible + * implementation that is guaranteed to find a the shortest route (for example there + * is no branch-and-bound optimisation). + * @author Daniel Dyer + */ +public class BruteForceTravellingSalesman implements TravellingSalesmanStrategy +{ + private final DistanceLookup distances; + + + /** + * @param distances Information about the distances between cities. + */ + public BruteForceTravellingSalesman(DistanceLookup distances) + { + this.distances = distances; + } + + + /** + * {@inheritDoc} + */ + public String getDescription() + { + return "Brute Force"; + } + + + /** + * To reduce the search space we will only consider routes that start + * and end at one city (whichever is first in the collection). All other + * possible routes are equivalent to one of these routes since start city + * is irrelevant in determining the shortest cycle. + * @param cities The list of destinations, each of which must be visited + * once. + * @param progressListener Call-back for receiving the status of the + * algorithm as it progresses. May be null. + * @return The shortest route that visits each of the specified cities once. + */ + public List<String> calculateShortestRoute(Collection<String> cities, + ProgressListener progressListener) + { + Iterator<String> iterator = cities.iterator(); + String startCity = iterator.next(); + Collection<String> destinations = new ArrayList<String>(cities.size() - 1); + while (iterator.hasNext()) + { + destinations.add(iterator.next()); + } + + FitnessEvaluator<List<String>> evaluator = new RouteEvaluator(distances); + PermutationGenerator<String> generator = new PermutationGenerator<String>(destinations); + long totalPermutations = generator.getTotalPermutations(); + long count = 0; + List<String> shortestRoute = null; + double shortestDistance = Double.POSITIVE_INFINITY; + List<String> currentRoute = new ArrayList<String>(cities.size()); + while (generator.hasMore()) + { + List<String> route = generator.nextPermutationAsList(currentRoute); + route.add(0, startCity); + double distance = evaluator.getFitness(route, null); + if (distance < shortestDistance) + { + shortestDistance = distance; + shortestRoute = new ArrayList<String>(route); + } + ++count; + if (count % 1000 == 0 && progressListener != null) + { + progressListener.updateProgress(((double) count) / totalPermutations * 100); + } + } + if (progressListener != null) + { + progressListener.updateProgress(100); // Finished. + } + return shortestRoute; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/DistanceLookup.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/DistanceLookup.java new file mode 100644 index 0000000..9013d75 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/DistanceLookup.java @@ -0,0 +1,39 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.List; + +/** + * Strategy interface for providing distances between cities in the + * Travelling Salesman problem. + * @author Daniel Dyer + */ +public interface DistanceLookup +{ + /** + * @return The list of cities that this object knows about. + */ + List<String> getKnownCities(); + + /** + * Looks-up the distance between two cities. + * @param startingCity The city to start from. + * @param destinationCity The city to end in. + * @return The distance (in kilometres) between the two cities. + */ + int getDistance(String startingCity, String destinationCity); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookup.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookup.java new file mode 100644 index 0000000..547347f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookup.java @@ -0,0 +1,326 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class contains data about cities in Europe and the distances + * between them. + * @author Daniel Dyer + */ +public final class EuropeanDistanceLookup implements DistanceLookup +{ + private static final Map<String, Map<String, Integer>> DISTANCES = new HashMap<String, Map<String, Integer>>(15); + static + { + // Distances are in km as the crow flies (from http://www.indo.com/distance/) + + Map<String, Integer> amsterdam = new HashMap<String, Integer>(20); + amsterdam.put("Amsterdam", 0); + amsterdam.put("Athens", 2162); + amsterdam.put("Berlin", 576); + amsterdam.put("Brussels", 171); + amsterdam.put("Copenhagen", 622); + amsterdam.put("Dublin", 757); + amsterdam.put("Helsinki", 1506); + amsterdam.put("Lisbon", 1861); + amsterdam.put("London", 356); + amsterdam.put("Luxembourg", 318); + amsterdam.put("Madrid", 1477); + amsterdam.put("Paris", 429); + amsterdam.put("Rome", 1304); + amsterdam.put("Stockholm", 1132); + amsterdam.put("Vienna", 938); + DISTANCES.put("Amsterdam", amsterdam); + + Map<String, Integer> athens = new HashMap<String, Integer>(20); + athens.put("Amsterdam", 2162); + athens.put("Athens", 0); + athens.put("Berlin", 1801); + athens.put("Brussels", 2089); + athens.put("Copenhagen", 2140); + athens.put("Dublin", 2860); + athens.put("Helsinki", 2464); + athens.put("Lisbon", 2854); + athens.put("London", 2391); + athens.put("Luxembourg", 1901); + athens.put("Madrid", 2374); + athens.put("Paris", 2097); + athens.put("Rome", 1040); + athens.put("Stockholm", 2410); + athens.put("Vienna", 1280); + DISTANCES.put("Athens", athens); + + Map<String, Integer> berlin = new HashMap<String, Integer>(20); + berlin.put("Amsterdam", 576); + berlin.put("Athens", 1801); + berlin.put("Berlin", 0); + berlin.put("Brussels", 648); + berlin.put("Copenhagen", 361); + berlin.put("Dublin", 1315); + berlin.put("Helsinki", 1108); + berlin.put("Lisbon", 2310); + berlin.put("London", 929); + berlin.put("Luxembourg", 595); + berlin.put("Madrid", 1866); + berlin.put("Paris", 877); + berlin.put("Rome", 1185); + berlin.put("Stockholm", 818); + berlin.put("Vienna", 525); + DISTANCES.put("Berlin", berlin); + + Map<String, Integer> brussels = new HashMap<String, Integer>(20); + brussels.put("Amsterdam", 171); + brussels.put("Athens", 2089); + brussels.put("Berlin", 648); + brussels.put("Brussels", 0); + brussels.put("Copenhagen", 764); + brussels.put("Dublin", 780); + brussels.put("Helsinki", 1649); + brussels.put("Lisbon", 1713); + brussels.put("London", 321); + brussels.put("Luxembourg", 190); + brussels.put("Madrid", 1315); + brussels.put("Paris", 266); + brussels.put("Rome", 1182); + brussels.put("Stockholm", 1284); + brussels.put("Vienna", 917); + DISTANCES.put("Brussels", brussels); + + Map<String, Integer> copenhagen = new HashMap<String, Integer>(20); + copenhagen.put("Amsterdam", 622); + copenhagen.put("Athens", 2140); + copenhagen.put("Berlin", 361); + copenhagen.put("Brussels", 764); + copenhagen.put("Copenhagen", 0); + copenhagen.put("Dublin", 1232); + copenhagen.put("Helsinki", 885); + copenhagen.put("Lisbon", 2477); + copenhagen.put("London", 953); + copenhagen.put("Luxembourg", 799); + copenhagen.put("Madrid", 2071); + copenhagen.put("Paris", 1028); + copenhagen.put("Rome", 1540); + copenhagen.put("Stockholm", 526); + copenhagen.put("Vienna", 876); + DISTANCES.put("Copenhagen", copenhagen); + + Map<String, Integer> dublin = new HashMap<String, Integer>(20); + dublin.put("Amsterdam", 757); + dublin.put("Athens", 2860); + dublin.put("Berlin", 1315); + dublin.put("Brussels", 780); + dublin.put("Copenhagen", 1232); + dublin.put("Dublin", 0); + dublin.put("Helsinki", 2021); + dublin.put("Lisbon", 1652); + dublin.put("London", 469); + dublin.put("Luxembourg", 961); + dublin.put("Madrid", 1458); + dublin.put("Paris", 787); + dublin.put("Rome", 1903); + dublin.put("Stockholm", 1625); + dublin.put("Vienna", 1687); + DISTANCES.put("Dublin", dublin); + + Map<String, Integer> helsinki = new HashMap<String, Integer>(20); + helsinki.put("Amsterdam", 1506); + helsinki.put("Athens", 2464); + helsinki.put("Berlin", 1108); + helsinki.put("Brussels", 1649); + helsinki.put("Copenhagen", 885); + helsinki.put("Dublin", 2021); + helsinki.put("Helsinki", 0); + helsinki.put("Lisbon", 3362); + helsinki.put("London", 1823); + helsinki.put("Luxembourg", 1667); + helsinki.put("Madrid", 2949); + helsinki.put("Paris", 1912); + helsinki.put("Rome", 2202); + helsinki.put("Stockholm", 396); + helsinki.put("Vienna", 1439); + DISTANCES.put("Helsinki", helsinki); + + Map<String, Integer> lisbon = new HashMap<String, Integer>(20); + lisbon.put("Amsterdam", 1861); + lisbon.put("Athens", 2854); + lisbon.put("Berlin", 2310); + lisbon.put("Brussels", 1713); + lisbon.put("Copenhagen", 2477); + lisbon.put("Dublin", 1652); + lisbon.put("Helsinki", 3362); + lisbon.put("Lisbon", 0); + lisbon.put("London", 1585); + lisbon.put("Luxembourg", 1716); + lisbon.put("Madrid", 501); + lisbon.put("Paris", 1452); + lisbon.put("Rome", 1873); + lisbon.put("Stockholm", 2993); + lisbon.put("Vienna", 2300); + DISTANCES.put("Lisbon", lisbon); + + Map<String, Integer> london = new HashMap<String, Integer>(20); + london.put("Amsterdam", 356); + london.put("Athens", 2391); + london.put("Berlin", 929); + london.put("Brussels", 321); + london.put("Copenhagen", 953); + london.put("Dublin", 469); + london.put("Helsinki", 1823); + london.put("Lisbon", 1585); + london.put("London", 0); + london.put("Luxembourg", 494); + london.put("Madrid", 1261); + london.put("Paris", 343); + london.put("Rome", 1444); + london.put("Stockholm", 1436); + london.put("Vienna", 1237); + DISTANCES.put("London", london); + + Map<String, Integer> luxembourg = new HashMap<String, Integer>(20); + luxembourg.put("Amsterdam", 318); + luxembourg.put("Athens", 1901); + luxembourg.put("Berlin", 595); + luxembourg.put("Brussels", 190); + luxembourg.put("Copenhagen", 799); + luxembourg.put("Dublin", 961); + luxembourg.put("Helsinki", 1667); + luxembourg.put("Lisbon", 1716); + luxembourg.put("London", 494); + luxembourg.put("Luxembourg", 0); + luxembourg.put("Madrid", 1282); + luxembourg.put("Paris", 294); + luxembourg.put("Rome", 995); + luxembourg.put("Stockholm", 1325); + luxembourg.put("Vienna", 761); + DISTANCES.put("Luxembourg", luxembourg); + + Map<String, Integer> madrid = new HashMap<String, Integer>(20); + madrid.put("Amsterdam", 1477); + madrid.put("Athens", 2374); + madrid.put("Berlin", 1866); + madrid.put("Brussels", 1315); + madrid.put("Copenhagen", 2071); + madrid.put("Dublin", 1458); + madrid.put("Helsinki", 2949); + madrid.put("Lisbon", 501); + madrid.put("London", 1261); + madrid.put("Luxembourg", 1282); + madrid.put("Madrid", 0); + madrid.put("Paris", 1050); + madrid.put("Rome", 1377); + madrid.put("Stockholm", 2596); + madrid.put("Vienna", 1812); + DISTANCES.put("Madrid", madrid); + + Map<String, Integer> paris = new HashMap<String, Integer>(20); + paris.put("Amsterdam", 429); + paris.put("Athens", 2097); + paris.put("Berlin", 877); + paris.put("Brussels", 266); + paris.put("Copenhagen", 1028); + paris.put("Dublin", 787); + paris.put("Helsinki", 1912); + paris.put("Lisbon", 1452); + paris.put("London", 343); + paris.put("Luxembourg", 294); + paris.put("Madrid", 1050); + paris.put("Paris", 0); + paris.put("Rome", 1117); + paris.put("Stockholm", 1549); + paris.put("Vienna", 1037); + DISTANCES.put("Paris", paris); + + Map<String, Integer> rome = new HashMap<String, Integer>(20); + rome.put("Amsterdam", 1304); + rome.put("Athens", 1040); + rome.put("Berlin", 1185); + rome.put("Brussels", 1182); + rome.put("Copenhagen", 1540); + rome.put("Dublin", 1903); + rome.put("Helsinki", 2202); + rome.put("Lisbon", 1873); + rome.put("London", 1444); + rome.put("Luxembourg", 995); + rome.put("Madrid", 1377); + rome.put("Paris", 1117); + rome.put("Rome", 0); + rome.put("Stockholm", 1984); + rome.put("Vienna", 765); + DISTANCES.put("Rome", rome); + + Map<String, Integer> stockholm = new HashMap<String, Integer>(20); + stockholm.put("Amsterdam", 1132); + stockholm.put("Athens", 2410); + stockholm.put("Berlin", 818); + stockholm.put("Brussels", 1284); + stockholm.put("Copenhagen", 526); + stockholm.put("Dublin", 1625); + stockholm.put("Helsinki", 396); + stockholm.put("Lisbon", 2993); + stockholm.put("London", 1436); + stockholm.put("Luxembourg", 1325); + stockholm.put("Madrid", 2596); + stockholm.put("Paris", 1549); + stockholm.put("Rome", 1984); + stockholm.put("Stockholm", 0); + stockholm.put("Vienna", 1247); + DISTANCES.put("Stockholm", stockholm); + + Map<String, Integer> vienna = new HashMap<String, Integer>(20); + vienna.put("Amsterdam", 938); + vienna.put("Athens", 1280); + vienna.put("Berlin", 525); + vienna.put("Brussels", 917); + vienna.put("Copenhagen", 876); + vienna.put("Dublin", 1687); + vienna.put("Helsinki", 1439); + vienna.put("Lisbon", 2300); + vienna.put("London", 1237); + vienna.put("Luxembourg", 761); + vienna.put("Madrid", 1812); + vienna.put("Paris", 1037); + vienna.put("Rome", 765); + vienna.put("Stockholm", 1247); + vienna.put("Vienna", 0); + DISTANCES.put("Vienna", vienna); + } + + + /** + * {@inheritDoc} + */ + public List<String> getKnownCities() + { + List<String> cities = new ArrayList<String>(DISTANCES.keySet()); + Collections.sort(cities); + return cities; + } + + + /** + * {@inheritDoc} + */ + public int getDistance(String startingCity, String destinationCity) + { + return DISTANCES.get(startingCity).get(destinationCity); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionPanel.java new file mode 100644 index 0000000..82089d5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionPanel.java @@ -0,0 +1,119 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.FlowLayout; +import java.util.List; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SpringLayout; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.swing.SelectionStrategyControl; + +/** + * Controls for configuring an {@link EvolutionaryTravellingSalesman} object. + * @author Daniel Dyer + */ +final class EvolutionPanel extends JPanel +{ + private final JLabel populationLabel; + private final JSpinner populationSpinner; + private final JLabel elitismLabel; + private final JSpinner elitismSpinner; + private final JLabel generationsLabel; + private final JSpinner generationsSpinner; + private final JLabel selectionLabel; + private final SelectionStrategyControl<List<String>> selectionControl; + private final JCheckBox crossoverCheckbox; + private final JCheckBox mutationCheckbox; + private final DistanceLookup distances; + + EvolutionPanel(DistanceLookup distances) + { + super(new FlowLayout(FlowLayout.LEFT, 0, 0)); + this.distances = distances; + JPanel innerPanel = new JPanel(new SpringLayout()); + + populationLabel = new JLabel("Population Size: "); + populationSpinner = new JSpinner(new SpinnerNumberModel(300, 2, 10000, 1)); + populationLabel.setLabelFor(populationSpinner); + innerPanel.add(populationLabel); + innerPanel.add(populationSpinner); + + elitismLabel = new JLabel("Elitism: "); + elitismSpinner = new JSpinner(new SpinnerNumberModel(3, 0, 10000, 1)); + elitismLabel.setLabelFor(elitismSpinner); + innerPanel.add(elitismLabel); + innerPanel.add(elitismSpinner); + + generationsLabel = new JLabel("Number of Generations: "); + generationsSpinner = new JSpinner(new SpinnerNumberModel(100, 1, 10000, 1)); + generationsLabel.setLabelFor(generationsSpinner); + innerPanel.add(generationsLabel); + innerPanel.add(generationsSpinner); + + selectionLabel = new JLabel("Selection Strategy: "); + List<SelectionStrategy<? super List<String>>> strategies + = SelectionStrategyControl.createDefaultOptions(new Probability(0.95d), 0.5d); + this.selectionControl = new SelectionStrategyControl<List<String>>(strategies); + innerPanel.add(selectionLabel); + selectionControl.getControl().setSelectedIndex(selectionControl.getControl().getItemCount() - 1); + innerPanel.add(selectionControl.getControl()); + + crossoverCheckbox = new JCheckBox("Cross-over", true); + mutationCheckbox = new JCheckBox("Mutation", true); + + innerPanel.add(crossoverCheckbox); + innerPanel.add(mutationCheckbox); + + SpringUtilities.makeCompactGrid(innerPanel, 5, 2, 30, 6, 6, 6); + add(innerPanel); + } + + + @Override + public void setEnabled(boolean b) + { + populationLabel.setEnabled(b); + populationSpinner.setEnabled(b); + elitismLabel.setEnabled(b); + elitismSpinner.setEnabled(b); + generationsLabel.setEnabled(b); + generationsSpinner.setEnabled(b); + selectionLabel.setEnabled(b); + selectionControl.getControl().setEnabled(b); + crossoverCheckbox.setEnabled(b); + mutationCheckbox.setEnabled(b); + super.setEnabled(b); + } + + + public TravellingSalesmanStrategy getStrategy() + { + return new EvolutionaryTravellingSalesman(distances, + selectionControl.getSelectionStrategy(), + (Integer) populationSpinner.getValue(), + (Integer) elitismSpinner.getValue(), + (Integer) generationsSpinner.getValue(), + crossoverCheckbox.isSelected(), + mutationCheckbox.isSelected()); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesman.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesman.java new file mode 100644 index 0000000..6814101 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesman.java @@ -0,0 +1,152 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.PoissonGenerator; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.factories.ListPermutationFactory; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.operators.ListOrderCrossover; +import org.uncommons.watchmaker.framework.operators.ListOrderMutation; +import org.uncommons.watchmaker.framework.termination.GenerationCount; + +/** + * Evolutionary algorithm for finding (approximate) solutions to the + * travelling salesman problem. + * @author Daniel Dyer + */ +public class EvolutionaryTravellingSalesman implements TravellingSalesmanStrategy +{ + private final DistanceLookup distances; + private final SelectionStrategy<? super List<String>> selectionStrategy; + private final int populationSize; + private final int eliteCount; + private final int generationCount; + private final boolean crossover; + private final boolean mutation; + + /** + * Creates an evolutionary Travelling Salesman solver with the + * specified configuration. + * @param distances Information about the distances between cities. + * @param selectionStrategy The selection implementation to use for + * the evolutionary algorithm. + * @param populationSize The number of candidates in the population + * of evolved routes. + * @param eliteCount The number of candidates to preserve via elitism + * at each generation. + * @param generationCount The number of iterations of evolution to perform. + * @param crossover Whether or not to use a cross-over operator in the evolution. + * @param mutation Whether or not to use a mutation operator in the evolution. + */ + public EvolutionaryTravellingSalesman(DistanceLookup distances, + SelectionStrategy<? super List<String>> selectionStrategy, + int populationSize, + int eliteCount, + int generationCount, + boolean crossover, + boolean mutation) + { + if (!crossover && !mutation) + { + throw new IllegalArgumentException("At least one of cross-over or mutation must be selected."); + } + this.distances = distances; + this.selectionStrategy = selectionStrategy; + this.populationSize = populationSize; + this.eliteCount = eliteCount; + this.generationCount = generationCount; + this.crossover = crossover; + this.mutation = mutation; + } + + + /** + * {@inheritDoc} + */ + public String getDescription() + { + String selectionName = selectionStrategy.toString(); + return "Evolution (pop: " + populationSize + ", gen: " + generationCount + + ", elite: " + eliteCount + ", " + selectionName + ")"; + } + + + /** + * Calculates the shortest route using a generational evolutionary + * algorithm with a single ordered mutation operator and truncation + * selection. + * @param cities The list of destinations, each of which must be visited + * once. + * @param progressListener Call-back for receiving the status of the + * algorithm as it progresses. + * @return The (approximate) shortest route that visits each of the + * specified cities once. + */ + public List<String> calculateShortestRoute(Collection<String> cities, + final ProgressListener progressListener) + { + Random rng = new MersenneTwisterRNG(); + + // Set-up evolution pipeline (cross-over followed by mutation). + List<EvolutionaryOperator<List<String>>> operators = new ArrayList<EvolutionaryOperator<List<String>>>(2); + if (crossover) + { + operators.add(new ListOrderCrossover<String>()); + } + if (mutation) + { + operators.add(new ListOrderMutation<String>(new PoissonGenerator(1.5, rng), + new PoissonGenerator(1.5, rng))); + } + + EvolutionaryOperator<List<String>> pipeline = new EvolutionPipeline<List<String>>(operators); + + CandidateFactory<List<String>> candidateFactory + = new ListPermutationFactory<String>(new LinkedList<String>(cities)); + EvolutionEngine<List<String>> engine + = new GenerationalEvolutionEngine<List<String>>(candidateFactory, + pipeline, + new RouteEvaluator(distances), + selectionStrategy, + rng); + if (progressListener != null) + { + engine.addEvolutionObserver(new EvolutionObserver<List<String>>() + { + public void populationUpdate(PopulationData<? extends List<String>> data) + { + progressListener.updateProgress(((double) data.getGenerationNumber() + 1) / generationCount * 100); + } + }); + } + return engine.evolve(populationSize, + eliteCount, + new GenerationCount(generationCount)); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ExecutionPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ExecutionPanel.java new file mode 100644 index 0000000..e4d5be0 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ExecutionPanel.java @@ -0,0 +1,107 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.event.ActionListener; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +/** + * Panel for controlling the execution of the Travelling Salesman applet. + * Contains controls for starting and stopping the route-finding algorithms + * and for displaying progress and results. + * @author Daniel Dyer + */ +final class ExecutionPanel extends JPanel implements ProgressListener +{ + private final JButton startButton; + private final JTextArea output; + private final JScrollPane scroller; + private final JProgressBar progressBar; + + + ExecutionPanel() + { + super(new BorderLayout()); + JPanel controlPanel = new JPanel(new BorderLayout()); + startButton = new JButton("Start"); + controlPanel.add(startButton, BorderLayout.WEST); + progressBar = new JProgressBar(0, 100); + controlPanel.add(progressBar, BorderLayout.CENTER); + add(controlPanel, BorderLayout.NORTH); + output = new JTextArea(); + output.setEditable(false); + output.setLineWrap(true); + output.setWrapStyleWord(true); + output.setFont(new Font("Monospaced", Font.PLAIN, 12)); + scroller = new JScrollPane(output); + scroller.setBorder(BorderFactory.createTitledBorder("Results")); + add(scroller, BorderLayout.CENTER); + } + + + /** + * Adds logic to the start button so that something happens when + * it is clicked. + * @param actionListener The action to perform when the button is + * clicked. + */ + public void addActionListener(ActionListener actionListener) + { + startButton.addActionListener(actionListener); + } + + + /** + * Updates the position of the progress bar. + */ + public void updateProgress(final double percentComplete) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + progressBar.setValue((int) percentComplete); + } + }); + } + + + /** + * Appends the specified text to this panel's text area. + * @param text The text to append. + */ + public void appendOutput(String text) + { + output.append(text); + } + + + @Override + public void setEnabled(boolean b) + { + startButton.setEnabled(b); + scroller.setEnabled(b); + super.setEnabled(b); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanel.java new file mode 100644 index 0000000..0e5c154 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanel.java @@ -0,0 +1,114 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JPanel; + +/** + * Component for selecting which cities are to be visited by the + * travelling salesman. + * @author Daniel Dyer + */ +final class ItineraryPanel extends JPanel +{ + private final Collection<JCheckBox> checkBoxes; + private final JButton selectAllButton; + private final JButton clearButton; + + ItineraryPanel(List<String> cities) + { + super(new BorderLayout()); + + JPanel checkBoxPanel = new JPanel(new GridLayout(0, 1)); + checkBoxes = new ArrayList<JCheckBox>(cities.size()); + for (String city : cities) + { + JCheckBox checkBox = new JCheckBox(city, false); + checkBox.setName(city); // Helps to find the checkbox from a unit test. + checkBoxes.add(checkBox); + checkBoxPanel.add(checkBox); + } + add(checkBoxPanel, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new GridLayout(2, 1)); + selectAllButton = new JButton("Select All"); + selectAllButton.setName("All"); + buttonPanel.add(selectAllButton); + clearButton = new JButton("Clear Selection"); + clearButton.setName("None"); + buttonPanel.add(clearButton); + ActionListener buttonListener = new ActionListener() + { + + public void actionPerformed(ActionEvent actionEvent) + { + boolean select = actionEvent.getSource() == selectAllButton; + for (JCheckBox checkBox : checkBoxes) + { + checkBox.setSelected(select); + } + } + }; + selectAllButton.addActionListener(buttonListener); + clearButton.addActionListener(buttonListener); + add(buttonPanel, BorderLayout.SOUTH); + + setBorder(BorderFactory.createTitledBorder("Itinerary")); + } + + + /** + * Returns the cities that have been selected as part of the itinerary. + * @return A list of cities. + */ + public Collection<String> getSelectedCities() + { + Set<String> cities = new TreeSet<String>(); + for (JCheckBox checkBox : checkBoxes) + { + if (checkBox.isSelected()) + { + cities.add(checkBox.getText()); + } + } + return cities; + } + + + @Override + public void setEnabled(boolean b) + { + for (JCheckBox checkBox : checkBoxes) + { + checkBox.setEnabled(b); + } + selectAllButton.setEnabled(b); + clearButton.setEnabled(b); + super.setEnabled(b); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ProgressListener.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ProgressListener.java new file mode 100644 index 0000000..661a5e5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ProgressListener.java @@ -0,0 +1,31 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +/** + * Call-back interface for keeping track of the progress of a + * {@link TravellingSalesmanStrategy} implementation. + * @author Daniel Dyer + */ +public interface ProgressListener +{ + /** + * Call-back method that informs the implementing object + * of the current completion percentage. + * @param percentComplete A percentage between 0 and 100. + */ + void updateProgress(double percentComplete); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluator.java new file mode 100644 index 0000000..144d593 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluator.java @@ -0,0 +1,74 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.List; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Fitness evalator that measures the total distance of a route in the travelling salesman + * problem. The fitness score of a route is the total distance (in km). A route + * is represented as a list of cities in the order that they will be visited. + * The last leg of the journey is from the last city in the list back to the + * first. + * @author Daniel Dyer + */ +public class RouteEvaluator implements FitnessEvaluator<List<String>> +{ + private final DistanceLookup distances; + + + /** + * @param distances Provides distances between a set of cities. + */ + public RouteEvaluator(DistanceLookup distances) + { + this.distances = distances; + } + + + /** + * Calculates the length of an evolved route. + * @param candidate The route to evaluate. + * @param population {@inheritDoc} + * @return The total distance (in kilometres) of a journey that visits + * each city in order and returns to the starting point. + */ + public double getFitness(List<String> candidate, + List<? extends List<String>> population) + { + int totalDistance = 0; + int cityCount = candidate.size(); + for (int i = 0; i < cityCount; i++) + { + int nextIndex = i < cityCount - 1 ? i + 1 : 0; + totalDistance += distances.getDistance(candidate.get(i), + candidate.get(nextIndex)); + } + return totalDistance; + } + + + /** + * {@inheritDoc} + * Returns false since shorter distances represent fitter candidates. + * @return false + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanel.java new file mode 100644 index 0000000..99f727d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanel.java @@ -0,0 +1,92 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JPanel; +import javax.swing.JRadioButton; + +/** + * Panel for configuring a route-finding strategy for the travelling + * salesman problem. + * @author Daniel Dyer + */ +final class StrategyPanel extends JPanel +{ + private final DistanceLookup distances; + private final JRadioButton evolutionOption; + private final JRadioButton bruteForceOption; + private final EvolutionPanel evolutionPanel; + + /** + * Creates a panel with components for controlling the route-finding + * strategy. + * @param distances Data used by the strategy in order to calculate + * shortest routes. + */ + StrategyPanel(DistanceLookup distances) + { + super(new BorderLayout()); + this.distances = distances; + evolutionOption = new JRadioButton("Evolution", true); + evolutionOption.setName("EvolutionOption"); // Helps to find the radio button from a unit test. + bruteForceOption = new JRadioButton("Brute Force", false); + bruteForceOption.setName("BruteForceOption"); // Helps to find the radio button from a unit test. + evolutionOption.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + evolutionPanel.setEnabled(evolutionOption.isSelected()); + } + }); + ButtonGroup strategyGroup = new ButtonGroup(); + strategyGroup.add(evolutionOption); + strategyGroup.add(bruteForceOption); + evolutionPanel = new EvolutionPanel(distances); + evolutionPanel.setName("EvolutionPanel"); // Helps to find the panel from a unit test. + add(evolutionOption, BorderLayout.NORTH); + add(evolutionPanel, BorderLayout.CENTER); + add(bruteForceOption, BorderLayout.SOUTH); + setBorder(BorderFactory.createTitledBorder("Route-Finding Strategy")); + } + + + public TravellingSalesmanStrategy getStrategy() + { + if (bruteForceOption.isSelected()) + { + return new BruteForceTravellingSalesman(distances); + } + else + { + return evolutionPanel.getStrategy(); + } + } + + + @Override + public void setEnabled(boolean b) + { + evolutionOption.setEnabled(b); + bruteForceOption.setEnabled(b); + evolutionPanel.setEnabled(b && evolutionOption.isSelected()); + super.setEnabled(b); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanApplet.java new file mode 100644 index 0000000..96490b2 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanApplet.java @@ -0,0 +1,185 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collection; +import java.util.List; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Applet for comparing evolutionary and brute force approaches to the + * Travelling Salesman problem. + * @author Daniel Dyer + */ +public final class TravellingSalesmanApplet extends AbstractExampleApplet +{ + private final DistanceLookup distances = new EuropeanDistanceLookup(); + private final FitnessEvaluator<List<String>> evaluator = new RouteEvaluator(distances); + + private ItineraryPanel itineraryPanel; + private StrategyPanel strategyPanel; + private ExecutionPanel executionPanel; + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + itineraryPanel = new ItineraryPanel(distances.getKnownCities()); + strategyPanel = new StrategyPanel(distances); + executionPanel = new ExecutionPanel(); + + container.add(itineraryPanel, BorderLayout.WEST); + JPanel innerPanel = new JPanel(new BorderLayout()); + innerPanel.add(strategyPanel, BorderLayout.NORTH); + innerPanel.add(executionPanel, BorderLayout.CENTER); + container.add(innerPanel, BorderLayout.CENTER); + + executionPanel.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent actionEvent) + { + Collection<String> cities = itineraryPanel.getSelectedCities(); + if (cities.size() < 4) + { + JOptionPane.showMessageDialog(TravellingSalesmanApplet.this, + "Itinerary must include at least 4 cities.", + "Error", + JOptionPane.ERROR_MESSAGE); + } + else + { + try + { + setEnabled(false); + createTask(cities).execute(); + } + catch (IllegalArgumentException ex) + { + JOptionPane.showMessageDialog(TravellingSalesmanApplet.this, + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + setEnabled(true); + } + } + } + }); + container.validate(); + } + + + /** + * Helper method to create a background task for running the travelling + * salesman algorithm. + * @param cities The set of cities to generate a route for. + * @return A Swing task that will execute on a background thread and update + * the GUI when it is done. + */ + private SwingBackgroundTask<List<String>> createTask(final Collection<String> cities) + { + final TravellingSalesmanStrategy strategy = strategyPanel.getStrategy(); + return new SwingBackgroundTask<List<String>>() + { + private long elapsedTime = 0; + + @Override + protected List<String> performTask() + { + long startTime = System.currentTimeMillis(); + List<String> result = strategy.calculateShortestRoute(cities, executionPanel); + elapsedTime = System.currentTimeMillis() - startTime; + return result; + } + + @Override + protected void postProcessing(List<String> result) + { + executionPanel.appendOutput(createResultString(strategy.getDescription(), + result, + evaluator.getFitness(result, null), + elapsedTime)); + setEnabled(true); + } + }; + } + + + /** + * Helper method for formatting a result as a string for display. + */ + private String createResultString(String strategyDescription, + List<String> shortestRoute, + double distance, + long elapsedTime) + { + StringBuilder buffer = new StringBuilder(); + buffer.append('['); + buffer.append(strategyDescription); + buffer.append("]\n"); + buffer.append("ROUTE: "); + for (String s : shortestRoute) + { + buffer.append(s); + buffer.append(" -> "); + } + buffer.append(shortestRoute.get(0)); + buffer.append('\n'); + buffer.append("TOTAL DISTANCE: "); + buffer.append(String.valueOf(distance)); + buffer.append("km\n"); + buffer.append("(Search Time: "); + double seconds = (double) elapsedTime / 1000; + buffer.append(String.valueOf(seconds)); + buffer.append(" seconds)\n\n"); + return buffer.toString(); + } + + + /** + * Toggles whether the controls are enabled for input or not. + * @param b Enables the controls if this flag is true, disables them otherwise. + */ + @Override + public void setEnabled(boolean b) + { + itineraryPanel.setEnabled(b); + strategyPanel.setEnabled(b); + executionPanel.setEnabled(b); + super.setEnabled(b); + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + */ + public static void main(String[] args) + { + new TravellingSalesmanApplet().displayInFrame("Watchmaker Framework - Travelling Salesman Example"); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanStrategy.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanStrategy.java new file mode 100644 index 0000000..82f75c8 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanStrategy.java @@ -0,0 +1,44 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Collection; +import java.util.List; + +/** + * Defines methods that must be implemented by classes that provide + * solutions to the Travelling Salesman problem. + * @author Daniel Dyer + */ +public interface TravellingSalesmanStrategy +{ + /** + * @return A description of the strategy. + */ + String getDescription(); + + /** + * Calculates the shortest round trip distance that visits each + * of the specified cities once and returns to the starting point. + * @param cities The destination that must each be visited for the route + * to be valid. + * @param progressListener A call-back for keeping track of the route-finding + * algorithm's progress. + * @return The shortest route found for the given list of destinations. + */ + List<String> calculateShortestRoute(Collection<String> cities, + ProgressListener progressListener); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/package-info.java new file mode 100644 index 0000000..59f25f5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/package-info.java @@ -0,0 +1,21 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary approach to tackling the well-known Travelling Salesman + * problem. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.travellingsalesman; diff --git a/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/lighthouse.jpg b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/lighthouse.jpg Binary files differnew file mode 100644 index 0000000..fcd57f6 --- /dev/null +++ b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/lighthouse.jpg diff --git a/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/monalisa.jpg b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/monalisa.jpg Binary files differnew file mode 100644 index 0000000..61ae626 --- /dev/null +++ b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/monalisa.jpg diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/AbstractExampleAppletTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/AbstractExampleAppletTest.java new file mode 100644 index 0000000..f762147 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/AbstractExampleAppletTest.java @@ -0,0 +1,94 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.awt.BorderLayout; +import java.awt.Container; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.FrameMatcher; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link AbstractExampleAppletTest} class. + * @author Daniel Dyer + */ +public class AbstractExampleAppletTest +{ + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testPreparationOnEDT() + { + final boolean[] onEDT = new boolean[1]; + AbstractExampleApplet applet = new AbstractExampleApplet() + { + @Override + protected void prepareGUI(Container container) + { + onEDT[0] = SwingUtilities.isEventDispatchThread(); + } + }; + applet.init(); + assert onEDT[0] : "Prepare method was not called on Event Dispatch Thread."; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testDisplayInFrame() + { + AbstractExampleApplet applet = new AbstractExampleApplet() + { + @Override + protected void prepareGUI(Container container) + { + JLabel label = new JLabel("Test"); + label.setName("Test"); + container.add(label, BorderLayout.CENTER); + } + }; + applet.displayInFrame("ExampleFrame"); + robot.waitForIdle(); + // There ought to be a visible frame containing the example GUI. + JFrame frame = (JFrame) robot.finder().find(FrameMatcher.withTitle("ExampleFrame").andShowing()); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + FrameFixture frameFixture = new FrameFixture(robot, frame); + assert frameFixture.label("Test").component().isShowing() : "GUI not displayed correctly."; + frameFixture.close(); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/ExamplesTestUtils.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/ExamplesTestUtils.java new file mode 100644 index 0000000..5affc17 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/ExamplesTestUtils.java @@ -0,0 +1,48 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.util.Random; +import org.uncommons.maths.random.XORShiftRNG; + +/** + * Utility methods for Watchmaker examples unit tests. Provides + * access to shared resources used by tests. + * @author Daniel Dyer + */ +public final class ExamplesTestUtils +{ + private static final Random RNG = new XORShiftRNG(); + + private ExamplesTestUtils() + { + // Prevent instantiation. + } + + + /** + * Returns the singleton RNG shared by all tests. It might be preferable + * to have a separate RNG for each test (for true separation) but this + * causes problems. Seeding dozens of RNGs can exhaust the system's + * available entropy (the Uncommons Maths RNGs seed themselves from + * /dev/random by default). + * @return A random number generator. + */ + public static Random getRNG() + { + return RNG; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactoryTest.java new file mode 100644 index 0000000..c1d1cce --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactoryTest.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for the biomorph candidate factory. + * @author Daniel Dyer + */ +public class BiomorphFactoryTest +{ + /** + * Ensures that biomorphs created by the factory are valid. + */ + @Test + public void testValidity() + { + CandidateFactory<Biomorph> factory = new BiomorphFactory(); + List<Biomorph> biomorphs = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (Biomorph biomorph : biomorphs) + { + // Returns 9 genes, last one is the length gene. + int[] genes = biomorph.getGenotype(); + for (int i = 0; i < Biomorph.GENE_COUNT - 1; i++) + { + assert genes[i] >= Biomorph.GENE_MIN && genes[i] <= Biomorph.GENE_MAX + : "Gene " + i + " is out of range: " + genes[i]; + } + int length = biomorph.getLengthPhenotype(); + assert length >= Biomorph.LENGTH_GENE_MIN && length <= Biomorph.LENGTH_GENE_MAX + : "Length gene is out of range: " + length; + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphTest.java new file mode 100644 index 0000000..b62c8a2 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphTest.java @@ -0,0 +1,59 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.Arrays; +import org.testng.annotations.Test; + +/** + * Some basic sanity checks for the {@link Biomorph} type used in the + * interactive evolution example program. + * @author Daniel Dyer + */ +public class BiomorphTest +{ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInsufficientGenes() + { + new Biomorph(new int[Biomorph.GENE_COUNT - 1]); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManyGenes() + { + new Biomorph(new int[Biomorph.GENE_COUNT + 1]); + } + + + @Test + public void testEquality() + { + Biomorph biomorph1 = new Biomorph(new int[Biomorph.GENE_COUNT]); + Biomorph biomorph2 = new Biomorph(new int[Biomorph.GENE_COUNT]); + int[] genes = new int[Biomorph.GENE_COUNT]; + Arrays.fill(genes, 2); + Biomorph biomorph3 = new Biomorph(genes); + + assert biomorph1.equals(biomorph1) : "Equality must be reflexive."; + assert biomorph1.equals(biomorph2) : "Biomorphs with identical genes should be considered equal."; + assert biomorph2.equals(biomorph1) : "Equality must be reflective."; + assert biomorph1.hashCode() == biomorph2.hashCode() : "Equal objects must have identical hash codes."; + assert !biomorph1.equals(biomorph3) : "Biomorphs with different genes should not be considered equal."; + + assert !biomorph1.equals(null) : "No object should be considered equal to a null reference."; + assert !biomorph3.equals(genes) : "Biomorphs should not be considered equal to objects of different types."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutationTest.java new file mode 100644 index 0000000..09efe3e --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutationTest.java @@ -0,0 +1,72 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for non-random mutation used by Biomorph example application. + * @author Daniel Dyer + */ +public class DawkinsBiomorphMutationTest +{ + /** + * Ensure that each possible mutation occurs exactly once. + */ + @Test + public void testMutations() + { + Biomorph source = new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 2, 8}); + List<Biomorph> originalPopulation = new ArrayList<Biomorph>(18); + for (int i = 0; i < 18; i++) + { + originalPopulation.add(source); + } + EvolutionaryOperator<Biomorph> mutation = new DawkinsBiomorphMutation(); + List<Biomorph> mutatedPopulation = mutation.apply(originalPopulation, + ExamplesTestUtils.getRNG()); // RNG should be ignored. + assert mutatedPopulation.size() == originalPopulation.size() : "Mutated population is wrong size."; + // Lazy way of checking for duplicates. Add all mutations to a set. If there are any + // duplicates, the size of the set will be shorter than the list. + Set<Biomorph> distinctBiomorphs = new HashSet<Biomorph>(mutatedPopulation); + assert distinctBiomorphs.size() == mutatedPopulation.size() : "Mutated population contains duplicates."; + // Check for each of the expected mutations (mutations should differ from the original in only one gene). + assert distinctBiomorphs.contains(new Biomorph(new int[]{5, -4, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-4, -4, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -5, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -3, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -4, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -2, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -3, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -1, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -2, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, 0, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, -1, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 1, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 0, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 2, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 1, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 3, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 2, 7})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 2, 1})) : "Missing mutation."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutationTest.java new file mode 100644 index 0000000..5c0890d --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutationTest.java @@ -0,0 +1,59 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for random mutation operator for biomorphs. + * @author Daniel Dyer + */ +public class RandomBiomorphMutationTest +{ + @Test + public void testValidity() + { + EvolutionaryOperator<Biomorph> mutation = new RandomBiomorphMutation(Probability.ONE); // Mutate every gene. + List<Biomorph> population = new ArrayList<Biomorph>(3); + population.add(new Biomorph(new int[]{5, -4, -3, -2, -1, 0, 1, 2, 8})); + population.add(new Biomorph(new int[]{-5, 4, 4, -5, 5, 3, 0, 2, 2})); + population.add(new Biomorph(new int[]{-4, -1, 0, 0, 3, 0, 4, 5, 4})); + Random rng = ExamplesTestUtils.getRNG(); + for (int i = 0; i < 20; i++) // Perform several mutations to cover more possibilties. + { + population = mutation.apply(population, rng); + for (Biomorph biomorph : population) + { + // Returns 9 genes, last one is the length gene. + int[] genes = biomorph.getGenotype(); + for (int j = 0; j < Biomorph.GENE_COUNT - 1; j++) + { + assert genes[j] >= Biomorph.GENE_MIN && genes[j] <= Biomorph.GENE_MAX + : "Gene " + j + " is out of range: " + genes[j]; + } + int length = biomorph.getLengthPhenotype(); + assert length >= Biomorph.LENGTH_GENE_MIN && length <= Biomorph.LENGTH_GENE_MAX + : "Length gene is out of range: " + length; + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/bits/BitsExampleTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/bits/BitsExampleTest.java new file mode 100644 index 0000000..737ed55 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/bits/BitsExampleTest.java @@ -0,0 +1,34 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.bits; + +import org.testng.annotations.Test; +import org.uncommons.maths.binary.BitString; + +/** + * Simple unit test for the bits example. Makes sure that the evolution engine + * eventually returns the expected result. + * @author Daniel Dyer + */ +public class BitsExampleTest +{ + @Test + public void testEvolution() + { + BitString result = BitsExample.evolveBits(8); + assert result.toString().equals("11111111") : "Wrong result returned: " + result.toString(); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/AdditionTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/AdditionTest.java new file mode 100644 index 0000000..1ad9a46 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/AdditionTest.java @@ -0,0 +1,112 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Addition} node type. + * @author Daniel Dyer + */ +public class AdditionTest +{ + @Test + public void testEvaluation() + { + Node node = new Addition(new Constant(1), new Parameter(0)); + double value = node.evaluate(new double[]{3}); // 1 + 3 + assert value == 4 : "Wrong result: " + value; + } + + @Test + public void testStringRepresentation() + { + Node node = new Addition(new Constant(1), new Constant(3)); + assert node.print().equals("(1.0 + 3.0)") : "Wrong string representation: " + node.print(); + } + + + /** + * If the arguments to the add function are both constants then the addition node + * should be replaced by a constant node containing the evaluation of this sum. + */ + @Test + public void testSimplifyConstants() + { + Node node = new Addition(new Constant(7), new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 12; + + } + + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new Addition(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new Addition(new Parameter(0), + new Addition(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof Addition + : "Simplified node should be Addition, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } + + + @Test + public void testSimplifyAddZero() + { + Node node = new Addition(new Parameter(0), new Constant(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + } + + + @Test + public void testSimplifyAddToZero() + { + Node node = new Addition(new Constant(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ConstantTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ConstantTest.java new file mode 100644 index 0000000..1a5eb36 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ConstantTest.java @@ -0,0 +1,55 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Unit test for the {@link Constant} node type. + * @author Daniel Dyer + */ +public class ConstantTest +{ + @Test + public void testEquality() + { + Constant zero = new Constant(0); + Constant one = new Constant(1); + Constant anotherOne = new Constant(1); + + assert zero.equals(zero) : "Equality must be reflexive."; + assert one.equals(anotherOne) : "Same-valued constants must be equal."; + assert anotherOne.equals(one) : "Equality must be symmetric."; + assert one.hashCode() == anotherOne.hashCode() : "Equal objects must have equal hash codes."; + assert !zero.equals(one) : "Different valued constants must be non-equal."; + assert !zero.equals(null) : "No non-null object should not be considered equal to null."; + assert !zero.equals(Double.valueOf(0)) : "Objects of different types should not be equal."; + } + + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testGetInvalidNodeIndex() + { + new Constant(1).getNode(1); // Should throw an exception. + } + + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testReplaceInvalidNodeIndex() + { + new Constant(1).replaceNode(1, new Constant(2)); // Should throw an exception. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExampleTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExampleTest.java new file mode 100644 index 0000000..3d254fd --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExampleTest.java @@ -0,0 +1,50 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.HashMap; +import java.util.Map; +import org.testng.annotations.Test; + +/** + * Test for the {@link GeneticProgrammingExample} example application. + * @author Daniel Dyer + */ +public class GeneticProgrammingExampleTest +{ + @Test + public void testApplication() + { + Map<double[], Double> testData = new HashMap<double[], Double>(); + testData.put(new double[]{26, 35}, 165.0d); + testData.put(new double[]{8, 24}, 64.0d); + testData.put(new double[]{20, 1}, 101.0d); + testData.put(new double[]{33, 11}, 176.0d); + testData.put(new double[]{37, 16}, 201.0d); + + Node evolvedProgram = GeneticProgrammingExample.evolveProgram(testData); + + // Check that evolved program works for test data. + double result1 = evolvedProgram.evaluate(new double[]{8, 24}); + assert result1 == 64.0d : "Incorrect result: " + result1; + + // Check that the evolved program generalises. + double result2 = evolvedProgram.evaluate(new double[]{10, 7}); + assert result2 == 57.0d : "Incorrect result: " + result2; + double result3 = evolvedProgram.evaluate(new double[]{13, 22}); + assert result3 == 87.0d : "Incorrect result: " + result3; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElseTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElseTest.java new file mode 100644 index 0000000..13ab813 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElseTest.java @@ -0,0 +1,248 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link IfThenElse} node type. + * @author Daniel Dyer + */ +public class IfThenElseTest +{ + @Test + public void testIfBranch() + { + Node node = new IfThenElse(new Constant(1), + new Constant(5), + new Constant(10)); + // Condition (1) is true, so result should be 5. + double value = node.evaluate(new double[0]); + assert value == 5 : "Wrong answer: " + value; + } + + + @Test + public void testElseBranch() + { + Node node = new IfThenElse(new Constant(0), + new Constant(5), + new Constant(10)); + // Condition (0) is true, so result should be 10. + double value = node.evaluate(new double[0]); + assert value == 10 : "Wrong answer: " + value; + } + + + /** + * Count nodes when the sub-trees each consist of only a single leaf node. + */ + @Test + public void testCountNodesSimple() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + int nodeCount = node.countNodes(); + assert nodeCount == 4 : "Tree has 4 nodes (root + 3 children), not " + nodeCount; + } + + + /** + * Count nodes when the sub-trees consist of non-leaf nodes. + */ + @Test + public void testCountNodesComplex() + { + Node node = new IfThenElse(new Addition(new Constant(1), new Constant(2)), + new Subtraction(new Constant(3), new Constant(4)), + new Multiplication(new Constant(5), new Constant(6))); + int nodeCount = node.countNodes(); + assert nodeCount == 10 : "Tree has 10 nodes (root + 3 children and 6 grand-children), not " + nodeCount; + } + + + @Test + public void testGetNode() + { + Node condition = new Constant(1); + Node then = new Constant(2); + Constant sub1 = new Constant(3); + Constant sub2 = new Constant(4); + Node otherwise = new Addition(sub1, sub2); + Node node = new IfThenElse(condition, then, otherwise); + + assert node.getNode(0) == node : "Node zero should be root node."; + assert node.getNode(1) == condition : "Node one should be condition node."; + assert node.getNode(2) == then : "Node one should be then node."; + assert node.getNode(3) == otherwise : "Node one should be else node."; + assert node.getNode(4) == sub1 : "Node 4 should be first sub-node of else node."; + assert node.getNode(5) == sub2 : "Node 5 should be second sub-node of else node."; + } + + + @Test + public void testReplaceRootNode() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + Constant constant = new Constant(0); + Node newNode = node.replaceNode(0, constant); + assert newNode == constant : "Root node should be replaced by new node."; + } + + + @Test(dependsOnMethods = "testIfBranch") + public void testReplaceConditionNode() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + Node newNode = node.replaceNode(1, new Constant(0)); + assert newNode instanceof IfThenElse : "Replacing condition node should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 3d : "Changing condition node should change evaluation."; + } + + + @Test(dependsOnMethods = "testIfBranch") + public void testReplaceThenNode() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + Node newNode = node.replaceNode(2, new Constant(4)); + assert newNode instanceof IfThenElse : "Replacing then node should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 4d : "Changing then node should change evaluation."; + } + + + @Test(dependsOnMethods = "testElseBranch") + public void testReplaceElseNode() + { + Node node = new IfThenElse(new Constant(0), new Constant(2), new Constant(3)); + Node newNode = node.replaceNode(3, new Constant(5)); + assert newNode instanceof IfThenElse : "Replacing else node should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 5d : "Changing then node should change evaluation."; + } + + + /** + * Test replacing of nodes which are not direct sub-nodes of this node but are further down + * the tree. + */ + @Test(dependsOnMethods = "testReplaceElseNode") + public void testReplaceSubNodes() + { + Node node = new IfThenElse(new Constant(0), new Constant(2), new Addition(new Constant(3), new Constant(4))); + // Replace both of the constants summed by the addition node on the else branch. + Node newNode = node.replaceNode(4, new Constant(5)); + newNode = newNode.replaceNode(5, new Constant(6)); + assert newNode instanceof IfThenElse : "Replacing sub-nodes should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 11d : "Changing sub-nodes should change evaluation."; + } + + + @Test + public void testZeroProbabilityMutation() + { + Node node = new IfThenElse(new Constant(0), new Constant(2), new Addition(new Constant(3), new Constant(4))); + double value = node.evaluate(BinaryNode.NO_ARGS); + String string = node.print(); + + Node mutated = node.mutate(ExamplesTestUtils.getRNG(), Probability.ZERO, null); + assert mutated == node : "Node should not have changed."; + assert value == mutated.evaluate(BinaryNode.NO_ARGS) : "Node should not have been altered."; + assert string.equals(mutated.print()) : "Node should not have been altered."; + } + + + @Test + public void testSimplify() + { + Node node = new IfThenElse(new Constant(0), new Constant(1), new Constant(2)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new IfThenElse(new Constant(4), + new IfThenElse(new Constant(1), new Constant(2), new Constant(3)), + new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 2; + } + + + /** + * Make sure that sub-nodes are simplified and that these simplfications are taken into + * account when optimising the parent node (in other words the child nodes should be + * simplified first). + */ + @Test + public void testSimplifyComplex() + { + Node node = new IfThenElse(new IfThenElse(new Constant(1), new Constant(2), new Constant(3)), + new Constant(4), + new Constant(5)); + Node simplified = node.simplify(); + // It is not sufficient to simplify this to an IfThenElse with a constant for each node, + // the fact that the condition node can be replaced by a Constant should result in the whole + // tree being reduced to a single constant (with a value of 4). + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 4; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new IfThenElse(new Parameter(0), new Constant(1), new Constant(2)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * If the two branches (then and else) are identical, it doesn't matter what the condition is, + * the tree can be replaced by either one of the branches. + */ + @Test + public void testSimplifyIdenticalBranches() + { + Node node = new IfThenElse(new Parameter(0), + new Constant(2), + new Constant(2)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{1}; // Need one argument for the parameter node to use. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 2; + + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IsGreaterTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IsGreaterTest.java new file mode 100644 index 0000000..91baeec --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IsGreaterTest.java @@ -0,0 +1,110 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link IsGreater} node type. + * @author Daniel Dyer + */ +public class IsGreaterTest +{ + /** + * If two nodes are equal then neither is greater than the other. + */ + @Test + public void testBothNodesEqual() + { + Node node = new IsGreater(new Constant(1), new Constant(1)); + assert node.evaluate(new double[0]) == 0 : "First node should not be greater than second."; + } + + + @Test + public void testFirstNodeGreater() + { + Node node = new IsGreater(new Constant(2), new Constant(1)); + assert node.evaluate(new double[0]) > 0 : "First node should be greater than second."; + } + + + @Test + public void testSecondNodeGreater() + { + Node node = new IsGreater(new Constant(-1), new Constant(1)); + assert node.evaluate(new double[0]) == 0 : "First node should be less than second."; + } + + + /** + * If the arguments to the IsGreater function are both constants then the node + * should be replaced by a constant node containing the evaluation of this expression. + */ + @Test + public void testSimplifyConstants() + { + Node node = new IsGreater(new Constant(7), new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 1; + + } + + + @Test + public void testSimplifyIdenticalArguments() + { + Node node = new IsGreater(new Parameter(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new IsGreater(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new IsGreater(new Parameter(0), + new IsGreater(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof IsGreater + : "Simplified node should be IsGreater, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/MultiplicationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/MultiplicationTest.java new file mode 100644 index 0000000..f81c367 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/MultiplicationTest.java @@ -0,0 +1,157 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Multiplication} node type. + * @author Daniel Dyer + */ +public class MultiplicationTest +{ + @Test + public void testEvaluation() + { + Node node = new Multiplication(new Constant(5), new Constant(2)); + double value = node.evaluate(new double[0]); + assert value == 10 : "Wrong result: " + value; + } + + + @Test + public void testStringRepresentation() + { + Node node = new Multiplication(new Constant(5), new Parameter(0)); + assert node.print().equals("(5.0 * arg0)") : "Wrong string representation: " + node.print(); + } + + + /** + * If the arguments to the multiply function are both constants then the multiplication node + * should be replaced by a constant node containing the evaluation of this product. + */ + @Test + public void testSimplifyConstants() + { + Node node = new Multiplication(new Constant(3), new Constant(4)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 12; + + } + + + /** + * If the lefthand argument is zero, the result will always be zero regardless of the righthand + * argument. + */ + @Test + public void testSimplifyMultiplyZero() + { + Node node = new Multiplication(new Constant(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 0; + + } + + + /** + * If the righthand argument is zero, the result will always be zero regardless of the lefthand + * argument. + */ + @Test + public void testSimplifyMultiplyByZero() + { + Node node = new Multiplication(new Parameter(0), new Constant(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 0; + } + + + /** + * If the lefthand argument is one, the result can be reduced to the righthand argument. + */ + @Test + public void testSimplifyMultiplyOne() + { + Node node = new Multiplication(new Constant(1), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 5; + + } + + + /** + * If the righthand argument is one, the result can be reduced to the lefthand argument. + */ + @Test + public void testSimplifyMultiplyByOne() + { + Node node = new Multiplication(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 5; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new Multiplication(new Parameter(0), new Constant(2)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new Multiplication(new Parameter(0), + new Multiplication(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof Multiplication + : "Simplified node should be Multiplication, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ParameterTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ParameterTest.java new file mode 100644 index 0000000..7324242 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ParameterTest.java @@ -0,0 +1,60 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Parameter} node type. + * @author Daniel Dyer + */ +public class ParameterTest +{ + @Test + public void testParameterSelection() + { + Parameter parameter = new Parameter(2); + double value = parameter.evaluate(new double[]{0, 1, 2, 3}); + assert value == 2 : "Incorect argument selected: " + value; + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidParameterSelection() + { + Parameter parameter = new Parameter(2); + // There is only one argument, so trying to select the 3rd one should + // result in an IllegalArgumentException. + parameter.evaluate(new double[]{0}); + } + + + @Test + public void testEquality() + { + Parameter zero = new Parameter(0); + Parameter one = new Parameter(1); + Parameter anotherOne = new Parameter(1); + + assert zero.equals(zero) : "Equality must be reflexive."; + assert one.equals(anotherOne) : "Same-index parameters must be equal."; + assert anotherOne.equals(one) : "Equality must be symmetric."; + assert one.hashCode() == anotherOne.hashCode() : "Equal objects must have equal hash codes."; + assert !zero.equals(one) : "Different index parameters must be non-equal."; + assert !zero.equals(null) : "No non-null object should not be considered equal to null."; + assert !zero.equals(Integer.valueOf(0)) : "Objects of different types should not be equal."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SimplificationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SimplificationTest.java new file mode 100644 index 0000000..f439021 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SimplificationTest.java @@ -0,0 +1,70 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link Simplification} evolutionary operator. + * @author Daniel Dyer + */ +public class SimplificationTest +{ + /** + * When the probability is 1, all candidates should be processed. + */ + @Test + public void testProbabilityOne() + { + Node node1 = new Addition(new Constant(1), new Constant(1)); + Node node2 = new Subtraction(new Constant(5), new Constant(4)); + Node node3 = new Multiplication(new Constant(3), new Constant(3)); + List<Node> population = Arrays.asList(node1, node2, node3); + + Simplification simplification = new Simplification(); + List<Node> evolved = simplification.apply(population, ExamplesTestUtils.getRNG()); + assert evolved.size() == population.size() : "Output should be same size as input."; + for (Node node : evolved) + { + assert node instanceof Constant : "Node was not simplified."; + } + } + + + /** + * When the probability is 0, no candidates should be processed. + */ + @Test + public void testProbabilityZero() + { + Node node1 = new Addition(new Constant(1), new Constant(1)); + Node node2 = new Subtraction(new Constant(5), new Constant(4)); + Node node3 = new Multiplication(new Constant(3), new Constant(3)); + List<Node> population = Arrays.asList(node1, node2, node3); + + Simplification simplification = new Simplification(Probability.ZERO); + List<Node> evolved = simplification.apply(population, ExamplesTestUtils.getRNG()); + assert evolved.size() == population.size() : "Output should be same size as input."; + for (Node node : evolved) + { + assert !(node instanceof Constant) : "Node should not have been simplified."; + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SubtractionTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SubtractionTest.java new file mode 100644 index 0000000..1682b25 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SubtractionTest.java @@ -0,0 +1,133 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Subtraction} node type. + * @author Daniel Dyer + */ +public class SubtractionTest +{ + @Test + public void testEvaluation() + { + Node node = new Subtraction(new Constant(7), new Constant(3)); + double value = node.evaluate(new double[0]); + assert value == 4 : "Wrong result: " + value; + } + + + @Test + public void testStringRepresentation() + { + Node node = new Subtraction(new Constant(7), new Constant(3)); + assert node.print().equals("(7.0 - 3.0)") : "Wrong string representation: " + node.print(); + } + + + /** + * If the arguments to the subtract function are both constants then the subtract node + * should be replaced by a constant node containing the evaluation of this sum. + */ + @Test + public void testSimplifyConstants() + { + Node node = new Subtraction(new Constant(7), new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 2; + } + + + /** + * If the arguments to the subtract function are identical, even if they are not constant, + * then the answer will always be zero, so this node should be replaced by the constant zero. + */ + @Test + public void testSimplifyIdenticalArguments() + { + Node node = new Subtraction(new Parameter(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 0; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new Subtraction(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * If the second argument is zero, the experession can be replaced by its lefthand side. + */ + @Test + public void testSimplifySubtractZero() + { + Node node = new Subtraction(new Parameter(0), new Constant(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 5; + } + + + /** + * But if the first argument is zero, the experession should not be simplified as it has + * the effect of negating the second argument. + */ + @Test + public void testSimplifySubtractFromZero() + { + Node node = new Subtraction(new Constant(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new Subtraction(new Parameter(0), + new Subtraction(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof Subtraction + : "Simplified node should be Subtraction, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRendererTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRendererTest.java new file mode 100644 index 0000000..c0ac452 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRendererTest.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.awt.Dimension; +import javax.swing.JComponent; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link SwingGPTreeRenderer} class. + * @author Daniel Dyer + */ +public class SwingGPTreeRendererTest +{ + // There's not much we can effectively test for this class, but we can at least + // make sure the generated component has the correct dimensions. + @Test + public void testSizes() + { + SwingGPTreeRenderer renderer = new SwingGPTreeRenderer(); + + // Simple case. + Node tree = new Constant(2); + JComponent component = renderer.render(tree); + Dimension minSize = component.getMinimumSize(); + assert minSize.width == 30 : "Wrong width: " + minSize.width; + assert minSize.height == 50 : "Wrong height: " + minSize.height; + + // More complicated case. + tree = new IfThenElse(new Parameter(0), + new Addition(new Constant(2), new Parameter(1)), + new Constant(5)); + component = renderer.render(tree); + minSize = component.getMinimumSize(); + assert minSize.width == 120 : "Wrong width: " + minSize.width; + assert minSize.height == 150 : "Wrong height: " + minSize.height; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossoverTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossoverTest.java new file mode 100644 index 0000000..5fdf777 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossoverTest.java @@ -0,0 +1,47 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link TreeCrossover} evolutionary operator used by + * the genetic programming example application. + * @author Daniel Dyer + */ +public class TreeCrossoverTest +{ + /** + * This is just a simple sanity check. Cross-over should result in the same + * number of individuals, all constructed from the same set of nodes that existed + * in the parent generation (but perhaps connected differently). + */ + @Test + public void testCrossover() + { + TreeCrossover crossover = new TreeCrossover(); + Node tree1 = new Multiplication(new Constant(1), new Constant(2)); + Node tree2 = new Subtraction(new Parameter(0), new Parameter(1)); + + List<Node> offspring = crossover.apply(Arrays.asList(tree1, tree2), ExamplesTestUtils.getRNG()); + assert offspring.size() == 2 : "Should be 2 offspring after cross-over."; + int totalNodeCount = offspring.get(0).countNodes() + offspring.get(1).countNodes(); + assert totalNodeCount == 6 : "Should be exactly 6 nodes in total, is " + totalNodeCount; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluatorTest.java new file mode 100644 index 0000000..7aec257 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluatorTest.java @@ -0,0 +1,78 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Unit test for the {@link FitnessEvaluator} used + * in the genetic programming example applicaiton. + * @author Daniel Dyer + */ +public class TreeEvaluatorTest +{ + /** + * A function that perfectly generates the correct output for all inputs + * should have a fitness of zero. + */ + @Test + public void testPerfectFunction() + { + Map<double[], Double> data = new HashMap<double[], Double>(); + // Data for multiplication program. + data.put(new double[]{5d, 3d}, 15d); + data.put(new double[]{3d, 8d}, 24d); + data.put(new double[]{7d, 2d}, 14d); + + FitnessEvaluator<Node> evaluator = new TreeEvaluator(data); + + // Program that multiplies its two inputs together. + Node program = new Multiplication(new Parameter(0), new Parameter(1)); + + double fitness = evaluator.getFitness(program, Arrays.asList(program)); + assert fitness == 0 : "Correct program should have zero fitness."; + } + + + /** + * A function that doesn't generate the correct output for all inputs + * should have a non-zero fitness. + */ + @Test + public void testIncorrectFunction() + { + Map<double[], Double> data = new HashMap<double[], Double>(); + // Data for multiplication program. + data.put(new double[]{5d, 3d}, 15d); + data.put(new double[]{3d, 8d}, 24d); + data.put(new double[]{7d, 2d}, 14d); + + FitnessEvaluator<Node> evaluator = new TreeEvaluator(data); + + // Program that multiplies its first input by 3 (will give the correct answer + // for the first set of inputs but the wrong answer for the other two). + Node program = new Multiplication(new Parameter(0), new Constant(3d)); + + double fitness = evaluator.getFitness(program, Arrays.asList(program)); + // Error on second example is 15, error on third is 7. + // 15^2 + 7^2 = 225 + 49 = 274 + assert fitness == 274d : "Wrong fitness for incorrect program."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactoryTest.java new file mode 100644 index 0000000..d4ed9e6 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactoryTest.java @@ -0,0 +1,66 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for the {@link TreeFactory} used by the gentic programming + * example. + * @author Daniel Dyer + */ +public class TreeFactoryTest +{ + @Test + public void testMaxDepth() + { + final int maxDepth = 3; + CandidateFactory<Node> factory = new TreeFactory(2, + maxDepth, + new Probability(0.6), + Probability.EVENS); + List<Node> trees = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (Node tree : trees) + { + // Make sure that each tree is no bigger than the maximum permitted. + assert tree.getDepth() <= maxDepth : "Generated tree is too deep: " + tree.getDepth(); + } + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidParameterCount() + { + new TreeFactory(-1, + 1, + Probability.EVENS, + Probability.EVENS); // Should throw an exception, parameter count can't be negative. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMaxDepth() + { + new TreeFactory(1, + 0, + Probability.EVENS, + Probability.EVENS); // Should throw an exception, depth must be at least one. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutationTest.java new file mode 100644 index 0000000..3bf6b0f --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutationTest.java @@ -0,0 +1,76 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link TreeMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class TreeMutationTest +{ + /** + * If the mutation doesn't happen, the candidates should be returned unaltered. + */ + @Test + public void testNoMutations() + { + TreeFactory treeFactory = new TreeFactory(0, 3, Probability.EVENS, Probability.EVENS); + EvolutionaryOperator<Node> mutation = new TreeMutation(treeFactory, + Probability.ZERO); // Zero probability means no mutations. + List<Node> candidates = Arrays.<Node>asList(new Addition(new Constant(3), new Constant(4))); + List<Node> result = mutation.apply(candidates, ExamplesTestUtils.getRNG()); + assert result.size() == 1 : "Wrong number of trees returned: " + result.size(); + assert candidates.get(0) == result.get(0) : "Tree should have been returned unmodified."; + } + + + /** + * If the mutation doesn't happen, the candidates should be returned unaltered. + */ + @Test + public void testSomeMutations() + { + TreeFactory treeFactory = new TreeFactory(1, 4, new Probability(0.6d), new Probability(0.2d)); + EvolutionaryOperator<Node> mutation = new TreeMutation(treeFactory, + Probability.ONE); // Probability of 1 means guaranteed mutations. + List<Node> candidates = treeFactory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + + Map<Node, Node> distinctTrees = new IdentityHashMap<Node, Node>(); + for (Node node : candidates) + { + distinctTrees.put(node, node); + } + + List<Node> result = mutation.apply(candidates, ExamplesTestUtils.getRNG()); + assert result.size() == 20 : "Wrong number of trees returned: " + result.size(); + for (Node node : result) + { + distinctTrees.put(node, node); + } + + // If none of the original trees are returned, we should have 40 distict trees. + assert distinctTrees.size() == 40 : "New trees should have been returned."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutationTest.java new file mode 100644 index 0000000..53c7aea --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutationTest.java @@ -0,0 +1,104 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link AddPolygonMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class AddPolygonMutationTest +{ + private final PolygonImageFactory factory = new PolygonImageFactory(new Dimension(200, 200)); + + @Test + public void testAddPolygon() + { + List<ColouredPolygon> image = factory.generateRandomCandidate(ExamplesTestUtils.getRNG()); + assert image.size() == 2 : "Image should have 2 polygons"; + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new AddPolygonMutation(Probability.ONE, + factory, + 5); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() + 1 : "Image should have 1 extra polygon after mutation."; + } + + + @Test + public void testZeroProbability() + { + List<ColouredPolygon> image = factory.generateRandomCandidate(ExamplesTestUtils.getRNG()); + assert image.size() == 2 : "Image should have 2 polygons"; + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new AddPolygonMutation(Probability.ZERO, + factory, + 5); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } + + + /** + * If the image already has the maximum permitted number of polygons, extra polygons + * should not be added by mutation. + */ + @Test + public void testAddMaxPolygons() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new AddPolygonMutation(Probability.ONE, + factory, + 3); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have no more than the maximum number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } + + + /** + * An image must have at least 2 polygons. The configured maximum must respect this. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMaximum() + { + new AddPolygonMutation(Probability.ONE, factory, 1); // Invalid, should throw IllegalArgumentException. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddVertexMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddVertexMutationTest.java new file mode 100644 index 0000000..50579a8 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddVertexMutationTest.java @@ -0,0 +1,100 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link AddVertexMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class AddVertexMutationTest +{ + private final Dimension canvasSize = new Dimension(200, 200); + private final PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + + @Test + public void testAddVertex() + { + ColouredPolygon polygon = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AddVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + List<Point> vertices = evolved.get(0).getVertices(); + assert vertices.size() == polygon.getVertices().size() + 1 + : "Polygon should have 1 extra point after mutation."; + } + + + @Test + public void testZeroProbability() + { + ColouredPolygon polygon = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AddVertexMutation(canvasSize, + Probability.ZERO); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no extra points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } + + + /** + * If the image already has the maximum permitted number of points, extra points + * should not be added by mutation. + */ + @Test + public void testAddMaxPoints() + { + List<Point> points = new ArrayList<Point>(AddVertexMutation.MAX_VERTEX_COUNT); + for (int i = 0; i < AddVertexMutation.MAX_VERTEX_COUNT; i++) + { + points.add(new Point(i, i)); + } + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AddVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no extra points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutationTest.java new file mode 100644 index 0000000..a4b8142 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutationTest.java @@ -0,0 +1,115 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link AdjustVertexMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class AdjustVertexMutationTest +{ + private final Dimension canvasSize = new Dimension(200, 200); + + @Test + public void testAdjustVertex() + { + final Point point1 = new Point(1, 1); + final Point point2 = new Point(2, 2); + final Point point3 = new Point(3, 3); + List<Point> points = Arrays.asList(point1, point2, point3); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + final int amount = 5; + EvolutionaryOperator<ColouredPolygon> mutation = new AdjustVertexMutation(canvasSize, + Probability.ONE, + new ConstantGenerator<Integer>(amount)); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have same number of points after mutation."; + + if (vertices.get(0) != point1) + { + Point newPoint = vertices.get(0); + assert newPoint.x == point1.x + 5 : "X-coordinate not mutated properly."; + assert newPoint.y == point1.y + 5 : "Y-coordinate not mutated properly."; + // If the first point is different the other two should be the same. + assert vertices.get(1) == point2 : "Second point should be unchanged."; + assert vertices.get(2) == point3 : "Third point should be unchanged."; + } + else if (vertices.get(1) != point2) + { + Point newPoint = vertices.get(1); + assert newPoint.x == point2.x + 5 : "X-coordinate not mutated properly."; + assert newPoint.y == point2.y + 5 : "Y-coordinate not mutated properly."; + // If the second point is different the other two should be the same. + assert vertices.get(0) == point1 : "First point should be unchanged."; + assert vertices.get(2) == point3 : "Third point should be unchanged."; + } + else if (vertices.get(2) != point3) + { + Point newPoint = vertices.get(2); + assert newPoint.x == point3.x + 5 : "X-coordinate not mutated properly."; + assert newPoint.y == point3.y + 5 : "Y-coordinate not mutated properly."; + // If the third point is different the other two should be the same. + assert vertices.get(0) == point1 : "Third point should be unchanged."; + assert vertices.get(1) == point2 : "Second point should be unchanged."; + } + } + + + @Test + public void testZeroProbability() + { + Point point1 = new Point(1, 1); + Point point2 = new Point(2, 2); + Point point3 = new Point(3, 3); + List<Point> points = Arrays.asList(point1, point2, point3); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AdjustVertexMutation(canvasSize, + Probability.ZERO, + new ConstantGenerator<Integer>(1)); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have same number of points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + assert vertices.get(0) == point1 : "First point should be unchanged."; + assert vertices.get(1) == point2 : "Second point should be unchanged."; + assert vertices.get(2) == point3 : "Third point should be unchanged."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutationTest.java new file mode 100644 index 0000000..cf679e8 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutationTest.java @@ -0,0 +1,75 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link MovePolygonMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class MovePolygonMutationTest +{ + private final PolygonImageFactory factory = new PolygonImageFactory(new Dimension(200, 200)); + + @Test + public void testMovePolygon() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new MovePolygonMutation(Probability.ONE); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons after mutation."; + // Can't reliably test that the order was mutated because the random selection may have moved + // the polygon back to its original index. + } + + + @Test + public void testZeroProbability() + { + ColouredPolygon polygon1 = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + ColouredPolygon polygon2 = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + ColouredPolygon polygon3 = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + List<ColouredPolygon> image = Arrays.asList(polygon1, polygon2, polygon3); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new MovePolygonMutation(Probability.ZERO); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + assert evolved.get(0).get(0) == polygon1 : "First polygon should not have moved."; + assert evolved.get(0).get(1) == polygon2 : "Second polygon should not have moved."; + assert evolved.get(0).get(2) == polygon3 : "Third polygon should not have moved."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutationTest.java new file mode 100644 index 0000000..bbdb1f9 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutationTest.java @@ -0,0 +1,69 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Point; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link PolygonColourMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class PolygonColourMutationTest +{ + @Test + public void testColourMutation() + { + PolygonColourMutation mutation = new PolygonColourMutation(Probability.ONE, // Guaranteed mutation. + new ConstantGenerator<Double>(1d)); + // A grey triangle. + final ColouredPolygon polygon = new ColouredPolygon(new Color(128, 128, 128, 128), + Arrays.asList(new Point(0, 0), + new Point(50, 50), + new Point(0, 75))); + List<ColouredPolygon> image = Arrays.asList(polygon); + List<ColouredPolygon> mutatedImage = mutation.apply(image, ExamplesTestUtils.getRNG()); + Color mutatedColour = mutatedImage.get(0).getColour(); + assert mutatedColour.getRed() == 129 : "Red component should have been incremented, is " + mutatedColour.getRed(); + assert mutatedColour.getGreen() == 129 : "Green component should have been incremented, is " + mutatedColour.getGreen(); + assert mutatedColour.getBlue() == 129 : "Blue component should have been incremented, is " + mutatedColour.getBlue(); + assert mutatedColour.getAlpha() == 129 : "Alpha component should have been incremented, is " + mutatedColour.getAlpha(); + } + + + @Test + public void testZeroProbability() + { + PolygonColourMutation mutation = new PolygonColourMutation(Probability.ZERO, + new ConstantGenerator<Double>(1d)); + // A grey triangle. + Color originalColour = new Color(128, 128, 128, 128); + final ColouredPolygon polygon = new ColouredPolygon(originalColour, + Arrays.asList(new Point(0, 0), + new Point(50, 50), + new Point(0, 75))); + List<ColouredPolygon> image = Arrays.asList(polygon); + List<ColouredPolygon> mutatedImage = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert mutatedImage.get(0) == polygon : "Polygon should not have changed at all."; + assert mutatedImage.get(0).getColour() == originalColour : "Colour should not have changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluatorTest.java new file mode 100644 index 0000000..fbd94e8 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluatorTest.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.MersenneTwisterRNG; + +/** + * Unit test for {@link PolygonImageEvaluator}. + * @author Daniel Dyer + */ +public class PolygonImageEvaluatorTest +{ + /** + * An image that is identical to the target image should have a fitness + * of zero. + */ + @Test(groups = "display-required") + public void testPerfectMatch() + { + Dimension canvasSize = new Dimension(100, 100); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + List<ColouredPolygon> image = factory.generateRandomCandidate(new MersenneTwisterRNG()); + + BufferedImage targetImage = new PolygonImageRenderer(canvasSize, false, null).render(image); + PolygonImageEvaluator evaluator = new PolygonImageEvaluator(targetImage); + + double fitness = evaluator.getFitness(image, null); + assert fitness == 0 : "Fitness should be zero when image is an exact match."; + } + + + /** + * An image that is different to the target image should have a non-zero fitness. + */ + @Test(groups = "display-required") + public void testDifferentImages() + { + Dimension canvasSize = new Dimension(100, 100); + List<ColouredPolygon> targetImage = Arrays.asList(new ColouredPolygon(Color.BLACK, + Arrays.asList(new Point(0, 0), + new Point(99, 0), + new Point(99, 99), + new Point(0, 99)))); + List<ColouredPolygon> candidateImage = Arrays.asList(new ColouredPolygon(Color.WHITE, + Arrays.asList(new Point(0, 0), + new Point(99, 0), + new Point(99, 99), + new Point(0, 99)))); + + BufferedImage renderedTarget = new PolygonImageRenderer(canvasSize, false, null).render(targetImage); + PolygonImageEvaluator evaluator = new PolygonImageEvaluator(renderedTarget); + + double fitness = evaluator.getFitness(candidateImage, null); + assert fitness > 0 : "Fitness should be non-zero when image does not match target."; + } + + + /** + * If the image is not INT_RGB, it will be converted. This should not affect the results. + */ + @Test(groups = "display-required") + public void testImageConversion() + { + Dimension canvasSize = new Dimension(100, 100); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + List<ColouredPolygon> image = factory.generateRandomCandidate(new MersenneTwisterRNG()); + + BufferedImage targetImage = new PolygonImageRenderer(canvasSize, false, null).render(image); + // Convert target image to some format that will have to be converted to INT_RGB. + BufferedImage newImage = new BufferedImage(targetImage.getWidth(), + targetImage.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); // Sub-optimal image type. + newImage.getGraphics().drawImage(targetImage, 0, 0, null); + + PolygonImageEvaluator evaluator = new PolygonImageEvaluator(newImage); + + double fitness = evaluator.getFitness(image, null); + assert fitness == 0 : "Fitness should be zero when image is an exact match."; + } + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactoryTest.java new file mode 100644 index 0000000..15b2db4 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactoryTest.java @@ -0,0 +1,59 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for {@link PolygonImageFactory}. + * @author Daniel Dyer + */ +public class PolygonImageFactoryTest +{ + /** + * Make sure that the generated images have the correct number of polygons, + * that each polygon has the correct number of points and that all of the + * points fall within the bounds of the specified canvas. + */ + @Test + public void testConstraints() + { + final int width = 100; + final int height = 50; + CandidateFactory<List<ColouredPolygon>> factory = new PolygonImageFactory(new Dimension(width, height)); + List<List<ColouredPolygon>> candidates = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (List<ColouredPolygon> image : candidates) + { + assert image.size() == PolygonImageFactory.MINIMUM_POLYGON_COUNT + : "Wrong number of polygons: " + image.size(); + for (ColouredPolygon polygon : image) + { + assert polygon.getVertices().size() == PolygonImageFactory.MINIMUM_VERTEX_COUNT + : "Wrong number of vertices: " + polygon.getVertices().size(); + for (Point point : polygon.getVertices()) + { + assert point.x >= 0 && point.x < width : "X out-of-range: " + point.x; + assert point.y >= 0 && point.y < height : "Y out-of-range: " + point.y; + } + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanelTest.java new file mode 100644 index 0000000..3e44663 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanelTest.java @@ -0,0 +1,132 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link ProbabilitiesPanel} class. + * @author Daniel Dyer + */ +public class ProbabilitiesPanelTest +{ + private Robot robot; + + @BeforeMethod(groups = "display-required") + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod(groups = "display-required") + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + @Test(groups = "display-required") + public void testSetAllProbabilitiesToZero() + { + ProbabilitiesPanel panel = new ProbabilitiesPanel(); + JFrame frame = new JFrame(); + frame.add(panel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(700, 300); + frame.validate(); + + frameFixture.show(); + frameFixture.panel("AddPolygon").slider().slideToMinimum(); + frameFixture.panel("RemovePolygon").slider().slideToMinimum(); + frameFixture.panel("MovePolygon").slider().slideToMinimum(); + frameFixture.panel("Cross-over").slider().slideToMinimum(); + frameFixture.panel("AddVertex").slider().slideToMinimum(); + frameFixture.panel("RemoveVertex").slider().slideToMinimum(); + frameFixture.panel("MoveVertex").slider().slideToMinimum(); + frameFixture.panel("ChangeColour").slider().slideToMinimum(); + + Dimension canvasSize = new Dimension(100, 100); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + EvolutionaryOperator<List<ColouredPolygon>> operator = panel.createEvolutionPipeline(factory, + canvasSize, + ExamplesTestUtils.getRNG()); + List<ColouredPolygon> candidate1 = Arrays.asList(new ColouredPolygon(Color.WHITE, + Arrays.asList(new Point(1, 1)))); + List<ColouredPolygon> candidate2 = Arrays.asList(new ColouredPolygon(Color.BLACK, + Arrays.asList(new Point(2, 2)))); + List<List<ColouredPolygon>> population = new ArrayList<List<ColouredPolygon>>(2); + population.add(candidate1); + population.add(candidate2); + + List<List<ColouredPolygon>> evolved = operator.apply(population, + ExamplesTestUtils.getRNG()); + // Candidate order may have changed, but individual candidates should remain unaltered. + assert (checkEquals(evolved.get(0), candidate1) && checkEquals(evolved.get(1), candidate2)) + || (checkEquals(evolved.get(0), candidate2) && checkEquals(evolved.get(1), candidate1)) + : "Candidates should be unaltered when all probabilities are zero."; + } + + + private boolean checkEquals(List<ColouredPolygon> candidate1, + List<ColouredPolygon> candidate2) + { + if (candidate1.size() != candidate2.size()) + { + return false; + } + for (int i = 0; i < candidate1.size(); i++) + { + ColouredPolygon polygon1 = candidate1.get(i); + ColouredPolygon polygon2 = candidate2.get(i); + if (!polygon1.getColour().equals(polygon2.getColour())) + { + return false; + } + List<Point> vertices1 = polygon1.getVertices(); + List<Point> vertices2 = polygon2.getVertices(); + if (vertices1.size() != vertices2.size()) + { + return false; + } + for (int j = 0; j < vertices1.size(); j++) + { + Point point1 = vertices1.get(j); + Point point2 = vertices2.get(j); + if (point1.x != point2.x || point1.y != point2.y) + { + return false; + } + } + } + return true; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutationTest.java new file mode 100644 index 0000000..bcd5f6c --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutationTest.java @@ -0,0 +1,91 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link RemovePolygonMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class RemovePolygonMutationTest +{ + private final PolygonImageFactory factory = new PolygonImageFactory(new Dimension(200, 200)); + + @Test + public void testRemovePolygon() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new RemovePolygonMutation(Probability.ONE); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() - 1 : "Image should have 1 fewer polygon after mutation."; + } + + + @Test + public void testZeroProbability() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new RemovePolygonMutation(Probability.ZERO); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } + + + /** + * If the image already has the minimum permitted number of polygons, further polygons + * should not be removed by mutation. + */ + @Test + public void testRemoveMinPolygons() + { + List<ColouredPolygon> image = factory.generateRandomCandidate(ExamplesTestUtils.getRNG()); + assert image.size() == 2 : "Image should have 2 polygons"; + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new RemovePolygonMutation(Probability.ONE); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have no fewer than the minimum number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutationTest.java new file mode 100644 index 0000000..e204d55 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutationTest.java @@ -0,0 +1,98 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link RemoveVertexMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class RemoveVertexMutationTest +{ + private final Dimension canvasSize = new Dimension(200, 200); + + @Test + public void testRemoveVertex() + { + List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2), new Point(3, 3), new Point(4, 4)); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new RemoveVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + List<Point> vertices = evolved.get(0).getVertices(); + assert vertices.size() == polygon.getVertices().size() - 1 + : "Polygon should have 1 fewer point after mutation."; + } + + + @Test + public void testZeroProbability() + { + List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2), new Point(3, 3), new Point(4, 4)); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new RemoveVertexMutation(canvasSize, + Probability.ZERO); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no fewer points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } + + + /** + * If the image already has the minimum permitted number of points, further points + * should not be removed by mutation. + */ + @Test + public void testAddMaxPoints() + { + List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2), new Point(3, 3)); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new RemoveVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no fewer points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringEvaluatorTest.java new file mode 100644 index 0000000..54a3dd4 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringEvaluatorTest.java @@ -0,0 +1,58 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Unit test for fitness function used by the strings example. + * @author Daniel Dyer + */ +public class StringEvaluatorTest +{ + @Test + public void testIdentical() + { + String target = "abcdefgh"; + String candidate = "abcdefgh"; + FitnessEvaluator<String> evaluator = new StringEvaluator(target); + int score = (int) evaluator.getFitness(candidate, null); + assert score == 0 : "Fitness should be zero for identical strings, is " + score; + } + + + @Test + public void testCompletelyDifferent() + { + String target = "abcdefgh"; + String candidate = "ijklmnop"; + FitnessEvaluator<String> evaluator = new StringEvaluator(target); + int score = (int) evaluator.getFitness(candidate, null); + assert score == target.length() : "Fitness should be " + target.length() + ", is " + score; + } + + + @Test + public void testPartialSolution() + { + String target = "abcdefgh"; + String candidate = "abcdxxxx"; + FitnessEvaluator<String> evaluator = new StringEvaluator(target); + int score = (int) evaluator.getFitness(candidate, null); + assert score == 4 : "Fitness score should be 4, is " + score; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringsExampleTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringsExampleTest.java new file mode 100644 index 0000000..ab54121 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringsExampleTest.java @@ -0,0 +1,34 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the Strings example. Makes sure that the evolution engine + * eventually returns the target String. + * @author Daniel Dyer + */ +public class StringsExampleTest +{ + @Test + public void testEvolution() + { + String target = "WATCHMAKER"; + String result = StringsExample.evolveString(target); + assert result.equals(target) : "Evolution returned wrong result: " + result; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuCellRendererTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuCellRendererTest.java new file mode 100644 index 0000000..e1f3df3 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuCellRendererTest.java @@ -0,0 +1,112 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.Color; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.TableCellRenderer; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link SudokuCellRenderer} class. + * @author Daniel Dyer + */ +public class SudokuCellRendererTest +{ + private JTable table; + + + @Test + public void testRenderFixedCells() + { + SudokuTableModel model = new SudokuTableModel(); + // Set the 2 lefthand cells in the top row. + model.setValueAt('1', 0, 0); + model.setValueAt('2', 0, 1); + + JTable table = new JTable(model); + TableCellRenderer renderer = new SudokuCellRenderer(); + + JLabel cell1 = (JLabel) renderer.getTableCellRendererComponent(table, '1', false, false, 0, 0); + assert cell1.getText().equals("1") : "Wrong text at cell 1: " + cell1.getText(); + assert cell1.getFont().isBold() : "Fixed cells should be rendered in bold."; + JLabel cell2 = (JLabel) renderer.getTableCellRendererComponent(table, '2', false, false, 0, 1); + assert cell2.getText().equals("2") : "Wrong text at cell 2: " + cell2.getText(); + assert cell2.getFont().isBold() : "Fixed cells should be rendered in bold."; + // Check an empty cell, it should have no text. + JLabel cell3 = (JLabel) renderer.getTableCellRendererComponent(table, null, false, false, 0, 2); + assert cell3.getText().length() == 0 : "Wrong text at cell 3: " + cell3.getText(); + } + + + @BeforeClass + public void createTable() + { + SudokuTableModel model = new SudokuTableModel(); + model.setSudoku(SudokuTestUtils.createSudoku(new int[][]{{3, 2, 4, 8, 9, 1, 7, 5, 6}, + {6, 9, 7, 1, 5, 2, 8, 4, 3}, + {8, 1, 5, 7, 3, 6, 4, 2, 9}, + {5, 2, 6, 9, 7, 4, 3, 1, 8}, + {4, 9, 8, 1, 2, 5, 6, 7, 3}, + {8, 7, 1, 3, 4, 2, 9, 6, 5}, + {2, 6, 3, 4, 8, 7, 5, 9, 1}, + {1, 3, 5, 8, 4, 9, 2, 6, 7}, + {7, 4, 2, 1, 5, 3, 9, 8, 6}})); + table = new JTable(model); + } + + + @Test + public void testRenderCellWithNoConflicts() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 1, false, false, 2, 1); + assert cell.getText().equals("1") : "Wrong value at cell (2, 1): " + cell.getText(); + assert cell.getBackground() == Color.WHITE : "Cell without conflicts should be white."; + } + + + @Test + public void testRenderCellWithOneConflict() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 3, false, false, 7, 1); + assert cell.getText().equals("3") : "Wrong value at cell (7, 1): " + cell.getText(); + assert cell.getBackground() == Color.YELLOW : "Cell with one conflict should be yellow."; + } + + + @Test + public void testRenderCellWithTwoConflicts() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 6, false, false, 8, 8); + assert cell.getText().equals("6") : "Wrong value at cell (8, 8): " + cell.getText(); + assert cell.getBackground() == Color.ORANGE : "Cell with two conflicts should be orange."; + } + + + @Test + public void testRenderCellWithThreeConflicts() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 1, false, false, 1, 3); + assert cell.getText().equals("1") : "Wrong value at cell (1, 3): " + cell.getText(); + assert cell.getBackground() == Color.RED : "Cell with two conflicts should be red."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluatorTest.java new file mode 100644 index 0000000..38864fd --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluatorTest.java @@ -0,0 +1,77 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Unit test for sudoku fitness evaluator. + * @author Daniel Dyer + */ +public class SudokuEvaluatorTest +{ + /** + * Ensure that the evaluator returns zero for a correct solution. + */ + @Test + public void testCorrectSolution() + { + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][] + { + {1, 2, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 1, 2, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + }); + FitnessEvaluator<Sudoku> evaluator = new SudokuEvaluator(); + int fitness = (int) evaluator.getFitness(sudoku, null); + assert fitness == 0 : "Fitness should be zero for correct solution, is " + fitness; + } + + + /** + * Ensure that the evaluator returns zero for a correct solution. + */ + @Test + public void testDuplicates() + { + // This sudoku as 4 invalid columns (0, 1, 3 and 5) and 2 invalid + // sub-grids (bottom-left and bottom-center). + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][] + { + {2, 1, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 2, 1, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + }); + FitnessEvaluator<Sudoku> evaluator = new SudokuEvaluator(); + int fitness = (int) evaluator.getFitness(sudoku, null); + assert fitness == 6 : "Fitness should be 6, is " + fitness; + } + + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuFactoryTest.java new file mode 100644 index 0000000..f81cfa6 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuFactoryTest.java @@ -0,0 +1,142 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for sudoku candidate solution factory. + * @author Daniel Dyer + */ +public class SudokuFactoryTest +{ + /** + * Checks to make sure that the givens are correctly placed and that each row + * contains each value exactly once. + */ + @Test + public void testValidity() + { + CandidateFactory<Sudoku> factory = new SudokuFactory(".9.......", + ".........", + "........5", + "....2....", + ".........", + ".........", + ".........", + "...1.....", + "........9"); + + List<Sudoku> population = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (Sudoku sudoku : population) + { + // Check givens are correctly placed. + assert sudoku.isFixed(2, 8) : "Cell (2, 8) should be fixed."; + assert sudoku.getValue(2, 8) == 5 : "Cell (2, 8) should contain 5."; + assert sudoku.isFixed(7, 3) : "Cell (7, 3) should be fixed."; + assert sudoku.getValue(7, 3) == 1 : "Cell (7, 3) should contain 1."; + assert sudoku.isFixed(3, 4) : "Cell (3, 4) should be fixed."; + assert sudoku.getValue(3, 4) == 2 : "Cell (3, 4) should contain 2."; + assert sudoku.isFixed(0, 1) : "Cell (0, 1) should be fixed."; + assert sudoku.getValue(0, 1) == 9 : "Cell (0, 1) should contain 9."; + assert sudoku.isFixed(8, 8) : "Cell (8, 8) should be fixed."; + assert sudoku.getValue(8, 8) == 9 : "Cell (8, 8) should contain 9."; + + // Check that each row has no duplicates. + Set<Integer> set = new HashSet<Integer>(); + for (int i = 0; i < 9; i++) + { + Sudoku.Cell[] row = sudoku.getRow(i); + for (Sudoku.Cell cell : row) + { + set.add(cell.getValue()); + } + if (set.size() < 9) + { + System.out.println(sudoku); + assert false : "Row " + i + " contains duplicates."; + } + } + } + } + + + /** + * If the pattern used to create a Sudoku factory contains any characters + * other than the values 1 - 9 or dots (which represent empty cells), then + * an appropriate exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidPatternChars() + { + // This pattern is the right size but contains an invalid character. + String[] pattern = {"....9....", + "2..3.....", + "........1", + "....a....", // Invalid character on this line. + "....4....", + ".........", + ".........", + ".........", + "........."}; + new SudokuFactory(pattern); + } + + + /** + * If the pattern used to create a Sudoku factory contains the wrong number + * of rows then an appropriate exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfRows() + { + // This pattern contains only valid characters and has the right number + // of columns but doesn't have enough rows. + String[] pattern = {"....9....", + "2..3.....", + "........1", + ".........", + "........."}; + new SudokuFactory(pattern); + } + + + /** + * If the pattern used to create a Sudoku factory contains the wrong number + * of columns then an appropriate exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfColumns() + { + // This pattern contains only valid characters and has the right number + // of rows but has too many columns in some + String[] pattern = {"....9....", + "2..3.....", + "........1", + ".........", + ".........7", + ".........", + ".4.......6", + "..1.3....", + "........8"}; + new SudokuFactory(pattern); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutationTest.java new file mode 100644 index 0000000..9386e83 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutationTest.java @@ -0,0 +1,127 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for Sudoku mutation operator. + * @author Daniel Dyer + */ +public class SudokuRowMutationTest +{ + /** + * Tests to ensure that rows are still valid after mutation. Each row + * should contain each value 1-9 exactly once. + */ + @Test + public void testValidity() + { + EvolutionaryOperator<Sudoku> mutation = new SudokuRowMutation(8, 1); + List<Sudoku> population = Arrays.asList(SudokuTestUtils.createSudoku(new int[][] + { + {1, 2, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 1, 2, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + })); + final Set<Integer> counts = new HashSet<Integer>(Sudoku.SIZE); + for (int i = 0; i < 20; i++) + { + population = mutation.apply(population, ExamplesTestUtils.getRNG()); + assert population.size() == 1 : "Population size changed after mutation(s)."; + Sudoku mutatedSudoku = population.get(0); + for (int j = 0; j < Sudoku.SIZE; j++) + { + Sudoku.Cell[] row = mutatedSudoku.getRow(j); + assert row.length == Sudoku.SIZE : "Row length is invalid: " + row.length; + for (Sudoku.Cell cell : row) + { + int value = cell.getValue(); + assert value > 0 && value <= Sudoku.SIZE : "Cell value out of range: " + value; + counts.add(value); + } + assert counts.size() == Sudoku.SIZE : "Row contains duplicates."; + counts.clear(); + } + } + } + + + /** + * Check that the mutation never modifies the value of fixed cells. + */ + @Test(dependsOnMethods = "testValidity") + public void testFixedConstraints() + { + EvolutionaryOperator<Sudoku> mutation = new SudokuRowMutation(8, 1); + Sudoku.Cell[][] cells = new Sudoku.Cell[Sudoku.SIZE][Sudoku.SIZE]; + // One cell in each row is fixed (cell 1 in row 1, cell 2 in row 2, etc.) + for (int row = 0; row < Sudoku.SIZE; row++) + { + for (int column = 0; column < Sudoku.SIZE; column++) + { + cells[row][column] = new Sudoku.Cell(column + 1, column == row); + } + } + List<Sudoku> population = Arrays.asList(new Sudoku(cells)); + for (int i = 0; i < 100; i++) // 100 generations of mutation. + { + population = mutation.apply(population, ExamplesTestUtils.getRNG()); + Sudoku sudoku = population.get(0); + for (int row = 0; row < Sudoku.SIZE; row++) + { + for (int column = 0; column < Sudoku.SIZE; column++) + { + if (row == column) + { + assert sudoku.isFixed(row, column) : "Fixed cell has become unfixed."; + assert sudoku.getValue(row, column) == (row + 1) : "Fixed cell has changed value."; + } + else + { + assert !sudoku.isFixed(row, column) : "Unfixed cell has become fixed."; + } + } + } + } + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMutationCount() + { + new SudokuRowMutation(0, 1); // Should throw an IllegalArgumentException. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMutationAmount() + { + new SudokuRowMutation(1, 0); // Should throw an IllegalArgumentException. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTableModelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTableModelTest.java new file mode 100644 index 0000000..30bbf18 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTableModelTest.java @@ -0,0 +1,113 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import org.testng.annotations.Test; + +/** + * Unit test for the table model used by the Sudoku example application. + * @author Daniel Dyer + */ +public class SudokuTableModelTest +{ + private static final String[] TEST_PUZZLE = {"4.5...9.7", + ".2..9..6.", + "39.6.7.28", + "9..3.2..6", + "7..9.6..3", + "5..4.8..1", + "28.1.5.49", + ".7..3..8.", + "6.4...3.2"}; + + /** + * Makes sure that the table model class correctly converts to and from the + * String patterns used by {@link SudokuFactory}. + */ + @Test + public void testPatternConversions() + { + SudokuTableModel model = new SudokuTableModel(); + model.setPattern(TEST_PUZZLE); + // Change a cell, to ensure we don't get exactly the same pattern back. + model.setValueAt('1', 8, 7); + String[] newPattern = model.getPattern(); + // Make sure the pattern is correct. + for (int i = 0; i < 8; i++) // Check the first 8 rows, which haven't changed. + { + assert newPattern[i].equals(TEST_PUZZLE[i]) : "Row " + i + " incorrect: " + newPattern[i]; + } + // Check modified row. + assert newPattern[8].equals("6.4...312") : "Row 8 incorrect: " + newPattern[8]; + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManyRowsInPattern() + { + SudokuTableModel model = new SudokuTableModel(); + model.setPattern(new String[]{".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + "........."}); // 10 rows should trigger an IllegalArgumentException. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManyColumnsInPattern() + { + SudokuTableModel model = new SudokuTableModel(); + model.setPattern(new String[]{"..........", + "..........", + "..........", + "..........", + "..........", + "..........", + "..........", + "..........", + ".........."}); // 10 columns in each row should trigger an IllegalArgumentException. + } + + + @Test + public void testGetValueAt() + { + SudokuTableModel model = new SudokuTableModel(); + model.setSudoku(SudokuTestUtils.createSudoku(new int[][]{{1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}})); + for (int row = 0; row < model.getRowCount(); row++) + { + for (int column = 0; column < model.getColumnCount(); column++) + { + int actualCell = (Integer) model.getValueAt(row, column); + assert actualCell == column + 1: "Wrong value at " + row + ", " + column + ": " + actualCell; + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTest.java new file mode 100644 index 0000000..798b0fa --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTest.java @@ -0,0 +1,97 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import org.testng.annotations.Test; + +/** + * Unit test for the {@link Sudoku} data structure. + * @author Daniel Dyer + */ +public class SudokuTest +{ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfRows() + { + // This grid has one row and nine columns. Constructing it should throw + // an IllegalArgumentException. + new Sudoku(new Sudoku.Cell[][]{{new Sudoku.Cell(1, false), + new Sudoku.Cell(2, false), + new Sudoku.Cell(3, false), + new Sudoku.Cell(4, false), + new Sudoku.Cell(5, false), + new Sudoku.Cell(6, false), + new Sudoku.Cell(7, false), + new Sudoku.Cell(8, false), + new Sudoku.Cell(9, false)}}); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfColumns() + { + // This grid has nine rows and one column. Constructing it should throw + // an IllegalArgumentException. + new Sudoku(new Sudoku.Cell[][]{{new Sudoku.Cell(1, false)}, + {new Sudoku.Cell(2, false)}, + {new Sudoku.Cell(3, false)}, + {new Sudoku.Cell(4, false)}, + {new Sudoku.Cell(5, false)}, + {new Sudoku.Cell(6, false)}, + {new Sudoku.Cell(7, false)}, + {new Sudoku.Cell(8, false)}, + {new Sudoku.Cell(9, false)}}); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testCellValueTooLow() + { + new Sudoku.Cell(0, false); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testCellValueTooHigh() + { + new Sudoku.Cell(10, false); + } + + + @Test + public void testToString() + { + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][]{{1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}}); + String text = sudoku.toString(); + assert text.equals(" 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n") : "Wrong string representation:\n" + text; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTestUtils.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTestUtils.java new file mode 100644 index 0000000..58dc4ea --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTestUtils.java @@ -0,0 +1,48 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +/** + * Utility methods that help in unit tests. + * @author Daniel Dyer + */ +class SudokuTestUtils +{ + private SudokuTestUtils() + { + // Prevents instantiation. + } + + + /** + * Convenience method for creating a Sudoku object from a 2D array of ints. + * Ignores which cells are fixed and which are not. + * @param values A 9x9 2-dimensional array that contains values (1-9) for + * each of the cells in a Sudoku grid. + */ + public static Sudoku createSudoku(int[][] values) + { + Sudoku.Cell[][] cells = new Sudoku.Cell[Sudoku.SIZE][Sudoku.SIZE]; + for (int i = 0; i < Sudoku.SIZE; i++) + { + for (int j = 0; j < Sudoku.SIZE; j++) + { + cells[i][j] = new Sudoku.Cell(values[i][j], false); + } + } + return new Sudoku(cells); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossoverTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossoverTest.java new file mode 100644 index 0000000..4f430be --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossoverTest.java @@ -0,0 +1,64 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for Sudoku cross-over operator. + * @author Daniel Dyer + */ +public class SudokuVerticalCrossoverTest +{ + /** + * Ensures that the simplest configuration (a single cross-over point) + * works as expected. + */ + @Test + public void testSinglePointCrossover() + { + EvolutionaryOperator<Sudoku> crossover = new SudokuVerticalCrossover(); + int[] forwards = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + int[] backwards = {9, 8, 7, 6, 5, 4, 3, 2, 1}; + int[][] parent1 = new int[Sudoku.SIZE][]; + int[][] parent2 = new int[Sudoku.SIZE][]; + for (int i = 0; i < Sudoku.SIZE; i++) + { + parent1[i] = forwards; + parent2[i] = backwards; + } + List<Sudoku> population = Arrays.asList(SudokuTestUtils.createSudoku(parent1), + SudokuTestUtils.createSudoku(parent2)); + population = crossover.apply(population, ExamplesTestUtils.getRNG()); + assert population.size() == 2 : "Population size changed after cross-over."; + Sudoku offspring1 = population.get(0); + Sudoku offspring2 = population.get(1); + // Two groups of rows should be different from each other (they come from different parents). + assert !Arrays.equals(offspring1.getRow(0), offspring1.getRow(8)) + : "Row 0 should not match row 8."; + assert !Arrays.equals(offspring2.getRow(0), offspring2.getRow(8)) + : "Row 0 should not match row 8."; + // The two offspring should have inherited different rows. + assert !Arrays.equals(offspring1.getRow(0), offspring2.getRow(0)) + : "Offspring 1 row 0 should not match offspring 2 row 0."; + assert !Arrays.equals(offspring1.getRow(8), offspring2.getRow(8)) + : "Offspring 1 row 8 should not match offspring 2 row 8."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuViewTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuViewTest.java new file mode 100644 index 0000000..999a92a --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuViewTest.java @@ -0,0 +1,117 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.data.TableCell; +import org.fest.swing.fixture.FrameFixture; +import org.fest.swing.fixture.JTableCellFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link SudokuView} class. + * @author Daniel Dyer + */ +public class SudokuViewTest +{ + private static final String[] TEST_PUZZLE = {"4.5...9.7", + ".2..9..6.", + "39.6.7.28", + "9..3.2..6", + "7..9.6..3", + "5..4.8..1", + "28.1.5.49", + ".7..3..8.", + "6.4...3.2"}; + + private Robot robot; + + @BeforeMethod(groups = "display-required") + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod(groups = "display-required") + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") + public void testDisplayPuzzle() + { + SudokuView view = new SudokuView(); + JFrame frame = new JFrame(); + frame.add(view, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 400); + frame.validate(); + + frameFixture.show(); + view.setPuzzle(TEST_PUZZLE); + + // Check a non-empty cell. + JTableCellFixture cell1 = frameFixture.table().cell(TableCell.row(0).column(0)); + cell1.requireEditable(); + cell1.requireValue("4"); + + // And an empty cell. + JTableCellFixture cell2 = frameFixture.table().cell(TableCell.row(0).column(1)); + cell2.requireEditable(); + cell2.requireValue(""); + } + + + @Test(groups = "display-required") + public void testDisplaySolution() + { + SudokuView view = new SudokuView(); + JFrame frame = new JFrame(); + frame.add(view, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 400); + frame.validate(); + + frameFixture.show(); + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][] + { + {1, 2, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 1, 2, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + }); + view.setSolution(sudoku); + + JTableCellFixture cell1 = frameFixture.table().cell(TableCell.row(0).column(0)); + cell1.requireNotEditable(); + cell1.requireValue("1"); + } + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesmanTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesmanTest.java new file mode 100644 index 0000000..ee7a144 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesmanTest.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; + +/** + * Test for the brute force solution to the travelling salesman + * problem. + * @author Daniel Dyer + */ +public class BruteForceTravellingSalesmanTest +{ + /** + * Make sure that the brute force implementation returns the correct result. + */ + @Test + public void testEvaluation() + { + DistanceLookup data = new TestDistances(); + TravellingSalesmanStrategy strategy = new BruteForceTravellingSalesman(data); + List<String> cities = Arrays.asList("City1", "City2", "City3", "City4"); + List<String> route = strategy.calculateShortestRoute(cities, null); + assert route.size() == 4 : "Route is wrong length: " + route.size(); + assert route.contains("City1") : "Route does not contain City1."; + assert route.contains("City2") : "Route does not contain City2."; + assert route.contains("City3") : "Route does not contain City3."; + assert route.contains("City4") : "Route does not contain City4."; + double distance = 0; + distance += data.getDistance(route.get(0), route.get(1)); + distance += data.getDistance(route.get(1), route.get(2)); + distance += data.getDistance(route.get(2), route.get(3)); + distance += data.getDistance(route.get(3), route.get(0)); + assert (long) distance == 47 : "Incorrect shortest route: " + distance; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookupTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookupTest.java new file mode 100644 index 0000000..debdc96 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookupTest.java @@ -0,0 +1,65 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import org.testng.annotations.Test; + +/** + * Unit test for the distance look-ups for European cities that is used by + * the Travelling Salesman applet. + * @author Daniel Dyer + */ +public class EuropeanDistanceLookupTest +{ + /** + * Check to make sure that each city's distance to itself is zero. + */ + @Test + public void testSelfDistances() + { + DistanceLookup europe = new EuropeanDistanceLookup(); + List<String> cities = europe.getKnownCities(); + for (String city : cities) + { + assert europe.getDistance(city, city) == 0 : city + " distance to self is non-zero."; + } + } + + + /** + * Check to make sure that for every pair of cities, the distance in one + * direction is equal to the distance in the other direction. + */ + @Test + public void testReturnDistances() + { + DistanceLookup europe = new EuropeanDistanceLookup(); + Queue<String> cities = new LinkedList<String>(europe.getKnownCities()); + while (!cities.isEmpty()) + { + String startCity = cities.remove(); + for(String city : cities) + { + int outDistance = europe.getDistance(startCity, city); + int returnDistance = europe.getDistance(city, startCity); + assert outDistance == returnDistance : "Return distance mismatch for " + startCity + " and " + city; + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesmanTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesmanTest.java new file mode 100644 index 0000000..c2ece30 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesmanTest.java @@ -0,0 +1,95 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.selection.TruncationSelection; + +/** + * Unit test for the evolutionary approach to the travelling salesman problem. + * This test does not check that the correct result is returned (we would need + * to run the full evolutionary algorithm for that, and even then optimality + * is not guaranteed). This just ensures that nothing invalid happens and + * provides coverage for the classes involved. + * @author Daniel Dyer + */ +public class EvolutionaryTravellingSalesmanTest +{ + private final DistanceLookup data = new TestDistances(); + + /** + * Ensure that the algorithm behaves when configured to use mutation. + */ + @Test + public void testWithMutation() + { + TravellingSalesmanStrategy strategy = new EvolutionaryTravellingSalesman(data, + new TruncationSelection(0.5), + 10, // Small population. + 0, // No elitism. + 3, // Only a few generations. + false, // Cross-over. + true); // Mutation. + List<String> cities = Arrays.asList("City1", "City2", "City3", "City4"); + List<String> route = strategy.calculateShortestRoute(cities, null); + assert route.size() == 4 : "Route is wrong length: " + route.size(); + assert route.contains("City1") : "Route does not contain City1."; + assert route.contains("City2") : "Route does not contain City2."; + assert route.contains("City3") : "Route does not contain City3."; + assert route.contains("City4") : "Route does not contain City4."; + } + + + /** + * Ensure that the algorithm behaves when configured to use cross-over. + */ + @Test + public void testWithCrossover() + { + TravellingSalesmanStrategy strategy = new EvolutionaryTravellingSalesman(data, + new TruncationSelection(0.5), + 10, // Small population. + 0, // No elitism. + 3, // Only a few generations. + true, // Cross-over + false); // Mutation. + List<String> cities = Arrays.asList("City1", "City2", "City3", "City4"); + List<String> route = strategy.calculateShortestRoute(cities, null); + assert route.size() == 4 : "Route is wrong length: " + route.size(); + assert route.contains("City1") : "Route does not contain City1."; + assert route.contains("City2") : "Route does not contain City2."; + assert route.contains("City3") : "Route does not contain City3."; + assert route.contains("City4") : "Route does not contain City4."; + } + + + /** + * The strategy must ensure that at least one of cross-over or mutation is chosen. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNoEvolution() + { + new EvolutionaryTravellingSalesman(data, + new TruncationSelection(0.5), + 10, + 0, + 3, + false, + false); // Should throw an IllegalArgumentException as no operators are enabled. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanelTest.java new file mode 100644 index 0000000..9f2244d --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanelTest.java @@ -0,0 +1,91 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.util.Collection; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link ItineraryPanel} class. Makes sure that the + * buttons operate as expected so that the right cities are returned. + * @author Daniel Dyer + */ +public class ItineraryPanelTest +{ + private static final TestDistances CITIES = new TestDistances(); + + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testSelectAll() + { + ItineraryPanel itineraryPanel = new ItineraryPanel(CITIES.getKnownCities()); + JFrame frame = new JFrame(); + frame.add(itineraryPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(100, 300); + frame.validate(); + frame.setVisible(true); + assert itineraryPanel.getSelectedCities().isEmpty() : "Should be no cities selected initially."; + frameFixture.button("All").click(); + Collection<String> selectedCities = itineraryPanel.getSelectedCities(); + assert selectedCities.size() == CITIES.getKnownCities().size() + : "All cities should be selected after button click."; + } + + + @Test(groups = "display-required", // Will fail if run in a headless environment. + dependsOnMethods = "testSelectAll") + public void testSelectNone() + { + ItineraryPanel itineraryPanel = new ItineraryPanel(CITIES.getKnownCities()); + JFrame frame = new JFrame(); + frame.add(itineraryPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(100, 300); + frame.validate(); + frame.setVisible(true); + frameFixture.button("All").click(); + Collection<String> selectedCities = itineraryPanel.getSelectedCities(); + assert selectedCities.size() == CITIES.getKnownCities().size() + : "All cities should be selected after all button click."; + frameFixture.button("None").click(); + assert itineraryPanel.getSelectedCities().isEmpty() + : "No cities should be selected after clear button is clicked."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluatorTest.java new file mode 100644 index 0000000..b6ca0ee --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluatorTest.java @@ -0,0 +1,70 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; + +/** + * Unit test for the route evaluator used by both Travelling Salesman + * implementations. Checks to make sure that route distances are + * calculated correctly. + * @author Daniel Dyer + */ +public class RouteEvaluatorTest +{ + /** + * Test different routes to make sure the distances are calculated correctly. + */ + @Test + public void testDistanceCalculations() + { + RouteEvaluator evaluator = new RouteEvaluator(new TestDistances()); + + List<String> route1 = Arrays.asList("City4", "City1", "City3", "City2"); + // Expected distance is sum of distances between adjacent cities on route + // including returning to the start city. + int expectedDistance1 = 8 + 5 + 13 + 21; + int actualDistance1 = (int) evaluator.getFitness(route1, null); + assert actualDistance1 == expectedDistance1 : "Distance should be " + expectedDistance1 + ", was " + actualDistance1; + + List<String> route2 = Arrays.asList("City3", "City4", "City2", "City1"); + // Expected distance is sum of distances between adjacent cities on route + // including returning to the start city. + int expectedDistance2 = 34 + 21 + 3 + 5; + int actualDistance2 = (int) evaluator.getFitness(route2, null); + assert actualDistance2 == expectedDistance2 : "Distance should be " + expectedDistance2 + ", was " + actualDistance2; + } + + + /** + * Make sure the route evaluator works for end cases. + */ + @Test + public void testSingleCityRoute() + { + RouteEvaluator evaluator = new RouteEvaluator(new TestDistances()); + + List<String> route = Arrays.asList("City1"); + // Expected distance is sum of distances between adjacent cities on route + // including returning to the start city. + int distance = (int) evaluator.getFitness(route, null); + assert distance == 0 : "Distance should be 0, was " + distance; + } + + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanelTest.java new file mode 100644 index 0000000..00519d9 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanelTest.java @@ -0,0 +1,119 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link StrategyPanel} used by the Travelling + * Salesman example applet. Makes sure that it returns the correct type + * of solver strategy depending on the radio button settings. + * @author Daniel Dyer + */ +public class StrategyPanelTest +{ + private static final TestDistances CITIES = new TestDistances(); + + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") + public void testBruteForceOption() + { + StrategyPanel strategyPanel = new StrategyPanel(CITIES); + JFrame frame = new JFrame(); + frame.add(strategyPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(500, 300); + frame.validate(); + frame.setVisible(true); + robot.waitForIdle(); + + // Evolution controls should be enabled by default. + frameFixture.panel("EvolutionPanel").requireEnabled(); + + frameFixture.radioButton("BruteForceOption").click(); + + // Evolution controls should be disabled when brute force option is selected. + frameFixture.panel("EvolutionPanel").requireDisabled(); + + TravellingSalesmanStrategy strategy = strategyPanel.getStrategy(); + assert strategy instanceof BruteForceTravellingSalesman : "Wrong strategy class: " + strategy.getClass(); + } + + + @Test(groups = "display-required") + public void testEvolutionOption() + { + StrategyPanel strategyPanel = new StrategyPanel(CITIES); + JFrame frame = new JFrame(); + frame.add(strategyPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(500, 300); + frame.validate(); + frame.setVisible(true); + robot.waitForIdle(); + + frameFixture.radioButton("EvolutionOption").click(); + TravellingSalesmanStrategy strategy = strategyPanel.getStrategy(); + assert strategy instanceof EvolutionaryTravellingSalesman : "Wrong strategy class: " + strategy.getClass(); + } + + + @Test(groups = "display-required") + public void testDisablePanel() + { + StrategyPanel strategyPanel = new StrategyPanel(CITIES); + JFrame frame = new JFrame(); + frame.add(strategyPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(500, 300); + frame.validate(); + frame.setVisible(true); + robot.waitForIdle(); + + // Components should be enabled initially. + frameFixture.radioButton("EvolutionOption").requireEnabled(); + frameFixture.panel("EvolutionPanel").requireEnabled(); + frameFixture.radioButton("BruteForceOption").requireEnabled(); + + strategyPanel.setEnabled(false); + frameFixture.radioButton("EvolutionOption").requireDisabled(); + frameFixture.panel("EvolutionPanel").requireDisabled(); + frameFixture.radioButton("BruteForceOption").requireDisabled(); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/TestDistances.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/TestDistances.java new file mode 100644 index 0000000..cc7da3a --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/TestDistances.java @@ -0,0 +1,70 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Test data for travelling salesman unit tests. The shortest route between each + * of the four cities in this data set is 47km. + */ +final class TestDistances implements DistanceLookup +{ + private static final Map<String, Map<String, Integer>> distances = new TreeMap<String, Map<String, Integer>>(); + static + { + Map<String, Integer> city1 = new TreeMap<String, Integer>(); + city1.put("City1", 0); + city1.put("City2", 3); + city1.put("City3", 5); + city1.put("City4", 8); + distances.put("City1", city1); + + Map<String, Integer> city2 = new TreeMap<String, Integer>(); + city2.put("City1", 3); + city2.put("City2", 0); + city2.put("City3", 13); + city2.put("City4", 21); + distances.put("City2", city2); + + Map<String, Integer> city3 = new TreeMap<String, Integer>(); + city3.put("City1", 5); + city3.put("City2", 13); + city3.put("City3", 0); + city3.put("City4", 34); + distances.put("City3", city3); + + Map<String, Integer> city4 = new TreeMap<String, Integer>(); + city4.put("City1", 8); + city4.put("City2", 21); + city4.put("City3", 34); + city4.put("City4", 0); + distances.put("City4", city4); + } + + public List<String> getKnownCities() + { + return new ArrayList<String>(distances.keySet()); + } + + public int getDistance(String startingCity, String destinationCity) + { + return distances.get(startingCity).get(destinationCity); + } +} |