diff options
Diffstat (limited to 'watchmaker/framework')
137 files changed, 11859 insertions, 0 deletions
diff --git a/watchmaker/framework/framework.iml b/watchmaker/framework/framework.iml new file mode 100644 index 0000000..82c64ca --- /dev/null +++ b/watchmaker/framework/framework.iml @@ -0,0 +1,25 @@ +<?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/test" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <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.8" 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" /> + </component> +</module> + diff --git a/watchmaker/framework/nb-configuration.xml b/watchmaker/framework/nb-configuration.xml new file mode 100644 index 0000000..ae35717 --- /dev/null +++ b/watchmaker/framework/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/framework/pom.xml b/watchmaker/framework/pom.xml new file mode 100644 index 0000000..51ecd7b --- /dev/null +++ b/watchmaker/framework/pom.xml @@ -0,0 +1,52 @@ +<?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-framework</artifactId> + + <dependencies> + <dependency> + <groupId>org.uncommons.maths</groupId> + <artifactId>uncommons-maths</artifactId> + <version>1.2.2</version> + </dependency> + <dependency> + <groupId>com.google.collections</groupId> + <artifactId>google-collections</artifactId> + <version>1.0</version> + </dependency> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <version>6.2.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <sourceDirectory>src/java/main</sourceDirectory> + <testSourceDirectory>src/java/test</testSourceDirectory> + </build> +</project> diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/concurrent/ConfigurableThreadFactory.java b/watchmaker/framework/src/java/main/org/uncommons/util/concurrent/ConfigurableThreadFactory.java new file mode 100644 index 0000000..ed35b70 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/concurrent/ConfigurableThreadFactory.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.util.concurrent; + +import java.util.concurrent.ThreadFactory; +import org.uncommons.util.id.IDSource; +import org.uncommons.util.id.IntSequenceIDSource; +import org.uncommons.util.id.StringPrefixIDSource; + +/** + * Thread factory that creates threads for use by a + * {@link java.util.concurrent.ThreadPoolExecutor}. The factory can be + * configured to customise the names, priority and daemon status of created + * threads. + * @author Daniel Dyer + */ +public class ConfigurableThreadFactory implements ThreadFactory +{ + /** + * A default exception handler that simply logs the stack trace of the exception. + */ + private static final Thread.UncaughtExceptionHandler DEFAULT_EXCEPTION_HANDLER + = new Thread.UncaughtExceptionHandler() + { + public void uncaughtException(Thread thread, Throwable throwable) + { + // Log any exceptions thrown. + throwable.printStackTrace(); + } + }; + + + private final IDSource<String> nameGenerator; + private final int priority; + private final boolean daemon; + private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + + /** + * @param namePrefix The String prefix used to assign identifiers to created threads. + * @param priority The initial priority for created threads. + * @param daemon Whether or not created threads should be daemon threads or user threads. + * The JVM exits when the only threads running are all daemon threads. + */ + public ConfigurableThreadFactory(String namePrefix, + int priority, + boolean daemon) + { + this(namePrefix, priority, daemon, DEFAULT_EXCEPTION_HANDLER); + } + + + /** + * @param namePrefix The String prefix used to assign identifiers to created threads. + * @param priority The initial priority for created threads. + * @param daemon Whether or not created threads should be daemon threads or user threads. + * The JVM exits when the only threads running are all daemon threads. + * @param uncaughtExceptionHandler A strategy for dealing with uncaught exceptions. + */ + public ConfigurableThreadFactory(String namePrefix, + int priority, + boolean daemon, + Thread.UncaughtExceptionHandler uncaughtExceptionHandler) + { + this.nameGenerator = new StringPrefixIDSource(namePrefix + '-', new IntSequenceIDSource()); + this.priority = priority; + this.daemon = daemon; + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + } + + + /** + * Creates a new thread configured according to this factory's parameters. + * @param runnable The runnable to be executed by the new thread. + * @return The created thread. + */ + public Thread newThread(Runnable runnable) + { + Thread thread = new Thread(runnable, nameGenerator.nextID()); + thread.setPriority(priority); + thread.setDaemon(daemon); + thread.setUncaughtExceptionHandler(uncaughtExceptionHandler); + return thread; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/concurrent/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/util/concurrent/package-info.java new file mode 100644 index 0000000..9ff32d1 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/concurrent/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. +//============================================================================= +/** + * Concurrency utility classes. + * @author Daniel Dyer + */ +package org.uncommons.util.concurrent; diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/CompositeIDSource.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/CompositeIDSource.java new file mode 100644 index 0000000..91635a9 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/CompositeIDSource.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.util.id;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * <p>Thread-safe source for partitioned unique IDs. A single instance of this class
+ * represents a single 'partition' in the space of possible IDs. By creating
+ * multiple instances with different discriminators, multiple entities may generate
+ * globally unique IDs independently.</p>
+ * <p>Any given instance of this class may generate a maximum of 2^31 unique values
+ * (the most significant 4 bytes are fixed and the least significant 4 bytes vary
+ * in sequence).</p>
+ * @author Daniel Dyer
+ */
+public final class CompositeIDSource implements IDSource<Long>
+{
+ private final Lock lock = new ReentrantLock();
+ private final long top32bits;
+ private final IDSource<Integer> sequence = new IntSequenceIDSource();
+
+
+ /**
+ * @param topPart The most significant 32 bits to use for the 64-bit IDs generated
+ * by this source. All IDs generated by this source will have the same top 4 bytes.
+ */
+ public CompositeIDSource(int topPart)
+ {
+ top32bits = ((long) topPart) << 32;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public Long nextID()
+ {
+ lock.lock();
+ try
+ {
+ // Top part is value provided in constructor, lower 32 bits are from the sequence.
+ return (top32bits + sequence.nextID());
+ }
+ finally
+ {
+ lock.unlock();
+ }
+ }
+}
diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/IDSource.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/IDSource.java new file mode 100644 index 0000000..ae5d642 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/IDSource.java @@ -0,0 +1,38 @@ +//=============================================================================
+// 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.util.id;
+
+import java.io.Serializable;
+
+/**
+ * Defines operations for classes that generate unique identifiers. Generated IDs must
+ * be of a {@link java.io.Serializable} type. The strategy used will vary between
+ * implementations. It may be a straightforward sequence or a more complex, less predictable
+ * algorithm.
+ * @param <T> The type of ID returned by this source.
+ * @author Daniel Dyer
+ */
+public interface IDSource<T extends Serializable>
+{
+ /**
+ * Implementing classes are responsible for synchronization if concurrent invocations
+ * of this method are required.
+ * @return The next ID.
+ * @throws IDSourceExhaustedException If this ID source cannot generate any more
+ * unique IDs.
+ */
+ T nextID();
+}
diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/IDSourceExhaustedException.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/IDSourceExhaustedException.java new file mode 100644 index 0000000..fbf7d3f --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/IDSourceExhaustedException.java @@ -0,0 +1,33 @@ +//============================================================================= +// 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.util.id; + +/** + * Unchecked exception thrown if an {@link IDSource} runs out of unique IDs. + * @author Daniel Dyer + */ +public class IDSourceExhaustedException extends RuntimeException +{ + public IDSourceExhaustedException(String string) + { + super(string); + } + + public IDSourceExhaustedException(String string, Throwable throwable) + { + super(string, throwable); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/IntSequenceIDSource.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/IntSequenceIDSource.java new file mode 100644 index 0000000..bf6c3cb --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/IntSequenceIDSource.java @@ -0,0 +1,81 @@ +//=============================================================================
+// 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.util.id;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Thread-safe source for unique IDs. This particular implementation restricts
+ * values to those positive integer values that can be represented by the int data type.
+ * Provides sequenced 32-bit IDs.
+ * @author Daniel Dyer
+ */
+public final class IntSequenceIDSource implements IDSource<Integer>
+{
+ private static final long SECONDS_IN_HOUR = 3600L;
+
+ private final Lock lock = new ReentrantLock();
+ private final long startTime;
+ private int lastID = -1;
+
+
+ /**
+ * @param firstValue The value at which to start the sequence (must
+ * be non-negative).
+ */
+ public IntSequenceIDSource(int firstValue)
+ {
+ if (firstValue < 0)
+ {
+ throw new IllegalArgumentException("Initial value must be non-negative.");
+ }
+ lastID = firstValue - 1;
+ startTime = System.currentTimeMillis();
+ }
+
+
+ /**
+ * Creates a sequence that starts at zero.
+ */
+ public IntSequenceIDSource()
+ {
+ this(0);
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public Integer nextID()
+ {
+ lock.lock();
+ try
+ {
+ if (lastID == Integer.MAX_VALUE)
+ {
+ long hours = (System.currentTimeMillis() - startTime) / SECONDS_IN_HOUR;
+ throw new IDSourceExhaustedException("32-bit ID source exhausted after " + hours + " hours.");
+ }
+ ++lastID;
+ return lastID;
+ }
+ finally
+ {
+ lock.unlock();
+ }
+ }
+}
diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/LongSequenceIDSource.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/LongSequenceIDSource.java new file mode 100644 index 0000000..3ea85a9 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/LongSequenceIDSource.java @@ -0,0 +1,81 @@ +//=============================================================================
+// 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.util.id;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Thread-safe source for unique IDs. This particular implementation restricts
+ * values to those positive integer values that can be represented by the long data type.
+ * Provides sequenced 64-bit IDs.
+ * @author Daniel Dyer
+ */
+public final class LongSequenceIDSource implements IDSource<Long>
+{
+ private static final int SECONDS_IN_DAY = 86400;
+
+ private final Lock lock = new ReentrantLock();
+ private final long startTime;
+ private long lastID = -1;
+
+
+ /**
+ * @param firstValue The value at which to start the sequence (must
+ * be non-negative).
+ */
+ public LongSequenceIDSource(long firstValue)
+ {
+ if (firstValue < 0)
+ {
+ throw new IllegalArgumentException("Initial value must be non-negative.");
+ }
+ lastID = firstValue - 1;
+ startTime = System.currentTimeMillis();
+ }
+
+
+ /**
+ * Creates a sequence that starts at zero.
+ */
+ public LongSequenceIDSource()
+ {
+ this(0);
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public Long nextID()
+ {
+ lock.lock();
+ try
+ {
+ if (lastID == Long.MAX_VALUE)
+ {
+ long days = (System.currentTimeMillis() - startTime) / SECONDS_IN_DAY;
+ throw new IDSourceExhaustedException("64-bit ID source exhausted after " + days + " days.");
+ }
+ ++lastID;
+ return lastID;
+ }
+ finally
+ {
+ lock.unlock();
+ }
+ }
+}
diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/StringPrefixIDSource.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/StringPrefixIDSource.java new file mode 100644 index 0000000..c4d683c --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/StringPrefixIDSource.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.util.id;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Thread-safe ID source that wraps another source of IDs and adds a fixed String
+ * prefix to each ID generated.
+ * @author Daniel Dyer
+ */
+public class StringPrefixIDSource implements IDSource<String>
+{
+ private final Lock lock = new ReentrantLock();
+ private final String prefix;
+ private final IDSource<?> source;
+
+ /**
+ * @param prefix A fixed String that is attached to the front of each ID.
+ * @param source The source of IDs to which the prefix is added.
+ */
+ public StringPrefixIDSource(String prefix, IDSource<?> source)
+ {
+ this.prefix = prefix;
+ this.source = source;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public String nextID()
+ {
+ lock.lock();
+ try
+ {
+ StringBuilder output = new StringBuilder(prefix);
+ output.append(source.nextID());
+ return output.toString();
+ }
+ finally
+ {
+ lock.unlock();
+ }
+ }
+}
diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/id/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/util/id/package-info.java new file mode 100644 index 0000000..323b7d3 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/id/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.
+//=============================================================================
+/**
+ * Strategies for providing unique identifiers of different types.
+ * @author Daniel Dyer
+ */
+package org.uncommons.util.id;
diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/reflection/ReflectionUtils.java b/watchmaker/framework/src/java/main/org/uncommons/util/reflection/ReflectionUtils.java new file mode 100644 index 0000000..e104798 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/reflection/ReflectionUtils.java @@ -0,0 +1,179 @@ +//============================================================================= +// 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.util.reflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Helper methods to simplify code that uses reflection. These methods handle the + * checked exceptions and throw only unchecked exceptions. They are useful for dealing + * with reflection when we know that there is no chance of a checked exception. We can + * use this class and avoid all of the boiler-plate exception handling. + * @author Daniel Dyer + */ +public final class ReflectionUtils +{ + private ReflectionUtils() + { + // Prevents instantiation. + } + + + /** + * Invokes the specified method without throwing any checked exceptions. + * This is only valid for methods that are not declared to throw any checked + * exceptions. Any unchecked exceptions thrown by the specified method will be + * re-thrown (in their original form, not wrapped in an InvocationTargetException + * as would be the case for a normal reflective invocation). + * @param method The method to invoke. Both the method and its class must have + * been declared public and non-abstract, otherwise they will be inaccessible. + * @param target The object on which to invoke the method. + * @param arguments The method arguments. + * @param <T> The return type of the method. The compiler can usually infer the + * correct type. + * @return The result of invoking the method, or null if the method is void. + */ + public static <T> T invokeUnchecked(Method method, Object target, Object... arguments) + { + try + { + @SuppressWarnings("unchecked") + T result = (T) method.invoke(target, arguments); + return result; + } + catch (IllegalAccessException ex) + { + // This cannot happen if the method is public. + throw new IllegalArgumentException("Method " + method.getName() + " is not publicly accessible.", ex); + } + catch (InvocationTargetException ex) + { + // If the method is not declared to throw any checked exceptions, + // the worst that can happen is a RuntimeException or an Error (we can, + // and should, re-throw both). + if (ex.getCause() instanceof Error) + { + throw (Error) ex.getCause(); + } + else + { + throw (RuntimeException) ex.getCause(); + } + } + } + + + /** + * Invokes the specified constructor without throwing any checked exceptions. + * This is only valid for constructors that are not declared to throw any checked + * exceptions. Any unchecked exceptions thrown by the specified constructor will be + * re-thrown (in their original form, not wrapped in an InvocationTargetException + * as would be the case for a normal reflective invocation). + * @param constructor The constructor to invoke. Both the constructor and its + * class must have been declared public, and the class must not be abstract, + * otherwise they will be inaccessible. + * @param arguments The method arguments. + * @param <T> The return type of the method. The compiler can usually infer the + * correct type. + * @return The object created by invoking the specified constructor with the specified + * arguments. + */ + public static <T> T invokeUnchecked(Constructor<T> constructor, Object... arguments) + { + try + { + return constructor.newInstance(arguments); + } + catch (IllegalAccessException ex) + { + // This cannot happen if the constructor is public. + throw new IllegalArgumentException("Constructor is not publicly accessible.", ex); + } + catch (InstantiationException ex) + { + // This can only happen if the constructor belongs to an + // abstract class. + throw new IllegalArgumentException("Constructor is part of an abstract class.", ex); + } + catch (InvocationTargetException ex) + { + // If the method is not declared to throw any checked exceptions, + // the worst that can happen is a RuntimeException or an Error (we can, + // and should, re-throw both). + if (ex.getCause() instanceof Error) + { + throw (Error) ex.getCause(); + } + else + { + throw (RuntimeException) ex.getCause(); + } + } + } + + + /** + * Looks up a method that is explicitly identified. This method should only + * be used for methods that definitely exist. It does not throw the checked + * NoSuchMethodException. If the method does not exist, it will instead fail + * with an unchecked IllegalArgumentException. + * @param aClass The class in which the method exists. + * @param name The name of the method. + * @param paramTypes The types of the method's parameters. + * @return The identified method. + */ + public static Method findKnownMethod(Class<?> aClass, + String name, + Class<?>... paramTypes) + { + try + { + return aClass.getMethod(name, paramTypes); + } + catch (NoSuchMethodException ex) + { + // This cannot happen if the method is correctly identified. + throw new IllegalArgumentException("Method " + name + " does not exist in class " + aClass.getName(), ex); + } + } + + + /** + * Looks up a constructor that is explicitly identified. This method should only + * be used for constructors that definitely exist. It does not throw the checked + * NoSuchMethodException. If the constructor does not exist, it will instead fail + * with an unchecked IllegalArgumentException. + * @param <T> The type of object that the constructor creates. + * @param aClass The class in which the constructor exists. + * @param paramTypes The types of the constructor's parameters. + * @return The identified constructor. + */ + public static <T> Constructor<T> findKnownConstructor(Class<T> aClass, + Class<?>... paramTypes) + { + try + { + return aClass.getConstructor(paramTypes); + } + catch (NoSuchMethodException ex) + { + // This cannot happen if the method is correctly identified. + throw new IllegalArgumentException("Specified constructor does not exist in class " + aClass.getName(), ex); + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/util/reflection/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/util/reflection/package-info.java new file mode 100644 index 0000000..253ac44 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/util/reflection/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. +//============================================================================= +/** + * Utilities for simplifying code that uses reflection. + * @author Daniel Dyer + */ +package org.uncommons.util.reflection; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/AbstractEvolutionEngine.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/AbstractEvolutionEngine.java new file mode 100644 index 0000000..486f10d --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/AbstractEvolutionEngine.java @@ -0,0 +1,341 @@ +//============================================================================= +// 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.framework; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * Base class for {@link EvolutionEngine} implementations. + * @param <T> The type of entity evolved by the evolution engine. + * @author Daniel Dyer + * @see CandidateFactory + * @see FitnessEvaluator + */ +public abstract class AbstractEvolutionEngine<T> implements EvolutionEngine<T> +{ + // A single multi-threaded worker is shared among multiple evolution engine instances. + private static FitnessEvaluationWorker concurrentWorker = null; + + private final Set<EvolutionObserver<? super T>> observers = new CopyOnWriteArraySet<EvolutionObserver<? super T>>(); + + private final Random rng; + private final CandidateFactory<T> candidateFactory; + private final FitnessEvaluator<? super T> fitnessEvaluator; + + private volatile boolean singleThreaded = false; + + private List<TerminationCondition> satisfiedTerminationConditions; + + + /** + * Creates a new evolution engine by specifying the various components required by + * an evolutionary algorithm. + * @param candidateFactory Factory used to create the initial population that is + * iteratively evolved. + * @param fitnessEvaluator A function for assigning fitness scores to candidate + * solutions. + * @param rng The source of randomness used by all stochastic processes (including + * evolutionary operators and selection strategies). + */ + protected AbstractEvolutionEngine(CandidateFactory<T> candidateFactory, + FitnessEvaluator<? super T> fitnessEvaluator, + Random rng) + { + this.candidateFactory = candidateFactory; + this.fitnessEvaluator = fitnessEvaluator; + this.rng = rng; + } + + + /** + * {@inheritDoc} + */ + public T evolve(int populationSize, + int eliteCount, + TerminationCondition... conditions) + { + return evolve(populationSize, + eliteCount, + Collections.<T>emptySet(), + conditions); + } + + + /** + * {@inheritDoc} + */ + public T evolve(int populationSize, + int eliteCount, + Collection<T> seedCandidates, + TerminationCondition... conditions) + { + return evolvePopulation(populationSize, + eliteCount, + seedCandidates, + conditions).get(0).getCandidate(); + } + + + /** + * {@inheritDoc} + */ + public List<EvaluatedCandidate<T>> evolvePopulation(int populationSize, + int eliteCount, + TerminationCondition... conditions) + { + return evolvePopulation(populationSize, + eliteCount, + Collections.<T>emptySet(), + conditions); + } + + + /** + * {@inheritDoc} + */ + public List<EvaluatedCandidate<T>> evolvePopulation(int populationSize, + int eliteCount, + Collection<T> seedCandidates, + TerminationCondition... conditions) + { + if (eliteCount < 0 || eliteCount >= populationSize) + { + throw new IllegalArgumentException("Elite count must be non-negative and less than population size."); + } + if (conditions.length == 0) + { + throw new IllegalArgumentException("At least one TerminationCondition must be specified."); + } + + satisfiedTerminationConditions = null; + int currentGenerationIndex = 0; + long startTime = System.currentTimeMillis(); + + List<T> population = candidateFactory.generateInitialPopulation(populationSize, + seedCandidates, + rng); + + // Calculate the fitness scores for each member of the initial population. + List<EvaluatedCandidate<T>> evaluatedPopulation = evaluatePopulation(population); + EvolutionUtils.sortEvaluatedPopulation(evaluatedPopulation, fitnessEvaluator.isNatural()); + PopulationData<T> data = EvolutionUtils.getPopulationData(evaluatedPopulation, + fitnessEvaluator.isNatural(), + eliteCount, + currentGenerationIndex, + startTime); + // Notify observers of the state of the population. + notifyPopulationChange(data); + + List<TerminationCondition> satisfiedConditions = EvolutionUtils.shouldContinue(data, conditions); + while (satisfiedConditions == null) + { + ++currentGenerationIndex; + evaluatedPopulation = nextEvolutionStep(evaluatedPopulation, eliteCount, rng); + EvolutionUtils.sortEvaluatedPopulation(evaluatedPopulation, fitnessEvaluator.isNatural()); + data = EvolutionUtils.getPopulationData(evaluatedPopulation, + fitnessEvaluator.isNatural(), + eliteCount, + currentGenerationIndex, + startTime); + // Notify observers of the state of the population. + notifyPopulationChange(data); + satisfiedConditions = EvolutionUtils.shouldContinue(data, conditions); + } + this.satisfiedTerminationConditions = satisfiedConditions; + return evaluatedPopulation; + } + + + /** + * This method performs a single step/iteration of the evolutionary process. + * @param evaluatedPopulation The population at the beginning of the process. + * @param eliteCount The number of the fittest individuals that must be preserved. + * @param rng A source of randomness. + * @return The updated population after the evolutionary process has proceeded + * by one step/iteration. + */ + protected abstract List<EvaluatedCandidate<T>> nextEvolutionStep(List<EvaluatedCandidate<T>> evaluatedPopulation, + int eliteCount, + Random rng); + + + /** + * Takes a population, assigns a fitness score to each member and returns + * the members with their scores attached, sorted in descending order of + * fitness (descending order of fitness score for natural scores, ascending + * order of scores for non-natural scores). + * @param population The population to evaluate (each candidate is assigned + * a fitness score). + * @return The evaluated population (a list of candidates with attached fitness + * scores). + */ + protected List<EvaluatedCandidate<T>> evaluatePopulation(List<T> population) + { + List<EvaluatedCandidate<T>> evaluatedPopulation = new ArrayList<EvaluatedCandidate<T>>(population.size()); + + if (singleThreaded) // Do fitness evaluations on the request thread. + { + for (T candidate : population) + { + evaluatedPopulation.add(new EvaluatedCandidate<T>(candidate, + fitnessEvaluator.getFitness(candidate, population))); + } + } + else + { + // Divide the required number of fitness evaluations equally among the + // available processors and coordinate the threads so that we do not + // proceed until all threads have finished processing. + try + { + List<T> unmodifiablePopulation = Collections.unmodifiableList(population); + List<Future<EvaluatedCandidate<T>>> results = new ArrayList<Future<EvaluatedCandidate<T>>>(population.size()); + // Submit tasks for execution and wait until all threads have finished fitness evaluations. + for (T candidate : population) + { + results.add(getSharedWorker().submit(new FitnessEvalutationTask<T>(fitnessEvaluator, + candidate, + unmodifiablePopulation))); + } + for (Future<EvaluatedCandidate<T>> result : results) + { + evaluatedPopulation.add(result.get()); + } + } + catch (ExecutionException ex) + { + throw new IllegalStateException("Fitness evaluation task execution failed.", ex); + } + catch (InterruptedException ex) + { + // Restore the interrupted status, allows methods further up the call-stack + // to abort processing if appropriate. + Thread.currentThread().interrupt(); + } + } + + return evaluatedPopulation; + } + + + + /** + * <p>Returns a list of all {@link TerminationCondition}s that are satisfied by the current + * state of the evolution engine. Usually this list will contain only one item, but it + * is possible that mutliple termination conditions will become satisfied at the same + * time. In this case the condition objects in the list will be in the same order that + * they were specified when passed to the engine.</p> + * + * <p>If the evolution has not yet terminated (either because it is still in progress or + * because it hasn't even been started) then an IllegalStateException will be thrown.</p> + * + * <p>If the evolution terminated because the request thread was interrupted before any + * termination conditions were satisfied then this method will return an empty list.</p> + * + * @throws IllegalStateException If this method is invoked on an evolution engine before + * evolution is started or while it is still in progress. + * + * @return A list of statisfied conditions. The list is guaranteed to be non-null. The + * list may be empty because it is possible for evolution to terminate without any conditions + * being matched. The only situation in which this occurs is when the request thread is + * interrupted. + */ + public List<TerminationCondition> getSatisfiedTerminationConditions() + { + if (satisfiedTerminationConditions == null) + { + throw new IllegalStateException("EvolutionEngine has not terminated."); + } + else + { + return Collections.unmodifiableList(satisfiedTerminationConditions); + } + } + + + /** + * Adds a listener to receive status updates on the evolution progress. + * Updates are dispatched synchronously on the request thread. Observers should + * complete their processing and return in a timely manner to avoid holding up + * the evolution. + * @param observer An evolution observer call-back. + * @see #removeEvolutionObserver(EvolutionObserver) + */ + public void addEvolutionObserver(EvolutionObserver<? super T> observer) + { + observers.add(observer); + } + + + /** + * Removes an evolution progress listener. + * @param observer An evolution observer call-back. + * @see #addEvolutionObserver(EvolutionObserver) + */ + public void removeEvolutionObserver(EvolutionObserver<? super T> observer) + { + observers.remove(observer); + } + + + /** + * Send the population data to all registered observers. + * @param data Information about the current state of the population. + */ + private void notifyPopulationChange(PopulationData<T> data) + { + for (EvolutionObserver<? super T> observer : observers) + { + observer.populationUpdate(data); + } + } + + + /** + * By default, fitness evaluations are performed on separate threads (as many as there are + * available cores/processors). Use this method to force evaluation to occur synchronously + * on the request thread. This is useful in restricted environments where programs are not + * permitted to start or control threads. It might also lead to better performance for + * programs that have extremely lightweight/trivial fitness evaluations. + * @param singleThreaded If true, fitness evaluations will be performed synchronously on the + * request thread. If false, fitness evaluations will be performed by worker threads. + */ + public void setSingleThreaded(boolean singleThreaded) + { + this.singleThreaded = singleThreaded; + } + + + /** + * Lazily create the multi-threaded worker for fitness evaluations. + */ + private static synchronized FitnessEvaluationWorker getSharedWorker() + { + if (concurrentWorker == null) + { + concurrentWorker = new FitnessEvaluationWorker(); + } + return concurrentWorker; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/CachingFitnessEvaluator.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/CachingFitnessEvaluator.java new file mode 100644 index 0000000..ed3d8d2 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/CachingFitnessEvaluator.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.framework; + +import com.google.common.collect.MapMaker; +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +/** + * <p>A wrapper that provides caching for {@link FitnessEvaluator} implementations. The + * results of fitness evaluations are stored in a cache so that if the same candidate + * is evaluated twice, the expense of the fitness calculation can be avoided the second + * time. The cache uses weak references in order to avoid memory leakage.</p> + * + * <p>Caching of fitness values can be a useful optimisation in situations where the + * fitness evaluation is expensive and there is a possibility that some candidates + * will survive from generation to generation unmodified. Programs that use elitism + * are one example of candidates surviving unmodified. Another scenario is when the + * configured evolutionary operator does not always modify every candidate in the + * population for every generation.</p> + * + * <p>Unmodified candidates are identified by reference equality. This is a valid + * assumption since evolutionary operators are required to return distinct objects, + * except when the candidate is unaffected by the evolution, as per the contract of the + * {@link EvolutionaryOperator} interface. In other words, the Watchmaker Framework + * treats candidate representations as immutable even when that is not strictly the case.</p> + * + * <p>Caching of fitness scores is provided as an option rather than as the default + * Watchmaker Framework behaviour because caching is only valid when fitness evaluations + * are <em>isolated</em> and repeatable. An isolated fitness evaluation is one where the + * result depends only upon the candidate being evaluated. This is not the case when + * candidates are evaluated against the other members of the population. So unless the + * fitness evaluator ignores the second parameter to the + * {@link #getFitness(Object, List)} method, caching must not be used.</p> + * @param <T> The type of evolvable entity that can be evaluated. + * + * @author Daniel Dyer + */ +public class CachingFitnessEvaluator<T> implements FitnessEvaluator<T> +{ + private final FitnessEvaluator<T> delegate; + + // 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 ConcurrentMap<T, Double> cache = new MapMaker().weakKeys().makeMap(); + + + /** + * Creates a caching fitness evaluator that wraps the specified evaluator. + * @param delegate The fitness evaluator that performs the actual calculations. + */ + public CachingFitnessEvaluator(FitnessEvaluator<T> delegate) + { + this.delegate = delegate; + } + + + /** + * {@inheritDoc} + * + * <p>This implementation performs a cache look-up every time it is invoked. If the + * fitness evaluator has already calculated the fitness score for the specified + * candidate that score is returned without delegating to the wrapped evaluator.</p> + */ + public double getFitness(T candidate, List<? extends T> population) + { + Double fitness = cache.get(candidate); + if (fitness == null) + { + fitness = delegate.getFitness(candidate, population); + cache.put(candidate, fitness); + } + return fitness; + } + + + /** + * {@inheritDoc} + */ + public boolean isNatural() + { + return delegate.isNatural(); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/CandidateFactory.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/CandidateFactory.java new file mode 100644 index 0000000..e20c552 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/CandidateFactory.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.framework; + +import java.util.Collection; +import java.util.List; +import java.util.Random; + +/** + * Creates new populations of candidates. For most implementations it + * will be easiest just to extend {@link org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory} and + * implement the method to generate a single random candidate. + * @param <T> The type of evolvable entity created by the factory. + * @author Daniel Dyer + */ +public interface CandidateFactory<T> +{ + /** + * Creates an initial population of candidates. If more control is required + * over the composition of the initial population, consider the overloaded + * {@link #generateInitialPopulation(int,Collection,Random)} method. + * @param populationSize The number of candidates to create. + * @param rng The random number generator to use when creating the initial + * candidates. + * @return An initial population of candidate solutions. + */ + List<T> generateInitialPopulation(int populationSize, + Random rng); + + /** + * Sometimes it is desirable to seed the initial population with some + * known good candidates, or partial solutions, in order to provide some + * hints for the evolution process. This method generates an initial + * population, seeded with some initial candidates. If the number of seed + * candidates is less than the required population size, the factory should + * generate additional candidates to fill the remaining spaces in the + * population. + * @param populationSize The size of the initial population. + * @param seedCandidates Candidates to seed the population with. Number + * of candidates must be no bigger than the population size. + * @param rng The random number generator to use when creating additional + * candidates to fill the population when the number of seed candidates is + * insufficient. This can be null if and only if the number of seed + * candidates provided is sufficient to fully populate the initial population. + * @return An initial population of candidate solutions, including the + * specified seed candidates. + */ + List<T> generateInitialPopulation(int populationSize, + Collection<T> seedCandidates, + Random rng); + + /** + * Randomly create a single candidate solution. + * @param rng The random number generator to use when creating the random + * candidate. + * @return A randomly-initialised candidate. + */ + T generateRandomCandidate(Random rng); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvaluatedCandidate.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvaluatedCandidate.java new file mode 100644 index 0000000..904ebd8 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvaluatedCandidate.java @@ -0,0 +1,109 @@ +//============================================================================= +// 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.framework; + +/** + * Immutable wrapper class for associating a candidate solution with its + * fitness score. + * @author Daniel Dyer. + * @param <T> The candidate type. + */ +public final class EvaluatedCandidate<T> implements Comparable<EvaluatedCandidate<T>> +{ + private final T candidate; + private final double fitness; + + + /** + * @param candidate The evolved candidate. + * @param fitness The candidates fitness score. + */ + public EvaluatedCandidate(T candidate, double fitness) + { + if (fitness < 0) + { + throw new IllegalArgumentException("Fitness score must be greater than or equal to zero."); + } + this.candidate = candidate; + this.fitness = fitness; + } + + + /** + * @return The evolved candidate solution. + */ + public T getCandidate() + { + return candidate; + } + + + /** + * @return The fitness score for the associated candidate. + */ + public double getFitness() + { + return fitness; + } + + + /** + * Compares this candidate's fitness score with that of the specified + * candidate. + * @param evaluatedCandidate The candidate to compare scores with. + * @return -1, 0 or 1 if this candidate's score is less than, equal to, + * or greater than that of the specified candidate. The comparison applies + * to the raw numerical score and does not consider whether that score is + * a natural fitness score or not. + */ + public int compareTo(EvaluatedCandidate<T> evaluatedCandidate) + { + return Double.compare(fitness, evaluatedCandidate.getFitness()); + } + + + /** + * Over-ridden to be consistent with {@link #compareTo(EvaluatedCandidate)}. + * @param o The object to check for equality. + * @return true If this object is logically equivalent to {code o}. + */ + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + final EvaluatedCandidate<?> that = (EvaluatedCandidate<?>) o; + return Double.compare(that.getFitness(), fitness) == 0; + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + final long temp = fitness == 0.0d ? 0L : Double.doubleToLongBits(fitness); + return (int) (temp ^ (temp >>> 32)); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionEngine.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionEngine.java new file mode 100644 index 0000000..4f53a69 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionEngine.java @@ -0,0 +1,163 @@ +//============================================================================= +// 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.framework; + +import java.util.Collection; +import java.util.List; + +/** + * Operations for classes that provide an evolution implementation. + * @param <T> The type of entity evolved by the evolution engine. + * @author Daniel Dyer + */ +public interface EvolutionEngine<T> +{ + /** + * Execute the evolutionary algorithm until one of the termination conditions is met, + * then return the fittest candidate from the final generation. To return the + * entire population rather than just the fittest candidate, use the + * {@link #evolvePopulation(int, int, TerminationCondition[])} method instead. + * @param populationSize The number of candidate solutions present in the population + * at any point in time. + * @param eliteCount The number of candidates preserved via elitism. In elitism, a + * sub-set of the population with the best fitness scores are preserved unchanged in + * the subsequent generation. Candidate solutions that are preserved unchanged through + * elitism remain eligible for selection for breeding the remainder of the next generation. + * This value must be non-negative and less than the population size. A value of zero + * means that no elitism will be applied. + * @param conditions One or more conditions that may cause the evolution to terminate. + * @return The fittest solution found by the evolutionary process. + * @see #evolve(int, int, Collection, TerminationCondition[]) + */ + T evolve(int populationSize, + int eliteCount, + TerminationCondition... conditions); + + + /** + * Execute the evolutionary algorithm until one of the termination conditions is met, + * then return the fittest candidate from the final generation. To return the + * entire population rather than just the fittest candidate, use the + * {@link #evolvePopulation(int, int, Collection, TerminationCondition[])} + * method instead. + * @param populationSize The number of candidate solutions present in the population + * at any point in time. + * @param eliteCount The number of candidates preserved via elitism. In elitism, a + * sub-set of the population with the best fitness scores are preserved unchanged in + * the subsequent generation. Candidate solutions that are preserved unchanged through + * elitism remain eligible for selection for breeding the remainder of the next generation. + * This value must be non-negative and less than the population size. A value of zero + * means that no elitism will be applied. + * @param seedCandidates A set of candidates to seed the population with. The size of + * this collection must be no greater than the specified population size. + * @param conditions One or more conditions that may cause the evolution to terminate. + * @return The fittest solution found by the evolutionary process. + * @see #evolve(int,int,TerminationCondition[]) + */ + T evolve(int populationSize, + int eliteCount, + Collection<T> seedCandidates, + TerminationCondition... conditions); + + + /** + * Execute the evolutionary algorithm until one of the termination conditions is met, + * then return all of the candidates from the final generation. To return just the + * fittest candidate rather than the entire population, use the + * {@link #evolve(int, int, TerminationCondition[])} method instead. + * @param populationSize The number of candidate solutions present in the population + * at any point in time. + * @param eliteCount The number of candidates preserved via elitism. In elitism, a + * sub-set of the population with the best fitness scores are preserved unchanged in + * the subsequent generation. Candidate solutions that are preserved unchanged through + * elitism remain eligible for selection for breeding the remainder of the next generation. + * This value must be non-negative and less than the population size. A value of zero + * means that no elitism will be applied. + * @param conditions One or more conditions that may cause the evolution to terminate. + * @return The fittest solution found by the evolutionary process. + * @see #evolve(int, int, Collection, TerminationCondition[]) + * @see #evolvePopulation(int, int, Collection, TerminationCondition[]) + */ + List<EvaluatedCandidate<T>> evolvePopulation(int populationSize, + int eliteCount, + TerminationCondition... conditions); + + + /** + * Execute the evolutionary algorithm until one of the termination conditions is met, + * then return all of the candidates from the final generation. To return just the + * fittest candidate rather than the entire population, use the + * {@link #evolve(int, int, Collection, TerminationCondition[])} method instead. + * @param populationSize The number of candidate solutions present in the population + * at any point in time. + * @param eliteCount The number of candidates preserved via elitism. In elitism, a + * sub-set of the population with the best fitness scores are preserved unchanged in + * the subsequent generation. Candidate solutions that are preserved unchanged through + * elitism remain eligible for selection for breeding the remainder of the next generation. + * This value must be non-negative and less than the population size. A value of zero + * means that no elitism will be applied. + * @param seedCandidates A set of candidates to seed the population with. The size of + * this collection must be no greater than the specified population size. + * @param conditions One or more conditions that may cause the evolution to terminate. + * @return The fittest solution found by the evolutionary process. + * @see #evolve(int, int, Collection, TerminationCondition[]) + * @see #evolvePopulation(int, int, Collection, TerminationCondition[]) + */ + List<EvaluatedCandidate<T>> evolvePopulation(int populationSize, + int eliteCount, + Collection<T> seedCandidates, + TerminationCondition... conditions); + + + /** + * Adds a listener to receive status updates on the evolution progress. + * @param observer An evolution observer call-back. + * @see #removeEvolutionObserver(EvolutionObserver) + */ + void addEvolutionObserver(EvolutionObserver<? super T> observer); + + + /** + * Removes an evolution progress listener. + * @param observer An evolution observer call-back. + * @see #addEvolutionObserver(EvolutionObserver) + */ + void removeEvolutionObserver(EvolutionObserver<? super T> observer); + + + /** + * Returns a list of all {@link TerminationCondition}s that are satisfied by the current + * state of the evolution engine. Usually this list will contain only one item, but it + * is possible that mutliple termination conditions will become satisfied at the same + * time. In this case the condition objects in the list will be in the same order that + * they were specified when passed to the engine. + * + * If the evolution has not yet terminated (either because it is still in progress or + * because it hasn't even been started) then an IllegalStateException will be thrown. + * + * If the evolution terminated because the request thread was interrupted before any + * termination conditions were satisfied then this method will return an empty list. + * + * @throws IllegalStateException If this method is invoked on an evolution engine before + * evolution is started or while it is still in progress. + * + * @return A list of statisfied conditions. The list is guaranteed to be non-null. The + * list may be empty because it is possible for evolution to terminate without any conditions + * being matched. The only situation in which this occurs is when the request thread is + * interrupted. + */ + List<TerminationCondition> getSatisfiedTerminationConditions(); +}
\ No newline at end of file diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionObserver.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionObserver.java new file mode 100644 index 0000000..fe3a165 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionObserver.java @@ -0,0 +1,57 @@ +//============================================================================= +// 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.framework; + +/** + * <p>Call-back interface so that programs can monitor the state of a + * long-running evolutionary algorithm.</p> + * <p>Depending on the parameters of the evolutionary program, an observer may + * be invoked dozens or hundreds of times a second, especially when the population + * size is small as this leads to shorter generations. The processing performed by an + * evolution observer should be reasonably short-lived so as to avoid slowing down + * the evolution.</p> + * <p><strong>Using an EvolutionObserver to update a Swing GUI:</strong> + * Evolution updates are dispatched on the request thread. To adhere to + * Swing threading rules you must use {@link javax.swing.SwingUtilities#invokeLater(Runnable)} + * or {@link javax.swing.SwingUtilities#invokeAndWait(Runnable)} to perform any updates to Swing + * components.</p> + * <p>Be aware that if there are too many Swing updates queued for asynchronous + * execution with {@link javax.swing.SwingUtilities#invokeLater(Runnable)}, due to a high + * number of generations per second, then the GUI will become sluggish and + * unresponsive. + * This situation can be mitigated by minimising the amount of work done by + * the evolution observer and/or by not updating the GUI every time the observer is + * notified.</p> + * <p>The unresponsive GUI problem does not occur when using + * {@link javax.swing.SwingUtilities#invokeAndWait(Runnable)} because updates are + * executed synchronously. The downside is that evolution threads are stalled/idle until + * Swing has finished performing the updates. This won't make much difference on a single + * core machine but will impact throughput on multi-core machines.</p> + * @param <T> The type of entity that exists in the evolving population + * that is being observed. This type can be bound to a super-type of the + * actual population type so as to allow a non-specific observer that can + * be re-used for different population types. + * @author Daniel Dyer + */ +public interface EvolutionObserver<T> +{ + /** + * Invoked when the state of the population has changed (typically + * at the end of a generation). + * @param data Statistics about the state of the current generation. + */ + void populationUpdate(PopulationData<? extends T> data); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionStrategyEngine.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionStrategyEngine.java new file mode 100644 index 0000000..c639b2c --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionStrategyEngine.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.framework; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * <p>General purpose engine for implementing Evolution Strategies. Both (μ+λ) and (μ,λ) + * strategies are supported (choose which to use by setting the boolean constructor parameter).</p> + * + * <p>Though this implementation accepts the {@code eliteCount} argument for each of its evolve + * methods in common with other {@link EvolutionEngine} implementations, it has no effect for + * evolution strategies. Elitism is implicit in a (μ+λ) ES and undesirable for a (μ,λ) ES.</p> + + * @param <T> The type of entity that is to be evolved. + * @see GenerationalEvolutionEngine + * @see SteadyStateEvolutionEngine + * @author Daniel Dyer + */ +public class EvolutionStrategyEngine<T> extends AbstractEvolutionEngine<T> +{ + private final EvolutionaryOperator<T> evolutionScheme; + private final FitnessEvaluator<? super T> fitnessEvaluator; + private final boolean plusSelection; + private final int offspringMultiplier; + + + /** + * Creates a new engine for an evolution strategy. + * @param candidateFactory Factory used to create the initial population that is + * iteratively evolved. + * @param evolutionScheme The combination of evolutionary operators used to evolve + * the population at each generation. + * @param fitnessEvaluator A function for assigning fitness scores to candidate + * solutions. + * @param plusSelection If true this object implements a (μ+λ) evolution strategy rather + * than (μ,λ). With plus-selection the parents are eligible for survival. With + * comma-selection only the offspring survive. + * @param offspringMultiplier How many offspring to create for each member of the parent + * population. This parameter effectively defines a multiplier for μ that gives λ. + * We define λ in this indirect way because we don't know the value of μ until + * it is passed as an argument to one of the evolve methods. + * For a 1+1 ES this parameter would be set to one. For other evolution strategies + * a higher value might be better. Eiben & Smith suggest 7 as a good value. + * @param rng The source of randomness used by all stochastic processes (including + * evolutionary operators and selection strategies). + */ + public EvolutionStrategyEngine(CandidateFactory<T> candidateFactory, + EvolutionaryOperator<T> evolutionScheme, + FitnessEvaluator<? super T> fitnessEvaluator, + boolean plusSelection, + int offspringMultiplier, + Random rng) + { + super(candidateFactory, fitnessEvaluator, rng); + this.evolutionScheme = evolutionScheme; + this.fitnessEvaluator = fitnessEvaluator; + this.plusSelection = plusSelection; + this.offspringMultiplier = offspringMultiplier; + } + + + /** + * This method performs a single step/iteration of the evolutionary process. + * @param evaluatedPopulation The population at the beginning of the process. + * @param eliteCount Ignored by evolution strategies. Elitism is implicit in a (μ+λ) + * ES and undesirable for a (μ,λ) ES. + * @param rng A source of randomness. + * @return The updated population after the evolution strategy has advanced. + */ + @Override + protected List<EvaluatedCandidate<T>> nextEvolutionStep(List<EvaluatedCandidate<T>> evaluatedPopulation, + int eliteCount, + Random rng) + { + // Elite count is ignored. If it's non-zero it doesn't really matter, but if assertions are + // enabled we will flag it as wrong. + assert eliteCount == 0 : "Explicit elitism is not supported for an ES, eliteCount should be 0."; + + // Select candidates that will be operated on to create the offspring. + int offspringCount = offspringMultiplier * evaluatedPopulation.size(); + List<T> parents = new ArrayList<T>(offspringCount); + for (int i = 0; i < offspringCount; i++) + { + parents.add(evaluatedPopulation.get(rng.nextInt(evaluatedPopulation.size())).getCandidate()); + } + + // Then evolve the parents. + List<T> offspring = evolutionScheme.apply(parents, rng); + + List<EvaluatedCandidate<T>> evaluatedOffspring = evaluatePopulation(offspring); + if (plusSelection) // Plus-selection means parents are considered for survival as well as offspring. + { + evaluatedOffspring.addAll(evaluatedPopulation); + } + EvolutionUtils.sortEvaluatedPopulation(evaluatedOffspring, fitnessEvaluator.isNatural()); + // Retain the fittest of the candidates that are eligible for survival. + return evaluatedOffspring.subList(0, evaluatedPopulation.size()); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionUtils.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionUtils.java new file mode 100644 index 0000000..2838e31 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionUtils.java @@ -0,0 +1,129 @@ +//============================================================================= +// 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.framework; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import org.uncommons.maths.statistics.DataSet; + +/** + * Utility methods used by different evolution implementations. This class exists to + * avoid duplication of this logic among multiple evolution implementations. + * @author Daniel Dyer + */ +public final class EvolutionUtils +{ + private EvolutionUtils() + { + // Prevents instantiation of utility class. + } + + + /** + * Given data about the current population and a set of termination conditions, determines + * whether or not the evolution should continue. + * @param data The current state of the population. + * @param conditions One or more termination conditions. The evolution should not continue if + * any of these is satisfied. + * @param <T> The type of entity that is being evolved. + * @return A list of satisfied termination conditions if the evolution has reached some + * pre-specified state, an empty list if the evolution should stop because of a thread + * interruption, or null if the evolution should continue. + */ + public static <T> List<TerminationCondition> shouldContinue(PopulationData<T> data, + TerminationCondition... conditions) + { + // If the thread has been interrupted, we should abort and return whatever + // result we currently have. + if (Thread.currentThread().isInterrupted()) + { + return Collections.emptyList(); + } + // Otherwise check the termination conditions for the evolution. + List<TerminationCondition> satisfiedConditions = new LinkedList<TerminationCondition>(); + for (TerminationCondition condition : conditions) + { + if (condition.shouldTerminate(data)) + { + satisfiedConditions.add(condition); + } + } + return satisfiedConditions.isEmpty() ? null : satisfiedConditions; + } + + + /** + * Sorts an evaluated population in descending order of fitness + * (descending order of fitness score for natural scores, ascending + * order of scores for non-natural scores). + * + * @param evaluatedPopulation The population to be sorted (in-place). + * @param naturalFitness True if higher fitness scores mean fitter individuals, false otherwise. + * @param <T> The type of entity that is being evolved. + */ + public static <T> void sortEvaluatedPopulation(List<EvaluatedCandidate<T>> evaluatedPopulation, + boolean naturalFitness) + { + // Sort candidates in descending order according to fitness. + if (naturalFitness) // Descending values for natural fitness. + { + Collections.sort(evaluatedPopulation, Collections.reverseOrder()); + } + else // Ascending values for non-natural fitness. + { + Collections.sort(evaluatedPopulation); + } + } + + + + /** + * Gets data about the current population, including the fittest candidate + * and statistics about the population as a whole. + * + * @param evaluatedPopulation Population of candidate solutions with their + * associated fitness scores. + * @param naturalFitness True if higher fitness scores mean fitter individuals, false otherwise. + * @param eliteCount The number of candidates preserved via elitism. + * @param iterationNumber The zero-based index of the current generation/epoch. + * @param startTime The time at which the evolution began, expressed as a number of milliseconds since + * 00:00 on 1st January 1970. + * @param <T> The type of entity that is being evolved. + * @return Statistics about the current generation of evolved individuals. + */ + public static <T> PopulationData<T> getPopulationData(List<EvaluatedCandidate<T>> evaluatedPopulation, + boolean naturalFitness, + int eliteCount, + int iterationNumber, + long startTime) + { + DataSet stats = new DataSet(evaluatedPopulation.size()); + for (EvaluatedCandidate<T> candidate : evaluatedPopulation) + { + stats.addValue(candidate.getFitness()); + } + return new PopulationData<T>(evaluatedPopulation.get(0).getCandidate(), + evaluatedPopulation.get(0).getFitness(), + stats.getArithmeticMean(), + stats.getStandardDeviation(), + naturalFitness, + stats.getSize(), + eliteCount, + iterationNumber, + System.currentTimeMillis() - startTime); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionaryOperator.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionaryOperator.java new file mode 100644 index 0000000..e733f06 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/EvolutionaryOperator.java @@ -0,0 +1,63 @@ +//============================================================================= +// 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.framework; + +import java.util.List; +import java.util.Random; + +/** + * <p>An evolutionary operator is a function that takes a population of + * candidates as an argument and returns a new population that is the + * result of applying a transformation to the original population.</p> + * <p><strong>An implementation of this class must not modify any of + * the selected candidate objects passed in.</strong> Doing so will + * affect the correct operation of the {@link EvolutionEngine}. Instead + * the operator should create and return new candidate objects. The + * operator is not required to create copies of unmodified individuals + * (for efficiency these may be returned directly).</p> + * @param <T> The type of evolvable entity that this operator accepts. + * @author Daniel Dyer + */ +public interface EvolutionaryOperator<T> +{ + /** + * <p>Apply the operation to each entry in the list of selected + * candidates. It is important to note that this method operates on + * the list of candidates returned by the selection strategy and not + * on the current population. Each entry in the list (not each + * individual - the list may contain the same individual more than + * once) must be operated on exactly once.</p> + * + * <p>Implementing classes should not assume any particular ordering + * (or lack of ordering) for the selection. If ordering or + * shuffling is required, it should be performed by the implementing + * class. The implementation should not re-order the list provided + * but instead should make a copy of the list and re-order that. + * The ordering of the selection should be totally irrelevant for + * operators that process each candidate in isolation, such as mutation. + * It should only be an issue for operators, such as cross-over, that + * deal with multiple candidates in a single operation.</p> + * <p><strong>The operator must not modify any of the candidates passed + * in</strong>. Instead it should return a list that contains evolved + * copies of those candidates (umodified candidates can be included in + * the results without having to be copied).</p> + * @param selectedCandidates The individuals to evolve. + * @param rng A source of randomness for stochastic operators (most + * operators will be stochastic). + * @return The evolved individuals. + */ + List<T> apply(List<T> selectedCandidates, Random rng); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvaluationWorker.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvaluationWorker.java new file mode 100644 index 0000000..9c6b410 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvaluationWorker.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.framework; + +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.uncommons.util.concurrent.ConfigurableThreadFactory; +import org.uncommons.util.id.IDSource; +import org.uncommons.util.id.IntSequenceIDSource; +import org.uncommons.util.id.StringPrefixIDSource; + +/** + * This is the class that actually runs the fitness evaluation tasks created by a + * {@link EvolutionEngine}. This responsibility is abstracted away from + * the evolution engine to permit the possibility of creating multiple instances + * across several machines, all fed by a single shared work queue, using Terracotta + * (http://www.terracotta.org) or similar. + * @author Daniel Dyer + */ +public class FitnessEvaluationWorker +{ + // Provide each worker instance with a unique name with which to prefix its threads. + private static final IDSource<String> WORKER_ID_SOURCE = new StringPrefixIDSource("FitnessEvaluationWorker", + new IntSequenceIDSource()); + + /** + * Share this field to use Terracotta to distribute fitness evaluations. + */ + private final LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(); + + + /** + * Thread pool that performs concurrent fitness evaluations. + */ + private final ThreadPoolExecutor executor; + + + /** + * Creates a FitnessEvaluationWorker that uses daemon threads. + */ + FitnessEvaluationWorker() + { + this(true); + } + + + /** + * @param daemonWorkerThreads If true, any worker threads created will be daemon threads. + */ + private FitnessEvaluationWorker(boolean daemonWorkerThreads) + { + ConfigurableThreadFactory threadFactory = new ConfigurableThreadFactory(WORKER_ID_SOURCE.nextID(), + Thread.NORM_PRIORITY, + daemonWorkerThreads); + this.executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors(), + 60, + TimeUnit.SECONDS, + workQueue, + threadFactory); + executor.prestartAllCoreThreads(); + } + + + public <T> Future<EvaluatedCandidate<T>> submit(FitnessEvalutationTask<T> task) + { + return executor.submit(task); + } + + + /** + * Entry-point for running this class standalone, as an additional node for fitness evaluations. + * If this method is invoked without using Terracotta (or similar) to share the work queue, the + * program will do nothing. + * @param args Program arguments, should be empty. + */ + public static void main(String[] args) + { + // The program will not exit immediately upon completion of the main method because + // the worker is configured to use non-daemon threads that keep the JVM alive. + new FitnessEvaluationWorker(false); + } + + + /** + * A FitnessWorker cannot be garbage-collected if its thread pool has not been shutdown. + * This method, invoked on garabage collection (or maybe not at all), shuts down the thread + * pool so that the threads can be released. + * @throws Throwable Any exception or error that occurs during finalisation. + */ + @Override + protected void finalize() throws Throwable + { + executor.shutdown(); + super.finalize(); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvaluator.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvaluator.java new file mode 100644 index 0000000..9a7c791 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvaluator.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.framework; + +import java.util.List; + +/** + * Calculates the fitness score of a given candidate of the appropriate type. + * Fitness evaluations may be executed concurrently and therefore any access + * to mutable shared state should be properly synchronised. + * @param <T> The type of evolvable entity that can be evaluated. + * @author Daniel Dyer + */ +public interface FitnessEvaluator<T> +{ + /** + * Calculates a fitness score for the given candidate. Whether + * a higher score indicates a fitter candidate or not depends on + * whether the fitness scores are natural (see {@link #isNatural}). + * This method must always return a value greater than or equal to + * zero. Framework behaviour is undefined for negative fitness scores. + * @param candidate The candidate solution to calculate fitness for. + * @param population The entire population. This will include the + * specified candidate. This is provided for fitness evaluators that + * evaluate individuals in the context of the population that they are + * part of (e.g. a program that evolves game-playing strategies may wish + * to play each strategy against each of the others). This parameter + * can be ignored by simple fitness evaluators. When iterating + * over the population, a simple reference equality check (==) can be + * used to identify which member of the population is the specified + * candidate. + * @return The fitness score for the specified candidate. Must always be + * a non-negative value regardless of natural or non-natural evaluation is + * being used. + */ + double getFitness(T candidate, + List<? extends T> population); + + /** + * <p>Specifies whether this evaluator generates <i>natural</i> fitness + * scores or not.</p> + * <p>Natural fitness scores are those in which the fittest + * individual in a population has the highest fitness value. In this + * case the algorithm is attempting to maximise fitness scores. + * There need not be a specified maximum possible value.</p> + * <p>In contrast, <i>non-natural</i> fitness evaluation results in fitter + * individuals being assigned lower scores than weaker individuals. + * In the case of non-natural fitness, the algorithm is attempting to + * minimise fitness scores.</p> + * <p>An example of a situation in which non-natural fitness scores are + * preferable is when the fitness corresponds to a cost and the algorithm + * is attempting to minimise that cost.</p> + * <p>The terminology of <i>natural</i> and <i>non-natural</i> fitness scores + * is introduced by the Watchmaker Framework to describe the two types of fitness + * scoring that exist within the framework. It does not correspond to either + * <i>standardised fitness</i> or <i>normalised fitness</i> in the EA + * literature. Standardised fitness evaluation generates non-natural + * scores with a score of zero corresponding to the best possible fitness. + * Normalised fitness evaluation is similar to standardised fitness but + * with the scores adjusted to fall within the range 0 - 1.</p> + * @return True if a high fitness score means a fitter candidate + * or false if a low fitness score means a fitter candidate. + */ + boolean isNatural(); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvalutationTask.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvalutationTask.java new file mode 100644 index 0000000..6f3f827 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/FitnessEvalutationTask.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.framework; + +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Callable task for performing parallel fitness evaluations. + * @param <T> The type of entity for which fitness is calculated. + * @author Daniel Dyer + */ +class FitnessEvalutationTask<T> implements Callable<EvaluatedCandidate<T>> +{ + private final FitnessEvaluator<? super T> fitnessEvaluator; + private final T candidate; + private final List<T> population; + + /** + * Creates a task for performing fitness evaluations. + * @param fitnessEvaluator The fitness function used to determine candidate fitness. + * @param candidate The candidate to evaluate. + * @param population The entire current population. This will include all + * of the candidates to evaluate along with any other individuals that are + * not being evaluated by this task. + */ + FitnessEvalutationTask(FitnessEvaluator<? super T> fitnessEvaluator, + T candidate, + List<T> population) + { + this.fitnessEvaluator = fitnessEvaluator; + this.candidate = candidate; + this.population = population; + } + + + public EvaluatedCandidate<T> call() + { + return new EvaluatedCandidate<T>(candidate, + fitnessEvaluator.getFitness(candidate, population)); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/GenerationalEvolutionEngine.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/GenerationalEvolutionEngine.java new file mode 100644 index 0000000..3e43915 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/GenerationalEvolutionEngine.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.framework; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.interactive.InteractiveSelection; + +/** + * <p>This class implements a general-purpose generational evolutionary algorithm. + * It supports optional concurrent fitness evaluations to take full advantage of + * multi-processor, multi-core and hyper-threaded machines.</p> + * + * <p>If multi-threading is enabled, evolution (mutation, cross-over, etc.) occurs + * on the request thread but fitness evaluations are delegated to a pool of worker + * threads. All of the host's available processing units are used (i.e. on a quad-core + * machine there will be four fitness evaluation worker threads).</p> + * + * <p>If multi-threading is disabled, all work is performed synchronously on the + * request thread. This strategy is suitable for restricted/managed environments where + * it is not permitted for applications to manage their own threads. If there are no + * restrictions on concurrency, applications should enable multi-threading for improved + * performance.</p> + * + * @param <T> The type of entity that is to be evolved. + * @see SteadyStateEvolutionEngine + * @see EvolutionStrategyEngine + * @author Daniel Dyer + */ +public class GenerationalEvolutionEngine<T> extends AbstractEvolutionEngine<T> +{ + private final EvolutionaryOperator<T> evolutionScheme; + private final FitnessEvaluator<? super T> fitnessEvaluator; + private final SelectionStrategy<? super T> selectionStrategy; + + /** + * Creates a new evolution engine by specifying the various components required by + * a generational evolutionary algorithm. + * @param candidateFactory Factory used to create the initial population that is + * iteratively evolved. + * @param evolutionScheme The combination of evolutionary operators used to evolve + * the population at each generation. + * @param fitnessEvaluator A function for assigning fitness scores to candidate + * solutions. + * @param selectionStrategy A strategy for selecting which candidates survive to + * be evolved. + * @param rng The source of randomness used by all stochastic processes (including + * evolutionary operators and selection strategies). + */ + public GenerationalEvolutionEngine(CandidateFactory<T> candidateFactory, + EvolutionaryOperator<T> evolutionScheme, + FitnessEvaluator<? super T> fitnessEvaluator, + SelectionStrategy<? super T> selectionStrategy, + Random rng) + { + super(candidateFactory, fitnessEvaluator, rng); + this.evolutionScheme = evolutionScheme; + this.fitnessEvaluator = fitnessEvaluator; + this.selectionStrategy = selectionStrategy; + } + + + /** + * Creates a new evolution engine for an interactive evolutionary algorithm. It + * is not necessary to specify a fitness evaluator for interactive evolution. + * @param candidateFactory Factory used to create the initial population that is + * iteratively evolved. + * @param evolutionScheme The combination of evolutionary operators used to evolve + * the population at each generation. + * @param selectionStrategy Interactive selection strategy configured with appropriate + * console. + * @param rng The source of randomness used by all stochastic processes (including + * evolutionary operators and selection strategies). + */ + public GenerationalEvolutionEngine(CandidateFactory<T> candidateFactory, + EvolutionaryOperator<T> evolutionScheme, + InteractiveSelection<T> selectionStrategy, + Random rng) + { + this(candidateFactory, + evolutionScheme, + new NullFitnessEvaluator(), // No fitness evaluations to perform. + selectionStrategy, + rng); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<EvaluatedCandidate<T>> nextEvolutionStep(List<EvaluatedCandidate<T>> evaluatedPopulation, + int eliteCount, + Random rng) + { + List<T> population = new ArrayList<T>(evaluatedPopulation.size()); + + // First perform any elitist selection. + List<T> elite = new ArrayList<T>(eliteCount); + Iterator<EvaluatedCandidate<T>> iterator = evaluatedPopulation.iterator(); + while (elite.size() < eliteCount) + { + elite.add(iterator.next().getCandidate()); + } + // Then select candidates that will be operated on to create the evolved + // portion of the next generation. + population.addAll(selectionStrategy.select(evaluatedPopulation, + fitnessEvaluator.isNatural(), + evaluatedPopulation.size() - eliteCount, + rng)); + // Then evolve the population. + population = evolutionScheme.apply(population, rng); + // When the evolution is finished, add the elite to the population. + population.addAll(elite); + return evaluatePopulation(population); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/NullFitnessEvaluator.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/NullFitnessEvaluator.java new file mode 100644 index 0000000..0fe0d3a --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/NullFitnessEvaluator.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.framework; + +import java.util.List; + +/** + * Fitness evaluation is not required for interactive selection, so this stub + * implementation is used to satisfy the framework requirements. + * @author Daniel Dyer + */ +class NullFitnessEvaluator implements FitnessEvaluator<Object> +{ + /** + * Returns a score of zero, regardless of the candidate being evaluated. + * @param candidate The individual to evaluate. + * @param population {@inheritDoc} + * @return Zero. + */ + public double getFitness(Object candidate, + List<?> population) + { + return 0; + } + + /** + * Always returns true. However, the return value of this method is + * irrelevant since no meaningful fitness scores are produced. + * @return True. + */ + public boolean isNatural() + { + return true; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/PopulationData.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/PopulationData.java new file mode 100644 index 0000000..a04f5bb --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/PopulationData.java @@ -0,0 +1,165 @@ +//============================================================================= +// 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.framework; + +/** + * Immutable data object containing statistics about the state of + * an evolved population and a reference to the fittest candidate + * solution in the population. + * @param <T> The type of evolved entity present in the population + * that this data describes. + * @see EvolutionObserver + * @author Daniel Dyer + */ +public final class PopulationData<T> +{ + private final T bestCandidate; + private final double bestCandidateFitness; + private final double meanFitness; + private final double fitnessStandardDeviation; + private final boolean naturalFitness; + private final int populationSize; + private final int eliteCount; + private final int generationNumber; + private final long elapsedTime; + + /** + * @param bestCandidate The fittest candidate present in the population. + * @param bestCandidateFitness The fitness score for the fittest candidate + * in the population. + * @param meanFitness The arithmetic mean of fitness scores for each member + * of the population. + * @param fitnessStandardDeviation A measure of the variation in fitness + * scores. + * @param naturalFitness True if higher fitness scores are better, false + * otherwise. + * @param populationSize The number of individuals in the population. + * @param eliteCount The number of candidates preserved via elitism. + * @param generationNumber The (zero-based) number of the last generation + * that was processed. + * @param elapsedTime The number of milliseconds since the start of the + */ + public PopulationData(T bestCandidate, + double bestCandidateFitness, + double meanFitness, + double fitnessStandardDeviation, + boolean naturalFitness, + int populationSize, + int eliteCount, + int generationNumber, + long elapsedTime) + { + this.bestCandidate = bestCandidate; + this.bestCandidateFitness = bestCandidateFitness; + this.meanFitness = meanFitness; + this.fitnessStandardDeviation = fitnessStandardDeviation; + this.naturalFitness = naturalFitness; + this.populationSize = populationSize; + this.eliteCount = eliteCount; + this.generationNumber = generationNumber; + this.elapsedTime = elapsedTime; + } + + + /** + * @return The fittest candidate present in the population. + * @see #getBestCandidateFitness() + */ + public T getBestCandidate() + { + return bestCandidate; + } + + + /** + * @return The fitness score of the fittest candidate. + * @see #getBestCandidateFitness() + */ + public double getBestCandidateFitness() + { + return bestCandidateFitness; + } + + + /** + * Returns the average fitness score of population members. + * @return The arithmetic mean fitness of individual candidates. + */ + public double getMeanFitness() + { + return meanFitness; + } + + + /** + * Returns a statistical measure of variation in fitness scores within + * the population. + * @return Population standard deviation for fitness scores. + */ + public double getFitnessStandardDeviation() + { + return fitnessStandardDeviation; + } + + + /** + * Indicates whether the fitness scores are natural or non-natural. + * @return True if higher fitness scores indicate fitter individuals, false + * otherwise. + */ + public boolean isNaturalFitness() + { + return naturalFitness; + } + + + /** + * @return The number of individuals in the current population. + */ + public int getPopulationSize() + { + return populationSize; + } + + + /** + * @return The number of candidates preserved via elitism. + */ + public int getEliteCount() + { + return eliteCount; + } + + + /** + * @return The number of this generation (zero-based). + */ + public int getGenerationNumber() + { + return generationNumber; + } + + + /** + * Returns the amount of time (in milliseconds) since the + * start of the evolutionary algorithm's execution. + * @return How long (in milliseconds) the algorithm has been running. + */ + public long getElapsedTime() + { + return elapsedTime; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/SelectionStrategy.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/SelectionStrategy.java new file mode 100644 index 0000000..a91f0fa --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/SelectionStrategy.java @@ -0,0 +1,49 @@ +//============================================================================= +// 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.framework; + +import java.util.List; +import java.util.Random; + +/** + * Strategy interface for "natural" selection. + * @param <T> The type of evolved entity that we are selecting. + * @author Daniel Dyer + */ +public interface SelectionStrategy<T> +{ + /** + * <p>Select the specified number of candidates from the population. + * Implementations may assume that the population is sorted in descending + * order according to fitness (so the fittest individual is the first item + * in the list).</p> + * <p>It is an error to call this method with an empty or null population.</p> + * @param <S> The type of evolved entity that we are selecting, a sub-type of T. + * @param population The population from which to select. + * @param naturalFitnessScores Whether higher fitness values represent fitter + * individuals or not. + * @param selectionSize The number of individual selections to make (not necessarily + * the number of distinct candidates to select, since the same individual may + * potentially be selected more than once). + * @param rng Source of randomness for stochastic selection strategies. + * @return A list containing the selected candidates. Some individual canidates may + * potentially have been selected multiple times. + */ + <S extends T> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/SteadyStateEvolutionEngine.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/SteadyStateEvolutionEngine.java new file mode 100644 index 0000000..3321db8 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/SteadyStateEvolutionEngine.java @@ -0,0 +1,147 @@ +//============================================================================= +// 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.framework; + +import java.util.List; +import java.util.Random; + +/** + * An implementation of steady-state evolution, which is a type of evolutionary algorithm + * where a population is changed incrementally, with one individual evolved at a time. This + * differs from {@link GenerationalEvolutionEngine} in which the entire population is evolved in + * parallel. + * + * @param <T> The type of entity that is to be evolved. + * @see GenerationalEvolutionEngine + * @see EvolutionStrategyEngine + * @author Daniel Dyer + */ +public class SteadyStateEvolutionEngine<T> extends AbstractEvolutionEngine<T> +{ + private final EvolutionaryOperator<T> evolutionScheme; + private final FitnessEvaluator<? super T> fitnessEvaluator; + private final SelectionStrategy<? super T> selectionStrategy; + private final int selectionSize; + private final boolean forceSingleCandidateUpdate; + + /** + * Create a steady-state evolution strategy in which one or more (usually just one) evolved + * offspring replace randomly-chosen individuals. + * @param candidateFactory Factory used to create the initial population that is + * iteratively evolved. + * @param evolutionScheme The evolutionary operator that modifies the population. The + * number of candidates used as input is controlled by the {@code selectionSize} parameter. + * The number of candidates that will be outputted depends on the implementation. Typically + * it will be the same as the input size, but this is not necessary. In fact, for steady-state + * evolution, it is typical that the output size is always 1, regardless of the input size, so + * that only one member of the population is replaced at a time. To acheive this using cross-over + * requires a cross-over implementation that returns only one offspring, rather than the normal + * two. + * @param fitnessEvaluator The fitness function. + * @param selectionStrategy The strategy for selecting which candidate(s) will be + * the parent(s) when evolving individuals. + * @param selectionSize How many parent candidates are required by the evolution scheme. + * This controls how many individuals will be provided to the evolutionary operator at + * each iteration. If you are just using mutation, this will typically be 1. For + * cross-over, two separate parents are required, so this must be set to 2. + * @param forceSingleCandidateUpdate Some evolutionary operators, specifically cross-over + * operators, generate more than one evolved individual. A true steady-state algorithm will + * only replace one individual at a time. Setting this parameter to true forces the evolution + * to discard any additional generated offspring so that for each iteration of the algorithm + * there is only one updated individual. This allows cross-over operators that were designed + * for generational evolutionary algorithms to be reused for steady-state evolution. A more + * efficient, but less straightforward, alternative would be to implement a steady-state-specific + * cross-over operator that returns only a single evolved individual. Setting this parameter to + * false permits multiple candidates to be replaced per iteration, depending on the specifics of + * the evolutionary operator(s). + * @param rng The source of randomness used by all stochastic processes (including + * evolutionary operators and selection strategies). + */ + public SteadyStateEvolutionEngine(CandidateFactory<T> candidateFactory, + EvolutionaryOperator<T> evolutionScheme, + FitnessEvaluator<? super T> fitnessEvaluator, + SelectionStrategy<? super T> selectionStrategy, + int selectionSize, + boolean forceSingleCandidateUpdate, + Random rng) + { + super(candidateFactory, fitnessEvaluator, rng); + this.fitnessEvaluator = fitnessEvaluator; + this.evolutionScheme = evolutionScheme; + this.selectionStrategy = selectionStrategy; + this.selectionSize = selectionSize; + this.forceSingleCandidateUpdate = forceSingleCandidateUpdate; + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<EvaluatedCandidate<T>> nextEvolutionStep(List<EvaluatedCandidate<T>> evaluatedPopulation, + int eliteCount, + Random rng) + { + EvolutionUtils.sortEvaluatedPopulation(evaluatedPopulation, fitnessEvaluator.isNatural()); + List<T> selectedCandidates = selectionStrategy.select(evaluatedPopulation, + fitnessEvaluator.isNatural(), + selectionSize, + rng); + List<EvaluatedCandidate<T>> offspring = evaluatePopulation(evolutionScheme.apply(selectedCandidates, rng)); + + doReplacement(evaluatedPopulation, offspring, eliteCount, rng); + + return evaluatedPopulation; + } + + + /** + * Add the offspring to the population, removing the same number of existing individuals to make + * space for them. + * This method randomly chooses which individuals should be replaced, but it can be over-ridden + * in sub-classes if alternative behaviour is required. + * @param existingPopulation The full popultation, sorted in descending order of fitness. + * @param newCandidates The (unsorted) newly-created individual(s) that should replace existing members + * of the population. + * @param eliteCount The number of the fittest individuals that should be exempt from being replaced. + * @param rng A source of randomness. + */ + protected void doReplacement(List<EvaluatedCandidate<T>> existingPopulation, + List<EvaluatedCandidate<T>> newCandidates, + int eliteCount, + Random rng) + { + assert newCandidates.size() < existingPopulation.size() - eliteCount : "Too many new candidates for replacement."; + // If this is strictly steady-state (only one updated individual per iteration), then we can't keep multiple + // evolved individuals, so just pick one at random and use that. + if (newCandidates.size() > 1 && forceSingleCandidateUpdate) + { + // Replace a randomly selected individual, but not one of the "elite" individuals at the + // beginning of the sorted population. + existingPopulation.set(rng.nextInt(existingPopulation.size() - eliteCount) + eliteCount, + newCandidates.get(rng.nextInt(newCandidates.size()))); + } + else + { + for (EvaluatedCandidate<T> candidate : newCandidates) + { + // Replace a randomly selected individual, but not one of the "elite" individuals at the + // beginning of the sorted population. + existingPopulation.set(rng.nextInt(existingPopulation.size() - eliteCount) + eliteCount, candidate); + } + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/TerminationCondition.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/TerminationCondition.java new file mode 100644 index 0000000..173cc1e --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/TerminationCondition.java @@ -0,0 +1,32 @@ +//============================================================================= +// 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.framework; + +/** + * Interface for implementing conditions used to terminate evolutionary algorithms. + * @author Daniel Dyer + */ +public interface TerminationCondition +{ + /** + * The condition is queried via this method to determine whether or not evolution + * should finish at the current point. + * @param populationData Information about the current state of evolution. This may + * be used to determine whether evolution should continue or not. + * @return true if evolution should be terminated, false otherwise. + */ + boolean shouldTerminate(PopulationData<?> populationData); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/AbstractCandidateFactory.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/AbstractCandidateFactory.java new file mode 100644 index 0000000..5a03985 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/AbstractCandidateFactory.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.framework.factories; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Convenient base class for implementations of + * {@link org.uncommons.watchmaker.framework.CandidateFactory}. + * @param <T> The type of entity evolved by this engine. + * @author Daniel Dyer + */ +public abstract class AbstractCandidateFactory<T> implements CandidateFactory<T> +{ + /** + * Randomly, create an initial population of candidates. If some + * control is required over the composition of the initial population, + * consider the overloaded {@link #generateInitialPopulation(int,Collection,Random)} + * method. + * @param populationSize The number of candidates to randomly create. + * @param rng The random number generator to use when creating the random + * candidates. + * @return A randomly generated initial population of candidate solutions. + */ + public List<T> generateInitialPopulation(int populationSize, Random rng) + { + List<T> population = new ArrayList<T>(populationSize); + for (int i = 0; i < populationSize; i++) + { + population.add(generateRandomCandidate(rng)); + } + return Collections.unmodifiableList(population); + } + + + /** + * {@inheritDoc} + * If the number of seed candidates is less than the required population + * size, the remainder of the population will be generated randomly via + * the {@link #generateRandomCandidate(Random)} method. + */ + public List<T> generateInitialPopulation(int populationSize, + Collection<T> seedCandidates, + Random rng) + { + if (seedCandidates.size() > populationSize) + { + throw new IllegalArgumentException("Too many seed candidates for specified population size."); + } + List<T> population = new ArrayList<T>(populationSize); + population.addAll(seedCandidates); + for (int i = seedCandidates.size(); i < populationSize; i++) + { + population.add(generateRandomCandidate(rng)); + } + return Collections.unmodifiableList(population); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/BitStringFactory.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/BitStringFactory.java new file mode 100644 index 0000000..3dd4fc7 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/BitStringFactory.java @@ -0,0 +1,53 @@ +//============================================================================= +// 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.framework.factories; + +import java.util.Random; +import org.uncommons.maths.binary.BitString; + +/** + * General purpose candidate factory for generating bit strings for + * genetic algorithms. + * @see BitString + * @author Daniel Dyer + */ +public class BitStringFactory extends AbstractCandidateFactory<BitString> +{ + private final int length; + + + /** + * @param length The length of all bit strings created by this + * factory. + */ + public BitStringFactory(int length) + { + this.length = length; + } + + + /** + * Generates a random bit string, with a uniform distribution of + * ones and zeroes. + * @param rng The source of randomness for setting the bits. + * @return A random bit string of the length configured for this + * factory. + */ + public BitString generateRandomCandidate(Random rng) + { + return new BitString(length, rng); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/ListPermutationFactory.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/ListPermutationFactory.java new file mode 100644 index 0000000..406b899 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/ListPermutationFactory.java @@ -0,0 +1,57 @@ +//============================================================================= +// 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.framework.factories; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * Generates random candidates from a set of elements. Each candidate is a random + * permutation of the full set of elements. + * @param <T> The component type of the lists created by this factory. + * @author Daniel Dyer + */ +public class ListPermutationFactory<T> extends AbstractCandidateFactory<List<T>> +{ + private final List<T> elements; + + /** + * Creates a factory that creates lists that contain each of the specified + * elements exactly once. The ordering of those elements within generated + * lists is random. + * @param elements The elements to permute. + */ + public ListPermutationFactory(List<T> elements) + { + this.elements = elements; + } + + + /** + * Generates a random permutation from the configured elements. + * @param rng A source of randomness used to generate the random + * permutation. + * @return A random permutation. + */ + public List<T> generateRandomCandidate(Random rng) + { + List<T> candidate = new ArrayList<T>(elements); + Collections.shuffle(candidate, rng); + return candidate; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/ObjectArrayPermutationFactory.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/ObjectArrayPermutationFactory.java new file mode 100644 index 0000000..ab62b59 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/ObjectArrayPermutationFactory.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.framework.factories; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * Generates random candidates from a set of elements. Each candidate is a random + * permutation of the full set of elements. + * @author Daniel Dyer + * @param <T> The element type of the arrays created. + */ +public class ObjectArrayPermutationFactory<T> extends AbstractCandidateFactory<T[]> +{ + private final T[] elements; + + /** + * Creates a factory that creates arrays that contain each of the specified + * elements exactly once. The ordering of those elements within generated + * arrays is random. + * @param elements The elements to permute. + */ + public ObjectArrayPermutationFactory(T[] elements) + { + this.elements = elements.clone(); + } + + + /** + * Generates a random permutation from the configured elements. + * @param rng A source of randomness used to generate the random + * permutation. + * @return A random permutation. + */ + public T[] generateRandomCandidate(Random rng) + { + T[] candidate = elements.clone(); + List<T> list = Arrays.asList(candidate); + Collections.shuffle(list, rng); + return list.toArray(candidate); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/StringFactory.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/StringFactory.java new file mode 100644 index 0000000..364e9fd --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/StringFactory.java @@ -0,0 +1,62 @@ +//============================================================================= +// 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.framework.factories; + +import java.util.Random; + +/** + * General-purpose candidate factory for EAs that use a fixed-length String encoding. + * Generates random strings of a fixed length from a given alphabet. + * @author Daniel Dyer + */ +public class StringFactory extends AbstractCandidateFactory<String> +{ + private final char[] alphabet; + private final int stringLength; + + /** + * @param alphabet The set of characters that can legally occur within a + * string generated by this factory. + * @param stringLength The fixed length of all strings generated by this + * factory. + */ + public StringFactory(char[] alphabet, + int stringLength) + { + this.alphabet = alphabet.clone(); + this.stringLength = stringLength; + } + + + /** + * Generates a random string of a pre-configured length. Each character + * is randomly selected from the pre-configured alphabet. The same + * character may appear multiple times and some characters may not appear + * at all. + * @param rng A source of randomness used to select characters to make up + * the string. + * @return A randomly generated string. + */ + public String generateRandomCandidate(Random rng) + { + char[] chars = new char[stringLength]; + for (int i = 0; i < stringLength; i++) + { + chars[i] = alphabet[rng.nextInt(alphabet.length)]; + } + return new String(chars); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/package-info.java new file mode 100644 index 0000000..7a91d34 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/factories/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. +//============================================================================= +/** + * Provides convenient general-purpose {@link org.uncommons.watchmaker.framework.CandidateFactory} + * implementations for common candidate representations such as strings, lists and arrays. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.framework.factories; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/Console.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/Console.java new file mode 100644 index 0000000..80c106b --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/Console.java @@ -0,0 +1,36 @@ +//============================================================================= +// 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.framework.interactive; + +import java.util.List; + +/** + * A console provides users with a mechanism for interacting with an + * evolutionary algorithm. + * @param <T> The type of entity that can be presented by this console. + * Evolutionary algorithms that evolve a different type can work with + * a console via a {@link Renderer} that performs the necessary conversions. + * @author Daniel Dyer + */ +public interface Console<T> +{ + /** + * @param renderedEntities A list of the suitably transformed entities + * that will be presented to the user for selection. + * @return The index of the selected entity. + */ + int select(List<? extends T> renderedEntities); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/InteractiveSelection.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/InteractiveSelection.java new file mode 100644 index 0000000..36a7883 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/InteractiveSelection.java @@ -0,0 +1,177 @@ +//============================================================================= +// 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.framework.interactive; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.uncommons.util.reflection.ReflectionUtils; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Special selection strategy used for interactive evolutionary algorithms. + * @param <T> The type of evolved entity that can be selected by this class. + * @author Daniel Dyer + */ +public class InteractiveSelection<T> implements SelectionStrategy<T> +{ + private final Console<?> console; + private final Renderer<T, ?> renderer; + private final int groupSize; + private final int maxSelectionsPerGeneration; + + + /** + * @param <R> The type of object that can be displayed by the specified + * console. The specified renderer must be able to map evolved entities + * into objects of this type. + * @param console The user interface (graphical, textual or other) used + * to present a selection choice to the user. + * @param renderer A renderer used to map the evolved entities to objects + * that can be processed by the supplied console. + * @param groupSize The number of candidates to present to the user at + * once (the user selects one from this number). + * @param maxSelectionsPerGeneration The maximum number of selections that + * the user will be asked to make for each generation of the evolutionary + * algorithm. If this number is lower than the required selection size, + * the user's selections will be repeated to make up the shortfall. The + * purpose of this setting is two-fold. Firstly it minimises user fatigue. + * Secondly, it can be used to increase selection pressure. In the extreme + * case, a setting of 1 will ensure that members of the subsequent generation + * are all descended from a single parent. + */ + public <R> InteractiveSelection(Console<R> console, + Renderer<T, R> renderer, + int groupSize, + int maxSelectionsPerGeneration) + { + if (groupSize < 2) + { + throw new IllegalArgumentException("Group size must be at least 2."); + } + if (maxSelectionsPerGeneration < 1) + { + throw new IllegalArgumentException("Maximum selections must be 1 or more."); + } + this.console = console; + this.renderer = renderer; + this.groupSize = groupSize; + this.maxSelectionsPerGeneration = maxSelectionsPerGeneration; + } + + + /** + * @param console The user interface (graphical, textual or other) used + * to present a selection choice to the user. + * @param groupSize The number of candidates to present to the user at + * once (the user selects one from this number). + * @param maxSelectionsPerGeneration The maximum number of selections that + * the user will be asked to make for each generation of the evolutionary + * algorithm. If this number is lower than the required selection size, + * the user's selections will be repeated to make up the shortfall. The + * purpose of this setting is two-fold. Firstly it minimises user fatigue. + * Secondly, it can be used to increase selection pressure. In the extreme + * case, a setting of 1 will ensure that members of the subsequent generation + * are all descended from a single parent. + */ + public InteractiveSelection(Console<T> console, + int groupSize, + int maxSelectionsPerGeneration) + { + this(console, new NoOpRenderer<T>(), groupSize, maxSelectionsPerGeneration); + } + + + /** + * {@inheritDoc} + */ + public <S extends T> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + if (population.size() < groupSize) + { + throw new IllegalArgumentException("Population is too small for selection group size of " + groupSize); + } + + int selectionCount = Math.min(selectionSize, maxSelectionsPerGeneration); + List<S> selection = new ArrayList<S>(selectionCount); + for (int i = 0; i < selectionCount; i++) + { + // Pick candidates at random (without replacement). + List<S> group = new ArrayList<S>(groupSize); + List<EvaluatedCandidate<S>> candidates = new ArrayList<EvaluatedCandidate<S>>(population); + Collections.shuffle(candidates); + for (int j = 0; j < groupSize; j++) + { + group.add(candidates.get(j).getCandidate()); + } + // Get the user to pick which one should survive to reproduce. + selection.add(select(group)); + } + + // If the selection is not big enough, extend it by randomly duplicating some + // of the selections. + if (selectionCount < selectionSize) + { + List<S> extendedSelection = new ArrayList<S>(selectionSize); + extendedSelection.addAll(selection); + for (int i = 0; i < selectionSize - selectionCount; i++) + { + extendedSelection.add(selection.get(selectionCount == 1 ? 0 : rng.nextInt(selectionCount))); + } + return extendedSelection; + } + else + { + return selection; + } + } + + + private <S extends T> S select(List<S> candidates) + { + List<Object> renderedCandidates = new ArrayList<Object>(candidates.size()); + for (S candidate : candidates) + { + renderedCandidates.add(renderer.render(candidate)); + } + Method consoleSelectMethod = ReflectionUtils.findKnownMethod(Console.class, + "select", + List.class); + Integer selection = ReflectionUtils.invokeUnchecked(consoleSelectMethod, + console, + renderedCandidates); + return candidates.get(selection); + } + + + /** + * Renderer that does nothing. Used when the console already supports the + * evolved type. + */ + private static final class NoOpRenderer<T> implements Renderer<T, T> + { + public T render(T entity) + { + return entity; + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/Renderer.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/Renderer.java new file mode 100644 index 0000000..9360763 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/Renderer.java @@ -0,0 +1,36 @@ +//============================================================================= +// 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.framework.interactive; + +/** + * Maps objects of one type to objects of a different type. For example, + * this class could be used to render dates as Strings or to render arrays + * as GUI list components. + * @param <T> The input type for the renderer. + * @param <S> The output type for the renderer. + * @author Daniel Dyer + */ +public interface Renderer<T, S> +{ + /** + * Renders an object of one type as an instance of another. For example, + * if the generic types of this renderer are Date and String, this method + * would return a String representation of a Date. + * @param entity An object to render as a different type. + * @return A rendering of the parameter. + */ + S render(T entity); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/RendererAdapter.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/RendererAdapter.java new file mode 100644 index 0000000..b8f669a --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/RendererAdapter.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.framework.interactive; + +import java.lang.reflect.Method; +import org.uncommons.util.reflection.ReflectionUtils; + +/** + * Adapter class for chaining together two renderers in series to provide + * flexibility. For example, if we have a Long -> Date renderer that turns + * a number of milliseconds since epoch into a Java date, and a Date -> String + * renderer that converts a Java date into its String representation in a + * particular locale, we can combine the two to create a Long -> String renderer + * without having to write a separate implementation of the {@link Renderer} + * interface. + * @param <T> The input type for the renderer. + * @param <S> The output type for the renderer. + * @author Daniel Dyer + */ +public class RendererAdapter<T, S> implements Renderer<T, S> +{ + private final Renderer<T, ?> renderer1; + private final Renderer<?, S> renderer2; + + + /** + * Creates an adapter that feeds the output of renderer1 into renderer2. + * @param <R> The intermediate type when transforming objects of type T to + * objects of type S. + * @param renderer1 A renderer that will translate an object of the input type + * (T) into an object of the intermediate type (R). + * @param renderer2 A renderer that will translate an object of the intermediate type + * (R) into an object of the output type (S). + */ + public <R> RendererAdapter(Renderer<T, ? extends R> renderer1, + Renderer<R, S> renderer2) + { + this.renderer1 = renderer1; + this.renderer2 = renderer2; + } + + + /** + * {@inheritDoc} + */ + public S render(T entity) + { + // This reflection charade is necessary because we can't convince the + // compiler that the output of renderer1 is compatible with the input + // of renderer2 without exposing a redundant "intermediate" type parameter + // in the class definition. I don't what to do that, I'd rather have + // the ugliness encapsulated here than complicate code that uses this class. + Method renderMethod = ReflectionUtils.findKnownMethod(Renderer.class, + "render", + Object.class); + return ReflectionUtils.<S>invokeUnchecked(renderMethod, + renderer2, + renderer1.render(entity)); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/package-info.java new file mode 100644 index 0000000..516de90 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/interactive/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. +//============================================================================= +/** + * Classes for implementing interactive evolutionary algorithms. In interactive + * evolutionary algorithms, user-guided selection is used instead of selection based + * on automated fitness evaluations. + */ +package org.uncommons.watchmaker.framework.interactive; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/Epoch.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/Epoch.java new file mode 100644 index 0000000..d4cd5d1 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/Epoch.java @@ -0,0 +1,53 @@ +//============================================================================= +// 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.framework.islands; + +import java.util.List; +import java.util.concurrent.Callable; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * @author Daniel Dyer + */ +class Epoch<T> implements Callable<List<EvaluatedCandidate<T>>> +{ + private final EvolutionEngine<T> island; + private final int populationSize; + private final int eliteCount; + private final List<T> seedCandidates; + private final TerminationCondition[] terminationConditions; + + Epoch(EvolutionEngine<T> island, + int populationSize, + int eliteCount, + List<T> seedCandidates, + TerminationCondition... terminationConditions) + { + this.island = island; + this.populationSize = populationSize; + this.eliteCount = eliteCount; + this.seedCandidates = seedCandidates; + this.terminationConditions = terminationConditions; + } + + + public List<EvaluatedCandidate<T>> call() throws Exception + { + return island.evolvePopulation(populationSize, eliteCount, seedCandidates, terminationConditions); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/IslandEvolution.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/IslandEvolution.java new file mode 100644 index 0000000..300dce9 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/IslandEvolution.java @@ -0,0 +1,375 @@ +//============================================================================= +// 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.framework.islands; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FitnessEvaluator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.TerminationCondition; +import org.uncommons.watchmaker.framework.termination.GenerationCount; + +/** + * An implementation of island evolution in which multiple independent populations are evolved in + * parallel with periodic migration of individuals between islands. + * @param <T> The type of entity that is to be evolved. + * @author Daniel Dyer + */ +public class IslandEvolution<T> +{ + private final List<EvolutionEngine<T>> islands; + private final Migration migration; + private final boolean naturalFitness; + private final Random rng; + + private final Set<IslandEvolutionObserver<? super T>> observers + = new CopyOnWriteArraySet<IslandEvolutionObserver<? super T>>(); + + private List<TerminationCondition> satisfiedTerminationConditions; + + + /** + * Create an island system with the specified number of identically-configured islands. + * If you want more fine-grained control over the configuration of each island, use the + * {@link #IslandEvolution(List, Migration, boolean, Random)} constructor, which accepts + * a list of pre-created islands (each is an instance of {@link EvolutionEngine}). + * @param islandCount The number of separate islands that will be part of the system. + * @param migration A migration strategy for moving individuals between islands at the + * end of an epoch. + * @param candidateFactory Generates the initial population for each island. + * @param evolutionScheme The evolutionary operator, or combination of evolutionary operators, + * used on each island. + * @param fitnessEvaluator The fitness function used on each island. + * @param selectionStrategy The selection strategy used on each island. + * @param rng A source of randomness, used by all islands. + * @see #IslandEvolution(List, Migration, boolean, Random) + */ + public IslandEvolution(int islandCount, + Migration migration, + CandidateFactory<T> candidateFactory, + EvolutionaryOperator<T> evolutionScheme, + FitnessEvaluator<? super T> fitnessEvaluator, + SelectionStrategy<? super T> selectionStrategy, + Random rng) + { + this(createIslands(islandCount, + candidateFactory, + evolutionScheme, + fitnessEvaluator, + selectionStrategy, + rng), + migration, + fitnessEvaluator.isNatural(), + rng); + } + + + /** + * Create an island evolution system from a list of pre-configured islands. This constructor + * gives more control over the configuration of individual islands than the alternative constructor. + * The other constructor should be used where possible to avoid having to explicitly create each + * island. + * @param islands A list of pre-configured islands. + * @param migration A migration strategy for moving individuals between islands at the + * end of an epoch. + * @param naturalFitness If true, indicates that higher fitness values mean fitter + * individuals. If false, indicates that fitter individuals will have lower scores. + * @param rng A source of randomness, used by all islands. + * @see #IslandEvolution(int, Migration, CandidateFactory, EvolutionaryOperator, FitnessEvaluator, + * SelectionStrategy, Random) + */ + public IslandEvolution(List<EvolutionEngine<T>> islands, + Migration migration, + boolean naturalFitness, + Random rng) + { + this.islands = islands; + this.migration = migration; + this.naturalFitness = naturalFitness; + this.rng = rng; + + for (int i = 0; i < islands.size(); i++) + { + final int islandIndex = i; + EvolutionEngine<T> island = islands.get(islandIndex); + island.addEvolutionObserver(new EvolutionObserver<T>() + { + public void populationUpdate(PopulationData<? extends T> populationData) + { + for (IslandEvolutionObserver<? super T> islandObserver : observers) + { + islandObserver.islandPopulationUpdate(islandIndex, populationData); + } + } + }); + } + } + + + /** + * Helper method used by the constructor to create the individual islands if they haven't + * been provided already (via the other constructor). + */ + private static <T> List<EvolutionEngine<T>> createIslands(int islandCount, + CandidateFactory<T> candidateFactory, + EvolutionaryOperator<T> evolutionScheme, + FitnessEvaluator<? super T> fitnessEvaluator, + SelectionStrategy<? super T> selectionStrategy, + Random rng) + { + List<EvolutionEngine<T>> islands = new ArrayList<EvolutionEngine<T>>(islandCount); + for (int i = 0; i < islandCount; i++) + { + GenerationalEvolutionEngine<T> island = new GenerationalEvolutionEngine<T>(candidateFactory, + evolutionScheme, + fitnessEvaluator, + selectionStrategy, + rng); + island.setSingleThreaded(true); // Don't need fine-grained concurrency when each island is on a separate thread. + islands.add(island); + } + return islands; + } + + + /** + * <p>Start the evolutionary process on each island and return the fittest candidate so far at the point + * any of the termination conditions is satisfied.</p> + * + * <p><em>If you interrupt the request thread before this method returns, the + * method will return prematurely (with the best individual found so far). + * After returning in this way, the current thread's interrupted flag + * will be set. It is preferable to use an appropritate + * {@link org.uncommons.watchmaker.framework.TerminationCondition} rather than interrupting the evolution in + * this way.</em></p> + * + * @param populationSize The population size <em>for each island</em>. Therefore, if you have 5 islands, + * setting this parameter to 200 will result in 1000 individuals overall, 200 on each island. + * @param eliteCount The number of candidates preserved via elitism <em>on each island</em>. In elitism, + * a sub-set of the population with the best fitness scores are preserved unchanged in + * the subsequent generation. Candidate solutions that are preserved unchanged through + * elitism remain eligible for selection for breeding the remainder of the next generation. + * This value must be non-negative and less than the population size. A value of zero + * means that no elitism will be applied. + * @param epochLength The number of generations that make up an epoch. Islands evolve independently for + * this number of generations and then migration occurs at the end of the epoch and the next epoch starts. + * @param migrantCount The number of individuals that will be migrated from each island at the end of each + * epoch. + * @param conditions One or more conditions that may cause the evolution to terminate. + * @return The fittest solution found by the evolutionary process on any of the islands. + */ + public T evolve(int populationSize, + int eliteCount, + int epochLength, + int migrantCount, + TerminationCondition... conditions) + { + ExecutorService threadPool = Executors.newFixedThreadPool(islands.size()); + List<List<T>> islandPopulations = new ArrayList<List<T>>(islands.size()); + List<EvaluatedCandidate<T>> evaluatedCombinedPopulation = new ArrayList<EvaluatedCandidate<T>>(); + + PopulationData<T> data = null; + List<TerminationCondition> satisfiedConditions = null; + int currentEpochIndex = 0; + long startTime = System.currentTimeMillis(); + while (satisfiedConditions == null) + { + List<Callable<List<EvaluatedCandidate<T>>>> islandEpochs = createEpochTasks(populationSize, + eliteCount, + epochLength, + islandPopulations); + try + { + List<Future<List<EvaluatedCandidate<T>>>> futures = threadPool.invokeAll(islandEpochs); + + evaluatedCombinedPopulation.clear(); + List<List<EvaluatedCandidate<T>>> evaluatedPopulations + = new ArrayList<List<EvaluatedCandidate<T>>>(islands.size()); + for (Future<List<EvaluatedCandidate<T>>> future : futures) + { + List<EvaluatedCandidate<T>> evaluatedIslandPopulation = future.get(); + evaluatedCombinedPopulation.addAll(evaluatedIslandPopulation); + evaluatedPopulations.add(evaluatedIslandPopulation); + } + + migration.migrate(evaluatedPopulations, migrantCount, rng); + + EvolutionUtils.sortEvaluatedPopulation(evaluatedCombinedPopulation, naturalFitness); + data = EvolutionUtils.getPopulationData(evaluatedCombinedPopulation, + naturalFitness, + eliteCount, + currentEpochIndex, + startTime); + notifyPopulationChange(data); + + islandPopulations.clear(); + for (List<EvaluatedCandidate<T>> evaluatedPopulation : evaluatedPopulations) + { + islandPopulations.add(toCandidateList(evaluatedPopulation)); + } + ++currentEpochIndex; + } + catch (InterruptedException ex) + { + Thread.currentThread().interrupt(); + } + catch (ExecutionException ex) + { + throw new IllegalStateException(ex); + } + satisfiedConditions = EvolutionUtils.shouldContinue(data, conditions); + } + threadPool.shutdownNow(); + + this.satisfiedTerminationConditions = satisfiedConditions; + return evaluatedCombinedPopulation.get(0).getCandidate(); + } + + + /** + * Create the concurrently-executed tasks that perform evolution on each island. + */ + private List<Callable<List<EvaluatedCandidate<T>>>> createEpochTasks(int populationSize, + int eliteCount, + int epochLength, + List<List<T>> islandPopulations) + { + List<Callable<List<EvaluatedCandidate<T>>>> islandEpochs + = new ArrayList<Callable<List<EvaluatedCandidate<T>>>>(islands.size()); + for (int i = 0; i < islands.size(); i++) + { + islandEpochs.add(new Epoch<T>(islands.get(i), + populationSize, + eliteCount, + islandPopulations.isEmpty() ? Collections.<T>emptyList() : islandPopulations.get(i), + new GenerationCount(epochLength))); + } + return islandEpochs; + } + + + /** + * Convert a list of {@link EvaluatedCandidate}s into a simple list of candidates. + * @param evaluatedCandidates The population of candidate objects to relieve of their + * evaluation wrappers. + * @param <T> The type of entity that is being evolved. + * @return The candidates, stripped of their fitness scores. + */ + private static <T> List<T> toCandidateList(List<EvaluatedCandidate<T>> evaluatedCandidates) + { + List<T> candidates = new ArrayList<T>(evaluatedCandidates.size()); + for (EvaluatedCandidate<T> evaluatedCandidate : evaluatedCandidates) + { + candidates.add(evaluatedCandidate.getCandidate()); + } + return candidates; + } + + + /** + * <p>Returns a list of all {@link TerminationCondition}s that are satisfied by the current + * state of the island evolution. Usually this list will contain only one item, but it + * is possible that mutliple termination conditions will become satisfied at the same + * time. In this case the condition objects in the list will be in the same order that + * they were specified when passed to the engine.</p> + * + * <p>If the evolution has not yet terminated (either because it is still in progress or + * because it hasn't even been started) then an IllegalStateException will be thrown.</p> + * + * <p>If the evolution terminated because the request thread was interrupted before any + * termination conditions were satisfied then this method will return an empty list.</p> + * + * @throws IllegalStateException If this method is invoked on an island system before + * evolution is started or while it is still in progress. + * + * @return A list of statisfied conditions. The list is guaranteed to be non-null. The + * list may be empty because it is possible for evolution to terminate without any conditions + * being matched. The only situation in which this occurs is when the request thread is + * interrupted. + */ + public List<TerminationCondition> getSatisfiedTerminationConditions() + { + if (satisfiedTerminationConditions == null) + { + throw new IllegalStateException("EvolutionEngine has not terminated."); + } + else + { + return Collections.unmodifiableList(satisfiedTerminationConditions); + } + } + + + /** + * <p>Adds an observer to the evolution. Observers will receives two types of updates: + * updates from each individual island at the end of each generation, and updates for + * the combined global population at the end of each epoch.</p> + * + * <p>Updates are dispatched synchronously on the request thread. Observers should + * complete their processing and return in a timely manner to avoid holding up + * the evolution.</p> + * + * @param observer The callback that will be notified at the end of each generation and epoch. + * + * @see #removeEvolutionObserver(IslandEvolutionObserver) + */ + public void addEvolutionObserver(final IslandEvolutionObserver<? super T> observer) + { + observers.add(observer); + } + + + /** + * Remove the specified observer. + * @param observer The observer to remove (if it is registered). + * + * @see #addEvolutionObserver(IslandEvolutionObserver) + */ + public void removeEvolutionObserver(final IslandEvolutionObserver<? super T> observer) + { + observers.remove(observer); + } + + + /** + * Send the population data to all registered observers. + * @param data Information about the current state of the population. + */ + private void notifyPopulationChange(PopulationData<T> data) + { + for (IslandEvolutionObserver<? super T> observer : observers) + { + observer.populationUpdate(data); + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/IslandEvolutionObserver.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/IslandEvolutionObserver.java new file mode 100644 index 0000000..7613ff2 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/IslandEvolutionObserver.java @@ -0,0 +1,38 @@ +//============================================================================= +// 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.framework.islands; + +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.PopulationData; + +/** + * A specialisation of {@link org.uncommons.watchmaker.framework.EvolutionObserver} that, as well as + * receiving global population updates (at the end of each epoch), can receive individual island + * population updates (at the end of each generation on each island). + * @param <T> The type of entity being evolved. + * @author Daniel Dyer + */ +public interface IslandEvolutionObserver<T> extends EvolutionObserver<T> +{ + /** + * Method called to notify the listener of the state of the population of an individual + * island. This will be called once for each generation on each island. + * @param islandIndex Identifies which individual island the data comes from. + * Indices start at zero and are sequential. + * @param data The latest data from the evolution on the specified island. + */ + void islandPopulationUpdate(int islandIndex, PopulationData<? extends T> data); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/Migration.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/Migration.java new file mode 100644 index 0000000..e9b985f --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/Migration.java @@ -0,0 +1,36 @@ +//============================================================================= +// 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.framework.islands; + +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; + +/** + * Strategy interface for different ways of migrating individuals between islands + * in {@link IslandEvolution}. + * @author Daniel Dyer + */ +public interface Migration +{ + /** + * @param islandPopulations The populations of each island in the system. + * @param migrantCount The number of individuals to move from each island. + * @param rng A source of randomness. + * @param <T> The type of the individual members of the island populations. + */ + <T> void migrate(List<List<EvaluatedCandidate<T>>> islandPopulations, int migrantCount, Random rng); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/RandomMigration.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/RandomMigration.java new file mode 100644 index 0000000..13e3bab --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/RandomMigration.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.framework.islands; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; + +/** + * Migrates a fixed number of candidates away from each island. Which individuals are migrated is determined + * randomly and which islands they move to is also random. This contrasts with the more ordered migration offered + * by {@link RingMigration}. If the migration count is greater than one, it is possible (probable) that migrants + * from the same island will be moved to different islands. It is also possible that when a migrant's destination is + * randomly chosen, it gets sent back to the island that it came from. + * @author Daniel Dyer + */ +public class RandomMigration implements Migration +{ + /** + * Migrates a fixed number of candidates away from each island. Which individuals are migrated is determined + * randomly and which islands they move to is also random. If the migration count is greater than one, it is + * possible (probable) that migrants from the same island will be moved to different islands. It is also possible + * that when a migrant's destination is randomly chosen, it gets sent back to the island that it came from. + * @param islandPopulations A list of the populations of each island. + * @param migrantCount The number of (randomly selected) individuals to be moved on from + * each island. + * @param rng A source of randomness. + * @param <T> The type of entity being evolved. + */ + public <T> void migrate(List<List<EvaluatedCandidate<T>>> islandPopulations, int migrantCount, Random rng) + { + List<EvaluatedCandidate<T>> migrants = new ArrayList<EvaluatedCandidate<T>>(migrantCount * islandPopulations.size()); + for (List<EvaluatedCandidate<T>> island : islandPopulations) + { + Collections.shuffle(island, rng); + for (int i = 0; i < migrantCount; i++) + { + migrants.add(island.remove(island.size() - 1)); + } + } + Collections.shuffle(migrants); + Iterator<EvaluatedCandidate<T>> iterator = migrants.iterator(); + for (List<EvaluatedCandidate<T>> island : islandPopulations) + { + for (int i = 0; i < migrantCount; i++) + { + island.add(iterator.next()); + } + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/RingMigration.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/RingMigration.java new file mode 100644 index 0000000..ca31cf9 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/RingMigration.java @@ -0,0 +1,68 @@ +//============================================================================= +// 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.framework.islands; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; + +/** + * Migrates a fixed number of individuals from each island to the adjacent island. + * Operates as if the islands are arranged in a ring with migration occurring in a + * clockwise direction. The individuals to be migrated are chosen completely at random. + * @author Daniel Dyer + */ +public class RingMigration implements Migration +{ + /** + * Migrates a fixed number of individuals from each island to the adjacent island. + * Operates as if the islands are arranged in a ring with migration occurring in a + * clockwise direction. The individuals to be migrated are chosen completely at random. + * @param islandPopulations A list of the populations of each island. + * @param migrantCount The number of (randomly selected) individuals to be moved on from + * each island. + * @param rng A source of randomness. + * @param <T> The type of entity being evolved. + */ + public <T> void migrate(List<List<EvaluatedCandidate<T>>> islandPopulations, int migrantCount, Random rng) + { + // The first batch of immigrants is from the last island to the first. + List<EvaluatedCandidate<T>> lastIsland = islandPopulations.get(islandPopulations.size() - 1); + Collections.shuffle(lastIsland, rng); + List<EvaluatedCandidate<T>> migrants = lastIsland.subList(lastIsland.size() - migrantCount, lastIsland.size()); + + for (List<EvaluatedCandidate<T>> island : islandPopulations) + { + // Migrants from the last island are immigrants for this island. + List<EvaluatedCandidate<T>> immigrants = migrants; + if (island != lastIsland) // We've already migrated individuals from the last island. + { + // Select the migrants that will move to the next island to make room for the immigrants here. + // Randomise the population so that there is no bias concerning which individuals are migrated. + Collections.shuffle(island, rng); + migrants = new ArrayList<EvaluatedCandidate<T>>(island.subList(island.size() - migrantCount, island.size())); + } + // Copy the immigrants over the last members of the population (those that are themselves + // migrating to the next island). + for (int i = 0; i < immigrants.size(); i++) + { + island.set(island.size() - migrantCount + i, immigrants.get(i)); + } + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/package-info.java new file mode 100644 index 0000000..4509026 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/islands/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. +//============================================================================= +/** + * An implementation of island model evolution. Manages parallel evolution across + * multiple {@link org.uncommons.watchmaker.framework.EvolutionEngine}s (islands) + * with periodic migration between them. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.framework.islands; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/AbstractCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/AbstractCrossover.java new file mode 100644 index 0000000..b0a9fba --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/AbstractCrossover.java @@ -0,0 +1,184 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +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; + +/** + * Generic base class for cross-over implementations. Supports all + * cross-over processes that operate on a pair of parent candidates. + * @param <T> The type of evolved candidates that are operated on by + * this cross-over implementation. + * @author Daniel Dyer + */ +public abstract class AbstractCrossover<T> implements EvolutionaryOperator<T> +{ + private final NumberGenerator<Integer> crossoverPointsVariable; + private final NumberGenerator<Probability> crossoverProbabilityVariable; + + /** + * Sets up a fixed-point cross-over implementation. Cross-over is + * applied to all pairs of parents. To apply cross-over only to a + * proportion of parent pairs, use the {@link #AbstractCrossover(int, Probability)} + * constructor. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + protected AbstractCrossover(int crossoverPoints) + { + this(crossoverPoints, Probability.ONE); + } + + + /** + * Sets up a cross-over implementation that uses a fixed number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbability} parameter. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. Must be in the range + * {@literal 0 < crossoverProbability <= 1} + */ + protected AbstractCrossover(int crossoverPoints, + Probability crossoverProbability) + { + this(new ConstantGenerator<Integer>(crossoverPoints), + new ConstantGenerator<Probability>(crossoverProbability)); + if (crossoverPoints <= 0) + { + throw new IllegalArgumentException("Number of cross-over points must be positive."); + } + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to all pairs of parents. To apply cross-over + * only to a proportion of parent pairs, use the + * {@link #AbstractCrossover(NumberGenerator, NumberGenerator)} constructor. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + protected AbstractCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + this(crossoverPointsVariable, + new ConstantGenerator<Probability>(Probability.ONE)); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + protected AbstractCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + this.crossoverPointsVariable = crossoverPointsVariable; + this.crossoverProbabilityVariable = crossoverProbabilityVariable; + } + + + /** + * Applies the cross-over operation to the selected candidates. Pairs of + * candidates are chosen randomly and subjected to cross-over to produce + * a pair of offspring candidates. + * @param selectedCandidates The evolved individuals that have survived to + * be eligible to reproduce. + * @param rng A source of randomness used to determine the location of + * cross-over points. + * @return The combined set of evolved offspring generated by applying + * cross-over to the the selected candidates. + */ + public List<T> apply(List<T> selectedCandidates, Random rng) + { + // Shuffle the collection before applying each operation so that the + // evolution is not influenced by any ordering artifacts from previous + // operations. + List<T> selectionClone = new ArrayList<T>(selectedCandidates); + Collections.shuffle(selectionClone, rng); + + List<T> result = new ArrayList<T>(selectedCandidates.size()); + Iterator<T> iterator = selectionClone.iterator(); + while (iterator.hasNext()) + { + T parent1 = iterator.next(); + if (iterator.hasNext()) + { + T parent2 = iterator.next(); + // Randomly decide (according to the current cross-over probability) + // whether to perform cross-over for these 2 parents. + int crossoverPoints = crossoverProbabilityVariable.nextValue().nextEvent(rng) + ? crossoverPointsVariable.nextValue() + : 0; + if (crossoverPoints > 0) + { + result.addAll(mate(parent1, parent2, crossoverPoints, rng)); + } + else + { + // If there is no cross-over to perform, just add the parents to the + // results unaltered. + result.add(parent1); + result.add(parent2); + } + } + else + { + // If we have an odd number of selected candidates, we can't pair up + // the last one so just leave it unmodified. + result.add(parent1); + } + } + return result; + } + + + /** + * Perform cross-over on a pair of parents to generate a pair of offspring. + * @param parent1 One of two individuals that provides the source material + * for generating offspring. + * @param parent2 One of two individuals that provides the source material + * for generating offspring. + * @param numberOfCrossoverPoints The number of cross-overs performed on the + * two parents. + * @param rng A source of randomness used to determine the location of + * cross-over points. + * @return A list containing two evolved offspring. + */ + protected abstract List<T> mate(T parent1, + T parent2, + int numberOfCrossoverPoints, + Random rng); +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/BitStringCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/BitStringCrossover.java new file mode 100644 index 0000000..4f9bdd3 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/BitStringCrossover.java @@ -0,0 +1,128 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.binary.BitString; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Cross-over with a configurable number of points (fixed or random) for + * bit strings. + * @see BitString + * @author Daniel Dyer + */ +public class BitStringCrossover extends AbstractCrossover<BitString> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public BitStringCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public BitStringCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public BitStringCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public BitStringCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public BitStringCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<BitString> mate(BitString parent1, + BitString parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.getLength() != parent2.getLength()) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + BitString offspring1 = parent1.clone(); + BitString offspring2 = parent2.clone(); + // Apply as many cross-overs as required. + 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(parent1.getLength() - 1)); + offspring1.swapSubstring(offspring2, 0, crossoverIndex); + } + List<BitString> result = new ArrayList<BitString>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/BitStringMutation.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/BitStringMutation.java new file mode 100644 index 0000000..fe849f9 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/BitStringMutation.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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.binary.BitString; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutation of individual bits in a {@link BitString} according to some + * probability. + * @see org.uncommons.maths.binary.BitString + * @author Daniel Dyer + */ +public class BitStringMutation implements EvolutionaryOperator<BitString> +{ + private final NumberGenerator<Probability> mutationProbability; + private final NumberGenerator<Integer> mutationCount; + + + /** + * Creates a mutation operator for bit strings with the specified probability that a given + * bit string will be mutated, with exactly one bit being flipped. + * @param mutationProbability The probability of a candidate being mutated. + */ + public BitStringMutation(Probability mutationProbability) + { + this(new ConstantGenerator<Probability>(mutationProbability), + new ConstantGenerator<Integer>(1)); + } + + + /** + * Creates a mutation operator for bit strings, with the probability that any + * given bit will be flipped governed by the specified number generator. + * @param mutationProbability The (possibly variable) probability of a candidate + * bit string being mutated at all. + * @param mutationCount The (possibly variable) number of bits that will be flipped + * on any candidate bit string that is selected for mutation. + */ + public BitStringMutation(NumberGenerator<Probability> mutationProbability, + NumberGenerator<Integer> mutationCount) + { + this.mutationProbability = mutationProbability; + this.mutationCount = mutationCount; + } + + + public List<BitString> apply(List<BitString> selectedCandidates, Random rng) + { + List<BitString> mutatedPopulation = new ArrayList<BitString>(selectedCandidates.size()); + for (BitString b : selectedCandidates) + { + mutatedPopulation.add(mutateBitString(b, rng)); + } + return mutatedPopulation; + } + + + /** + * Mutate a single bit string. Zero or more bits may be flipped. The + * probability of any given bit being flipped is governed by the probability + * generator configured for this mutation operator. + * @param bitString The bit string to mutate. + * @param rng A source of randomness. + * @return The mutated bit string. + */ + private BitString mutateBitString(BitString bitString, Random rng) + { + if (mutationProbability.nextValue().nextEvent(rng)) + { + BitString mutatedBitString = bitString.clone(); + int mutations = mutationCount.nextValue(); + for (int i = 0; i < mutations; i++) + { + mutatedBitString.flipBit(rng.nextInt(mutatedBitString.getLength())); + } + return mutatedBitString; + } + return bitString; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ByteArrayCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ByteArrayCrossover.java new file mode 100644 index 0000000..c80647b --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ByteArrayCrossover.java @@ -0,0 +1,131 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Cross-over with a configurable number of points (fixed or random) for + * arrays of primitive bytes. + * @author Daniel Dyer + */ +public class ByteArrayCrossover extends AbstractCrossover<byte[]> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public ByteArrayCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public ByteArrayCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public ByteArrayCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public ByteArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public ByteArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<byte[]> mate(byte[] parent1, + byte[] parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.length != parent2.length) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + byte[] offspring1 = new byte[parent1.length]; + System.arraycopy(parent1, 0, offspring1, 0, parent1.length); + byte[] offspring2 = new byte[parent2.length]; + System.arraycopy(parent2, 0, offspring2, 0, parent2.length); + // Apply as many cross-overs as required. + byte[] temp = new byte[parent1.length]; + 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(parent1.length - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + List<byte[]> result = new ArrayList<byte[]>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/CharArrayCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/CharArrayCrossover.java new file mode 100644 index 0000000..8e7dc66 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/CharArrayCrossover.java @@ -0,0 +1,131 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Cross-over with a configurable number of points (fixed or random) for + * arrays of primitive chars. + * @author Daniel Dyer + */ +public class CharArrayCrossover extends AbstractCrossover<char[]> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public CharArrayCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public CharArrayCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public CharArrayCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public CharArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public CharArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<char[]> mate(char[] parent1, + char[] parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.length != parent2.length) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + char[] offspring1 = new char[parent1.length]; + System.arraycopy(parent1, 0, offspring1, 0, parent1.length); + char[] offspring2 = new char[parent2.length]; + System.arraycopy(parent2, 0, offspring2, 0, parent2.length); + // Apply as many cross-overs as required. + char[] temp = new char[parent1.length]; + 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(parent1.length - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + List<char[]> result = new ArrayList<char[]>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/DoubleArrayCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/DoubleArrayCrossover.java new file mode 100644 index 0000000..459e6a7 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/DoubleArrayCrossover.java @@ -0,0 +1,131 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Cross-over with a configurable number of points (fixed or random) for + * arrays of primitive doubles. + * @author Daniel Dyer + */ +public class DoubleArrayCrossover extends AbstractCrossover<double[]> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public DoubleArrayCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public DoubleArrayCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public DoubleArrayCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public DoubleArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public DoubleArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<double[]> mate(double[] parent1, + double[] parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.length != parent2.length) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + double[] offspring1 = new double[parent1.length]; + System.arraycopy(parent1, 0, offspring1, 0, parent1.length); + double[] offspring2 = new double[parent2.length]; + System.arraycopy(parent2, 0, offspring2, 0, parent2.length); + // Apply as many cross-overs as required. + double[] temp = new double[parent1.length]; + 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(parent1.length - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + List<double[]> result = new ArrayList<double[]>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/EvolutionPipeline.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/EvolutionPipeline.java new file mode 100644 index 0000000..eafff64 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/EvolutionPipeline.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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * <p>A compound evolutionary operator that applies multiple operators (of the + * same type) in series.</p> + * + * <p>By combining EvolutionPipeline operators with {@link SplitEvolution} operators, + * elaborate evolutionary schemes can be constructed.</p> + * + * @param <T> The type of evolved candidate that this pipeline operates on. + * @author Daniel Dyer + */ +public class EvolutionPipeline<T> implements EvolutionaryOperator<T> +{ + private final List<EvolutionaryOperator<T>> pipeline; + + + /** + * Creates a pipeline consisting of the specified operators in + * the order that they are supplied. + * @param pipeline An ordered list of operators that make up the + * pipeline. + */ + public EvolutionPipeline(List<EvolutionaryOperator<T>> pipeline) + { + if (pipeline.isEmpty()) + { + throw new IllegalArgumentException("Pipeline must contain at least one operator."); + } + this.pipeline = new ArrayList<EvolutionaryOperator<T>>(pipeline); + } + + + /** + * Applies each operation in the pipeline in turn to the selection. + * @param selectedCandidates The candidates to subjected to evolution. + * @param rng A source of randomness used by all stochastic processes in + * the pipeline. + * @return A list of evolved candidates. + */ + public List<T> apply(List<T> selectedCandidates, Random rng) + { + List<T> population = selectedCandidates; + for (EvolutionaryOperator<T> operator : pipeline) + { + population = operator.apply(population, rng); + } + return population; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/IdentityOperator.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/IdentityOperator.java new file mode 100644 index 0000000..ff31177 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/IdentityOperator.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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Evolutionary operator that simply returns the selected candidates unaltered. + * This can be useful when combined with {@link SplitEvolution} so that a + * proportion of the selected candidates can be copied unaltered into the next + * generation while the remainder are evolved. + * @param <T> The type of evolvable entity that this operator is used with. + * @author Daniel Dyer + */ +public class IdentityOperator<T> implements EvolutionaryOperator<T> +{ + /** + * Returns the selected candidates unaltered. + * @param selectedCandidates The candidates to "evolve" (or do + * nothing to in this case). + * @param rng A source of randomness (not used). + * @return The unaltered candidates. + */ + public List<T> apply(List<T> selectedCandidates, Random rng) + { + return new ArrayList<T>(selectedCandidates); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/IntArrayCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/IntArrayCrossover.java new file mode 100644 index 0000000..f416ecd --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/IntArrayCrossover.java @@ -0,0 +1,131 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Cross-over with a configurable number of points (fixed or random) for + * arrays of primitive ints. + * @author Daniel Dyer + */ +public class IntArrayCrossover extends AbstractCrossover<int[]> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public IntArrayCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public IntArrayCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public IntArrayCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public IntArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public IntArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<int[]> mate(int[] parent1, + int[] parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.length != parent2.length) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + int[] offspring1 = new int[parent1.length]; + System.arraycopy(parent1, 0, offspring1, 0, parent1.length); + int[] offspring2 = new int[parent2.length]; + System.arraycopy(parent2, 0, offspring2, 0, parent2.length); + // Apply as many cross-overs as required. + int[] temp = new int[parent1.length]; + 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(parent1.length - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + List<int[]> result = new ArrayList<int[]>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListCrossover.java new file mode 100644 index 0000000..698200d --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListCrossover.java @@ -0,0 +1,129 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Variable-point (fixed or random) cross-over for arbitrary lists. + * @param <T> The component type of the lists that are combined. + * @author Daniel Dyer + */ +public class ListCrossover<T> extends AbstractCrossover<List<T>> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public ListCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public ListCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public ListCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public ListCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Cross-over with a variable number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public ListCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<List<T>> mate(List<T> parent1, + List<T> parent2, + int numberOfCrossoverPoints, + Random rng) + { + List<T> offspring1 = new ArrayList<T>(parent1); // Use a random-access list for performance. + List<T> offspring2 = new ArrayList<T>(parent2); + // Apply as many cross-overs as required. + 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 max = Math.min(parent1.size(), parent2.size()); + if (max > 1) // Don't perform cross-over if there aren't at least 2 elements in each list. + { + int crossoverIndex = (1 + rng.nextInt(max - 1)); + for (int j = 0; j < crossoverIndex; j++) + { + T temp = offspring1.get(j); + offspring1.set(j, offspring2.get(j)); + offspring2.set(j, temp); + } + } + } + List<List<T>> result = new ArrayList<List<T>>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListInversion.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListInversion.java new file mode 100644 index 0000000..09e1602 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListInversion.java @@ -0,0 +1,87 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.Collections; +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; + +/** + * An evolutionary operator that randomly reverses a subsection of a list. + * @author Daniel Dyer + * @param <T> The type of entity being evolved. + */ +public class ListInversion<T> implements EvolutionaryOperator<List<T>> +{ + private final NumberGenerator<Probability> inversionProbability; + + + /** + * @param inversionProbability The probability that an individual list will have some + * subsection inverted. + */ + public ListInversion(Probability inversionProbability) + { + this(new ConstantGenerator<Probability>(inversionProbability)); + } + + + /** + * @param inversionProbability A variable that controls the probability that an + * individual list will have some subsection inverted. + */ + public ListInversion(NumberGenerator<Probability> inversionProbability) + { + this.inversionProbability = inversionProbability; + } + + + public List<List<T>> apply(List<List<T>> selectedCandidates, Random rng) + { + List<List<T>> result = new ArrayList<List<T>>(selectedCandidates.size()); + for (List<T> candidate : selectedCandidates) + { + if (inversionProbability.nextValue().nextEvent(rng)) + { + List<T> newCandidate = new ArrayList<T>(candidate); + int length = newCandidate.size(); + int start = rng.nextInt(length); + int offset = 2 + rng.nextInt(length - 2); // Make sure segment length is at least 2. + int end = (start + offset) % length; + int segmentLength = end - start; + if (segmentLength < 0) + { + segmentLength += length; + } + for (int i = 0; i < segmentLength / 2; i++) + { + Collections.swap(newCandidate, (start + i) % length, (end - i + length) % length); + } + result.add(newCandidate); + } + else + { + result.add(candidate); + } + } + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOperator.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOperator.java new file mode 100644 index 0000000..cb75511 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOperator.java @@ -0,0 +1,68 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * <p>A higher-order evolutionary operator that is applied to populations made + * up of lists. In such populations, each candidate solution is itself a list + * and this operator is applied to the list contents rather than the candidate. + * It is analogous to the map function in functional programming languages.</p> + * + * <p>For example, if the evolved population consists of candidates that are + * lists of strings, we could use a ListOperator to wrap an operator of type + * String and convert it to an operator that works with lists of Strings.</p> + * + * @param <T> The element type of the lists to be mutated. + * @author Daniel Dyer + */ +public class ListOperator <T> implements EvolutionaryOperator<List<T>> +{ + private final EvolutionaryOperator<T> delegate; + + /** + * @param delegate The evolutionary operator that will be applied to each + * list candidate. + */ + public ListOperator(EvolutionaryOperator<T> delegate) + { + this.delegate = delegate; + } + + + /** + * Applies the configured operator to each list candidate, operating on the elements + * that make up a candidate rather than on the list of candidates. + * candidates and returns the results. + * @param selectedCandidates A list of list candidates. + * @param rng A source of randomness. + * @return The result of applying the configured operator to each element + * in each list candidates. + */ + public List<List<T>> apply(List<List<T>> selectedCandidates, Random rng) + { + List<List<T>> output = new ArrayList<List<T>>(selectedCandidates.size()); + for (List<T> item : selectedCandidates) + { + output.add(delegate.apply(item, rng)); + } + return output; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOrderCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOrderCrossover.java new file mode 100644 index 0000000..5748e79 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOrderCrossover.java @@ -0,0 +1,164 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Implements ordered cross-over between arbitrary lists. The algorithm is + * the Partially Mapped Cross-over (PMX) algorithm. + * @param <T> The component type of the lists that are combined. + * @author Daniel Dyer + */ +public class ListOrderCrossover<T> extends AbstractCrossover<List<T>> +{ + /** + * Creates a cross-over operator with a cross-over probability of 1. + */ + public ListOrderCrossover() + { + this(Probability.ONE); + } + + + /** + * Creates a cross-over operator with the specified cross-over probability. + * @param crossoverProbability The probability that cross-over will be performed + * for any given pair. + */ + public ListOrderCrossover(Probability crossoverProbability) + { + super(2, // Requires exactly two cross-over points. + crossoverProbability); + } + + + /** + * Creates a cross-over operator where cross-over may or may not be applied to a + * given pair of parents depending on the {@code crossoverProbability}. + * @param crossoverProbabilityVariable The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public ListOrderCrossover(NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(new ConstantGenerator<Integer>(2), // Requires exactly two cross-over points. + crossoverProbabilityVariable); + } + + + + /** + * {@inheritDoc} + */ + @Override + protected List<List<T>> mate(List<T> parent1, + List<T> parent2, + int numberOfCrossoverPoints, + Random rng) + { + assert numberOfCrossoverPoints == 2 : "Expected number of cross-over points to be 2."; + + if (parent1.size() != parent2.size()) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + + List<T> offspring1 = new ArrayList<T>(parent1); // Use a random-access list for performance. + List<T> offspring2 = new ArrayList<T>(parent2); + + int point1 = rng.nextInt(parent1.size()); + int point2 = rng.nextInt(parent1.size()); + + int length = point2 - point1; + if (length < 0) + { + length += parent1.size(); + } + + Map<T, T> mapping1 = new HashMap<T, T>(length * 2); // Big enough map to avoid re-hashing. + Map<T, T> mapping2 = new HashMap<T, T>(length * 2); + for (int i = 0; i < length; i++) + { + int index = (i + point1) % parent1.size(); + T item1 = offspring1.get(index); + T item2 = offspring2.get(index); + offspring1.set(index, item2); + offspring2.set(index, item1); + mapping1.put(item1, item2); + mapping2.put(item2, item1); + } + + checkUnmappedElements(offspring1, mapping2, point1, point2); + checkUnmappedElements(offspring2, mapping1, point1, point2); + + List<List<T>> result = new ArrayList<List<T>>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } + + + /** + * Checks elements that are outside of the partially mapped section to + * see if there are any duplicate items in the list. If there are, they + * are mapped appropriately. + */ + private void checkUnmappedElements(List<T> offspring, + Map<T, T> mapping, + int mappingStart, + int mappingEnd) + { + for (int i = 0; i < offspring.size(); i++) + { + if (!isInsideMappedRegion(i, mappingStart, mappingEnd)) + { + T mapped = offspring.get(i); + while (mapping.containsKey(mapped)) + { + mapped = mapping.get(mapped); + } + offspring.set(i, mapped); + } + } + } + + + /** + * Checks whether a given list position is within the partially mapped + * region used for cross-over. + * @param position The list position to check. + * @param startPoint The starting index (inclusive) of the mapped region. + * @param endPoint The end index (exclusive) of the mapped region. + * @return True if the specified position is in the mapped region, false + * otherwise. + */ + private boolean isInsideMappedRegion(int position, + int startPoint, + int endPoint) + { + boolean enclosed = (position < endPoint && position >= startPoint); + boolean wrapAround = (startPoint > endPoint && (position >= startPoint || position < endPoint)); + return enclosed || wrapAround; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOrderMutation.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOrderMutation.java new file mode 100644 index 0000000..a218510 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ListOrderMutation.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.framework.operators; + +import java.util.ArrayList; +import java.util.Collections; +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; + +/** + * A special mutation implementation that instead of changing the + * genes of the candidate, re-orders them. A single mutation involves + * swapping a random element in the list with the element immediately + * after it. This operation can either apply a fixed number of + * mutations to each candidate or it can draw values from a random + * sequence, typically a poisson distribution (see + * {@link org.uncommons.maths.random.PoissonGenerator}), to determine how + * many mutations to apply. + * @param <T> The component type of the lists that are mutated. + * @author Daniel Dyer + */ +public class ListOrderMutation<T> implements EvolutionaryOperator<List<T>> +{ + private final NumberGenerator<Integer> mutationCountVariable; + private final NumberGenerator<Integer> mutationAmountVariable; + + /** + * Default is one mutation per candidate. + */ + public ListOrderMutation() + { + this(1, 1); + } + + /** + * @param mutationCount The constant number of mutations + * to apply to each individual in the population. + * @param mutationAmount The constant number of positions by + * which a list element will be displaced as a result of mutation. + */ + public ListOrderMutation(int mutationCount, int mutationAmount) + { + this(new ConstantGenerator<Integer>(mutationCount), + new ConstantGenerator<Integer>(mutationAmount)); + } + + + /** + * 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 individual. + * @param mutationAmount A random variable that provides a number + * of positions by which to displace an element when mutating. + */ + public ListOrderMutation(NumberGenerator<Integer> mutationCount, + NumberGenerator<Integer> mutationAmount) + { + this.mutationCountVariable = mutationCount; + this.mutationAmountVariable = mutationAmount; + } + + + public List<List<T>> apply(List<List<T>> selectedCandidates, Random rng) + { + List<List<T>> result = new ArrayList<List<T>>(selectedCandidates.size()); + for (List<T> candidate : selectedCandidates) + { + List<T> newCandidate = new ArrayList<T>(candidate); + int mutationCount = Math.abs(mutationCountVariable.nextValue()); + for (int i = 0; i < mutationCount; i++) + { + int fromIndex = rng.nextInt(newCandidate.size()); + int mutationAmount = mutationAmountVariable.nextValue(); + int toIndex = (fromIndex + mutationAmount) % newCandidate.size(); + if (toIndex < 0) + { + toIndex += newCandidate.size(); + } + // Swap the randomly selected element with the one that is the + // specified displacement distance away. + Collections.swap(newCandidate, fromIndex, toIndex); + } + result.add(newCandidate); + } + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ObjectArrayCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ObjectArrayCrossover.java new file mode 100644 index 0000000..176bfaa --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/ObjectArrayCrossover.java @@ -0,0 +1,136 @@ +//============================================================================= +// 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.framework.operators; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Cross-over with a configurable number of points (fixed or random) for + * arrays of reference types. + * @param <T> The component type of the arrays that are being evolved. + * @author Daniel Dyer + */ +public class ObjectArrayCrossover<T> extends AbstractCrossover<T[]> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public ObjectArrayCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public ObjectArrayCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public ObjectArrayCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public ObjectArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public ObjectArrayCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<T[]> mate(T[] parent1, + T[] parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.length != parent2.length) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + // Create the most specific-type arrays possible. + @SuppressWarnings("unchecked") + T[] offspring1 = (T[]) Array.newInstance(parent1.getClass().getComponentType(), parent1.length); + System.arraycopy(parent1, 0, offspring1, 0, parent1.length); + @SuppressWarnings("unchecked") + T[] offspring2 = (T[]) Array.newInstance(parent2.getClass().getComponentType(), parent2.length); + System.arraycopy(parent2, 0, offspring2, 0, parent2.length); + // Apply as many cross-overs as required. + Object[] temp = new Object[parent1.length]; + 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(parent1.length - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + List<T[]> result = new ArrayList<T[]>(2); + result.add(offspring1); + result.add(offspring2); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/Replacement.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/Replacement.java new file mode 100644 index 0000000..94b8b5f --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/Replacement.java @@ -0,0 +1,96 @@ +//============================================================================= +// 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.framework.operators; + +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.CandidateFactory; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * An evolutionary operator that replaces individuals with randomly-generated + * new individuals, according to some specified probability. The new individuals + * are not derived from the selected individuals, they are completely random. This + * operator provides a way to prevent stagnation by occassionally introducing + * new genetic material into the population. + * @param <T> The type of evolvable entity that this operator applies to. + * @author Daniel Dyer + */ +public class Replacement<T> implements EvolutionaryOperator<T> +{ + private final CandidateFactory<T> factory; + private final NumberGenerator<Probability> replacementProbability; + + + /** + * Creates a replacement operator that replaces individuals according to + * the specified probability. New individuals are obtained from the factory + * provided. + * @param factory A source of new individuals. + * @param replacementProbability The probability that any given individual will + * be replaced by a new individual. This should typically be quite low. If it is + * too high, it will undermine the evolutionary progress. + */ + public Replacement(CandidateFactory<T> factory, + Probability replacementProbability) + { + this(factory, new ConstantGenerator<Probability>(replacementProbability)); + } + + + /** + * Creates a replacement operator that replaces individuals according to + * a variable probability. New individuals are obtained from the factory + * provided. + * @param factory A source of new individuals. + * @param replacementProbability A {@link NumberGenerator} that provides + * a probability of replacement. The probablity may be constant, or it may change + * over time. The probability should typically be quite low. If it is too high, + * it will undermine the evolutionary progress. + */ + public Replacement(CandidateFactory<T> factory, + NumberGenerator<Probability> replacementProbability) + { + this.factory = factory; + this.replacementProbability = replacementProbability; + } + + + /** + * Randomly replace zero or more of the selected candidates with new, + * independent individuals that are randomly created. + * @param selectedCandidates The selected candidates, some of these may be + * discarded and replaced with new individuals. + * @param rng A source of randomness. + * @return The remaining candidates after some (or none) have been replaced + * with new individuals. + */ + public List<T> apply(List<T> selectedCandidates, Random rng) + { + List<T> output = new ArrayList<T>(selectedCandidates.size()); + for (T candidate : selectedCandidates) + { + output.add(replacementProbability.nextValue().nextEvent(rng) + ? factory.generateRandomCandidate(rng) + : candidate); + } + return output; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/SplitEvolution.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/SplitEvolution.java new file mode 100644 index 0000000..4c6d5fb --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/SplitEvolution.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.framework.operators; + +import java.util.ArrayList; +import java.util.Collections; +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; + +/** + * <p>Compound evolutionary operator that allows the evolution of a population + * to be split into two separate streams. A percentage of the population + * will be evolved according to one specified operator and the remainder + * according to another operator. When both streams have been executed, the + * resulting offspring will be returned as a single combined population.</p> + * + * <p>This kind of separation is common in a genetic programming context where, + * for example, 10% of the population is mutated and the remaining 90% + * undergoes cross-over independently.</p> + * + * <p>To split evolution into more than two streams, multiple SplitEvolution operators + * can be combined. By combining SplitEvolution operators with + * {@link EvolutionPipeline} operators, elaborate evolutionary schemes can be + * constructed.</p> + * + * @param <T> The type of evolved entity dealt with by this operator. + * @author Daniel Dyer + */ +public class SplitEvolution<T> implements EvolutionaryOperator<T> +{ + private final EvolutionaryOperator<T> operator1; + private final EvolutionaryOperator<T> operator2; + private final NumberGenerator<Double> weightVariable; + + /** + * @param operator1 The operator that will apply to the first part of the + * population (as determined by the {@code weight} parameter). + * @param operator2 The operator that will apply to the second part of the + * population (as determined by the {@code weight} parameter). + * @param weight The proportion (as a real number between zero and 1 exclusive) + * of the population that will be evolved by {@code operator1}. The + * remainder will be evolved by {@code operator2}. + */ + public SplitEvolution(EvolutionaryOperator<T> operator1, + EvolutionaryOperator<T> operator2, + double weight) + { + this(operator1, operator2, new ConstantGenerator<Double>(weight)); + if (weight <= 0 || weight >= 1) + { + throw new IllegalArgumentException("Split ratio must be greater than 0 and less than 1."); + } + } + + + /** + * @param operator1 The operator that will apply to the first part of the + * population (as determined by the {@code weightVariable} parameter). + * @param operator2 The operator that will apply to the second part of the + * population (as determined by the {@code weightVariable} parameter). + * @param weightVariable A random variable that provides the ratio for + * dividing the population between the two evolutionary streams. Must + * only generate values in the range {@literal 0 < ratio < 1}. + */ + public SplitEvolution(EvolutionaryOperator<T> operator1, + EvolutionaryOperator<T> operator2, + NumberGenerator<Double> weightVariable) + { + this.operator1 = operator1; + this.operator2 = operator2; + this.weightVariable = weightVariable; + } + + + /** + * Applies one evolutionary operator to part of the population and another + * to the remainder. Returns a list combining the output of both. Which + * candidates are submitted to which stream is determined randomly. + * @param selectedCandidates A list of the candidates that survived to be + * eligible for evolution. + * @param rng A source of randomness passed to each of the two delegate + * evolutionary operators. + * @return The combined results from the two streams of evolution. + */ + public List<T> apply(List<T> selectedCandidates, Random rng) + { + double ratio = weightVariable.nextValue(); + int size = (int) Math.round(ratio * selectedCandidates.size()); + + // Shuffle the collection before applying each operation so that the + // split is not influenced by any ordering artifacts from previous + // operations. + List<T> selectionClone = new ArrayList<T>(selectedCandidates); + Collections.shuffle(selectionClone, rng); + + List<T> list1 = selectionClone.subList(0, size); + List<T> list2 = selectionClone.subList(size, selectedCandidates.size()); + List<T> result = new ArrayList<T>(selectedCandidates.size()); + result.addAll(operator1.apply(list1, rng)); + result.addAll(operator2.apply(list2, rng)); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/StringCrossover.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/StringCrossover.java new file mode 100644 index 0000000..e73119a --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/StringCrossover.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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Variable-point (fixed or random) cross-over for String candidates. + * This implementation assumes that all candidate Strings are the same + * length. If they are not, an exception will be thrown at runtime. + * @author Daniel Dyer + */ +public class StringCrossover extends AbstractCrossover<String> +{ + /** + * Default is single-point cross-over, applied to all parents. + */ + public StringCrossover() + { + this(1); + } + + + /** + * Cross-over with a fixed number of cross-over points. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + */ + public StringCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Cross-over with a fixed number of cross-over points. Cross-over + * may or may not be applied to a given pair of parents depending on + * the {@code crossoverProbability}. + * @param crossoverPoints The constant number of cross-over points + * to use for all cross-over operations. + * @param crossoverProbability The probability that, once selected, + * a pair of parents will be subjected to cross-over rather than + * being copied, unchanged, into the output population. + */ + public StringCrossover(int crossoverPoints, Probability crossoverProbability) + { + super(crossoverPoints, crossoverProbability); + } + + + /** + * Cross-over with a variable number of cross-over points. + * @param crossoverPointsVariable A random variable that provides a number + * of cross-over points for each cross-over operation. + */ + public StringCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Sets up a cross-over implementation that uses a variable number of cross-over + * points. Cross-over is applied to a proportion of selected parent pairs, with + * the remainder copied unchanged into the output population. The size of this + * evolved proportion is controlled by the {@code crossoverProbabilityVariable} + * parameter. + * @param crossoverPointsVariable A variable that provides a (possibly constant, + * possibly random) number of cross-over points for each cross-over operation. + * @param crossoverProbabilityVariable A variable that controls the probability + * that, once selected, a pair of parents will be subjected to cross-over rather + * than being copied, unchanged, into the output population. + */ + public StringCrossover(NumberGenerator<Integer> crossoverPointsVariable, + NumberGenerator<Probability> crossoverProbabilityVariable) + { + super(crossoverPointsVariable, crossoverProbabilityVariable); + } + + + /** + * {@inheritDoc} + */ + @Override + protected List<String> mate(String parent1, + String parent2, + int numberOfCrossoverPoints, + Random rng) + { + if (parent1.length() != parent2.length()) + { + throw new IllegalArgumentException("Cannot perform cross-over with different length parents."); + } + StringBuilder offspring1 = new StringBuilder(parent1); + StringBuilder offspring2 = new StringBuilder(parent2); + // Apply as many cross-overs as required. + 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(parent1.length() - 1)); + for (int j = 0; j < crossoverIndex; j++) + { + char temp = offspring1.charAt(j); + offspring1.setCharAt(j, offspring2.charAt(j)); + offspring2.setCharAt(j, temp); + } + } + List<String> result = new ArrayList<String>(2); + result.add(offspring1.toString()); + result.add(offspring2.toString()); + return result; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/StringMutation.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/StringMutation.java new file mode 100644 index 0000000..6e2e485 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/StringMutation.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.framework.operators; + +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; + +/** + * Mutation of individual characters in a string according to some + * probability. + * @author Daniel Dyer + */ +public class StringMutation implements EvolutionaryOperator<String> +{ + private final char[] alphabet; + private final NumberGenerator<Probability> mutationProbability; + + /** + * Creates a mutation operator that is applied with the given + * probability and draws its characters from the specified alphabet. + * @param alphabet The permitted values for each character in a string. + * @param mutationProbability The probability that a given character + * is changed. + */ + public StringMutation(char[] alphabet, Probability mutationProbability) + { + this(alphabet, new ConstantGenerator<Probability>(mutationProbability)); + } + + + /** + * Creates a mutation operator that is applied with the given + * probability and draws its characters from the specified alphabet. + * @param alphabet The permitted values for each character in a string. + * @param mutationProbability The (possibly variable) probability that a + * given character is changed. + */ + public StringMutation(char[] alphabet, + NumberGenerator<Probability> mutationProbability) + { + this.alphabet = alphabet.clone(); + this.mutationProbability = mutationProbability; + } + + + public List<String> apply(List<String> selectedCandidates, Random rng) + { + List<String> mutatedPopulation = new ArrayList<String>(selectedCandidates.size()); + for (String s : selectedCandidates) + { + mutatedPopulation.add(mutateString(s, rng)); + } + return mutatedPopulation; + } + + + /** + * Mutate a single string. Zero or more characters may be modified. The + * probability of any given character being modified is governed by the + * probability generator configured for this mutation operator. + * @param s The string to mutate. + * @param rng A source of randomness. + * @return The mutated string. + */ + private String mutateString(String s, Random rng) + { + StringBuilder buffer = new StringBuilder(s); + for (int i = 0; i < buffer.length(); i++) + { + if (mutationProbability.nextValue().nextEvent(rng)) + { + buffer.setCharAt(i, alphabet[rng.nextInt(alphabet.length)]); + } + } + return buffer.toString(); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/package-info.java new file mode 100644 index 0000000..ee33629 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/operators/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. +//============================================================================= +/** + * Provides several ready-to-use standard evolutionary operators for commonly + * used data types such as arrays, lists and strings. + */ +package org.uncommons.watchmaker.framework.operators; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/package-info.java new file mode 100644 index 0000000..4f3f095 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/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. +//============================================================================= +/** + * This package provides a framework for evolutionary computation. It defines generic + * interfaces for evolutionary operators, fitness functions and selection strategies. + * It also provides an all-purpose {@link org.uncommons.watchmaker.framework.EvolutionEngine}. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.framework; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/RankSelection.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/RankSelection.java new file mode 100644 index 0000000..1b12b54 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/RankSelection.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.framework.selection; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * <p>A selection strategy that is similar to fitness-proportionate selection + * except that is uses relative fitness rather than absolute fitness in order to + * determine the probability of selection for a given individual (i.e. the actual + * numerical fitness values are ignored and only the ordering of the sorted + * population is considered).</p> + * <p>Rank selection is implemented in terms of a mapping function ({@link + * #mapRankToScore(int, int)}) and delegation to a fitness-proportionate selector. The + * mapping function converts ranks into relative fitness scores that are used to + * drive the delegate selector.</p> + * @author Daniel Dyer + */ +public class RankSelection implements SelectionStrategy<Object> +{ + private final SelectionStrategy<Object> delegate; + + /** + * Creates a default rank-based selector with a linear + * mapping function and selection frequencies that correspond + * to expected values. + */ + public RankSelection() + { + this(new StochasticUniversalSampling()); + } + + + /** + * Creates a rank-based selector with a linear mapping function and + * configurable delegate for performing the proportionate selection. + * @param delegate The proportionate selector that will be delegated + * to after converting rankings into relative fitness scores. + */ + public RankSelection(SelectionStrategy<Object> delegate) + { + this.delegate = delegate; + } + + + /** + * {@inheritDoc} + */ + public <S> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + List<EvaluatedCandidate<S>> rankedPopulation = new ArrayList<EvaluatedCandidate<S>>(population.size()); + Iterator<EvaluatedCandidate<S>> iterator = population.iterator(); + int index = -1; + while (iterator.hasNext()) + { + S candidate = iterator.next().getCandidate(); + rankedPopulation.add(new EvaluatedCandidate<S>(candidate, + mapRankToScore(++index, + population.size()))); + } + return delegate.select(rankedPopulation, true, selectionSize, rng); + } + + + /** + * <p>Maps a population index to a relative pseudo-fitness score that can be used for + * fitness-proportionate selection. The general contract for the mapping function + * {@code f} is: {@code f(rank) >= f(rank + 1)} for all legal values of + * {@code rank}, assuming natural scores.</p> + * <p>The default mapping function is a simple linear transformation, but this + * can be over-ridden in sub-classes. Alternative implementations can be linear or + * non-linear and either natural or non-natural.</p> + * @param rank A zero-based index into the population + * {@code (0 <= rank < populationSize)}. + * @param populationSize The number of individuals in the population. + * @return {@code populationSize - rank} + */ + protected double mapRankToScore(int rank, int populationSize) + { + return populationSize - rank; + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return "Rank Selection"; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/RouletteWheelSelection.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/RouletteWheelSelection.java new file mode 100644 index 0000000..a964f57 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/RouletteWheelSelection.java @@ -0,0 +1,118 @@ +//============================================================================= +// 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.framework.selection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * <p>Implements selection of <i>n</i> candidates from a population by selecting + * <i>n</i> candidates at random where the probability of each candidate getting + * selected is proportional to its fitness score. This is analogous to each + * candidate being assigned an area on a roulette wheel proportionate to its fitness + * and the wheel being spun <n>i</n> times. Candidates may be selected more than + * once.</p> + * + * <p>In some instances, particularly with small population sizes, the randomness + * of selection may result in excessively high occurrences of particular candidates. + * If this is a problem, {@link StochasticUniversalSampling} provides an alternative + * fitness-proportionate strategy for selection.</p> + * + * @author Daniel Dyer + */ +public class RouletteWheelSelection implements SelectionStrategy<Object> +{ + /** + * Selects the required number of candidates from the population with + * the probability of selecting any particular candidate being proportional + * to that candidate's fitness score. Selection is with replacement (the same + * candidate may be selected multiple times). + * @param <S> The type of the evolved objects in the population. + * @param population The candidates to select from. + * @param naturalFitnessScores True if higher fitness scores indicate fitter + * individuals, false if lower fitness scores indicate fitter individuals. + * @param selectionSize The number of selections to make. + * @param rng A source of randomness. + * @return The selected candidates. + */ + public <S> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + // Record the cumulative fitness scores. It doesn't matter whether the + // population is sorted or not. We will use these cumulative scores to work out + // an index into the population. The cumulative array itself is implicitly + // sorted since each element must be greater than the previous one. The + // numerical difference between an element and the previous one is directly + // proportional to the probability of the corresponding candidate in the population + // being selected. + double[] cumulativeFitnesses = new double[population.size()]; + cumulativeFitnesses[0] = getAdjustedFitness(population.get(0).getFitness(), + naturalFitnessScores); + for (int i = 1; i < population.size(); i++) + { + double fitness = getAdjustedFitness(population.get(i).getFitness(), + naturalFitnessScores); + cumulativeFitnesses[i] = cumulativeFitnesses[i - 1] + fitness; + } + + List<S> selection = new ArrayList<S>(selectionSize); + for (int i = 0; i < selectionSize; i++) + { + double randomFitness = rng.nextDouble() * cumulativeFitnesses[cumulativeFitnesses.length - 1]; + int index = Arrays.binarySearch(cumulativeFitnesses, randomFitness); + if (index < 0) + { + // Convert negative insertion point to array index. + index = Math.abs(index + 1); + } + selection.add(population.get(index).getCandidate()); + } + return selection; + } + + + private double getAdjustedFitness(double rawFitness, + boolean naturalFitness) + { + if (naturalFitness) + { + return rawFitness; + } + else + { + // If standardised fitness is zero we have found the best possible + // solution. The evolutionary algorithm should not be continuing + // after finding it. + return rawFitness == 0 ? Double.POSITIVE_INFINITY : 1 / rawFitness; + } + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return "Roulette Wheel Selection"; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/SigmaScaling.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/SigmaScaling.java new file mode 100644 index 0000000..814b0b7 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/SigmaScaling.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.framework.selection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.statistics.DataSet; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * An alternative to straightforward fitness-proportionate selection such as that offered + * by {@link RouletteWheelSelection} and {@link StochasticUniversalSampling}. Uses the + * mean population fitness and fitness standard deviation to adjust individual fitness + * scores. Early on in an evolutionary algorithm this helps to avoid premature convergence + * caused by the dominance of one or two relatively fit candidates in a population of mostly + * unfit individuals. It also helps to amplify minor fitness differences in a more mature + * population where the rate of improvement has slowed. + * @author Daniel Dyer + */ +public class SigmaScaling implements SelectionStrategy<Object> +{ + private final SelectionStrategy<Object> delegate; + + /** + * Creates a default sigma-scaled selection strategy. + */ + public SigmaScaling() + { + this(new StochasticUniversalSampling()); + } + + + /** + * Creates a sigma-scaled selection strategy that delegates to the specified selection + * strategy after adjusting individual fitness scores using sigma-scaling. + * @param delegate The proportionate selector that will be delegated + * to after fitness scores have been adjusted using sigma-scaling. + */ + public SigmaScaling(SelectionStrategy<Object> delegate) + { + this.delegate = delegate; + } + + + /** + * {@inheritDoc} + */ + public <S> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + DataSet statistics = new DataSet(population.size()); + for (EvaluatedCandidate<S> candidate : population) + { + statistics.addValue(candidate.getFitness()); + } + + List<EvaluatedCandidate<S>> scaledPopulation = new ArrayList<EvaluatedCandidate<S>>(population.size()); + for (EvaluatedCandidate<S> candidate : population) + { + double scaledFitness = getSigmaScaledFitness(candidate.getFitness(), + statistics.getArithmeticMean(), + statistics.getStandardDeviation()); + scaledPopulation.add(new EvaluatedCandidate<S>(candidate.getCandidate(), + scaledFitness)); + } + return delegate.select(scaledPopulation, naturalFitnessScores, selectionSize, rng); + } + + + private double getSigmaScaledFitness(double candidateFitness, + double populationMeanFitness, + double fitnessStandardDeviation) + { + if (fitnessStandardDeviation == 0) + { + return 1; + } + else + { + double scaledFitness = 1 + (candidateFitness - populationMeanFitness) / (2 * fitnessStandardDeviation); + // Don't allow negative expected frequencies, use an arbitrary low but still positive + // frequency of 1 time in 10 for extremely unfit individuals (relative to the remainder + // of the population). + return scaledFitness > 0 ? scaledFitness : 0.1; + } + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return "Sigma Scaling"; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/StochasticUniversalSampling.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/StochasticUniversalSampling.java new file mode 100644 index 0000000..7e1bf9b --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/StochasticUniversalSampling.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.framework.selection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * An alternative to {@link RouletteWheelSelection} + * as a fitness-proportionate selection strategy. Ensures that the frequency of selection for + * each candidate is consistent with its expected frequency of selection. + * @author Daniel Dyer + */ +public class StochasticUniversalSampling implements SelectionStrategy<Object> +{ + public <S> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + // Calculate the sum of all fitness values. + double aggregateFitness = 0; + for (EvaluatedCandidate<S> candidate : population) + { + aggregateFitness += getAdjustedFitness(candidate.getFitness(), + naturalFitnessScores); + } + + List<S> selection = new ArrayList<S>(selectionSize); + // Pick a random offset between 0 and 1 as the starting point for selection. + double startOffset = rng.nextDouble(); + double cumulativeExpectation = 0; + int index = 0; + for (EvaluatedCandidate<S> candidate : population) + { + // Calculate the number of times this candidate is expected to + // be selected on average and add it to the cumulative total + // of expected frequencies. + cumulativeExpectation += getAdjustedFitness(candidate.getFitness(), + naturalFitnessScores) / aggregateFitness * selectionSize; + + // If f is the expected frequency, the candidate will be selected at + // least as often as floor(f) and at most as often as ceil(f). The + // actual count depends on the random starting offset. + while (cumulativeExpectation > startOffset + index) + { + selection.add(candidate.getCandidate()); + index++; + } + } + return selection; + } + + + private double getAdjustedFitness(double rawFitness, boolean naturalFitness) + { + if (naturalFitness) + { + return rawFitness; + } + else + { + // If standardised fitness is zero we have found the best possible + // solution. The evolutionary algorithm should not be continuing + // after finding it. + return rawFitness == 0 ? Double.POSITIVE_INFINITY : 1 / rawFitness; + } + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return "Stochastic Universal Sampling"; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/TournamentSelection.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/TournamentSelection.java new file mode 100644 index 0000000..78d8107 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/TournamentSelection.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.framework.selection; + +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.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Selection strategy that picks a pair of candidates at random and then + * selects the fitter of the two candidates with probability p, where p + * is the configured selection probability (therefore the probability of + * the less fit candidate being selected is 1 - p). + * @author Daniel Dyer + */ +public class TournamentSelection implements SelectionStrategy<Object> +{ + private final NumberGenerator<Probability> selectionProbability; + + private String description = "Tournament Selection"; + + /** + * Creates a tournament selection strategy that is controlled by the + * variable selection probability provided by the specified + * {@link NumberGenerator}. + * @param selectionProbability A number generator that produces values in + * the range {@literal 0.5 < p < 1}. These values are used as the probability + * of the fittest candidate being selected in any given tournament. + */ + public TournamentSelection(NumberGenerator<Probability> selectionProbability) + { + this.selectionProbability = selectionProbability; + } + + + /** + * Creates a tournament selection strategy with a fixed probability. + * @param selectionProbability The probability that the fitter of two randomly + * chosen candidates will be selected. Since this is a probability it must be + * between 0.0 and 1.0. This implementation adds the further restriction that + * the probability must be greater than 0.5 since any lower value would favour + * weaker candidates over strong ones, negating the "survival of the fittest" + * aspect of the evolutionary algorithm. + */ + public TournamentSelection(Probability selectionProbability) + { + this(new ConstantGenerator<Probability>(selectionProbability)); + if (selectionProbability.doubleValue() <= 0.5) + { + throw new IllegalArgumentException("Selection threshold must be greater than 0.5."); + } + this.description = "Tournament Selection (p = " + selectionProbability.toString() + ')'; + } + + + public <S> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + List<S> selection = new ArrayList<S>(selectionSize); + for (int i = 0; i < selectionSize; i++) + { + // Pick two candidates at random. + EvaluatedCandidate<S> candidate1 = population.get(rng.nextInt(population.size())); + EvaluatedCandidate<S> candidate2 = population.get(rng.nextInt(population.size())); + + // Use a random value to decide wether to select the fitter individual or the weaker one. + boolean selectFitter = selectionProbability.nextValue().nextEvent(rng); + if (selectFitter == naturalFitnessScores) + { + // Select the fitter candidate. + selection.add(candidate2.getFitness() > candidate1.getFitness() + ? candidate2.getCandidate() + : candidate1.getCandidate()); + } + else + { + // Select the less fit candidate. + selection.add(candidate2.getFitness() > candidate1.getFitness() + ? candidate1.getCandidate() + : candidate2.getCandidate()); + } + } + return selection; + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return description; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/TruncationSelection.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/TruncationSelection.java new file mode 100644 index 0000000..594df97 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/TruncationSelection.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.framework.selection; + +import java.text.DecimalFormat; +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.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Implements selection of <i>n</i> candidates from a population by simply + * selecting the <i>n</i> candidates with the highest fitness scores (the + * rest are discarded). A candidate is never selected more than once. + * @author Daniel Dyer + */ +public class TruncationSelection implements SelectionStrategy<Object> +{ + private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#0.###%"); + private final NumberGenerator<Double> selectionRatio; + + private String description = "Truncation Selection"; + + /** + * Creates a truncation selection strategy that is controlled by the + * variable selection ratio provided by the specified + * {@link NumberGenerator}. + * @param selectionRatio A number generator that produces values in + * the range {@literal 0 < r < 1}. These values are used to determine + * the proportion of the population that is retained in any given selection. + */ + public TruncationSelection(NumberGenerator<Double> selectionRatio) + { + this.selectionRatio = selectionRatio; + } + + + /** + * @param selectionRatio The proportion of the highest ranked candidates to + * select from the population. The value must be positive and less than 1. + */ + public TruncationSelection(double selectionRatio) + { + this(new ConstantGenerator<Double>(selectionRatio)); + if (selectionRatio <= 0 || selectionRatio >= 1) + { + throw new IllegalArgumentException("Selection ratio must be greater than 0 and less than 1."); + } + this.description = "Truncation Selection (" + PERCENT_FORMAT.format(selectionRatio) + ")"; + } + + + /** + * Selects the fittest candidates. If the selectionRatio results in + * fewer selected candidates than required, then these candidates are + * selected multiple times to make up the shortfall. + * @param population The population of evolved and evaluated candidates + * from which to select. + * @param naturalFitnessScores Whether higher fitness values represent fitter + * individuals or not. + * @param selectionSize The number of candidates to select from the + * evolved population. + * @param rng A source of randomness (not used by this selection + * implementation since truncation selection is deterministic). + * @param <S> The type of evolved entity that is being selected. + * @return The selected candidates. + */ + public <S> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + List<S> selection = new ArrayList<S>(selectionSize); + + double ratio = selectionRatio.nextValue(); + assert ratio < 1 && ratio > 0 : "Selection ratio out-of-range: " + ratio; + + int eligibleCount = (int) Math.round(ratio * population.size()); + eligibleCount = eligibleCount > selectionSize ? selectionSize : eligibleCount; + + do + { + int count = Math.min(eligibleCount, selectionSize - selection.size()); + for (int i = 0; i < count; i++) + { + selection.add(population.get(i).getCandidate()); + } + } while (selection.size() < selectionSize); + return selection; + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return description; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/package-info.java new file mode 100644 index 0000000..73f682a --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/selection/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. +//============================================================================= +/** + * Various selection strategies for use with evolutionary algorithms. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.framework.selection; diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/ElapsedTime.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/ElapsedTime.java new file mode 100644 index 0000000..bddea38 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/ElapsedTime.java @@ -0,0 +1,52 @@ +//============================================================================= +// 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.framework.termination; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Terminates evolution after a pre-determined period of time has elapsed. + * @author Daniel Dyer + */ +public class ElapsedTime implements TerminationCondition +{ + private final long maxDuration; + + /** + * @param maxDuration The maximum period of time (in milliseconds) before + * evolution will be terminated. + */ + public ElapsedTime(long maxDuration) + { + if (maxDuration <= 0) + { + throw new IllegalArgumentException("Duration must be positive."); + } + this.maxDuration = maxDuration; + } + + + /** + * {@inheritDoc} + * This implementation terminates evolution if the pre-configured maximum + * permitted time has elapsed. + */ + public boolean shouldTerminate(PopulationData<?> populationData) + { + return populationData.getElapsedTime() >= maxDuration; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/GenerationCount.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/GenerationCount.java new file mode 100644 index 0000000..a0ec88c --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/GenerationCount.java @@ -0,0 +1,49 @@ +//============================================================================= +// 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.framework.termination; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Terminates evolution after a set number of generations have passed. + * @author Daniel Dyer + */ +public class GenerationCount implements TerminationCondition +{ + private final int generationCount; + + /** + * @param generationCount The maximum number of generations that the + * evolutionary algorithm will permit before terminating. + */ + public GenerationCount(int generationCount) + { + if (generationCount <= 0) + { + throw new IllegalArgumentException("Generation count must be positive."); + } + this.generationCount = generationCount; + } + + /** + * {@inheritDoc} + */ + public boolean shouldTerminate(PopulationData<?> populationData) + { + return populationData.getGenerationNumber() + 1 >= generationCount; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/Stagnation.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/Stagnation.java new file mode 100644 index 0000000..b0bab4a --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/Stagnation.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.framework.termination; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * A {@link TerminationCondition} that halts evolution if no improvement in fitness + * is observed within a specified number of generations. + * @author Daniel Dyer + */ +public class Stagnation implements TerminationCondition +{ + private final int generationLimit; + private final boolean naturalFitness; + private final boolean usePopulationAverage; + + private double bestFitness; + private int fittestGeneration; + + /** + * Creates a {@link TerminationCondition} that will halt evolution after the + * specified number of generations passes without any improvement in the population's + * fittest individual. + * @param generationLimit The number of generations without improvement that + * will lead to termination. + * @param naturalFitness True if higher fitness scores are better, false otherwise. + */ + public Stagnation(int generationLimit, + boolean naturalFitness) + { + this(generationLimit, naturalFitness, false); + } + + + /** + * Creates a {@link TerminationCondition} that will halt evolution after the + * specified number of generations passes without any improvement in the population's + * fitness (either the fittest individual or the mean fitness of the entire population, + * depending on the final parameter). + * @param generationLimit The number of generations without improvement that + * will lead to termination. + * @param naturalFitness True if higher fitness scores are better, false otherwise. + * @param usePopulationAverage If true uses the mean fitness of the population as the + * criteria, otherwise uses the fittest individual. + */ + public Stagnation(int generationLimit, + boolean naturalFitness, + boolean usePopulationAverage) + { + this.generationLimit = generationLimit; + this.naturalFitness = naturalFitness; + this.usePopulationAverage = usePopulationAverage; + } + + + /** + * {@inheritDoc} + */ + public boolean shouldTerminate(PopulationData<?> populationData) + { + double fitness = getFitness(populationData); + if (populationData.getGenerationNumber() == 0 || hasFitnessImproved(fitness)) + { + bestFitness = fitness; + fittestGeneration = populationData.getGenerationNumber(); + } + + return populationData.getGenerationNumber() - fittestGeneration >= generationLimit; + } + + + /** + * Determines the fitness of the current population (either best fitness or + * mean fitness depending on how the termination condition is configured). + * @param populationData Data about the current generation. + * @return The fitness measure used to decide whether the evolution has stagnated + * or not. + */ + private double getFitness(PopulationData<?> populationData) + { + return usePopulationAverage + ? populationData.getMeanFitness() + : populationData.getBestCandidateFitness(); + } + + + /** + * Determine whether the population fitness is better than the best seen so far. + * @param fitness The fitness of the current population (either best fitness or mean + * fitness depending on how the termination condition is configured). + * @return True if the fitness has improved in the current generation, false otherwise. + */ + private boolean hasFitnessImproved(double fitness) + { + return (naturalFitness && fitness > bestFitness) + || (!naturalFitness && fitness < bestFitness); + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/TargetFitness.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/TargetFitness.java new file mode 100644 index 0000000..26c3298 --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/TargetFitness.java @@ -0,0 +1,63 @@ +//============================================================================= +// 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.framework.termination; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Terminates evolution once at least one candidate in the population has equalled + * or bettered a pre-determined fitness score. + * @author Daniel Dyer + */ +public class TargetFitness implements TerminationCondition +{ + private final double targetFitness; + private final boolean natural; + + /** + * @param targetFitness The fitness score that must be achieved by at least + * one individual in the population in order for this condition to be satisfied. + * @param natural Whether fitness scores are natural or non-natural. If fitness + * is natural, the condition will be satisfied if any individual has a fitness + * that is greater than or equal to the target fitness. If fitness is non-natural, + * the condition will be satisfied in any individual has a fitness that is less + * than or equal to the target fitness. + * @see org.uncommons.watchmaker.framework.FitnessEvaluator + */ + public TargetFitness(double targetFitness, boolean natural) + { + this.targetFitness = targetFitness; + this.natural = natural; + } + + /** + * {@inheritDoc} + */ + public boolean shouldTerminate(PopulationData<?> populationData) + { + if (natural) + { + // If we're using "natural" fitness scores, higher values are better. + return populationData.getBestCandidateFitness() >= targetFitness; + } + else + { + // If we're using "non-natural" fitness scores, lower values are better. + return populationData.getBestCandidateFitness() <= targetFitness; + } + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/UserAbort.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/UserAbort.java new file mode 100644 index 0000000..dce1aab --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/UserAbort.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.framework.termination; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * {@link TerminationCondition} implementation that allows for user-initiated + * termination of an evolutionary algorithm. This condition can be used, for + * instance, to provide a button on a GUI that terminates execution. The + * application should retain a reference to the instance after passing it to + * the evolution engine and should invoke the {@link #abort()} method to make + * the evolution terminate at the end of the current generation. + * @see org.uncommons.watchmaker.swing.AbortControl + * @author Daniel Dyer + */ +public final class UserAbort implements TerminationCondition +{ + private volatile boolean aborted = false; + + /** + * {@inheritDoc} + */ + public boolean shouldTerminate(PopulationData<?> populationData) + { + return isAborted(); + } + + + /** + * Aborts any evolutionary algorithms that monitor this termination condition + * instance. + */ + public void abort() + { + aborted = true; + } + + + /** + * @return true if the {@link #abort()} method has been invoked, false otherwise. + */ + public boolean isAborted() + { + return aborted; + } + + + /** + * Resets the abort condition to false so that it may be reused. + */ + public void reset() + { + aborted = false; + } +} diff --git a/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/package-info.java b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/package-info.java new file mode 100644 index 0000000..e68cb9d --- /dev/null +++ b/watchmaker/framework/src/java/main/org/uncommons/watchmaker/framework/termination/package-info.java @@ -0,0 +1,19 @@ +//============================================================================= +// 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. +//============================================================================= +/** + * Configurable conditions for terminating evolutionary algorithms. + */ +package org.uncommons.watchmaker.framework.termination; diff --git a/watchmaker/framework/src/java/test/org/uncommons/util/concurrent/ConfigurableThreadFactoryTest.java b/watchmaker/framework/src/java/test/org/uncommons/util/concurrent/ConfigurableThreadFactoryTest.java new file mode 100644 index 0000000..8aaae2f --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/util/concurrent/ConfigurableThreadFactoryTest.java @@ -0,0 +1,137 @@ +//============================================================================= +// 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.util.concurrent; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import org.testng.annotations.Test; + +/** + * Unit test for the general-purpose thread factory implementation. + * @author Daniel Dyer + */ +public class ConfigurableThreadFactoryTest +{ + @Test + public void testDaemonThreads() + { + ThreadFactory threadFactory = new ConfigurableThreadFactory("Test", + Thread.MIN_PRIORITY, + true); + Runnable doNothing = new Runnable() + { + public void run() + { + // Do nothing. + } + }; + Thread thread1 = threadFactory.newThread(doNothing); + assert thread1.getName().startsWith("Test") : "Wrong thread name: " + thread1.getName(); + assert thread1.getPriority() == Thread.MIN_PRIORITY : "Wrong priority: " + thread1.getPriority(); + assert thread1.isDaemon() : "Thread should be a daemon."; + + // Second thread should have a different name. + Thread thread2 = threadFactory.newThread(doNothing); + assert thread2.getName().startsWith("Test") : "Wrong thread name: " + thread2.getName(); + assert !thread1.getName().equals(thread2.getName()) : "Thread names should be different."; + } + + + @Test + public void testNonDaemonThreads() + { + ThreadFactory threadFactory = new ConfigurableThreadFactory("Test", + Thread.MAX_PRIORITY, + false); + Runnable doNothing = new Runnable() + { + public void run() + { + // Do nothing. + } + }; + Thread thread = threadFactory.newThread(doNothing); + assert thread.getName().startsWith("Test") : "Wrong thread name: " + thread.getName(); + assert thread.getPriority() == Thread.MAX_PRIORITY : "Wrong priority: " + thread.getPriority(); + assert !thread.isDaemon() : "Thread should not be a daemon."; + } + + + @Test + public void testDefaultExceptionHandler() throws InterruptedException + { + // Intercept std. err. + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + System.setErr(new PrintStream(byteStream)); + + ThreadFactory threadFactory = new ConfigurableThreadFactory("Test", + Thread.MAX_PRIORITY, + false); + Runnable doNothing = new Runnable() + { + public void run() + { + throw new IllegalStateException("This is a test."); + } + }; + Thread thread = threadFactory.newThread(doNothing); + thread.start(); + thread.join(); + + String output = byteStream.toString(); + assert output.startsWith("java.lang.IllegalStateException") : "Exception handler failed to log exception."; + } + + + @Test + public void testCustomExceptionHandler() throws InterruptedException + { + ExceptionHandler exceptionHandler = new ExceptionHandler(); + ThreadFactory threadFactory = new ConfigurableThreadFactory("Test", + Thread.MAX_PRIORITY, + false, + exceptionHandler); + Runnable doNothing = new Runnable() + { + public void run() + { + throw new IllegalStateException("This is a test."); + } + }; + Thread thread = threadFactory.newThread(doNothing); + thread.start(); + thread.join(); + assert exceptionHandler.getExceptionCount() == 1 : "Exception not thrown."; + } + + + private static final class ExceptionHandler implements Thread.UncaughtExceptionHandler + { + private final AtomicInteger count = new AtomicInteger(); + + public void uncaughtException(Thread thread, Throwable throwable) + { + count.incrementAndGet(); + } + + public int getExceptionCount() + { + return count.get(); + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/util/id/CompositeIDSourceTest.java b/watchmaker/framework/src/java/test/org/uncommons/util/id/CompositeIDSourceTest.java new file mode 100644 index 0000000..79946c7 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/util/id/CompositeIDSourceTest.java @@ -0,0 +1,36 @@ +//============================================================================= +// 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.util.id; + +import org.testng.annotations.Test; + +/** + * Unit test for composite ID source. + * @author Daniel Dyer + */ +public class CompositeIDSourceTest +{ + @Test + public void testCombination() + { + int topPart = 15; + IDSource<Long> idSource = new CompositeIDSource(topPart); + long firstID = idSource.nextID(); + long secondID = idSource.nextID(); + assert 64424509440L == firstID : "First ID should be 2^36 - 2^32 (or 15 shifted left 32 places)."; + assert secondID == firstID + 1 : "Second ID should be first ID plus 1."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/util/id/IntSequenceIDSourceTest.java b/watchmaker/framework/src/java/test/org/uncommons/util/id/IntSequenceIDSourceTest.java new file mode 100644 index 0000000..e9ae567 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/util/id/IntSequenceIDSourceTest.java @@ -0,0 +1,56 @@ +//============================================================================= +// 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.util.id; + +import org.testng.annotations.Test; + +/** + * Unit test for 32-bit ID sequence. + * @author Daniel Dyer + */ +public class IntSequenceIDSourceTest +{ + @Test + public void testSequence() + { + IntSequenceIDSource idSource = new IntSequenceIDSource(); + long firstID = idSource.nextID(); + long secondID = idSource.nextID(); + long thirdID = idSource.nextID(); + assert firstID == 0 : "First ID should be 0."; + assert secondID == firstID + 1 : "Second ID should be 1 more than first ID."; + assert thirdID == secondID + 1 : "Third ID should be 1 more than second ID."; + } + + + @Test(expectedExceptions = IDSourceExhaustedException.class) + public void testExhaustion() + { + IDSource<Integer> idSource = new IntSequenceIDSource(Integer.MAX_VALUE); + // Should be able to get one ID from this. + int id = idSource.nextID(); + assert id == Integer.MAX_VALUE : "Incorrect initial value: " + id; + // But the next invocation should result in an exception. + idSource.nextID(); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidInitialValue() + { + new IntSequenceIDSource(-1); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/util/id/LongSequenceIDSourceTest.java b/watchmaker/framework/src/java/test/org/uncommons/util/id/LongSequenceIDSourceTest.java new file mode 100644 index 0000000..4cfe0e0 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/util/id/LongSequenceIDSourceTest.java @@ -0,0 +1,56 @@ +//============================================================================= +// 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.util.id; + +import org.testng.annotations.Test; + +/** + * Unit test for 64-bit ID sequence. + * @author Daniel Dyer + */ +public class LongSequenceIDSourceTest +{ + @Test + public void testSequence() + { + LongSequenceIDSource idSource = new LongSequenceIDSource(); + long firstID = idSource.nextID(); + long secondID = idSource.nextID(); + long thirdID = idSource.nextID(); + assert firstID == 0 : "First ID should be 0."; + assert secondID == firstID + 1 : "Second ID should be 1 more than first ID."; + assert thirdID == secondID + 1 : "Third ID should be 1 more than second ID."; + } + + + @Test(expectedExceptions = IDSourceExhaustedException.class) + public void testExhaustion() + { + IDSource<Long> idSource = new LongSequenceIDSource(Long.MAX_VALUE); + // Should be able to get one ID from this. + long id = idSource.nextID(); + assert id == Long.MAX_VALUE : "Incorrect initial value: " + id; + // But the next invocation should result in an exception. + idSource.nextID(); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidInitialValue() + { + new LongSequenceIDSource(-1); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/util/id/StringPrefixIDSourceTest.java b/watchmaker/framework/src/java/test/org/uncommons/util/id/StringPrefixIDSourceTest.java new file mode 100644 index 0000000..546c78c --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/util/id/StringPrefixIDSourceTest.java @@ -0,0 +1,35 @@ +//============================================================================= +// 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.util.id; + +import org.testng.annotations.Test; + +/** + * Unit test for prefixed ID source. + * @author Daniel Dyer + */ +public class StringPrefixIDSourceTest +{ + @Test + public void testPrefix() + { + IDSource<String> idSource = new StringPrefixIDSource("Watchmaker", new IntSequenceIDSource()); + String id1 = idSource.nextID(); + assert id1.equals("Watchmaker0") : "Wrong ID: " + id1; + String id2 = idSource.nextID(); + assert id2.equals("Watchmaker1") : "Wrong ID: " + id2; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/util/reflection/ReflectionUtilsTest.java b/watchmaker/framework/src/java/test/org/uncommons/util/reflection/ReflectionUtilsTest.java new file mode 100644 index 0000000..5d660e0 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/util/reflection/ReflectionUtilsTest.java @@ -0,0 +1,198 @@ +//============================================================================= +// 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.util.reflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import org.testng.annotations.Test; + +/** + * Unit test for {@link ReflectionUtils}. + * @author Daniel Dyer + */ +public class ReflectionUtilsTest +{ + @Test + public void testFindMethod() + { + Method toString = ReflectionUtils.findKnownMethod(Object.class, "toString"); + assert toString.getName().equals("toString") : "Wrong method returned: " + toString.getName(); + // Make sure that the method returned is from the correct class and not some other class. + Class<?> declaringClass = toString.getDeclaringClass(); + assert declaringClass.equals(Object.class) : "Method from wrong class returned: " + declaringClass.getName(); + } + + + @Test(dependsOnMethods = "testFindMethod") + public void testSuccessfulInvocation() + { + Method toString = ReflectionUtils.findKnownMethod(Object.class, "toString"); + String result = ReflectionUtils.invokeUnchecked(toString, "Hello"); + assert result.equals("Hello") : "Wrong value returned by method."; + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNoSuchMethod() + { + ReflectionUtils.findKnownMethod(Object.class, "noSuchMethod"); + } + + + /** + * Invoking a method that throws a RuntimeException should result in that + * exception being thrown back to the caller. The RuntimeException should not + * be wrapped in an InvocationTargetException as would be the case for a + * normal reflective invocation. + */ + @Test(dependsOnMethods = "testFindMethod", + expectedExceptions = ArithmeticException.class) + public void testMethodRuntimeExceptions() + { + Method divide = ReflectionUtils.findKnownMethod(BigDecimal.class, "divide", BigDecimal.class); + ReflectionUtils.invokeUnchecked(divide, BigDecimal.ONE, BigDecimal.ZERO); // Should throw ArithmeticException. + } + + + /** + * Invoking a method that throws an Error should result in that + * error being thrown back to the caller. The Error should not + * be wrapped in an InvocationTargetException as would be the case for a + * normal reflective invocation. + */ + @Test(dependsOnMethods = "testFindMethod", + expectedExceptions = InternalError.class) + public void testErrors() + { + class ErrorTest + { + public void doError() + { + throw new InternalError(); + } + } + Method error = ReflectionUtils.findKnownMethod(ErrorTest.class, "doError"); + ReflectionUtils.invokeUnchecked(error, new ErrorTest()); // Should throw InternalError. + } + + + @Test + public void testFindConstructor() + { + Constructor<Object> defaultConstructor = ReflectionUtils.findKnownConstructor(Object.class); + // Make sure that the method returned is from the correct class and not some other class. + Class<?> declaringClass = defaultConstructor.getDeclaringClass(); + assert declaringClass.equals(Object.class) : "Constructor from wrong class returned: " + declaringClass.getName(); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNoSuchConstructor() + { + ReflectionUtils.findKnownConstructor(Object.class, String.class); + } + + + @Test(dependsOnMethods = "testFindConstructor", + expectedExceptions = IllegalStateException.class) + public void testExceptionInConstructor() + { + Constructor<ExceptionTest> constructor = ReflectionUtils.findKnownConstructor(ExceptionTest.class); + ReflectionUtils.invokeUnchecked(constructor); // Should throw exception. + } + + + @Test(dependsOnMethods = "testFindConstructor", + expectedExceptions = InternalError.class) + public void testErrorInConstructor() + { + Constructor<ErrorTest> constructor = ReflectionUtils.findKnownConstructor(ErrorTest.class); + ReflectionUtils.invokeUnchecked(constructor); // Should throw error. + } + + + @Test(dependsOnMethods = "testFindConstructor", + expectedExceptions = IllegalArgumentException.class) + public void testConstructingAbstractClass() + { + Constructor<Abstract> constructor = ReflectionUtils.findKnownConstructor(Abstract.class); + ReflectionUtils.invokeUnchecked(constructor); // Should throw an exception. + } + + + @Test(dependsOnMethods = "testFindConstructor", + expectedExceptions = IllegalArgumentException.class) + public void testUnconstructable() throws NoSuchMethodException + { + Constructor<Unconstructable> constructor = Unconstructable.class.getDeclaredConstructor(); + ReflectionUtils.invokeUnchecked(constructor); // Should throw an exception. + } + + + @Test(dependsOnMethods = "testFindMethod", + expectedExceptions = IllegalArgumentException.class) + public void testInaccessibleMethod() throws NoSuchMethodException + { + Method method = Inaccessible.class.getDeclaredMethod("inaccessibleMethod"); + ReflectionUtils.invokeUnchecked(method, new Inaccessible()); // Should throw an exception. + } + + + private static class ExceptionTest + { + public ExceptionTest() + { + throw new IllegalStateException(); + } + } + + + private static class ErrorTest + { + public ErrorTest() + { + throw new InternalError(); + } + } + + + private abstract static class Abstract + { + public Abstract() + { + // Do nothing. + } + } + + + private static class Unconstructable + { + private Unconstructable() + { + // Do nothing. + } + } + + + private static class Inaccessible + { + private void inaccessibleMethod() + { + // Do nothing. + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/CachingFitnessEvaluatorTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/CachingFitnessEvaluatorTest.java new file mode 100644 index 0000000..f77bcff --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/CachingFitnessEvaluatorTest.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.framework; + +import java.util.Collections; +import java.util.List; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link CachingFitnessEvaluator} wrapper. + * @author Daniel Dyer + */ +public class CachingFitnessEvaluatorTest +{ + @Test + public void testCacheMiss() + { + FitnessEvaluator<String> evaluator = new CachingFitnessEvaluator<String>(new IncrementingEvaluator(true)); + double fitness = evaluator.getFitness("Test1", Collections.<String>emptyList()); + assert fitness == 1 : "Wrong fitness: " + fitness; + // Different candidate so shouldn't return a cached value. + fitness = evaluator.getFitness("Test2", Collections.<String>emptyList()); + assert fitness == 2 : "Wrong fitness: " + fitness; + + } + + + @Test + public void testCacheHit() + { + FitnessEvaluator<String> evaluator = new CachingFitnessEvaluator<String>(new IncrementingEvaluator(true)); + double fitness = evaluator.getFitness("Test", Collections.<String>emptyList()); + assert fitness == 1 : "Wrong fitness: " + fitness; + fitness = evaluator.getFitness("Test", Collections.<String>emptyList()); + // If the value is found in the cache it won't have changed. If it is recalculated, it will have. + assert fitness == 1 : "Expected cached value (1), got " + fitness; + } + + + @Test + public void testNatural() + { + FitnessEvaluator<String> evaluator = new CachingFitnessEvaluator<String>(new IncrementingEvaluator(true)); + assert evaluator.isNatural() : "Wrapper for natural scores should also be natural."; + } + + + @Test + public void testNonNatural() + { + FitnessEvaluator<String> evaluator = new CachingFitnessEvaluator<String>(new IncrementingEvaluator(false)); + assert !evaluator.isNatural() : "Wrapper for non-natural scores should also be non-natural."; + } + + + /** + * This breaks the rules for the caching evaluator in that it is not repeatable + * (it returns different values when invoked multiple times for the same candidate), + * but it allows us to see whether we are getting a cached value or a new value. + */ + private static final class IncrementingEvaluator implements FitnessEvaluator<String> + { + private final boolean natural; + private int count = 0; + + IncrementingEvaluator(boolean natural) + { + this.natural = natural; + } + + public double getFitness(String candidate, List<? extends String> population) + { + return ++count; + } + + public boolean isNatural() + { + return natural; + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/EvaluatedCandidateTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/EvaluatedCandidateTest.java new file mode 100644 index 0000000..c0d74d4 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/EvaluatedCandidateTest.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.framework; + +import org.testng.annotations.Test; + +/** + * Unit test for the simple {@link EvaluatedCandidate} class. Ensures that + * the equals and hashCode methods function correctly. + * @author Daniel Dyer + */ +public class EvaluatedCandidateTest +{ + @Test + public void testEquality() + { + // Equality is determined only by fitness score, the actual candidate + // representation is irrelevant. These two candidates should be considered + // equal. + EvaluatedCandidate<String> candidate1 = new EvaluatedCandidate<String>("AAAA", 5); + EvaluatedCandidate<String> candidate2 = new EvaluatedCandidate<String>("BBBB", 5); + + assert candidate1.equals(candidate1) : "Equality must be reflexive."; + assert candidate2.equals(candidate2) : "Equality must be reflexive."; + + assert candidate1.hashCode() == candidate2.hashCode() : "Hash codes must be identical for equal objects."; + assert candidate1.compareTo(candidate2) == 0 : "compareTo() must be consistent with equals()"; + + assert candidate1.equals(candidate2) : "Candidates with equal fitness should be equal."; + assert candidate2.equals(candidate1) : "Equality must be symmetric."; + } + + + @Test + public void testNotEqual() + { + // Equality is determined only by fitness score, the actual candidate + // representation is irrelevant. These two candidates should be considered + // unequal. + EvaluatedCandidate<String> candidate1 = new EvaluatedCandidate<String>("AAAA", 5); + EvaluatedCandidate<String> candidate2 = new EvaluatedCandidate<String>("AAAA", 7); + + assert !candidate1.equals(candidate2) : "Candidates with equal fitness should be equal."; + assert !candidate2.equals(candidate1) : "Equality must be symmetric."; + + assert candidate1.compareTo(candidate2) != 0 : "compareTo() must be consistent with equals()"; + } + + + @Test + public void testNullEquality() + { + EvaluatedCandidate<String> candidate = new EvaluatedCandidate<String>("AAAA", 5); + assert !candidate.equals(null) : "Object must not be considered equal to null reference."; + } + + + @Test + public void testDifferentClassEquality() + { + EvaluatedCandidate<String> candidate = new EvaluatedCandidate<String>("AAAA", 5); + assert !candidate.equals(new Object()) : "Object must not be equal to instances of other classes."; + } + + + /** + * Comparisons are based only on fitness score. + */ + @Test(dependsOnMethods = "testEquality") + public void testComparisons() + { + // Only test greater than and less than comparisons here since we've already + // done equality. + EvaluatedCandidate<String> candidate1 = new EvaluatedCandidate<String>("AAAA", 5); + EvaluatedCandidate<String> candidate2 = new EvaluatedCandidate<String>("AAAA", 7); + assert candidate1.compareTo(candidate2) < 0 : "Incorrect ordering."; + assert candidate2.compareTo(candidate1) > 0 : "Incorrect ordering."; + } + + + /** + * Negative fitness scores are not supported. An informative exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNegativeFitness() + { + new EvaluatedCandidate<String>("ABC", -1); // Should throw an exception. + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/EvolutionStrategyEngineTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/EvolutionStrategyEngineTest.java new file mode 100644 index 0000000..f07c354 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/EvolutionStrategyEngineTest.java @@ -0,0 +1,68 @@ +//============================================================================= +// 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.framework; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.factories.StubIntegerFactory; +import org.uncommons.watchmaker.framework.operators.IntegerAdjuster; + +/** + * Unit test for {@link EvolutionStrategyEngine} class. + * @author Daniel Dyer + */ +public class EvolutionStrategyEngineTest +{ + @Test + public void testOnePlusOneEvolutionStrategy() + { + EvolutionStrategyEngine<Integer> engine = new EvolutionStrategyEngine<Integer>(new StubIntegerFactory(), + new IntegerAdjuster(-1), + new IntegerEvaluator(), + true, + 1, + FrameworkTestUtils.getRNG()); + @SuppressWarnings("unchecked") + List<EvaluatedCandidate<Integer>> population = Arrays.asList(new EvaluatedCandidate<Integer>(1, 1)); + + List<EvaluatedCandidate<Integer>> evolvedPopulation + = engine.nextEvolutionStep(population, 0, FrameworkTestUtils.getRNG()); + assert evolvedPopulation.size() == 1 : "Population size should be 1, is " + evolvedPopulation.size(); + // The offspring is less fit than the parent (due to the -1 operator) so the parent should be retained. + assert evolvedPopulation.get(0).getCandidate() == 1 : "Wrong individual after evolution."; + } + + + @Test + public void testOneCommaOneEvolutionStrategy() + { + EvolutionStrategyEngine<Integer> engine = new EvolutionStrategyEngine<Integer>(new StubIntegerFactory(), + new IntegerAdjuster(-1), + new IntegerEvaluator(), + false, + 1, + FrameworkTestUtils.getRNG()); + @SuppressWarnings("unchecked") + List<EvaluatedCandidate<Integer>> population = Arrays.asList(new EvaluatedCandidate<Integer>(1, 1)); + + List<EvaluatedCandidate<Integer>> evolvedPopulation + = engine.nextEvolutionStep(population, 0, FrameworkTestUtils.getRNG()); + assert evolvedPopulation.size() == 1 : "Population size should be 1, is " + evolvedPopulation.size(); + // The offspring is less fit than the parent (due to the -1 operator) but the parent is not allowed to survive. + assert evolvedPopulation.get(0).getCandidate() == 0 : "Wrong individual after evolution."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/FrameworkTestUtils.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/FrameworkTestUtils.java new file mode 100644 index 0000000..316cd85 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/FrameworkTestUtils.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.framework; + +import java.util.Random; +import org.uncommons.maths.random.XORShiftRNG; + +/** + * Utility methods for Watchmaker Framework unit tests. Provides + * access to shared resources used by tests. + * @author Daniel Dyer + */ +public final class FrameworkTestUtils +{ + private static final Random RNG = new XORShiftRNG(); + + private FrameworkTestUtils() + { + // 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/framework/src/java/test/org/uncommons/watchmaker/framework/GenerationalEvolutionEngineTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/GenerationalEvolutionEngineTest.java new file mode 100644 index 0000000..6622229 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/GenerationalEvolutionEngineTest.java @@ -0,0 +1,169 @@ +//============================================================================= +// 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.framework; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.factories.StubIntegerFactory; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.ElapsedTime; +import org.uncommons.watchmaker.framework.termination.GenerationCount; + +/** + * Unit test for the {@link GenerationalEvolutionEngine} class. + * @author Daniel Dyer + */ +public class GenerationalEvolutionEngineTest +{ + private EvolutionEngine<Integer> engine; + + + @BeforeMethod + public void prepareEngine() + { + this.engine = new GenerationalEvolutionEngine<Integer>(new StubIntegerFactory(), + new IntegerZeroMaker(), + new IntegerEvaluator(), + new RouletteWheelSelection(), + FrameworkTestUtils.getRNG()); + } + + + @Test + public void testElitism() + { + class ElitismObserver implements EvolutionObserver<Integer> + { + private PopulationData<? extends Integer> data; + + public void populationUpdate(PopulationData<? extends Integer> data) + { + this.data = data; + } + + public double getAverageFitness() + { + return data.getMeanFitness(); + } + } + ElitismObserver observer = new ElitismObserver(); + engine.addEvolutionObserver(observer); + List<Integer> elite = new ArrayList<Integer>(3); + // Add the following seed candidates, all better than any others that can possibly + // get into the population (since every other candidate will always be zero). + elite.add(7); // This candidate should be discarded by elitism. + elite.add(11); + elite.add(13); + engine.evolve(10, + 2, + elite, + new GenerationCount(2)); // Do at least 2 generations because the first is just the initial population. + // Then when we have run the evolution, if the elite canidates were preserved they will + // lift the average fitness above zero. The exact value of the expected average fitness + // is easy to calculate, it is the aggregate fitness divided by the population size. + assert observer.getAverageFitness() == 24d / 10 : "Elite candidates not preserved correctly: " + observer.getAverageFitness(); + engine.removeEvolutionObserver(observer); + } + + + /** + * The number of candidates preserved by elitism must be less than the total + * population size. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEliteCountTooHigh() + { + engine.evolve(10, 10, new GenerationCount(10)); // Should throw exception because elite is too big. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEliteCountTooLow() + { + engine.evolvePopulation(10, -1, new GenerationCount(10)); // Should throw exception because elite is negative. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNoTerminationConditions() + { + engine.evolve(10, 0); // Should throw exception because there are no termination conditions. + } + + + @Test + public void testInterrupt() + { + final long timeout = 1000L; + final Thread requestThread = Thread.currentThread(); + engine.addEvolutionObserver(new EvolutionObserver<Integer>() + { + public void populationUpdate(PopulationData<? extends Integer> populationData) + { + if (populationData.getElapsedTime() > timeout / 2) + { + requestThread.interrupt(); + } + } + }); + long startTime = System.currentTimeMillis(); + engine.evolve(10, 0, new ElapsedTime(timeout)); + long elapsedTime = System.currentTimeMillis() - startTime; + assert Thread.interrupted() : "Thread was not interrupted before timeout."; + assert elapsedTime < timeout : "Engine did not respond to interrupt before timeout."; + assert engine.getSatisfiedTerminationConditions().isEmpty() + : "Interrupted engine should have no satisfied termination conditions."; + } + + + @Test + public void testGetSatisfiedTerminationConditions() + { + GenerationCount generationsCondition = new GenerationCount(1); + engine.evolve(10, 0, generationsCondition); + List<TerminationCondition> satisfiedConditions = engine.getSatisfiedTerminationConditions(); + assert satisfiedConditions.size() == 1 : "Wrong number of conditions: " + satisfiedConditions.size(); + assert satisfiedConditions.get(0) == generationsCondition : "Wrong condition returned."; + } + + + @Test(expectedExceptions = IllegalStateException.class) + public void testGetSatisfiedTerminationConditionsBeforeStart() + { + // Should throw an IllegalStateException because evolution hasn't started, let alone terminated. + engine.getSatisfiedTerminationConditions(); + } + + + /** + * Trivial test operator that mutates all integers into zeroes. + */ + private static final class IntegerZeroMaker implements EvolutionaryOperator<Integer> + { + public List<Integer> apply(List<Integer> selectedCandidates, Random rng) + { + List<Integer> result = new ArrayList<Integer>(selectedCandidates.size()); + for (int i = 0; i < selectedCandidates.size(); i++) + { + result.add(0); + } + return result; + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/IntegerEvaluator.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/IntegerEvaluator.java new file mode 100644 index 0000000..fc54bcb --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/IntegerEvaluator.java @@ -0,0 +1,37 @@ +//============================================================================= +// 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.framework; + +import java.util.List; + +/** + * Trivial fitness evaluator for integers. Used by unit tests. + * @author Daniel Dyer + */ +final class IntegerEvaluator implements FitnessEvaluator<Integer> +{ + + public double getFitness(Integer candidate, + List<? extends Integer> population) + { + return candidate; + } + + public boolean isNatural() + { + return true; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/SteadyStateEvolutionEngineTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/SteadyStateEvolutionEngineTest.java new file mode 100644 index 0000000..6678269 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/SteadyStateEvolutionEngineTest.java @@ -0,0 +1,138 @@ +//============================================================================= +// 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.framework; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.factories.StubIntegerFactory; +import org.uncommons.watchmaker.framework.operators.IntegerAdjuster; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; + +/** + * Unit test for the {@link SteadyStateEvolutionEngine} class. + * @author Daniel Dyer + */ +public class SteadyStateEvolutionEngineTest +{ + /** + * A single iteration should update only a single candidate. + */ + @Test + public void testIncrementalEvolution() + { + SteadyStateEvolutionEngine<Integer> steadyState = new SteadyStateEvolutionEngine<Integer>(new StubIntegerFactory(), + new IntegerAdjuster(5), + new NullFitnessEvaluator(), + new RouletteWheelSelection(), + 1, + true, + FrameworkTestUtils.getRNG()); + @SuppressWarnings("unchecked") + List<EvaluatedCandidate<Integer>> population = Arrays.asList(new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0)); + List<EvaluatedCandidate<Integer>> evaluatedPopulation = steadyState.nextEvolutionStep(population, + 0, + FrameworkTestUtils.getRNG()); + assert evaluatedPopulation.size() == 5 : "Population size should be unchanged."; + int unchangedCount = 0; + for (EvaluatedCandidate<Integer> candidate : evaluatedPopulation) + { + if (candidate.getCandidate() == 1) + { + ++unchangedCount; + } + } + assert unchangedCount == 4 : "Should be 4 out of 5 candidates unchanged, is " + unchangedCount; + } + + + /** + * Even if the evolutionary operator generates multiple offspring, only a single individual should be + * replaced if the forceSingleUpdate flag is set. + */ + @Test + public void testForcedSingleCandidateUpdate() + { + SteadyStateEvolutionEngine<Integer> steadyState = new SteadyStateEvolutionEngine<Integer>(new StubIntegerFactory(), + new IntegerAdjuster(5), + new NullFitnessEvaluator(), + new RouletteWheelSelection(), + 2, + true, // Force single update. + FrameworkTestUtils.getRNG()); + @SuppressWarnings("unchecked") + List<EvaluatedCandidate<Integer>> population = Arrays.asList(new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0), + new EvaluatedCandidate<Integer>(1, 0)); + List<EvaluatedCandidate<Integer>> evaluatedPopulation = steadyState.nextEvolutionStep(population, + 0, + FrameworkTestUtils.getRNG()); + assert evaluatedPopulation.size() == 5 : "Population size should be unchanged."; + int unchangedCount = 0; + for (EvaluatedCandidate<Integer> candidate : evaluatedPopulation) + { + if (candidate.getCandidate() == 1) + { + ++unchangedCount; + } + } + assert unchangedCount == 4 : "Should be 4 out of 5 candidates unchanged, is " + unchangedCount; + } + + + + @Test + public void testElitism() + { + SteadyStateEvolutionEngine<Integer> steadyState = new SteadyStateEvolutionEngine<Integer>(new StubIntegerFactory(), + new IntegerAdjuster(10), + new NullFitnessEvaluator(), + new RouletteWheelSelection(), + 1, + true, + FrameworkTestUtils.getRNG()); + @SuppressWarnings("unchecked") + List<EvaluatedCandidate<Integer>> population = Arrays.asList(new EvaluatedCandidate<Integer>(1, 1), + new EvaluatedCandidate<Integer>(2, 2), + new EvaluatedCandidate<Integer>(3, 3), + new EvaluatedCandidate<Integer>(4, 4), + new EvaluatedCandidate<Integer>(5, 5)); + // The fittest candidate should always be preserved. + for (int i = 0; i < 20; i++) // Once is not enough to be confident. + { + List<EvaluatedCandidate<Integer>> evaluatedPopulation = steadyState.nextEvolutionStep(population, + 1, + FrameworkTestUtils.getRNG()); + assert evaluatedPopulation.size() == 5 : "Population size should be unchanged."; + boolean found = false; + for (EvaluatedCandidate<Integer> candidate : evaluatedPopulation) + { + if (candidate.getCandidate() == 5) + { + found = true; + break; + } + } + assert found : "Elite candidate should be preserved."; + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/BitStringFactoryTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/BitStringFactoryTest.java new file mode 100644 index 0000000..0e06412 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/BitStringFactoryTest.java @@ -0,0 +1,93 @@ +//============================================================================= +// 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.framework.factories; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.binary.BitString; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for bit string candidate factory. + * @author Daniel Dyer + */ +public class BitStringFactoryTest +{ + private final int candidateLength = 10; + private final int populationSize = 5; + + @Test + public void testUnseededPopulation() + { + CandidateFactory<BitString> factory = new BitStringFactory(candidateLength); + List<BitString> population = factory.generateInitialPopulation(populationSize, FrameworkTestUtils.getRNG()); + validatePopulation(population); + } + + + @Test + public void testSeededPopulation() + { + CandidateFactory<BitString> factory = new BitStringFactory(candidateLength); + BitString seed1 = new BitString("1111100000"); + BitString seed2 = new BitString("1010101010"); + List<BitString> seeds = new ArrayList<BitString>(2); + seeds.add(seed1); + seeds.add(seed2); + List<BitString> population = factory.generateInitialPopulation(populationSize, + seeds, + FrameworkTestUtils.getRNG()); + + // Check that the seed candidates appear in the generated population. + assert population.contains(seed1) : "Population does not contain seed candidate 1."; + assert population.contains(seed2) : "Population does not contain seed candidate 2."; + validatePopulation(population); + } + + + /** + * It is an error if the number of seed candidates is greater than the + * population size. In this case an exception should be thrown. Not + * throwing an exception is wrong because it would permit undetected bugs + * in programs that use the factory. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManySeedCandidates() + { + CandidateFactory<BitString> factory = new BitStringFactory(candidateLength); + BitString candidate = new BitString(candidateLength); + // The following call should cause an exception since the 3 seed candidates + // won't fit into a population of size 2. + factory.generateInitialPopulation(2, + Arrays.asList(candidate, candidate, candidate), + FrameworkTestUtils.getRNG()); + } + + + private void validatePopulation(List<BitString> population) + { + // Make sure the correct number of candidates were generated. + assert population.size() == populationSize : "Wrong size population generated: " + population.size(); + // Make sure that each individual is the right length. + for (BitString bitString : population) + { + assert bitString.getLength() == candidateLength : "Bit string is wrong length: " + bitString.getLength(); + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/ListPermutationFactoryTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/ListPermutationFactoryTest.java new file mode 100644 index 0000000..b2df2e9 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/ListPermutationFactoryTest.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.framework.factories; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for the list permutation candidate factory. Checks that it + * correctly generates populations of permutations. + * @author Daniel Dyer + */ +public class ListPermutationFactoryTest +{ + private final int candidateLength = 10; + private final int populationSize = 5; + private final List<Integer> elements = new ArrayList<Integer>(candidateLength); + { + for (int i = 1; i <= candidateLength; i++) + { + elements.add(i); + } + } + + + /** + * Generate a completely random population. Checks to make + * sure that the correct number of candidate solutions is + * generated and that each is valid. + */ + @Test + public void testUnseededPopulation() + { + CandidateFactory<List<Integer>> factory = new ListPermutationFactory<Integer>(elements); + List<List<Integer>> population = factory.generateInitialPopulation(populationSize, FrameworkTestUtils.getRNG()); + assert population.size() == populationSize : "Wrong size population generated: " + population.size(); + + validatePopulation(population); + } + + + /** + * Generate a random population with some seed candidates. Checks to make + * sure that the correct number of candidate solutions is generated and that + * each is valid. + */ + @Test + public void testSeededPopulation() + { + CandidateFactory<List<Integer>> factory = new ListPermutationFactory<Integer>(elements); + + List<Integer> seed1 = new ArrayList<Integer>(elements); + List<Integer> seed2 = new ArrayList<Integer>(elements); + Collections.reverse(elements); + List<List<Integer>> seeds = new ArrayList<List<Integer>>(2); + seeds.add(seed1); + seeds.add(seed2); + + List<List<Integer>> population = factory.generateInitialPopulation(populationSize, + seeds, + FrameworkTestUtils.getRNG()); + // Check that the seed candidates appear in the generated population. + assert population.contains(seed1) : "Population does not contain seed candidate 1."; + assert population.contains(seed2) : "Population does not contain seed candidate 2."; + + validatePopulation(population); + } + + + /** + * It is an error if the number of seed candidates is greater than the + * population size. In this case an exception should be thrown. Not + * throwing an exception is wrong because it would permit undetected bugs + * in programs that use the factory. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManySeedCandidates() + { + CandidateFactory<List<Integer>> factory = new ListPermutationFactory<Integer>(elements); + + List<List<Integer>> seeds = new LinkedList<List<Integer>>(); + seeds.add(elements); + seeds.add(elements); + seeds.add(elements); + // The following call should cause an exception since the 3 seed candidates + // won't fit into a population of size 2. + factory.generateInitialPopulation(2, seeds, FrameworkTestUtils.getRNG()); + } + + + /** + * Make sure each candidate is valid (contains each element exactly once). + * @param population The population to be validated. + */ + private void validatePopulation(List<List<Integer>> population) + { + assert population.size() == populationSize : "Wrong size population generated: " + population.size(); + for (List<Integer> candidate : population) + { + assert candidate.size() == candidateLength : "Wrong size candidate generated: " + candidate.size(); + for (int i = 1; i < candidateLength; i++) + { + assert candidate.contains(i) : "Candidate is missing element " + i + "."; + } + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/ObjectArrayPermutationFactoryTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/ObjectArrayPermutationFactoryTest.java new file mode 100644 index 0000000..e4145c1 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/ObjectArrayPermutationFactoryTest.java @@ -0,0 +1,126 @@ +//============================================================================= +// 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.framework.factories; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for the array permutation candidate factory. Checks that it + * correctly generates populations of permutations. + * @author Daniel Dyer + */ +public class ObjectArrayPermutationFactoryTest +{ + private final int candidateLength = 10; + private final int populationSize = 5; + private final Integer[] elements = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + + /** + * Generate a completely random population. Checks to make + * sure that the correct number of candidate solutions is + * generated and that each is valid. + */ + @Test + public void testUnseededPopulation() + { + CandidateFactory<Integer[]> factory = new ObjectArrayPermutationFactory<Integer>(elements); + List<Integer[]> population = factory.generateInitialPopulation(populationSize, FrameworkTestUtils.getRNG()); + assert population.size() == populationSize : "Wrong size population generated: " + population.size(); + + validatePopulation(population); + } + + + /** + * Generate a random population with some seed candidates. Checks to make + * sure that the correct number of candidate solutions is generated and that + * each is valid. + */ + @Test + public void testSeededPopulation() + { + CandidateFactory<Integer[]> factory = new ObjectArrayPermutationFactory<Integer>(elements); + + Integer[] seed1 = elements.clone(); + Integer[] seed2 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + + List<Integer[]> population = factory.generateInitialPopulation(populationSize, + Arrays.asList(seed1, seed2), + FrameworkTestUtils.getRNG()); + // Check that the seed candidates appear in the generated population. + assert population.contains(seed1) : "Population does not contain seed candidate 1."; + assert population.contains(seed2) : "Population does not contain seed candidate 2."; + + validatePopulation(population); + } + + + /** + * It is an error if the number of seed candidates is greater than the + * population size. In this case an exception should be thrown. Not + * throwing an exception is wrong because it would permit undetected bugs + * in programs that use the factory. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManySeedCandidates() + { + CandidateFactory<Integer[]> factory = new ObjectArrayPermutationFactory<Integer>(elements); + // The following call should cause an exception since the 3 seed candidates + // won't fit into a population of size 2. + factory.generateInitialPopulation(2, + Arrays.asList(elements, elements, elements), + FrameworkTestUtils.getRNG()); + } + + + /** + * Make sure each candidate is valid (contains each element exactly once). + * @param population The population to be validated. + */ + private void validatePopulation(List<Integer[]> population) + { + assert population.size() == populationSize : "Wrong size population generated: " + population.size(); + for (Integer[] candidate : population) + { + assert candidate.length == candidateLength : "Wrong size candidate generated: " + candidate.length; + for (int i = 1; i < candidateLength; i++) + { + assert contains(candidate, i) : "Candidate is missing element " + i + "."; + } + } + } + + + /** + * Check whether the specified element occurs in the specified array. + */ + private boolean contains(Integer[] array, int element) + { + for (int i : array) + { + if (i == element) + { + return true; + } + } + return false; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/StringFactoryTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/StringFactoryTest.java new file mode 100644 index 0000000..350a024 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/StringFactoryTest.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.framework.factories; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * @author Daniel Dyer + */ +public class StringFactoryTest +{ + private final int candidateLength = 8; + private final int populationSize = 5; + + private final char[] alphabet = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}; + + + /** + * Generate a completely random population. Checks to make + * sure that the correct number of candidate solutions is + * generated and that each is valid. + */ + @Test + public void testUnseededPopulation() + { + CandidateFactory<String> factory = new StringFactory(alphabet, candidateLength); + List<String> population = factory.generateInitialPopulation(populationSize, FrameworkTestUtils.getRNG()); + assert population.size() == populationSize : "Wrong size population generated: " + population.size(); + + validatePopulation(population); + } + + + /** + * Generate a random population with some seed candidates. Checks to make + * sure that the correct number of candidate solutions is generated and that + * each is valid. + */ + @Test + public void testSeededPopulation() + { + CandidateFactory<String> factory = new StringFactory(alphabet, candidateLength); + + String seed1 = "cdefghij"; + String seed2 = "bbbbbbbb"; + + List<String> population = factory.generateInitialPopulation(populationSize, + Arrays.asList(seed1, seed2), + FrameworkTestUtils.getRNG()); + // Check that the seed candidates appear in the generated population. + assert population.contains(seed1) : "Population does not contain seed candidate 1."; + assert population.contains(seed2) : "Population does not contain seed candidate 2."; + + validatePopulation(population); + } + + + /** + * It is an error if the number of seed candidates is greater than the + * population size. In this case an exception should be thrown. Not + * throwing an exception is wrong because it would permit undetected bugs + * in programs that use the factory. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManySeedCandidates() + { + CandidateFactory<String> factory = new StringFactory(alphabet, candidateLength); + // The following call should cause an exception since the 3 seed candidates + // won't fit into a population of size 2. + factory.generateInitialPopulation(2, + Arrays.asList("abcdefgh", "ijklmnop", "qrstuvwx"), + FrameworkTestUtils.getRNG()); + } + + + + + /** + * Make sure each candidate is valid (is the right length and contains only + * valid characters). + * @param population The population to be validated. + */ + private void validatePopulation(List<String> population) + { + for (String candidate : population) + { + assert candidate.length() == candidateLength : "Wrong length candidate: " + candidate.length(); + for (char c : candidate.toCharArray()) + { + assert c >= 'a' && c <= 'j' : "Invalid character: " + c; + } + } + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/StubIntegerFactory.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/StubIntegerFactory.java new file mode 100644 index 0000000..ecfe422 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/factories/StubIntegerFactory.java @@ -0,0 +1,30 @@ +//============================================================================= +// 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.framework.factories; + +import java.util.Random; + +/** + * Stub candidate factory for tests. Always returns zero-valued integers. + * @author Daniel Dyer + */ +public final class StubIntegerFactory extends AbstractCandidateFactory<Integer> +{ + public Integer generateRandomCandidate(Random rng) + { + return 0; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/interactive/InteractiveSelectionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/interactive/InteractiveSelectionTest.java new file mode 100644 index 0000000..a3f26b6 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/interactive/InteractiveSelectionTest.java @@ -0,0 +1,146 @@ +//============================================================================= +// 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.framework.interactive; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for user-guided selection strategy. + * @author Daniel Dyer + */ +public class InteractiveSelectionTest +{ + @Test + public void testSingleSelectionPerGeneration() + { + final int groupSize = 2; + RandomConsole<Integer> console = new RandomConsole<Integer>(groupSize); + SelectionStrategy<Integer> strategy = new InteractiveSelection<Integer>(console, + groupSize, + 1); + List<EvaluatedCandidate<Integer>> population = new ArrayList<EvaluatedCandidate<Integer>>(5); + population.add(new EvaluatedCandidate<Integer>(1, 0)); + population.add(new EvaluatedCandidate<Integer>(2, 0)); + population.add(new EvaluatedCandidate<Integer>(3, 0)); + population.add(new EvaluatedCandidate<Integer>(4, 0)); + population.add(new EvaluatedCandidate<Integer>(5, 0)); + + List<Integer> selection = strategy.select(population, true, 3, FrameworkTestUtils.getRNG()); + assert selection.size() == 3 : "Incorrect selection size: " + selection.size(); + assert console.getSelectionCount() == 1 : "Wrong number of user selections: " + console.getSelectionCount(); + // All 3 selected individuals should be the same since the strategy doubles up + // selections when configured to restrict the number of user choices per generation. + assert selection.get(0).equals(selection.get(1)) : "Incorrect selection."; + assert selection.get(1).equals(selection.get(2)) : "Incorrect selection."; + assert selection.get(0).equals(selection.get(2)) : "Incorrect selection."; + } + + + @Test + public void testMultipleSelectionsPerGeneration() + { + final int groupSize = 5; + RandomConsole<Integer> console = new RandomConsole<Integer>(groupSize); + SelectionStrategy<Integer> strategy = new InteractiveSelection<Integer>(console, + groupSize, + 3); + List<EvaluatedCandidate<Integer>> population = new ArrayList<EvaluatedCandidate<Integer>>(5); + population.add(new EvaluatedCandidate<Integer>(1, 0)); + population.add(new EvaluatedCandidate<Integer>(2, 0)); + population.add(new EvaluatedCandidate<Integer>(3, 0)); + population.add(new EvaluatedCandidate<Integer>(4, 0)); + population.add(new EvaluatedCandidate<Integer>(5, 0)); + + List<Integer> selection = strategy.select(population, true, 3, FrameworkTestUtils.getRNG()); + assert selection.size() == 3 : "Incorrect selection size."; + assert console.getSelectionCount() == 3 : "Wrong number of user selections: " + console.getSelectionCount(); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMaxSelections() + { + final int groupSize = 5; + RandomConsole<Integer> console = new RandomConsole<Integer>(groupSize); + // This should throw an exception because max selections must be at least 1. + new InteractiveSelection<Integer>(console, groupSize, 0); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidGroupSize() + { + final int groupSize = 1; + RandomConsole<Integer> console = new RandomConsole<Integer>(groupSize); + // This should throw an exception because group size must be at least 2. + new InteractiveSelection<Integer>(console, groupSize, 1); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testGroupSizeTooBigForPopulation() + { + final int groupSize = 5; + RandomConsole<Integer> console = new RandomConsole<Integer>(groupSize); + SelectionStrategy<Integer> strategy = new InteractiveSelection<Integer>(console, + groupSize, + 1); + List<EvaluatedCandidate<Integer>> population = new LinkedList<EvaluatedCandidate<Integer>>(); + population.add(new EvaluatedCandidate<Integer>(1, 1.0)); + population.add(new EvaluatedCandidate<Integer>(1, 2.0)); + // This should fail because a population of 2 is not big enough with a + // group size of 5. + strategy.select(population, true, 2, FrameworkTestUtils.getRNG()); + } + + + /** + * Automated test console implementation that simply selects an + * individual at random. + */ + private final class RandomConsole<T> implements Console<T> + { + private final int expectedGroupSize; + + /** Count how many times the select method is called. */ + private int selectionCount = 0; + + RandomConsole(int expectedGroupSize) + { + this.expectedGroupSize = expectedGroupSize; + } + + + public int select(List<? extends T> renderedEntities) + { + assert renderedEntities.size() == expectedGroupSize : "Wrong selection group size."; + ++selectionCount; + return FrameworkTestUtils.getRNG().nextInt(renderedEntities.size()); + } + + + public int getSelectionCount() + { + return selectionCount; + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/interactive/RendererAdapterTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/interactive/RendererAdapterTest.java new file mode 100644 index 0000000..717f65e --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/interactive/RendererAdapterTest.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.framework.interactive; + +import java.util.Date; +import org.testng.annotations.Test; + +/** + * Unit test to ensure that renderer chaining works correctly. + * @author Daniel Dyer + */ +public class RendererAdapterTest +{ + @Test + public void testChaining() + { + Renderer<Long, Date> longToDate = new TimestampToDateRenderer(); + Renderer<Date, String> dateToString = new DateToStringRenderer(); + + long currentTime = System.currentTimeMillis(); + Date date = longToDate.render(currentTime); + String expectedOutput = dateToString.render(date); + + Renderer<Long, String> longToString = new RendererAdapter<Long, String>(longToDate, + dateToString); + String actualOutput = longToString.render(currentTime); + assert actualOutput.equals(expectedOutput) : "Actual/expected output mismatch: " + actualOutput; + } + + + /** + * Example renderer for converting a number of milliseconds since 00:00 on 1st + * January 1970 into a Java {@link Date} object. + */ + private static final class TimestampToDateRenderer implements Renderer<Long, Date> + { + public Date render(Long timestamp) + { + return new Date(timestamp); + } + } + + + /** + * Example renderer for converting a Java {@link Date} object into a date String. + */ + private static final class DateToStringRenderer implements Renderer<Date, String> + { + public String render(Date date) + { + return date.toString(); + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/IslandEvolutionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/IslandEvolutionTest.java new file mode 100644 index 0000000..98fc00d --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/IslandEvolutionTest.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.framework.islands; + +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.factories.StubIntegerFactory; +import org.uncommons.watchmaker.framework.operators.IntegerAdjuster; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.ElapsedTime; +import org.uncommons.watchmaker.framework.termination.GenerationCount; + +/** + * Unit test for the {@link IslandEvolution} class. + * @author Daniel Dyer + */ +public class IslandEvolutionTest +{ + /** + * This test makes sure that the evolution observer global method only gets invoked at + * the end of each epoch, and that the island method gets invoked for each generation on each + * island. + */ + @Test + public void testListeners() + { + final int islandCount = 3; + final int epochCount = 2; + final int generationCount = 5; + + IslandEvolution<Integer> islandEvolution = new IslandEvolution<Integer>(islandCount, + new RingMigration(), + new StubIntegerFactory(), + new IntegerAdjuster(2), + new DummyFitnessEvaluator(), + new RouletteWheelSelection(), + FrameworkTestUtils.getRNG()); + final int[] observedEpochCount = new int[1]; + final int[] observedGenerationCounts = new int[islandCount]; + + islandEvolution.addEvolutionObserver(new IslandEvolutionObserver<Integer>() + { + public void populationUpdate(PopulationData<? extends Integer> populationData) + { + observedEpochCount[0]++; + } + + + public void islandPopulationUpdate(int islandIndex, PopulationData<? extends Integer> populationData) + { + observedGenerationCounts[islandIndex]++; + } + }); + islandEvolution.evolve(5, 0, 5, 0, new GenerationCount(2)); + assert observedEpochCount[0] == 2 : "Listener should have been notified twice, was " + observedEpochCount[0]; + for (int i = 0; i < islandCount; i++) + { + int expected = epochCount * generationCount; + assert observedGenerationCounts[i] == expected + : "Genertion count for island " + i + " should be " + expected + ", is " + observedGenerationCounts[i]; + } + } + + + @Test + public void testInterrupt() + { + IslandEvolution<Integer> islandEvolution = new IslandEvolution<Integer>(2, + new RingMigration(), + new StubIntegerFactory(), + new IntegerAdjuster(2), + new DummyFitnessEvaluator(), + new RouletteWheelSelection(), + FrameworkTestUtils.getRNG()); + final long timeout = 1000L; + final Thread requestThread = Thread.currentThread(); + islandEvolution.addEvolutionObserver(new IslandEvolutionObserver<Integer>() + { + public void populationUpdate(PopulationData<? extends Integer> populationData) + { + if (populationData.getElapsedTime() > timeout / 2) + { + requestThread.interrupt(); + } + } + + + public void islandPopulationUpdate(int islandIndex, PopulationData<? extends Integer> populationData){} + }); + long startTime = System.currentTimeMillis(); + islandEvolution.evolve(10, 0, 10, 0, new ElapsedTime(timeout)); + long elapsedTime = System.currentTimeMillis() - startTime; + assert Thread.interrupted() : "Thread was not interrupted before timeout."; + assert elapsedTime < timeout : "Engine did not respond to interrupt before timeout."; + assert islandEvolution.getSatisfiedTerminationConditions().isEmpty() + : "Interrupted islands should have no satisfied termination conditions."; + } + + + @Test(expectedExceptions = IllegalStateException.class) + public void testGetSatisfiedTerminationConditionsBeforeStart() + { + IslandEvolution<Integer> islandEvolution = new IslandEvolution<Integer>(3, + new RingMigration(), + new StubIntegerFactory(), + new IntegerAdjuster(2), + new DummyFitnessEvaluator(), + new RouletteWheelSelection(), + FrameworkTestUtils.getRNG()); + // Should throw an IllegalStateException because evolution has started, let alone terminated. + islandEvolution.getSatisfiedTerminationConditions(); + } + + + private static class DummyFitnessEvaluator implements FitnessEvaluator<Integer> + { + public double getFitness(Integer candidate, List<? extends Integer> population) + { + return 0; + } + + public boolean isNatural() + { + return true; + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/MigrationTestUtils.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/MigrationTestUtils.java new file mode 100644 index 0000000..3564a31 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/MigrationTestUtils.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.framework.islands; + +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import java.util.List; +import java.util.ArrayList; + +/** + * Utility methods used by unit tests for migration strategies. + * @author Daniel Dyer + */ +class MigrationTestUtils +{ + private MigrationTestUtils() + { + // Prevents instantiation. + } + + + public static <T> List<EvaluatedCandidate<T>> createTestPopulation(T... members) + { + List<EvaluatedCandidate<T>> population = new ArrayList<EvaluatedCandidate<T>>(members.length); + for (T member : members) + { + population.add(new EvaluatedCandidate<T>(member, 0)); + } + return population; + } + + + public static void testPopulationContents(List<EvaluatedCandidate<String>> actualPopulation, + String... expectedPopulation) + { + assert actualPopulation.size() == expectedPopulation.length : "Wrong population size after migration."; + for (int i = 0; i < actualPopulation.size(); i++) + { + assert actualPopulation.get(i).getCandidate().equals(expectedPopulation[i]) : "Wrong value at index " + i; + } + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/RandomMigrationTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/RandomMigrationTest.java new file mode 100644 index 0000000..566f68b --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/RandomMigrationTest.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.framework.islands; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for the {@link RingMigration} class. + * @author Daniel Dyer + */ +public class RandomMigrationTest +{ + /** + * Make sure that nothing strange happens when there is no migration. + */ + @Test + public void testZeroMigration() + { + Migration migration = new RandomMigration(); + @SuppressWarnings("unchecked") + List<List<EvaluatedCandidate<String>>> islandPopulations = Arrays.asList(MigrationTestUtils.createTestPopulation("A", "A", "A"), + MigrationTestUtils.createTestPopulation("B", "B", "B"), + MigrationTestUtils.createTestPopulation("C", "C", "C")); + migration.migrate(islandPopulations, 0, FrameworkTestUtils.getRNG()); + assert islandPopulations.size() == 3 : "Wrong number of populations after migration."; + MigrationTestUtils.testPopulationContents(islandPopulations.get(0), "A", "A", "A"); + MigrationTestUtils.testPopulationContents(islandPopulations.get(1), "B", "B", "B"); + MigrationTestUtils.testPopulationContents(islandPopulations.get(2), "C", "C", "C"); + } + + + /** + * Make sure that nothing strange happens when the entire island is migrated. + */ + @Test + public void testNonZeroMigration() + { + Migration migration = new RingMigration(); + @SuppressWarnings("unchecked") + List<List<EvaluatedCandidate<String>>> islandPopulations = Arrays.asList(MigrationTestUtils.createTestPopulation("A", "A", "A"), + MigrationTestUtils.createTestPopulation("B", "B", "B"), + MigrationTestUtils.createTestPopulation("C", "C", "C")); + migration.migrate(islandPopulations, 3, FrameworkTestUtils.getRNG()); + assert islandPopulations.size() == 3: "Wrong number of populations after migration."; + // Each population should still have 3 members (but it's not sure which members). + assert islandPopulations.get(0).size() == 3 : "Wrong population size."; + assert islandPopulations.get(1).size() == 3 : "Wrong population size."; + assert islandPopulations.get(2).size() == 3 : "Wrong population size."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/RingMigrationTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/RingMigrationTest.java new file mode 100644 index 0000000..ce86058 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/islands/RingMigrationTest.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.framework.islands; + +import java.util.Arrays; +import java.util.List; +import org.testng.Reporter; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for the {@link RingMigration} class. + * @author Daniel Dyer + */ +public class RingMigrationTest +{ + /** + * Make sure that nothing strange happens when there is no migration. + */ + @Test + public void testZeroMigration() + { + Migration migration = new RingMigration(); + @SuppressWarnings("unchecked") + List<List<EvaluatedCandidate<String>>> islandPopulations = Arrays.asList(MigrationTestUtils.createTestPopulation("A", "A", "A"), + MigrationTestUtils.createTestPopulation("B", "B", "B"), + MigrationTestUtils.createTestPopulation("C", "C", "C")); + migration.migrate(islandPopulations, 0, FrameworkTestUtils.getRNG()); + assert islandPopulations.size() == 3 : "Wrong number of populations after migration."; + MigrationTestUtils.testPopulationContents(islandPopulations.get(0), "A", "A", "A"); + MigrationTestUtils.testPopulationContents(islandPopulations.get(1), "B", "B", "B"); + MigrationTestUtils.testPopulationContents(islandPopulations.get(2), "C", "C", "C"); + } + + + /** + * Make sure that nothing strange happens when the entire island is migrated. + */ + @Test + public void testFullMigration() + { + Migration migration = new RingMigration(); + @SuppressWarnings("unchecked") + List<List<EvaluatedCandidate<String>>> islandPopulations = Arrays.asList(MigrationTestUtils.createTestPopulation("A", "A", "A"), + MigrationTestUtils.createTestPopulation("B", "B", "B"), + MigrationTestUtils.createTestPopulation("C", "C", "C")); + migration.migrate(islandPopulations, 3, FrameworkTestUtils.getRNG()); + assert islandPopulations.size() == 3: "Wrong number of populations after migration."; + MigrationTestUtils.testPopulationContents(islandPopulations.get(0), "C", "C", "C"); + MigrationTestUtils.testPopulationContents(islandPopulations.get(1), "A", "A", "A"); + MigrationTestUtils.testPopulationContents(islandPopulations.get(2), "B", "B", "B"); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/BitStringCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/BitStringCrossoverTest.java new file mode 100644 index 0000000..58d4127 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/BitStringCrossoverTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.binary.BitString; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.factories.BitStringFactory; + +/** + * Unit test for cross-over applied to bit strings. + * @author Daniel Dyer + */ +public class BitStringCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<BitString> operator = new BitStringCrossover(); + CandidateFactory<BitString> factory = new BitStringFactory(50); + List<BitString> population = factory.generateInitialPopulation(2, FrameworkTestUtils.getRNG()); + // Test to make sure that cross-over correctly preserves all genetic material + // originally present in the population and does not introduce anything new. + int totalSetBits = population.get(0).countSetBits() + population.get(1).countSetBits(); + for (int i = 0; i < 50; i++) // Test several generations. + { + population = operator.apply(population, FrameworkTestUtils.getRNG()); + int setBits = population.get(0).countSetBits() + population.get(1).countSetBits(); + assert setBits == totalSetBits : "Total number of set bits in population changed during cross-over."; + } + } + + + /** + * The {@link BitStringCrossover} operator is only defined to work on populations + * containing Strings of equal lengths. Any attempt to apply the operation to + * populations that contain different length Strings should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<BitString> crossover = new BitStringCrossover(new ConstantGenerator<Integer>(1)); + List<BitString> population = new ArrayList<BitString>(2); + population.add(new BitString(32, FrameworkTestUtils.getRNG())); + population.add(new BitString(33, FrameworkTestUtils.getRNG())); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/BitStringMutationTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/BitStringMutationTest.java new file mode 100644 index 0000000..ccc5d9b --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/BitStringMutationTest.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.framework.operators; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.binary.BitString; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for mutation of bit strings. + * @author Daniel Dyer + */ +public class BitStringMutationTest +{ + /** + * Ensures that mutation occurs correctly. Because of the random + * aspect we can't actually make many assertions. This just ensures + * that there are no unexpected exceptions and that the length of + * the bit strings remains as expected. + */ + @Test + public void testRandomMutation() + { + EvolutionaryOperator<BitString> mutation = new BitStringMutation(Probability.EVENS); + BitString original = new BitString("111100101"); + List<BitString> population = Arrays.asList(original); + for (int i = 0; i < 20; i++) // Perform several iterations to get different mutations. + { + population = mutation.apply(population, FrameworkTestUtils.getRNG()); + BitString mutated = population.get(0); + assert mutated.getLength() == 9 : "Mutated bit string changed length."; + } + } + + + /** + * Ensures that mutation occurs correctly. Uses a probability of 1 to + * make the outcome predictable (all bits will be flipped). + */ + @Test + public void testSingleBitMutation() + { + BitString original = new BitString("111100101"); + EvolutionaryOperator<BitString> mutation = new BitStringMutation(Probability.ONE); + List<BitString> population = Arrays.asList(original); + population = mutation.apply(population, FrameworkTestUtils.getRNG()); + BitString mutated = population.get(0); + assert !mutated.equals(original) : "Mutation should be different from original."; + assert mutated.getLength() == 9 : "Mutated bit string changed length."; + int set = mutated.countSetBits(); + int unset = mutated.countUnsetBits(); + assert set == 5 || set == 7 : "Mutated bit string has wrong number of 1s: " + set; + assert unset == 2 || unset == 4 : "Mutated bit string has wrong number of 0s: " + unset; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ByteArrayCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ByteArrayCrossoverTest.java new file mode 100644 index 0000000..82a4b02 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ByteArrayCrossoverTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with byte arrays. + * @author Daniel Dyer + */ +public class ByteArrayCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<byte[]> crossover = new ByteArrayCrossover(); + List<byte[]> population = new ArrayList<byte[]>(4); + population.add(new byte[]{1, 2, 3, 4, 5}); + population.add(new byte[]{6, 7, 8, 9, 10}); + population.add(new byte[]{11, 12, 13, 14, 15}); + population.add(new byte[]{16, 17, 18, 19, 20}); + Set<Byte> values = new HashSet<Byte>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (byte[] individual : population) + { + assert individual.length == 5 : "Invalid candidate length: " + individual.length; + for (byte value : individual) + { + values.add(value); + } + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * The {@link ByteArrayCrossover} operator is only defined to work on populations + * containing arrays of equal lengths. Any attempt to apply the operation to + * populations that contain different length arrays should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<byte[]> crossover = new ByteArrayCrossover(1, Probability.ONE); + List<byte[]> population = new ArrayList<byte[]>(2); + population.add(new byte[]{1, 2, 3, 4, 5}); + population.add(new byte[]{2, 4, 8, 10, 12, 14, 16}); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/CharArrayCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/CharArrayCrossoverTest.java new file mode 100644 index 0000000..54e6c61 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/CharArrayCrossoverTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with character arrays. + * @author Daniel Dyer + */ +public class CharArrayCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<char[]> crossover = new CharArrayCrossover(); + List<char[]> population = new ArrayList<char[]>(4); + population.add(new char[]{'a', 'b', 'c', 'd', 'e'}); + population.add(new char[]{'f', 'g', 'h', 'i', 'j'}); + population.add(new char[]{'k', 'l', 'm', 'n', 'o'}); + population.add(new char[]{'p', 'q', 'r', 's', 't'}); + Set<Character> values = new HashSet<Character>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (char[] individual : population) + { + assert individual.length == 5 : "Invalid candidate length: " + individual.length; + for (char value : individual) + { + values.add(value); + } + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * The {@link CharArrayCrossover} operator is only defined to work on populations + * containing arrays of equal lengths. Any attempt to apply the operation to + * populations that contain different length arrays should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<char[]> crossover = new CharArrayCrossover(1, Probability.ONE); + List<char[]> population = new ArrayList<char[]>(2); + population.add(new char[]{'a', 'b', 'c', 'd', 'e'}); + population.add(new char[]{'f', 'g', 'h', 'i', 'j', 'k'}); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/DoubleArrayCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/DoubleArrayCrossoverTest.java new file mode 100644 index 0000000..9b46c61 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/DoubleArrayCrossoverTest.java @@ -0,0 +1,82 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with floating point arrays. + * @author Daniel Dyer + */ +public class DoubleArrayCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<double[]> crossover = new DoubleArrayCrossover(new ConstantGenerator<Integer>(1), + new ConstantGenerator<Probability>(Probability.ONE)); + List<double[]> population = new ArrayList<double[]>(4); + population.add(new double[]{1.1d, 2.2d, 3.3d, 4.4d, 5.5d}); + population.add(new double[]{6.6d, 7.7d, 8.8d, 9.9d, 10}); + population.add(new double[]{11, 12, 13, 14, 15}); + population.add(new double[]{16, 17, 18, 19, 20}); + Set<Double> values = new HashSet<Double>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (double[] individual : population) + { + assert individual.length == 5 : "Invalid candidate length: " + individual.length; + for (double value : individual) + { + values.add(value); + } + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * The {@link DoubleArrayCrossover} operator is only defined to work on populations + * containing arrays of equal lengths. Any attempt to apply the operation to + * populations that contain different length arrays should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<double[]> crossover = new DoubleArrayCrossover(); + List<double[]> population = new ArrayList<double[]>(2); + population.add(new double[]{1.1d, 2.2d, 3.3d, 4.4d, 5.5d}); + population.add(new double[]{6.6d, 7.7d, 8.8d}); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/EvolutionPipelineTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/EvolutionPipelineTest.java new file mode 100644 index 0000000..469f6e6 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/EvolutionPipelineTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for compound, sequential evolutionary schemes. + * @author Daniel Dyer + */ +public class EvolutionPipelineTest +{ + /** + * Make sure that multiple operators in a pipeline are applied correctly + * to the population and validate the cumulative effects. + */ + @Test + public void testCompoundEvolution() + { + List<Integer> population = new ArrayList<Integer>(10); + for (int i = 10; i <= 100; i += 10) + { + population.add(i); + } + // Increment 30% of the numbers and decrement the other 70%. + List<EvolutionaryOperator<Integer>> operators = new ArrayList<EvolutionaryOperator<Integer>>(2); + operators.add(new IntegerAdjuster(1)); + operators.add(new IntegerAdjuster(3)); + EvolutionPipeline<Integer> evolutionScheme = new EvolutionPipeline<Integer>(operators); + population = evolutionScheme.apply(population, FrameworkTestUtils.getRNG()); + // Net result should be each candidate increased by 4. + int aggregate = 0; + for (Integer i : population) + { + aggregate += i; + assert (i % 10 == 4) : "Candidate should have increased by 4, is " + i; + } + assert aggregate == 590 : "Aggregate should be 590 after mutations, is " + aggregate; + } + + + /** + * An empty pipeline is not allowed. An exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEmptyPipeline() + { + List<EvolutionaryOperator<String>> operators = Collections.emptyList(); + new EvolutionPipeline<String>(operators); // Should throw an IllegalArgumentException. + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/IntArrayCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/IntArrayCrossoverTest.java new file mode 100644 index 0000000..2ec9ef0 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/IntArrayCrossoverTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with integer arrays. + * @author Daniel Dyer + */ +public class IntArrayCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<int[]> crossover = new IntArrayCrossover(); + List<int[]> population = new ArrayList<int[]>(4); + population.add(new int[]{1, 2, 3, 4, 5}); + population.add(new int[]{6, 7, 8, 9, 10}); + population.add(new int[]{11, 12, 13, 14, 15}); + population.add(new int[]{16, 17, 18, 19, 20}); + Set<Integer> values = new HashSet<Integer>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (int[] individual : population) + { + assert individual.length == 5 : "Invalid candidate length: " + individual.length; + for (int value : individual) + { + values.add(value); + } + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * The {@link IntArrayCrossover} operator is only defined to work on populations + * containing arrays of equal lengths. Any attempt to apply the operation to + * populations that contain different length arrays should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<int[]> crossover = new IntArrayCrossover(1, Probability.ONE); + List<int[]> population = new ArrayList<int[]>(2); + population.add(new int[]{1, 2, 3, 4, 5}); + population.add(new int[]{2}); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/IntegerAdjuster.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/IntegerAdjuster.java new file mode 100644 index 0000000..e90b4a9 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/IntegerAdjuster.java @@ -0,0 +1,45 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Trivial test operator that mutates all integers by adding a fixed offset. + * @author Daniel Dyer + */ +public final class IntegerAdjuster implements EvolutionaryOperator<Integer> +{ + private final int adjustment; + + public IntegerAdjuster(int adjustment) + { + this.adjustment = adjustment; + } + + public List<Integer> apply(List<Integer> selectedCandidates, Random rng) + { + List<Integer> result = new ArrayList<Integer>(selectedCandidates.size()); + for (Integer i : selectedCandidates) + { + result.add(i + adjustment); + } + return result; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListCrossoverTest.java new file mode 100644 index 0000000..3bfc297 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListCrossoverTest.java @@ -0,0 +1,126 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with arbitrary lists. + * @author Daniel Dyer + */ +public class ListCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<List<Integer>> crossover = new ListCrossover<Integer>(); + List<List<Integer>> population = new ArrayList<List<Integer>>(4); + population.add(Arrays.asList(1, 2, 3, 4, 5)); + population.add(Arrays.asList(6, 7, 8, 9, 10)); + population.add(Arrays.asList(11, 12, 13, 14, 15)); + population.add(Arrays.asList(16, 17, 18, 19, 20)); + Set<Integer> values = new HashSet<Integer>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (List<Integer> individual : population) + { + assert individual.size() == 5 : "Invalid candidate length: " + individual.size(); + values.addAll(individual); + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * When applied to lists of different lenghts, the {@link ListCrossover} operator + * should pick a cross-over point that exists in both lists. Therefore, the two + * offspring will be the lengths of the two parents. + */ + @Test + public void testDifferentLengthParents() + { + EvolutionaryOperator<List<Integer>> crossover = new ListCrossover<Integer>(1, Probability.ONE); + List<List<Integer>> population = new ArrayList<List<Integer>>(2); + List<Integer> parent1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); + population.add(parent1); + List<Integer> parent2 = Arrays.asList(9, 10, 11); + population.add(parent2); + + List<List<Integer>> offspring = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert offspring.size() == 2 : "Should be 2 offspring, is " + offspring.size(); + // Should be 1 child of length 8 and one of length 3. Don't know which order though + // as parents are shuffled before cross-over is applied. + assert (offspring.get(0).size() == parent1.size() && offspring.get(1).size() == parent2.size()) + || (offspring.get(0).size() == parent2.size() && offspring.get(1).size() == parent1.size()) + : "Offspring are wrong lengths after cross-over."; + } + + + /** + * When the probability determines that no cross-over should be performed, the two parents + * should just be copied into the next generation unchanged. + */ + @Test + public void testZeroProbability() + { + EvolutionaryOperator<List<Integer>> crossover = new ListCrossover<Integer>(new ConstantGenerator<Integer>(1), + new ConstantGenerator<Probability>(Probability.ZERO)); + List<List<Integer>> population = new ArrayList<List<Integer>>(4); + List<Integer> parent1 = Arrays.asList(1, 2, 3, 4, 5); + List<Integer> parent2 = Arrays.asList(6, 7, 8, 9, 10); + population.add(parent1); + population.add(parent2); + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.contains(parent1) : "Parent should survive unaltered."; + assert population.contains(parent2) : "Parent should survive unaltered."; + } + + + /** + * If one or both of the parent lists has only one element, it can't participate in + * meaningful cross-over. In practice this situation is unlikely to occur (most + * programs won't be evolving single-element lists), but the operator should handle + * it gracefully. + */ + @Test + public void testParentTooShort() + { + EvolutionaryOperator<List<Integer>> crossover = new ListCrossover<Integer>(new ConstantGenerator<Integer>(1)); + List<List<Integer>> population = new ArrayList<List<Integer>>(2); + List<Integer> parent1 = Arrays.asList(1, 2, 3); + List<Integer> parent2 = Arrays.asList(4); // Too short for cross-over. + population.add(parent1); + population.add(parent2); + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.contains(parent1) : "Parent should survive unaltered."; + assert population.contains(parent2) : "Parent should survive unaltered."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListInversionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListInversionTest.java new file mode 100644 index 0000000..0a5a6a7 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListInversionTest.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.framework.operators; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for the {@link ListInversion} evolutionary operator. + * @author Daniel Dyer + */ +public class ListInversionTest +{ + @Test + public void testZeroProbability() + { + EvolutionaryOperator<List<Integer>> inversion = new ListInversion<Integer>(Probability.ZERO); + @SuppressWarnings("unchecked") + List<List<Integer>> selection = Arrays.asList(Arrays.asList(1, 2, 3)); + List<List<Integer>> evolvedSelection = inversion.apply(selection, FrameworkTestUtils.getRNG()); + assert evolvedSelection.size() == 1 : "Wrong number of individuals after evolution: " + evolvedSelection.size(); + assert evolvedSelection.get(0) == selection.get(0) : "Candidate should not have been modified."; + } + + + @Test + public void testInversion() + { + EvolutionaryOperator<List<Integer>> inversion = new ListInversion<Integer>(Probability.ONE); + @SuppressWarnings("unchecked") + List<List<Integer>> selection = Arrays.asList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + for (int i = 0; i < 50; i++) // Try several times so that different random numbers are generated. + { + List<List<Integer>> evolvedSelection = inversion.apply(selection, FrameworkTestUtils.getRNG()); + + // After inversion, candidate should have same elements but not in the same order. + assert evolvedSelection.size() == 1 : "Wrong number of individuals after evolution: " + evolvedSelection.size(); + assert evolvedSelection.get(0).size() == selection.get(0).size() : "Candidate length should be unchanged."; + assert !Arrays.deepEquals(evolvedSelection.get(0).toArray(), selection.get(0).toArray()) + : "Candidate should have been modified."; + assert new HashSet<Integer>(evolvedSelection.get(0)).size() == 8 + : "Evolved candidate should contain each element once."; + } + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOperatorTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOperatorTest.java new file mode 100644 index 0000000..4cd22f0 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOperatorTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for the {@link ListOperator} high-order evolutionary operator. + * @author Daniel Dyer + */ +public class ListOperatorTest +{ + /** + * Make sure that the delegate operator is applied to each list in the + * population. + */ + @Test + public void testApplication() + { + ListOperator<Integer> operator = new ListOperator<Integer>(new IntegerAdjuster(1)); + List<List<Integer>> selection = new ArrayList<List<Integer>>(3); + selection.add(Arrays.asList(1, 2, 3)); + selection.add(Arrays.asList(4, 5, 6)); + selection.add(Arrays.asList(7, 8, 9)); + + List<List<Integer>> mutations = operator.apply(selection, FrameworkTestUtils.getRNG()); + assert mutations.size() == 3 : "Wrong number of candidates after list operation: " + selection.size(); + + // Each element in each candidate list should have been incremented by the delegate operator. + for (int i = 0; i < selection.size(); i++) + { + List<Integer> original = selection.get(i); + List<Integer> mutation = mutations.get(i); + assert original.size() == mutation.size() : "Mutation size mismatch: " + mutation.size(); + for (int j = 0; j < original.size(); j++) + { + assert mutation.get(j) == original.get(j) + 1 : "List value not mutated correctly."; + } + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOrderCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOrderCrossoverTest.java new file mode 100644 index 0000000..22725b3 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOrderCrossoverTest.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.framework.operators; + +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.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test to validate the operation of the {@link ListOrderCrossover} operator. + * @author Daniel Dyer + */ +public class ListOrderCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<List<Integer>> operator = new ListOrderCrossover<Integer>(); + List<Integer> parent1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); + List<Integer> parent2 = Arrays.asList(3, 7, 5, 1, 6, 8, 2, 4); + List<List<Integer>> population = new ArrayList<List<Integer>>(2); + population.add(parent1); + population.add(parent2); + + for (int i = 0; i < 50; i++) // Do several cross-overs to check different cross-over points. + { + population = operator.apply(population, FrameworkTestUtils.getRNG()); + for (List<Integer> offspring : population) + { + for (int j = 1; j <= 8; j++) + { + assert offspring.contains(j) : "Evolved candidate missing required element " + j; + } + } + } + } + + + /** + * The {@link ListOrderCrossover} operator is only defined to work on populations + * containing lists of equal lengths. Any attempt to apply the operation to + * populations that contain different length lists should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<List<Integer>> crossover + = new ListOrderCrossover<Integer>(new ConstantGenerator<Probability>(Probability.ONE)); + List<List<Integer>> population = new ArrayList<List<Integer>>(2); + population.add(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + population.add(Arrays.asList(9, 10, 11)); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOrderMutationTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOrderMutationTest.java new file mode 100644 index 0000000..6612d4b --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ListOrderMutationTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.Reporter; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test to validate the operation of the {@link ListOrderMutation} operator. + * @author Daniel Dyer + */ +public class ListOrderMutationTest +{ + @Test + public void testMutation() + { + ListOrderMutation<Character> operator = new ListOrderMutation<Character>(); + List<Character> candidate = new ArrayList<Character>(5); + candidate.add('a'); + candidate.add('b'); + candidate.add('c'); + candidate.add('d'); + candidate.add('e'); + List<List<Character>> population = new ArrayList<List<Character>>(1); + population.add(candidate); + List<List<Character>> mutatedPopulation = operator.apply(population, FrameworkTestUtils.getRNG()); + assert mutatedPopulation.size() == population.size() : "Population size should be unchanged."; + List<Character> mutatedCandidate = mutatedPopulation.get(0); + Reporter.log("Original: " + Arrays.toString(candidate.toArray(new Character[candidate.size()]))); + Reporter.log("Mutation: " + Arrays.toString(mutatedCandidate.toArray(new Character[mutatedCandidate.size()]))); + assert mutatedCandidate.size() == candidate.size() : "Mutated candidate should be same length as original."; + // Mutated candidate should have same elements but in a different order. + int matchingPositions = 0; + for (int i = 0; i < candidate.size(); i++) + { + if (candidate.get(i).equals(mutatedCandidate.get(i))) + { + ++matchingPositions; + } + else + { + // If positions don't match, an adjacent character should be a match. + int nextPosition = (i + 1) % candidate.size(); + int previousPosition = ((i - 1) + candidate.size()) % candidate.size(); + boolean matchAdjacent = candidate.get(i).equals(mutatedCandidate.get(nextPosition)) + ^ candidate.get(i).equals(mutatedCandidate.get(previousPosition)); + assert matchAdjacent : "Mutated characters not in expected positions."; + } + } + assert matchingPositions == candidate.size() - 2 : "All but 2 positions should be unchanged."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ObjectArrayCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ObjectArrayCrossoverTest.java new file mode 100644 index 0000000..04160d6 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ObjectArrayCrossoverTest.java @@ -0,0 +1,81 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with arrays of objects. + * @author Daniel Dyer + */ +public class ObjectArrayCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<String[]> crossover = new ObjectArrayCrossover<String>(); + List<String[]> population = new ArrayList<String[]>(4); + population.add(new String[]{"1", "2", "3", "4", "5"}); + population.add(new String[]{"6", "7", "8", "9", "10"}); + population.add(new String[]{"11", "12", "13", "14", "15"}); + population.add(new String[]{"16", "17", "18", "19", "20"}); + Set<String> values = new HashSet<String>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (String[] individual : population) + { + assert individual.length == 5 : "Invalid candidate length: " + individual.length; + for (String value : individual) + { + values.add(value); + } + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * The {@link ObjectArrayCrossover} operator is only defined to work on populations + * containing arrays of equal lengths. Any attempt to apply the operation to + * populations that contain different length arrays should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<String[]> crossover = new ObjectArrayCrossover<String>(1, Probability.ONE); + List<String[]> population = new ArrayList<String[]>(2); + population.add(new String[]{"1", "2", "3", "4", "5"}); + population.add(new String[]{"6", "7", "8", "9", "10", "11", "12"}); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ReplacementTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ReplacementTest.java new file mode 100644 index 0000000..7a4ba59 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/ReplacementTest.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.framework.operators; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Unit test for the {@link Replacement} evolutionary operator. + * @author Daniel Dyer + */ +public class ReplacementTest +{ + @Test + public void testReplacement() + { + IntegerFactory factory = new IntegerFactory(); + List<Integer> candidates = Arrays.asList(10, 11, 12); + Replacement<Integer> replacement = new Replacement<Integer>(factory, + Probability.ONE); + // Numbers will be replaced with lower ones. + List<Integer> output = replacement.apply(candidates, FrameworkTestUtils.getRNG()); + assert output.size() == candidates.size() : "Candidate list should be same size."; + assert !output.contains(10) : "Candidate should have been replaced."; + assert !output.contains(11) : "Candidate should have been replaced."; + assert !output.contains(12) : "Candidate should have been replaced."; + } + + + @Test + public void testZeroProbability() + { + IntegerFactory factory = new IntegerFactory(); + List<Integer> candidates = Arrays.asList(10, 11, 12); + Replacement<Integer> replacement = new Replacement<Integer>(factory, + Probability.ZERO); + // Numbers will be replaced with lower ones. + List<Integer> output = replacement.apply(candidates, FrameworkTestUtils.getRNG()); + assert output.size() == candidates.size() : "Candidate list should be same size."; + assert output.contains(10) : "Candidate should not have been replaced."; + assert output.contains(11) : "Candidate should not have been replaced."; + assert output.contains(12) : "Candidate should not have been replaced."; + } + + + /** + * Non-random factory, for test purposes. + */ + protected static final class IntegerFactory extends AbstractCandidateFactory<Integer> + { + private int count = 0; + + public Integer generateRandomCandidate(Random rng) + { + return ++count; + } + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/SplitEvolutionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/SplitEvolutionTest.java new file mode 100644 index 0000000..f9773ca --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/SplitEvolutionTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for forked evolutionary schemes. + * @author Daniel Dyer + */ +public class SplitEvolutionTest +{ + /** + * Make sure that the correct proportions are mutated correctly. + */ + @Test + public void testSplit() + { + List<Integer> population = new ArrayList<Integer>(10); + for (int i = 10; i <= 100; i += 10) + { + population.add(i); + } + // Increment 30% of the numbers and decrement the other 70%. + SplitEvolution<Integer> evolutionScheme = new SplitEvolution<Integer>(new IntegerAdjuster(1), + new IntegerAdjuster(-1), + 0.3d); + population = evolutionScheme.apply(population, FrameworkTestUtils.getRNG()); + int aggregate = 0; + int incrementedCount = 0; + int decrementedCount = 0; + for (Integer i : population) + { + aggregate += i; + if (i % 10 == 1) + { + ++incrementedCount; + } + else if (i % 10 == 9) + { + ++decrementedCount; + } + else + { + assert false : "Mutation failed."; + } + } + assert incrementedCount == 3 : "Should be 3 incremented candidates, is " + incrementedCount; + assert decrementedCount == 7 : "Should be 7 decremented candidates, is " + decrementedCount; + assert aggregate == 546 : "Aggregate should be 546 after mutation, is " + aggregate; + } + + + /** + * Make sure that the split cannot be set-up with a negative weight. If + * this is attempted, an IllegalArgumentException should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNegativeWeight() + { + // Negative weight. + new SplitEvolution<Integer>(new IntegerAdjuster(1), new IdentityOperator<Integer>(), -0.01d); + } + + + /** + * Make sure that the split cannot be set-up with a weight greater than 1. If + * this is attempted, an IllegalArgumentException should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWeightTooHigh() + { + // Weight too high (must be less than or equal to one). + new SplitEvolution<Integer>(new IntegerAdjuster(1), new IdentityOperator<Integer>(), 1.01d); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/StringCrossoverTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/StringCrossoverTest.java new file mode 100644 index 0000000..3a74155 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/StringCrossoverTest.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.framework.operators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for cross-over with Strings. + * @author Daniel Dyer + */ +public class StringCrossoverTest +{ + @Test + public void testCrossover() + { + EvolutionaryOperator<String> crossover = new StringCrossover(); + List<String> population = new ArrayList<String>(4); + population.add("abcde"); + population.add("fghij"); + population.add("klmno"); + population.add("pqrst"); + Set<Character> values = new HashSet<Character>(20); + for (int i = 0; i < 20; i++) + { + population = crossover.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 4 : "Population size changed after cross-over."; + for (String individual : population) + { + assert individual.length() == 5 : "Invalid candidate length: " + individual.length(); + for (char value : individual.toCharArray()) + { + values.add(value); + } + } + // All of the individual elements should still be present, just jumbled up + // between individuals. + assert values.size() == 20 : "Information lost during cross-over."; + values.clear(); + } + } + + + /** + * The {@link StringCrossover} operator is only defined to work on populations + * containing Strings of equal lengths. Any attempt to apply the operation to + * populations that contain different length Strings should throw an exception. + * Not throwing an exception should be considered a bug since it could lead to + * hard to trace bugs elsewhere. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDifferentLengthParents() + { + EvolutionaryOperator<String> crossover = new StringCrossover(1, Probability.ONE); + List<String> population = new ArrayList<String>(2); + population.add("abcde"); + population.add("fghijklm"); + // This should cause an exception since the parents are different lengths. + crossover.apply(population, FrameworkTestUtils.getRNG()); + } + + + /** + * Number of cross-over points must be greater than zero otherwise the operator + * is a no-op. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testZeroCrossoverPoints() + { + new StringCrossover(0, Probability.EVENS); // Should throw an IllegalArgumentException. + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/StringMutationTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/StringMutationTest.java new file mode 100644 index 0000000..7827452 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/operators/StringMutationTest.java @@ -0,0 +1,54 @@ +//============================================================================= +// 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.framework.operators; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; + +/** + * Unit test for string mutation operator. + * @author Daniel Dyer + */ +public class StringMutationTest +{ + private final char[] alphabet = {'a', 'b', 'c', 'd'}; + + @Test + public void testMutation() + { + StringMutation mutation = new StringMutation(alphabet, Probability.EVENS); + String individual1 = "abcd"; + String individual2 = "abab"; + String individual3 = "cccc"; + List<String> population = Arrays.asList(individual1, individual2, individual3); + for (int i = 0; i < 20; i++) // Perform several iterations. + { + population = mutation.apply(population, FrameworkTestUtils.getRNG()); + assert population.size() == 3 : "Population size changed after mutation: " + population.size(); + for (String individual : population) // Check that each individual is still valid. + { + assert individual.length() == 4 : "Individual size changed after mutation: " + individual.length(); + for (char c : individual.toCharArray()) + { + assert c >= 'a' && c <= 'd' : "Mutation introduced invalid character: " + c; + } + } + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/RankSelectionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/RankSelectionTest.java new file mode 100644 index 0000000..a2f140a --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/RankSelectionTest.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.framework.selection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for rank-proportionate selection. + * @author Daniel Dyer + */ +public class RankSelectionTest +{ + /** + * Test selection when fitness scoring is natural (higher is better). + */ + @Test + public void testNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new RankSelection(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Higher score is better. + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.5); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 1.0); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0.5); + population.add(steve); + population.add(john); + population.add(mary); + population.add(gary); + List<String> selection = selector.select(population, true, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int steveCount = Collections.frequency(selection, steve.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + assert steveCount >= 1 && steveCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + steveCount + ")"; + assert johnCount >= 1 && johnCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + johnCount + ")"; + assert garyCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + garyCount + ")"; + assert maryCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + maryCount + ")"; + } + + + /** + * Test selection when fitness scoring is non-natural (lower is better). + */ + @Test + public void testNonNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new RankSelection(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Lower score is better. + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0.5); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 1.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.5); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(mary); + population.add(john); + population.add(steve); + List<String> selection = selector.select(population, false, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int steveCount = Collections.frequency(selection, steve.getCandidate()); + assert garyCount >= 1 && garyCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + garyCount + ")"; + assert maryCount >= 1 && maryCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + maryCount + ")"; + assert johnCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + johnCount + ")"; + assert steveCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + steveCount + ")"; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/RouletteWheelSelectionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/RouletteWheelSelectionTest.java new file mode 100644 index 0000000..b0b8282 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/RouletteWheelSelectionTest.java @@ -0,0 +1,99 @@ +//============================================================================= +// 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.framework.selection; + +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for roulette selection strategy. We cannot easily test + * that the correct candidates are returned because of the random aspect + * of the selection, but we can at least make sure the right number of + * candidates are selected. + * @author Daniel Dyer + */ +public class RouletteWheelSelectionTest +{ + @Test + public void testNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new RouletteWheelSelection(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 6.2); + population.add(steve); + population.add(mary); + population.add(john); + population.add(gary); + for (int i = 0; i < 20; i++) // Run several iterations to get different outcomes from the "roulette wheel". + { + List<String> selection = selector.select(population, true, 2, FrameworkTestUtils.getRNG()); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + } + } + + + @Test + public void testNonNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new RouletteWheelSelection(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 6.2); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(john); + population.add(mary); + population.add(steve); + for (int i = 0; i < 20; i++) // Run several iterations to get different outcomes from the "roulette wheel". + { + List<String> selection = selector.select(population, false, 2, FrameworkTestUtils.getRNG()); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + } + } + + + /** + * Make sure that the code still functions for non-natural fitness scores even + * when one of them is a zero (a perfect score). + */ + @Test + public void testNonNaturalFitnessPerfectSolution() + { + SelectionStrategy<Object> selector = new RouletteWheelSelection(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(john); + population.add(mary); + population.add(steve); + for (int i = 0; i < 20; i++) // Run several iterations to get different outcomes from the "roulette wheel". + { + List<String> selection = selector.select(population, false, 2, FrameworkTestUtils.getRNG()); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + } + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/SigmaScalingTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/SigmaScalingTest.java new file mode 100644 index 0000000..9d736d3 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/SigmaScalingTest.java @@ -0,0 +1,124 @@ +//============================================================================= +// 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.framework.selection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for the {@link SigmaScaling} selection strategy. + * @author Daniel Dyer + */ +public class SigmaScalingTest +{ + /** + * Test selection when fitness scoring is natural (higher is better). + */ + @Test + public void testNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new SigmaScaling(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Higher score is better. + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.5); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 1.0); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0.5); + population.add(steve); + population.add(john); + population.add(mary); + population.add(gary); + List<String> selection = selector.select(population, true, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int steveCount = Collections.frequency(selection, steve.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + assert steveCount >= 1 && steveCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + steveCount + ")"; + assert johnCount >= 1 && johnCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + johnCount + ")"; + assert garyCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + garyCount + ")"; + assert maryCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + maryCount + ")"; + } + + + /** + * If all fitness scores are equal, standard deviation is zero. Test that this case + * works correctly. + */ + @Test + public void testNoVariance() + { + SelectionStrategy<Object> selector = new SigmaScaling(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 4.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.0); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 4.0); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 4.0); + population.add(steve); + population.add(john); + population.add(mary); + population.add(gary); + List<String> selection = selector.select(population, true, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int steveCount = Collections.frequency(selection, steve.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + assert steveCount == 1 : "Candidate selected wrong number of times (should be 1, was " + steveCount + ")"; + assert johnCount == 1 : "Candidate selected wrong number of times (should be 1, was " + johnCount + ")"; + assert maryCount == 1 : "Candidate selected wrong number of times (should be 1, was " + maryCount + ")"; + assert garyCount == 1 : "Candidate selected wrong number of times (should be 1, was " + garyCount + ")"; + } + + + /** + * Test selection when fitness scoring is non-natural (lower is better). + */ + @Test + public void testNonNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new SigmaScaling(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Lower score is better. + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0.5); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 1.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.5); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(mary); + population.add(john); + population.add(steve); + List<String> selection = selector.select(population, false, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int steveCount = Collections.frequency(selection, steve.getCandidate()); + assert garyCount >= 1 && garyCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + garyCount + ")"; + assert maryCount >= 1 && maryCount <= 2 + : "Candidate selected wrong number of times (should be 1 or 2, was " + maryCount + ")"; + assert johnCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + johnCount + ")"; + assert steveCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + steveCount + ")"; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/StochasticUniversalSamplingTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/StochasticUniversalSamplingTest.java new file mode 100644 index 0000000..6480bf8 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/StochasticUniversalSamplingTest.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.framework.selection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for fitness proportionate selection where observed selection + * frequencies correspond to expected frequencies. + * @author Daniel Dyer + */ +public class StochasticUniversalSamplingTest +{ + /** + * Test selection when fitness scoring is natural (higher is better). + */ + @Test + public void testNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new StochasticUniversalSampling(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Higher score is better. + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.5); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 1.0); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0.5); + population.add(steve); + population.add(john); + population.add(mary); + population.add(gary); + List<String> selection = selector.select(population, true, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int steveCount = Collections.frequency(selection, steve.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + assert steveCount >= 2 && steveCount <= 3 : "Candidate selected wrong number of times (should be 2 or 3, was " + steveCount + ")"; + assert johnCount >= 1 && johnCount <= 2 : "Candidate selected wrong number of times (should be 1 or 2, was " + johnCount + ")"; + assert garyCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + garyCount + ")"; + assert maryCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + maryCount + ")"; + } + + + /** + * Test selection when fitness scoring is non-natural (lower is better). + */ + @Test + public void testNonNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new StochasticUniversalSampling(); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Lower score is better. + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 0.5); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 1.0); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 4.5); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(mary); + population.add(john); + population.add(steve); + List<String> selection = selector.select(population, false, 4, FrameworkTestUtils.getRNG()); + assert selection.size() == 4 : "Selection size is " + selection.size() + ", should be 4."; + int garyCount = Collections.frequency(selection, gary.getCandidate()); + int maryCount = Collections.frequency(selection, mary.getCandidate()); + int johnCount = Collections.frequency(selection, john.getCandidate()); + int steveCount = Collections.frequency(selection, steve.getCandidate()); + assert garyCount >= 2 && garyCount <= 3 : "Candidate selected wrong number of times (should be 2 or 3, was " + garyCount + ")"; + assert maryCount >= 1 && maryCount <= 2 : "Candidate selected wrong number of times (should be 1 or 2, was " + maryCount + ")"; + assert johnCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + johnCount + ")"; + assert steveCount <= 1 : "Candidate selected wrong number of times (should be 0 or 1, was " + steveCount + ")"; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/TournamentSelectionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/TournamentSelectionTest.java new file mode 100644 index 0000000..68f3b38 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/TournamentSelectionTest.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.framework.selection; + +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.FrameworkTestUtils; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for tournament selection strategy. We cannot easily test + * that the correct candidates are returned because of the random aspect + * of the selection, but we can at least make sure the right number of + * candidates are selected. + * @author Daniel Dyer + */ +public class TournamentSelectionTest +{ + @Test + public void testNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new TournamentSelection(new Probability(0.7d)); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 6.2); + population.add(steve); + population.add(mary); + population.add(john); + population.add(gary); + for (int i = 0; i < 20; i++) // Run several iterations so that we get different tournament outcomes. + { + List<String> selection = selector.select(population, true, 2, FrameworkTestUtils.getRNG()); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + } + } + + + @Test + public void testNonNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new TournamentSelection(new Probability(0.7d)); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 6.2); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(john); + population.add(mary); + population.add(steve); + for (int i = 0; i < 20; i++) // Run several iterations so that we get different tournament outcomes. + { + List<String> selection = selector.select(population, false, 2, FrameworkTestUtils.getRNG()); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + } + } + + + + /** + * The probability of selecting the fitter of two candidates must be greater than 0.5 to be + * useful (if it is not, there is no selection pressure, or the pressure is in favour of weaker + * candidates, which is counter-productive) . This test ensures that an appropriate exception + * is thrown if the probability is 0.5 or less. Not throwing an exception is an error because + * it permits undetected bugs in evolutionary programs. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testProbabilityTooLow() + { + new TournamentSelection(Probability.EVENS); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/TruncationSelectionTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/TruncationSelectionTest.java new file mode 100644 index 0000000..3e33d06 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/selection/TruncationSelectionTest.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.framework.selection; + +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; + +/** + * Unit test for truncation selection strategy. Ensures the + * correct candidates are selected. + * @author Daniel Dyer + */ +public class TruncationSelectionTest +{ + @Test + public void testNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new TruncationSelection(0.5d); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Higher score is better. + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 6.2); + population.add(steve); + population.add(mary); + population.add(john); + population.add(gary); + List<String> selection = selector.select(population, true, 2, null); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + assert selection.contains(steve.getCandidate()) : "Best candidate not selected."; + assert selection.contains(mary.getCandidate()) : "Second best candidate not selected."; + } + + + @Test + public void testNonNaturalFitnessSelection() + { + SelectionStrategy<Object> selector = new TruncationSelection(0.5d); + List<EvaluatedCandidate<String>> population = new ArrayList<EvaluatedCandidate<String>>(4); + // Lower score is better. + EvaluatedCandidate<String> gary = new EvaluatedCandidate<String>("Gary", 6.2); + EvaluatedCandidate<String> john = new EvaluatedCandidate<String>("John", 8.4); + EvaluatedCandidate<String> mary = new EvaluatedCandidate<String>("Mary", 9.1); + EvaluatedCandidate<String> steve = new EvaluatedCandidate<String>("Steve", 10.0); + population.add(gary); + population.add(john); + population.add(mary); + population.add(steve); + List<String> selection = selector.select(population, false, 2, null); + assert selection.size() == 2 : "Selection size is " + selection.size() + ", should be 2."; + assert selection.contains(gary.getCandidate()) : "Best candidate not selected."; + assert selection.contains(john.getCandidate()) : "Second best candidate not selected."; + } + + + /** + * The selection ratio must be greater than zero to be useful. This test + * ensures that an appropriate exception is thrown if the ratio is not positive. + * Not throwing an exception is an error because it permits undetected bugs in + * evolutionary programs. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testZeroRatio() + { + new TruncationSelection(0d); + } + + + /** + * The selection ratio must be less than 1 to be useful. This test + * ensures that an appropriate exception is thrown if the ratio is too high. + * Not throwing an exception is an error because it permits undetected bugs in + * evolutionary programs. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testRatioTooHigh() + { + new TruncationSelection(1d); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/ElapsedTimeTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/ElapsedTimeTest.java new file mode 100644 index 0000000..ea26876 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/ElapsedTimeTest.java @@ -0,0 +1,52 @@ +//============================================================================= +// 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.framework.termination; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Unit test for termination condition that checks the time taken so far by the + * evolutionary algorithm. + * @author Daniel Dyer + */ +public class ElapsedTimeTest +{ + @Test + public void testElapsedTimes() + { + TerminationCondition condition = new ElapsedTime(1000); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 0, 0, 0, true, 2, 0, 0, 100); + assert !condition.shouldTerminate(data) : "Should not terminate before timeout."; + data = new PopulationData<Object>(new Object(), 0, 0, 0, true, 2, 0, 0, 1000); + assert condition.shouldTerminate(data) : "Should terminate after timeout."; + } + + + /** + * The duration must be greater than zero to be useful. This test + * ensures that an appropriate exception is thrown if the duration is not positive. + * Not throwing an exception is an error because it permits undetected bugs in + * evolutionary programs. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testZeroRatio() + { + new ElapsedTime(0L); + } + +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/GenerationCountTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/GenerationCountTest.java new file mode 100644 index 0000000..64487a2 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/GenerationCountTest.java @@ -0,0 +1,52 @@ +//============================================================================= +// 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.framework.termination; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Unit test for termination condition that checks the number of evolved generations. + * @author Daniel Dyer + */ +public class GenerationCountTest +{ + @Test + public void testGenerationCounts() + { + TerminationCondition condition = new GenerationCount(5); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 0, 0, 0, true, 2, 0, 3, 100); + // Generation number 3 is the 4th generation (generation numbers are zero-based). + assert !condition.shouldTerminate(data) : "Should not terminate after 4th generation."; + data = new PopulationData<Object>(new Object(), 0, 0, 0, true, 2, 0, 4, 100); + // Generation number 4 is the 5th generation (generation numbers are zero-based). + assert condition.shouldTerminate(data) : "Should terminate after 5th generation."; + } + + + /** + * The generation count must be greater than zero to be useful. This test + * ensures that an appropriate exception is thrown if the count is not positive. + * Not throwing an exception is an error because it permits undetected bugs in + * evolutionary programs. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testZeroRatio() + { + new GenerationCount(0); + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/StagnationTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/StagnationTest.java new file mode 100644 index 0000000..6efe9fb --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/StagnationTest.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.framework.termination; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Unit test for the {@link Stagnation} termination condition. + * @author Daniel Dyer + */ +public class StagnationTest +{ + @Test + public void testFittestCandidateStagnation() + { + TerminationCondition stagnation = new Stagnation(2, true); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 2, 1, 0.1, true, 10, 0, 0, 1); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Best doesn't improve even though mean does. + data = new PopulationData<Object>(new Object(), 1.8, 1.2, 0.1, true, 10, 0, 1, 2); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 1 more generation."; + // Best doesn't improve even though mean does. + data = new PopulationData<Object>(new Object(), 2, 1.5, 0.1, true, 10, 0, 2, 3); + assert stagnation.shouldTerminate(data) : "Stagnation should be triggered after 2 generations without improvement."; + } + + @Test + public void testFittestCandidateStagnationNonNatural() + { + TerminationCondition stagnation = new Stagnation(2, false); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 2, 1.5, 0.1, true, 10, 0, 0, 1); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Best doesn't improve even though mean does. + data = new PopulationData<Object>(new Object(), 2.2, 1.2, 0.1, true, 10, 0, 1, 2); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 1 more generation."; + // Best doesn't improve even though mean does. + data = new PopulationData<Object>(new Object(), 2, 1, 0.1, true, 10, 0, 2, 3); + assert stagnation.shouldTerminate(data) : "Stagnation should be triggered after 2 generations without improvement."; + } + + + @Test + public void testPopulationMeanStagnation() + { + TerminationCondition stagnation = new Stagnation(2, true, true); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 2, 1, 0.1, true, 10, 0, 0, 1); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Best doesn't improve but mean does. + data = new PopulationData<Object>(new Object(), 2, 1.2, 0.1, true, 10, 0, 1, 2); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Best doesn't improve but mean does. + data = new PopulationData<Object>(new Object(), 2, 1.5, 0.1, true, 10, 0, 2, 3); + // Best has stagnated but mean hasn't so shouldn't terminate. + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Now we let the mean stagnate...and let the best candidate get fitter... + data = new PopulationData<Object>(new Object(), 2.1, 1.5, 0.1, true, 10, 0, 3, 4); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 1 more generation."; + data = new PopulationData<Object>(new Object(), 2.2, 1.5, 0.1, true, 10, 0, 4, 4); + assert stagnation.shouldTerminate(data) : "Stagnation should be triggered after 2 generations without improvement."; + } + + + @Test + public void testPopulationMeanStagnationNonNatural() + { + TerminationCondition stagnation = new Stagnation(2, false, true); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 2, 1.5, 0.1, true, 10, 0, 0, 1); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Best doesn't improve but mean does. + data = new PopulationData<Object>(new Object(), 2, 1.2, 0.1, true, 10, 0, 1, 2); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Best doesn't improve but mean does. + data = new PopulationData<Object>(new Object(), 2, 1, 0.1, true, 10, 0, 2, 3); + // Best has stagnated but mean hasn't so shouldn't terminate. + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 2 more generations."; + // Now we let the mean stagnate...and let the best candidate get fitter... + data = new PopulationData<Object>(new Object(), 2.1, 1.6, 0.1, true, 10, 0, 3, 4); + assert !stagnation.shouldTerminate(data) : "Stagnation should not be triggered for at least 1 more generation."; + data = new PopulationData<Object>(new Object(), 2.2, 1.5, 0.1, true, 10, 0, 4, 4); + assert stagnation.shouldTerminate(data) : "Stagnation should be triggered after 2 generations without improvement."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/TargetFitnessTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/TargetFitnessTest.java new file mode 100644 index 0000000..738f612 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/TargetFitnessTest.java @@ -0,0 +1,49 @@ +//============================================================================= +// 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.framework.termination; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.TerminationCondition; + +/** + * Unit test for termination condition that checks the best fitness attained so far + * against a pre-determined target. + * @author Daniel Dyer + */ +public class TargetFitnessTest +{ + @Test + public void testNaturalFitness() + { + TerminationCondition condition = new TargetFitness(10.0d, true); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 5.0d, 4.0d, 0, true, 2, 0, 0, 100); + assert !condition.shouldTerminate(data) : "Should not terminate before target fitness is reached."; + data = new PopulationData<Object>(new Object(), 10.0d, 8.0d, 0, true, 2, 0, 0, 100); + assert condition.shouldTerminate(data) : "Should terminate once target fitness is reached."; + } + + + @Test + public void testNonNaturalFitness() + { + TerminationCondition condition = new TargetFitness(1.0d, false); + PopulationData<Object> data = new PopulationData<Object>(new Object(), 5.0d, 4.0d, 0, true, 2, 0, 0, 100); + assert !condition.shouldTerminate(data) : "Should not terminate before target fitness is reached."; + data = new PopulationData<Object>(new Object(), 1.0d, 3.1d, 0, true, 2, 0, 0, 100); + assert condition.shouldTerminate(data) : "Should terminate once target fitness is reached."; + } +} diff --git a/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/UserAbortTest.java b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/UserAbortTest.java new file mode 100644 index 0000000..34f8ce8 --- /dev/null +++ b/watchmaker/framework/src/java/test/org/uncommons/watchmaker/framework/termination/UserAbortTest.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.framework.termination; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; + +/** + * Unit test for termination condition that checks an abort flag set by the user. + * @author Daniel Dyer + */ +public class UserAbortTest +{ + @Test + public void testAbort() + { + UserAbort condition = new UserAbort(); + // This population data should be irrelevant. + PopulationData<Object> data = new PopulationData<Object>(new Object(), 0, 0, 0, true, 2, 0, 0, 100); + assert !condition.shouldTerminate(data) : "Should not terminate without user abort."; + assert !condition.isAborted() : "Should not be aborted without user intervention."; + condition.abort(); + assert condition.shouldTerminate(data) : "Should terminate after user abort."; + assert condition.isAborted() : "Should be aborted after user intervention."; + } +} |