diff options
author | Yohann Roussel <yroussel@google.com> | 2014-03-19 16:25:37 +0100 |
---|---|---|
committer | Yohann Roussel <yroussel@google.com> | 2014-03-20 15:13:33 +0100 |
commit | 4eceb95409e844fdc33c9c706e1dc307bfd40303 (patch) | |
tree | ee9f4f3fc79f757c79081c336bce4f1782c6ccd8 /watchmaker | |
parent | 3d2402901b1a6462e2cf47a6fd09711f327961c3 (diff) | |
download | toolchain_jack-4eceb95409e844fdc33c9c706e1dc307bfd40303.zip toolchain_jack-4eceb95409e844fdc33c9c706e1dc307bfd40303.tar.gz toolchain_jack-4eceb95409e844fdc33c9c706e1dc307bfd40303.tar.bz2 |
Initial Jack import.
Change-Id: I953cf0a520195a7187d791b2885848ad0d5a9b43
Diffstat (limited to 'watchmaker')
335 files changed, 31198 insertions, 0 deletions
diff --git a/watchmaker/Android.mk b/watchmaker/Android.mk new file mode 100644 index 0000000..f83b49e --- /dev/null +++ b/watchmaker/Android.mk @@ -0,0 +1,33 @@ +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH:= $(call my-dir) + +# +# Watchmaker +# +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, framework/src/java/main) + +LOCAL_MODULE := watchmaker-jack + +LOCAL_MODULE_TAGS := optional + +LOCAL_JAVA_LIBRARIES := \ + maths-jack \ + guava-jack + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/watchmaker/CHANGELOG.txt b/watchmaker/CHANGELOG.txt new file mode 100644 index 0000000..af02f5f --- /dev/null +++ b/watchmaker/CHANGELOG.txt @@ -0,0 +1,431 @@ +Changes in version 0.7.2 +------------------------ + +* Fix for StatusBar timer being reset at each epoch in island evolution + (ISSUE#19). + +* Added RandomMigration as an alternative strategy for island evolution. + + +Changes in version 0.7.1 +------------------------ + +* Added EvolutionStrategyEngine to provide both (mu+lambda) and (mu,lambda) + evolution strategies. + +* Use final version of Google Collections 1.0. + +* Tweaked concurrency settings in IslandEvolution (no need for more than one + thread per island). + +* Fixed look-and-feel issues in example applets (make sure all components are + created after the look-and-feel is set. + +* Reverted 0.7.0 change to generation numbering in AbstractEvolutionEngine. + Numbering is correct if the initial population is considered to be the first + generation. + + +Changes in version 0.7.0 +------------------------ + +* Refactored internals to make it easier for the framework to provide different + evolutionary algorithm variants. Different types of evolutionary algorithm + are supported by different sub-classes of AbstractEvolutionEngine, with + different implementations of the nextEvolutionStep method. The main + EvolutionEngine implementation is now called GenerationalEvolutionEngine. + +* Concurrency control is no longer supported through inheritance. Removed + ConcurrentEvolutionEngine and SequentialEvolutionEngine and added the + setSingleThreaded method to AbstractEvolutionEngine. + +* Added first-class support for steady-state evolution in the form of the new + SteadyStateEvolutionEngine. + +* Added SigmaScaling selection strategy. + +* Added ListInversion evolutionary operator. Randomly reverses subsections of + lists. + +* Added variable probability constructor to ListOrderCrossover. + +* Introduced support for island model evolution. + +* Added islands view to the evolution monitor so that the state of individual + island populations can be tracked. + +* Fixed generation numbering in AbstractEvolutionEngine. It was supposed to be + zero-based but was starting at 1, which would cause the GenerationCount + termination condition to trigger prematurely. + +* Ugraded to Uncommons Maths 1.2.1 to take advantage of optimisations. + +* Optimised the implementation of BitStringCrossover to take advantage of + faster bit swapping operation in Uncommons Maths 1.2.1. + +* Converted BitStringMutation to use a more efficient approach. The + probability parameter is now for controlling whether an individual candidate + gets mutated, not each separate bit as was the case previously (existing + programs should now use higher probabilities accordingly). How many bits get + flipped is now controlled via a secondary parameter. + +* Upgraded to Google Collections 1.0 RC5. + +* Introduced Swing renderer for example GP trees. + + +Changes in version 0.6.2 +------------------------ + +* Fix for thread leak in ConcurrentEvolutionEngine (ISSUE#16). Added a + finalizer to shutdown the thread pool in FitnessEvaluationWorker so that the + active threads do not inhibit garbage collection. + +* Made the FitnessEvaluationWorker in ConcurrentEvolutionEngine static so that + it can be shared by all instances avoiding the unnecessary creation of new + thread pools (ISSUE#16). + +* Made fitness evaluation threads daemons again (as in 0.6.0). Only standalone + (i.e. Terracotta) FitnessEvaluationWorkers now use non-daemon threads. This + fixes the problem of the JVM not exiting when the program is finished + (ISSUE#17). + +* Better thread naming for fitness evaluation worker threads. + + +Changes in version 0.6.1 +------------------------ + +* Added getSatisfiedTerminationConditions() method to the EvolutionEngine + interface. This makes it easier to determine which TerminationCondition + caused the evolution to stop (ISSUE#13). + +* Improvements to the genetic programming example application. + +* Made StatusBar component public so that it can be used independently of the + Evolution Monitor. + +* Converted applet-based examples so that they can also run as applications in + JFrames. + +* Introduced Launcher class so that examples can be run easily from the command + line. + +* Changed the Mona Lisa example to accept the URL of an alternate image when + run from the command line. + +* Moved NullFitnessEvaluator from interactive package to framework package and + made it package scope. This class is an implementation detail and should not + be exposed by the API. + +* Refactored the ConcurrentEvolutionEngine by moving the code that actually + executes FitnessEvaluationTasks into a new class (FitnessEvaluationWorker). + This modified design makes it easy to distribute the fitness evaluations + using Terracotta (http://www.terracotta.org). + +* Documented the restriction that fitness scores are not allowed to be + negative. Made sure that the framework throws an appropriate, informative + exception if a negative fitness is encountered (ISSUE#15). + +* Documented potential pitfall with using EvolutionObservers to update Swing + GUIs (ISSUE#14). + + +Changes in version 0.6.0 +------------------------ + +* Added the option to view only the most recent 200 generations on the + population fitness graph of the evolution monitor. + +* Fixed a bug that prevented the population fitness view from being reset + between runs. + +* Added more information to the evolution monitor status bar. + +* Added a boolean parameter to the evolution monitor showInFrame method so that + you can specify whether closing the frame should terminate the program. + +* Enhancements to the Mona Lisa example application (it's now an applet). + +* Added methods to the EvolutionEngine interface to allow the entire population + to be returned from an evolutionary algorithm rather than just the fittest + candidate. + +* Introduced CachingFitnessEvaluator, a decorator for standard fitness + functions that caches the results of its calculations. If the evaluator is + invoked twice for the same candidate, the cached value is returned by the + second invocation thus avoiding the expense of recalculating the fitness + score. This is a useful optimisation in scenarios where some canidates + survive from generation to generation unmodified (elitism is one example of + this). + +* Introduced dependency on Google Collections (to support the + CachingFitnessEvaluator). + +* Fixed ProbabilityParameterControl so that it allows the slider to move all + the way to the maximum permitted value (ISSUE#10). + +* Moved classes from Uncommons Utils module into the main framework module and + removed the util module. + +* Added SelectionStrategyControl to the Swing module. This control enables the + user to change the selection strategy even while the evolution is running. + +* Upgraded to Uncommons Maths 1.2, which has its own Probability type, so we + now use that and the Probability class in the framework package has been + removed. + +* Removed call to System.out.println in ConcurrentEvolutionEngine (ISSUE#12). + +* Included source code in the release distribution in the form of source JARs + for the framework and swing modules (source code for the examples module is + already included) (ISSUE#11). + +* Modified cross-over implementation constructors to accept a number generator + for the probability parameter. This allows variable probabilities to be + used. + + +Changes in version 0.5.1 +------------------------ + +* Simplified generics for EvolutionaryOperators. It's now slightly less + flexible but the most common scenarios are less cumbersome since there are + no 'super' wildcards to deal with when constructing pipelines and no need to + deal with sub-types when implementing your own operators. + +* Added generic type parameters to ListCrossover, ListOrderCrossover and + ListOrderMutation to be compatible with the above change. + +* Added evolutionary art example appliction inspired by Roger Alsing's + evolution of the Mona Lisa, see + http://rogeralsing.com/2008/12/07/genetic-programming-evolution-of-mona-lisa/ + +* Added Replacement evolutionary operator that randomly replaces candidates + with independent, newly-generated random candidates. Useful for introducing + new genetic material into stagnating populations. + +* Improved ListCrossover operator so that it can work with parents of variable + lengths. + +* Added higher-order evolutionary operator, ListOperator that converts an + operator of a given type into a operator that works with lists of that type. + +* Enhancements to the Evolution Monitor component. + + +Changes in version 0.5.0 +------------------------ + +* Added wildcard to generic parameter of Console select method. + +* Added check to ensure that there is always at least one termination condition + specified. + +* Improved reflection in RendererAdapter and InteractiveSelection. Previously + they tried to access classes that may not have been visible to them. + +* Introduced tree-based genetic programming example application. + +* Added Probability class. This immutatble value type encapsulates a + probability value between zero and one. The class enforces the 0..1 bounds + and provides convenient methods for working with probabilities. Using the + Probability type is an improvement over the previous approach of using double + values since there is now no need to duplicate bounds-checking and other + logic throughout the code. + +* Introduced new Swing component (ProbabilityParameterControl) for manipulating + Probability parameters from a GUI. + +* Javadoc improvements. + +* Relaxed generic constraints on EvolutionObservers (generic type can now be + less specific than the EvolutionEngine type). Added wildcard to + populationUpdate method of EvolutionObserver to support this. + +* Renamed StandaloneEvolutionEngine to ConcurrentEvolutionEngine. + +* Introduced SequentialEvolutionEngine, which performs all work synchronously + on the request thread, making it suitable for use in restricted/managed + environments that do not permit direct control over threading. + +* Use only integer labels for the generations axis on the Evolution Monitor's + fitness view. + + +Changes in version 0.4.3 +------------------------ + +* Introduced Utilities module. + +* Extracted Uncommons Maths module into separate project + (see https://uncommons-maths.dev.java.net). + +* Added new constructor to BitString to simplify the creation of random bit + strings. + +* Improved algorithm for countSetBits() in BitString. New version is + significantly faster (10-15 times faster). + +* Moved BitString class from Framework module into Uncommons Maths. + +* Added new termination condition to detect when the evolution has stagnated + (i.e. the fitness has not improved for a certain number of generations). + +* Moved the the Swing-specific classes from the framework module into what was + the Uncommons GUI module, which is now the Watchmaker Swing module. + +* Introduced experimental Evolution Monitor Swing component. + + +Changes in version 0.4.2 +------------------------ + +* Added a second parameter to the FitnessEvaluator interface to enable fitness + calculations to take into account an individual's environment (the remainder + of the population) when assigning a score. + +* Minor tweak to improve the distribution of fitness evaluations between + threads on multi-processor machines. + +* Fix for ISSUE#3 (bad Throwable handling in InteractiveSelection and + RendererAdapter) + +* Made AbstractEvolutionEngine interruptible. + +* Added new constructor to StandaloneEvolutionEngine to allow users to provide + a custom ThreadFactory. + +* Fix for ISSUE#4 (unreliable final result with non-natural fitness function). + + +Changes in version 0.4.1 +------------------------ + +* Added new constructors to NumberGenerator implementations to enable the + parameters to be dynamic. + +* Several more unit tests for significantly improved coverage. Fixed 2 minor + bugs detected by these new tests. + +* Added some GUI control components to assist in building Swing GUIs for + evolutionary programs. + +* Moved the SwingConsole class into the new Swing components package + (org.uncommons.watchmaker.swing) so that the core framework does not contain + any dependencies on a particular presentation layer. + +* Added evolutionary Sudoku solver example application. + +* Included source code for the examples in the release archives. + + +Changes in version 0.4.0 +------------------------ + +* Introduced framework support for interactive evolutionary algorithms. + +* New example application based on Richard Dawkins' biomorph experiment. + Demonstrates the new interactive features of the framework. + +* Added new, extremely fast RNG - a Java port of Tony Pasqualoni's cellular + automaton RNG (http://home.southernct.edu/~pasqualonia1/ca/report.html). + +* Added ordered cross-over evolutionary operator for lists. + +* Updated Travelling Salesman applet to optionally use cross-over as well as + mutation. + +* Introduced TerminationCondition interface and useful default implementations. + Changed evolve methods in EvolutionEngine to take one or more conditions + instead of explicitly specifying parameters such as number of generations, + target fitness and timeout. + + +Changes in version 0.3.0 +------------------------ + +* Removed java.util.BitSet operators and candidate factory. BitSets are + somewhat lacking as a generic bit string for genetic algorithms. + +* Introduced a new general-purpose BitString type with an associated candidate + factory plus mutation and cross-over operators. + +* Added probabilities to cross-over implementations so that parents may + sometimes pass through the operator unaltered. A similar effect could have + been achieved previously by combining the cross-over operator with a + SplitEvolution operator that processed some of the individuals with an + IdentityOperator, but this is more straightforward. + +* Modified cross-over operators so that the cross-over index is always + non-zero. This means all cross-overs are meaningful. With a zero index, + each parent is split before the first position, making the cross-over + effectively a no-op. This change was required in order to honour the new + cross-over probability, and is also sensible in its own right. + +* Removed varargs constructor from EvolutionPipeline since it was impossible + to invoke without generating a compiler warning about generic array + creation. + +* Fixed very slightly skewed probability in StringMutation. + +* Fixed bug in RouletteWheelSelection. + +* Improved Travelling Salesman applet to allow different selection strategies + to be applied. + +* More unit test cases and improved API documentation. + + +Changes in version 0.2.2 +------------------------ + +* Renamed classes for generating random values from various probability + distributions. Now called "generators" rather than "sequences" in order to + avoid confusion with mathematical sequences. + +* Changed terminology for different fitness scoring schemes to be consistent + with the literature (what was called 'normalised fitness' is now called + 'natural fitness'). + +* Refactored fitness-proportionate selection strategies. + +* Fixed tournament selection bug. + +* Moved the compound evolutionary operators (SplitEvolution and + EvolutionPipeline) introduced in the previous release into the operators + package. + +* Introduced an IdentityOperator for use with SplitEvolution to enable some + candidates to be preserved unchanged (this is different from elitism + because it does not depend on fitness). + + +Changes in version 0.2.1 +------------------------ + +* Reworked evolutionary operators to allow more flexibility. Specifically, + evolution can now be split into separate streams, which enables common + genetic programming techniques to be used. + +* Simplified the use of generics within the API. + +* Fixed bug with pre-seeding populations. + + +Changes in version 0.2 +---------------------- + +* Added support for concurrent fitness evaluations to take advantage of + multi-core and multi-processor machines. + +* Modified the way elitism is configured (the number of candidates to preserve + is now specifed as an argument to the evolve method of the EvolutionEngine). + +* Converted the Travelling Salesman example program into an applet that allows + parameters to be tweaked and performance to be compared to a brute force + implementation. + +* Fixed RNG seeding to work in an untrusted applet environment (previously + SecurityExceptions were thrown when attempting to access resources for + seeding). diff --git a/watchmaker/LICENCE.txt b/watchmaker/LICENCE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/watchmaker/LICENCE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/watchmaker/NOTICE.txt b/watchmaker/NOTICE.txt new file mode 100644 index 0000000..1967b94 --- /dev/null +++ b/watchmaker/NOTICE.txt @@ -0,0 +1,43 @@ +_______________________________________________________________________________ + + The Watchmaker Framework for Evolutionary Computation - Version 0.7.0 + (http://watchmaker.uncommons.org) + Copyright 2006-2009 Daniel W. Dyer (http://www.dandyer.co.uk) +_______________________________________________________________________________ + +Acknowledgements: +----------------- +The Swing GUI library of the Watchmaker Framework depends on David Gilbert's +JFreeChart library (http://www.jfree.org/jfreechart/). JFreeChart and the +JCommon library are licensed under the terms of the GNU Lesser General Public +License (a copy of this licence can be found in the jfreechart-LICENCE.txt file +included with this distribution). + +The Watchmaker Framework depends on the Google Collections library +(http://code.google.com/p/google-collections/), which itself is released under +the same licence as the Watchmaker Framework (the Apache Licence Version 2.0). + +The Watchmaker Framework depends heavily on the Uncommons Maths library. +Uncommons Maths is also licensed under the terms of the Apache Licence Version +2.0. The NOTICE.txt file from the Uncommons Maths distribution is reproduced +below. + +_______________________________________________________________________________ + + Uncommons Maths (http://maths.uncommons.org) + Copyright 2006-2009 Daniel W. Dyer (http://www.dandyer.co.uk) +_______________________________________________________________________________ + +Acknowledgements: +----------------- +This software includes a Java port of the cellular automaton pseudorandom +number generator developed by Tony Pasqualoni +(http://home.southernct.edu/~pasqualonia1/ca/report.html). + +This software includes a Java port of the Mersenne Twister pseudorandom +number generator developed by Makoto Matsumoto and Takuji Nishimura +(http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html). + +This software also includes modified versions of the PermutationGenerator and +CombinationGenerator Java classes written by Michael Gilleland +(http://www.mgilleland.com/). diff --git a/watchmaker/README.android b/watchmaker/README.android new file mode 100644 index 0000000..9c3738a --- /dev/null +++ b/watchmaker/README.android @@ -0,0 +1,7 @@ +URL: http://watchmaker.uncommons.org/ +Version: 0.7.1 +License: Apache 2.O +Description: "Evolutionary Computation Framework." + +this code was taken from 9e8109d7fd84b7350a93d160690d91a9847b26a1 +(git clone git://github.com/dwdyer/watchmaker) diff --git a/watchmaker/README.txt b/watchmaker/README.txt new file mode 100644 index 0000000..3ddaeb2 --- /dev/null +++ b/watchmaker/README.txt @@ -0,0 +1,46 @@ +_______________________________________________________________________________ + + The Watchmaker Framework for Evolutionary Computation - Version 0.7.1 + (http://watchmaker.uncommons.org) + Copyright 2006-2010 Daniel W. Dyer (http://www.dandyer.co.uk) +_______________________________________________________________________________ + + +1). Getting Started +------------------- + +Please refer to the included LICENCE.txt and NOTICE.txt files for terms of use. + + User Manual: http://watchmaker.uncommons.org/manual/index.html + API Reference: http://watchmaker.uncommons.org/api/index.html + +Source code for several example programs is included in the distribution. + +The examples can be run with the following command: + + java -jar watchmaker-examples-0.7.1.jar + +This will list the names of available example applications. Then just run the +command again with one of those names as an argument. + + +2). Library Dependencies +------------------------ + +The following bundled JAR files are required by all programs that use the +Watchmaker Framework: + + watchmaker-framework-0.7.1.jar (Apache Licence 2.0) + uncommons-maths-1.2.1.jar (Apache Licence 2.0) + google-collect-1.0.jar (Apache Licence 2.0) + +These additional JAR files are required to use the Watchmaker Framework Swing +components: + + watchmaker-swing-0.7.1.jar (Apache Licence 2.0) + jfreechart-1.0.13.jar (GNU LGPL 2.1) + jcommon-1.0.16.jar (GNU LGPL 2.1) + +Example applications are included in the watchmaker-examples-0.7.1.jar file. +This file is not required by other applications that use the Watchmaker +Framework. diff --git a/watchmaker/book/book.iml b/watchmaker/book/book.iml new file mode 100644 index 0000000..d5c0743 --- /dev/null +++ b/watchmaker/book/book.iml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> + diff --git a/watchmaker/book/src/resources/antenna.jpg b/watchmaker/book/src/resources/antenna.jpg Binary files differnew file mode 100644 index 0000000..d706426 --- /dev/null +++ b/watchmaker/book/src/resources/antenna.jpg diff --git a/watchmaker/book/src/resources/docbook.css b/watchmaker/book/src/resources/docbook.css new file mode 100644 index 0000000..018291b --- /dev/null +++ b/watchmaker/book/src/resources/docbook.css @@ -0,0 +1,35 @@ +a {text-decoration: none; + color: #006699;} +a:visited {color: #003366;} +body {line-height: 1.8em; + font-size: 62.5%; + width: 960px; + margin-left: auto; + margin-right: auto; + font-family: Palatino Linotype, Palatino, serif; + background-color: #ffffff; + color: #444444;} +dl {margin: 0;} +h1, h2, h3 {font-weight: bold; + font-variant: small-caps;} +h1 {font-size: 3em; + margin-bottom: .6em;} +h2 {font-size: 2.4em; + margin-bottom: .75em;} +h3 {font-size: 1.8em; + margin-bottom: 1em;} +p, ul {font-size: 1.4em; + margin: 1.286em 0;} +dl, table {font-size: 1.4em;} +dl dl {font-size: 1em;} +pre {font-family: Monaco, Courier New, monospace; font-size: 1.2em; margin-bottom: 1.5em;} +strong {color: #000000;} + +h2.subtitle {font-variant: normal;} +h3.author {font-variant: normal; margin-top: 2em; margin-bottom: 0;} +.email {font-size: 1.2em;} +.informalfigure {text-align: center;} +.mediaobject {display: inline-block;} +.caption p {margin: 0; text-align: right; font-size: small;} + +.navfooter table tr td {width: 33.33%;}
\ No newline at end of file diff --git a/watchmaker/book/src/resources/travelling_salesman_problem.png b/watchmaker/book/src/resources/travelling_salesman_problem.png Binary files differnew file mode 100644 index 0000000..d6017c9 --- /dev/null +++ b/watchmaker/book/src/resources/travelling_salesman_problem.png diff --git a/watchmaker/book/src/xml/book.xml b/watchmaker/book/src/xml/book.xml new file mode 100644 index 0000000..24f0a09 --- /dev/null +++ b/watchmaker/book/src/xml/book.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8" ?> +<book xmlns="http://docbook.org/ns/docbook" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <info> + <title>Evolutionary Computation in Java</title> + <author> + <personname> + <firstname>Daniel</firstname> + <surname>Dyer</surname> + <othername>W.</othername> + </personname> + <email>dan@uncommons.org</email> + <uri>http://www.dandyer.co.uk</uri> + </author> + <copyright> + <year>2008</year> + <year>2009</year> + <year>2010</year> + </copyright> + <keywordset> + <keyword>algorithms</keyword> + <keyword>evolution</keyword> + <keyword>evolutionary algorithms</keyword> + <keyword>evolutionary computation</keyword> + <keyword>genetic algorithms</keyword> + <keyword>Java</keyword> + <keyword>programming</keyword> + <keyword>software</keyword> + </keywordset> + </info> + + <xi:include href="preface.xml" /> + + <!-- Chapters --> + <xi:include href="evolution.xml" /> + <xi:include href="watchmaker.xml" /> + <xi:include href="salesman.xml" /> + <xi:include href="selection.xml" /> + <xi:include href="sudoku.xml" /> + <xi:include href="islands.xml" /> + <xi:include href="interactive.xml" /> + <xi:include href="steadystate.xml" /> + <xi:include href="geneticprogramming.xml" /> + <xi:include href="classifiers.xml" /> + <xi:include href="multiobjective.xml" /> + + <!-- Appendices --> + <xi:include href="performance.xml" /> + <xi:include href="gui.xml" /> + <xi:include href="distributed.xml" /> + <xi:include href="furtherreading.xml" /> + + <index /> + +</book> diff --git a/watchmaker/book/src/xml/classifiers.xml b/watchmaker/book/src/xml/classifiers.xml new file mode 100644 index 0000000..79a433a --- /dev/null +++ b/watchmaker/book/src/xml/classifiers.xml @@ -0,0 +1,6 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Learning Classifier Systems</title> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/distributed.xml b/watchmaker/book/src/xml/distributed.xml new file mode 100644 index 0000000..08c54c0 --- /dev/null +++ b/watchmaker/book/src/xml/distributed.xml @@ -0,0 +1,13 @@ +<appendix xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Distributed Evolutionary Algorithms</title> + <section> + <title>Running Watchmaker Programs on Hadoop with Apache Mahout</title> + <para>TODO</para> + </section> + <section> + <title>Clustering Watchmaker Programs with Terracotta</title> + <para>TODO</para> + </section> +</appendix> diff --git a/watchmaker/book/src/xml/evolution.xml b/watchmaker/book/src/xml/evolution.xml new file mode 100644 index 0000000..0c134aa --- /dev/null +++ b/watchmaker/book/src/xml/evolution.xml @@ -0,0 +1,424 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>The Power of Evolution</title> + <para> + Software is normally developed in a very precise, deterministic way. The behaviour of a + computer is governed by strict logical rules. A computer invariably does exactly what + it is told to do. + </para> + <para> + When writing a program to solve a particular problem, software developers will identify + the necessary sub-tasks that the program must perform. Algorithms are chosen and + implemented for each task. The completed program becomes a detailed specification of + exactly how to get from A to B. Every aspect is carefully designed by its developers + who must understand how the various components interact to deliver the program's + functionality. + </para> + <para> + This prescriptive approach to solving problems with computers has served us well and is + responsible for most of the software applications that we use today. However, it is not + without limitations. + Solutions to problems are constrained by the intuition, knowledge and prejudices + of those who develop the software. + <emphasis>The programmers have to know exactly how to solve the problem.</emphasis> + </para> + <para> + Another characteristic of the prescriptive approach that is sometimes problematic is + that it is best suited to finding exact answers. Not all problems have exact solutions, + and some that do may be too computationally expensive to solve. Sometimes it is + more useful to be able to find an approximate answer quickly than to waste time searching + for a better solution. + </para> + <section> + <title>What are Evolutionary Algorithms?</title> + <indexterm><primary>evolutionary algorithm</primary></indexterm> + <indexterm><primary>Darwin, Charles</primary></indexterm> + <para> + Evolutionary algorithms (EAs) are inspired by the biological model of evolution and + natural selection first proposed by Charles Darwin in 1859. + In the natural world, evolution helps species adapt to their environments. + Environmental factors that influence the survival prospects of an organism + include climate, availability of food and the dangers of predators. + </para> + <indexterm><primary>natural selection</primary></indexterm> + <para> + Species change over the course of many generations. + Mutations occur randomly. Some mutations will be advantageous, but many will be + useless or detrimental. Progress comes from the feedback provided by non-random + natural selection. + For example, organisms that can survive for long periods without water will be + more likely to thrive in dry conditions than those that can't. + Likewise, animals that can run fast will be more successful at evading predators + than their slower rivals. + If a random genetic modification helps an organism to survive and to reproduce, + that modification will itself survive and spread throughout the population, via + the organism's offspring. + </para> + <para> + Evolutionary algorithms are based on a simplified model of this biological evolution. + To solve a particular problem we create an environment in which potential + solutions can evolve. The environment is shaped by the parameters of the problem + and encourages the evolution of good solutions. + </para> + <indexterm><primary>evolutionary computation</primary></indexterm> + <para> + The field of Evolutionary Computation encompasses several types of evolutionary + algorithm. These include <emphasis>Genetic Algorithms</emphasis> (GAs), + <emphasis>Evolution Strategies</emphasis>, <emphasis>Genetic Programming</emphasis> + (GP), <emphasis>Evolutionary Programming</emphasis> and <emphasis>Learning + Classifier Systems</emphasis>. + </para> + <indexterm><primary>genetic algorithms</primary></indexterm> + <para> + The most common type of evolutionary algorithm is the generational genetic + algorithm. We'll cover other EA variants in later chapters but, for now, + all of the evolutionary algorithms that we meet will be some kind of generational + GA. + </para> + <indexterm><primary>fitness function</primary></indexterm> + <indexterm><primary>population</primary></indexterm> + <para> + The basic outline of a generational GA is as follows (most other EA variants are + broadly similar). + A <emphasis>population</emphasis> of candidate solutions is iteratively evolved + over many <emphasis>generations</emphasis>. Mimicking the concept of + natural selection in biology, the survival of candidates (or their offspring) + from generation to generation in an EA is governed by a <emphasis>fitness + function</emphasis> that evaluates each candidate according to how close it is to + the desired outcome, and a <emphasis>selection strategy</emphasis> that favours + the better solutions. + Over time, the quality of the solutions in the population should improve. + If the program is successful, we can terminate the evolution once it has found + a solution that is good enough. + </para> + <section> + <title>An Example</title> + <para> + Now that we have introduced the basic concepts and terminology, I will attempt + to illustrate by way of an example. Suppose that we want to use evolution to generate + a particular character string, for example "HELLO WORLD". This is a contrived example + in as much as it assumes that we don't know how to create such a string and that + evolution is the best approach available to us. However, bear with me as this simple + example is useful for demonstrating exactly how the evolutionary approach works. + </para> + <para> + Each candidate solution in our population will be a string. We'll use a fixed-length + representation so that each string is 11 characters long. Each character in a string + will be one of the 27 valid characters (the upper case letters 'A' to 'Z' plus the space + character). + </para> + <para> + For the fitness function we'll use the simple approach of assigning a candidate + solution one point for each position in the string that has the correct character. + For the string "HELLO WORLD" this gives a maximum possible fitness score of 11 (the + length of the string). + </para> + <para> + The first task for the evolutionary algorithm is to randomly generate the initial + population. We can use any size population that we choose. + Typical EA population sizes can vary from tens to thousands of individuals. + For this example we will use a population size of 10. + After the initialisation of the population we might have the following candidates + (fitness scores in brackets): + <informalexample> + <programlisting> + 1. GERZUNFXCEN (1) + 2. HSFDAHDMUYZ (1) + 3. UQ IGARHGJN (0) + 4. ZASIB WSUVP (2) + 5. XIXROIUAZBH (1) + 6. VDLGCWMBFYA (1) + 7. SY YUHYRSEE (0) + 8. EUSVBIVFHFK (0) + 9. HHENRFZAMZH (1) + 10. UJBBDFZPLCN (0) + </programlisting> + </informalexample> + </para> + <para> + None of these candidate solutions is particularly good. The best (number 4) has just two + characters out of eleven that match the target string (the space character and the 'W'). + </para> + <para> + The next step is to select candidates based on their fitness and use them to create + a new generation. One technique for favouring the selection of fitter candidates over + weaker candidates is to assign each candidate a selection probability proportionate to + its fitness. + </para> + <indexterm><primary>fitness-proportionate selection</primary></indexterm> + <indexterm><primary>selection</primary><secondary>fitness-proportionate</secondary></indexterm> + <para> + If we use <emphasis>fitness-proportionate selection</emphasis>, none of the candidates + with zero fitness will be selected and the candidate with a fitness of 2 is twice as likely + to be selected as any of the candidates with a fitness of 1. For the next step we need to + select 10 parents, so it is obvious that some of the fit candidates are going to be selected + multiple times. + </para> + <indexterm><primary>cross-over</primary></indexterm> + <para> + Now that we have some parents, we can breed the next generation. We do this via a process + called <emphasis>cross-over</emphasis>, which is analogous to sexual reproduction in biology. + For each pair of parents, a cross-over point is selected randomly. Assuming that the first + two randomly selected parents are numbers 2 and 4, if the cross-over occurs after the + first four characters, we will get the following offspring: + <informalexample> + <programlisting> + Parent 1: <emphasis role="bold">HSFDAHDMUYZ</emphasis> + Parent 2: ZASIB WSUVP + + Offspring 1: <emphasis role="bold">HSFD</emphasis>B WSUVP + Offspring 2: ZASI<emphasis role="bold">AHDMUYZ</emphasis> + </programlisting> + </informalexample> + </para> + <indexterm><primary>mutation</primary></indexterm> + <para> + This recombination has given us two new candidates for the next generation, one of which is + better than either of the parents (offspring 1 has a fitness score of 3). This shows how + cross-over can lead towards better solutions. However, looking at the initial population as + a whole, we can see that no combination of cross-overs will ever result in a candidate with + a fitness higher than 6. This is because, among all 10 original candidates, there are only 6 + positions in which we have the correct character. This can be mitigated to some extent by + increasing the size of the population. With 100 individuals in the initial population we + would be much more likely to have the necessary building blocks for a perfect solution, but + there is no guarantee. This is where <emphasis>mutation</emphasis> comes in. + </para> + <para> + Mutation is implemented by modifying each character in a string according to some small + probability, say 0.02 or 0.05. This means that any single individual will be changed only + slightly by mutation, or perhaps not at all. + </para> + <para> + By applying mutation to each of the offspring produced by cross-over we will occasionally + introduce correct characters in new positions. We will also occasionally remove correct + characters but these bad mutations are unlikely to survive selection in the next generation, + so this is not a big problem. Advantageous mutations will be propagated by cross-over and + selection and will quickly spread throughout the population. + </para> + <para> + After repeating this process for dozens or perhaps even hundreds of generations we will + eventually converge on our desired solution. + </para> + <para> + This is a convoluted process for finding a string that we already knew to start with. + However, as we shall see in the remainder of this book, the evolutionary approach + generalises to deal with problems where we don't know what the best solution is and + therefore can't encode that knowledge in our fitness function. + </para> + <para> + The important point demonstrated by this example is that we can arrive at a satisfactory + solution without having to enumerate every possible candidate in the search space. + Even for this trivial example, a brute force search would involve generating and + checking approximately 5.6 quadrillion strings. + </para> + </section> + <section> + <title>The Outline of an Evolutionary Algorithm</title> + <procedure> + <step> + <title>Genesis</title> + <para> + Create an initial set (population) of <literal>n</literal> candidate solutions. + This may be done entirely randomly or the population may be seeded with some + hand-picked candidates. + </para> + </step> + <step> + <title>Evaluation</title> + <para> + Evaluate each member of the population using some fitness function. + </para> + </step> + <step> + <title>Survival of the Fittest</title> + <indexterm><primary>selection</primary></indexterm> + <para> + Select a number of members of the evaluated population, favouring those + with higher fitness scores. These will be the parents of the next generation. + </para> + </step> + <step> + <title>Evolution</title> + <indexterm><primary>cross-over</primary></indexterm> + <indexterm><primary>mutation</primary></indexterm> + <para> + Generate a new population of offspring by randomly altering and/or combining + elements of the parent candidates. The evolution is performed by one or more + <emphasis>evolutionary operators</emphasis>. The most common operators are + cross-over and mutation. + Cross-over takes two parents, cuts them each into two or more pieces and recombines + the pieces to create two new offspring. Mutation copies an individual but with + small, random modifications (such as flipping a bit from zero to one). + </para> + </step> + <step> + <title>Iteration</title> + <para> + Repeat steps 2-4 until a satisfactory solution is found or some other termination + condition is met (such as the number of generations or elapsed time). + </para> + </step> + </procedure> + </section> + </section> + <section> + <title>When are Evolutionary Algorithms Useful?</title> + <para> + Evolutionary algorithms are typically used to provide good approximate + solutions to problems that cannot be solved easily using other techniques. + Many optimisation problems fall into this category. It may be too + computationally-intensive to find an exact solution but sometimes a near-optimal + solution is sufficient. In these situations evolutionary techniques can be + effective. Due to their random nature, evolutionary algorithms are never guaranteed + to find an optimal solution for any problem, but they will often find a good solution + if one exists. + </para> + <para> + One example of this kind of optimisation problem is the challenge of timetabling. + Schools and universities must arrange room and staff allocations to suit the needs + of their curriculum. There are several constraints that must be satisfied. + A member of staff can only be in one place at a time, they can only teach classes + that are in their area of expertise, rooms cannot host lessons if they are already + occupied, and classes must not clash with other classes taken by the same students. + This is a combinatorial problem and known to be NP-Hard. + It is not feasible to exhaustively search for the optimal timetable due to the huge + amount of computation involved. Instead, heuristics must be used. + Genetic algorithms have proven to be a successful way of generating satisfactory + solutions to many scheduling problems. + </para> + <para> + Evolutionary algorithms can also be used to tackle problems that humans don't really + know how to solve. + An EA, free of any human preconceptions or biases, can generate surprising solutions + that are comparable to, or better than, the best human-generated efforts. + It is merely necessary that we can recognise a good solution if + it were presented to us, even if we don't know <emphasis>how</emphasis> to create a + good solution. + In other words, we need to be able to formulate an effective fitness function. + </para> + <para> + Engineers working for NASA know a lot about physics. They know exactly which + characteristics make for a good communications antenna. But the process + of designing an antenna so that it has the necessary properties is hard. Even + though the engineers know what is required from the final antenna, they may not know + how to design the antenna so that it satisfies those requirements. + </para> + <para> + NASA's Evolvable Systems Group has used evolutionary algorithms to successfully + evolve antennas for use on satellites. These evolved antennas have irregular shapes + with no obvious symmetry (one of these antennas is pictured below). + It is unlikely that a human expert would have arrived at such an unconventional design. + Despite this, when tested these antennas proved to be extremely well adapted to their + purpose. + </para> + <informalfigure> + <mediaobject> + <imageobject> + <imagedata fileref="antenna.jpg" format="JPG" width="50%" align="center"/> + </imageobject> + <caption> + <para> + <link xlink:href="http://ti.arc.nasa.gov/projects/esg/research/antenna.htm">NASA Evolvable Systems Group</link> + </para> + </caption> + </mediaobject> + </informalfigure> + <section> + <title>Pre-requisites</title> + <indexterm><primary>encoding</primary></indexterm> + <para> + There are two requirements that must be met before an evolutionary algorithm can + be used for a particular problem. + Firstly, we need a way to encode candidate solutions to the problem. The simplest + encoding, and that used by many genetic algorithms, is a bit string. Each candidate + is simply a sequence of zeros and ones. + This encoding makes cross-over and mutation very straightforward, but that does not + mean that you cannot use more complicated representations. In fact, we will see + several instances of more advanced candidate representations in later chapters. + As long as we can devise a scheme for evolving the candidates, there really is no + restriction on the types that we can use. + Genetic programming (GP) is a good example of this. GP evolves computer programs + represented as syntax trees. + </para> + <indexterm><primary>fitness function</primary></indexterm> + <para> + The second requirement for applying evolutionary algorithms is that there must be a + way of evaluating partial solutions to the problem - the fitness function. It is + not sufficient to evaluate solutions as right or wrong, the fitness score needs to + indicate <emphasis>how right</emphasis> or, if your glass is half empty, + <emphasis>how wrong</emphasis> a candidate solution is. So a function that returns + either 0 or 1 is useless. A function that returns a score on a scale of 1 - 100 is + better. We need shades of grey, not just black and white, since this is how the + algorithm guides the random evolution to find increasingly better solutions. + </para> + </section> + </section> + <section> + <title>Implementing Evolutionary Algorithms</title> + <para> + If an evolutionary algorithm is a good fit for a particular problem, there are plenty of + options when it comes to implementing it. + You may choose to use a high-level programming language for simplicity, or a low-level + language for performance. + You could write all of the code yourself from scratch, or you could reuse pre-written + components and libraries. + In this book we will necessarily be using one particular approach, but it is worth noting + that there are alternatives. + </para> + <section> + <title>Choice of Programming Language</title> + <para> + Evolutionary algorithms can be implemented in any general purpose programming language. + Most programmers will simply choose the language that they are most comfortable with. + A quick web search will return examples of evolutionary programs written in C, C++, + Java, C#, Python, Ruby, Perl, Lisp and several other languages. + </para> + <para> + Performance may be a consideration when choosing a language. + Almost all evolutionary algorithms are CPU-bound. For this reason, compiled languages + typically offer better EA performance than interpreted languages. For + short-lived programs the difference is unlikely to be significant, but for + long-running programs it could be considerable. + </para> + <indexterm><primary>Java</primary></indexterm> + <para> + If you can recall the title of this book it should come as no surprise that we will be + using Java for all of the example code. Java offers a good balance of performance, + ease-of-use and a rich standard library. + </para> + </section> + <section> + <title>Evolution Frameworks</title> + <indexterm><primary>frameworks</primary></indexterm> + <para> + As we saw above, the basic outline of an evolutionary algorithm is fairly + straightforward. It consists of a main loop that performs one generation per iteration, + supplemented by a few functions to perform fitness evaluation, selection and + mutation/cross-over. When implementing a simple EA, writing this structural code is + not particularly onerous. However, if you write many different evolutionary + programs, as we will be doing in the remainder of this book, you end up writing code + that is very similar over and over again. + </para> + <para> + A good programmer will usually want to extract and reuse this common code. + Once you have done this, you have the basis of an evolutionary computation framework. + Typically this will consist of an evolution engine that is reusable and that can + accept different functions to customise fitness evaluation, selection and evolutionary + operators. + </para> + <para> + An alternative to using a home-grown framework is to choose a ready-made one. There + are open source evolutionary computation frameworks available for most programming languages. + For popular languages, such as C, C++ and Java, there are dozens. + </para> + <para> + The advantage of a + ready-made framework that is used by many other programmers is that it will have been well + tested and should be free of significant bugs and performance problems. It may also provide + advanced features such as parallel and/or distributed processing. + </para> + </section> + </section> +</chapter> diff --git a/watchmaker/book/src/xml/furtherreading.xml b/watchmaker/book/src/xml/furtherreading.xml new file mode 100644 index 0000000..c0ab4c3 --- /dev/null +++ b/watchmaker/book/src/xml/furtherreading.xml @@ -0,0 +1,6 @@ +<appendix xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Further Reading</title> + <para>TODO</para> +</appendix> diff --git a/watchmaker/book/src/xml/geneticprogramming.xml b/watchmaker/book/src/xml/geneticprogramming.xml new file mode 100644 index 0000000..cb37ed6 --- /dev/null +++ b/watchmaker/book/src/xml/geneticprogramming.xml @@ -0,0 +1,6 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Genetic Programming</title> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/gui.xml b/watchmaker/book/src/xml/gui.xml new file mode 100644 index 0000000..f339f77 --- /dev/null +++ b/watchmaker/book/src/xml/gui.xml @@ -0,0 +1,22 @@ +<appendix xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Building Graphical User Interfaces for Evolutionary Programs</title> + <subtitle>Using the Watchmaker Swing Module</subtitle> + <section> + <title>Evolution Controls</title> + <para>TODO</para> + <section> + <title>Number Generators</title> + <para>TODO</para> + </section> + </section> + <section> + <title>Updating the GUI from an EvolutionObserver</title> + <para>TODO</para> + </section> + <section> + <title>The Evolution Monitor</title> + <para>TODO</para> + </section> +</appendix> diff --git a/watchmaker/book/src/xml/interactive.xml b/watchmaker/book/src/xml/interactive.xml new file mode 100644 index 0000000..fa7369a --- /dev/null +++ b/watchmaker/book/src/xml/interactive.xml @@ -0,0 +1,6 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Interactive Evolutionary Algorithms</title> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/islands.xml b/watchmaker/book/src/xml/islands.xml new file mode 100644 index 0000000..e7943f8 --- /dev/null +++ b/watchmaker/book/src/xml/islands.xml @@ -0,0 +1,175 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Island Models</title> + <indexterm><primary>island models</primary></indexterm> + <indexterm><primary>Australia</primary></indexterm> + <para> + In the natural world, populations of organisms might be separated by geography. Left to evolve in isolation + over millions of years, vastly different species will occur in different locations. Consider Australia, + an island continent protected by its seas. With little opportunity for outside organisms to + interfere, and few opportunities for its land-based organisms to migrate to other land masses, Australian + wildlife evolved to be distinctly different from that of other continents and countries. The majority of + Australia's plant and animal species, including 84% of its mammals, are endemic. They occur nowhere else + in the world. + </para> + <indexterm><primary>Darwin, Charles</primary></indexterm> + <indexterm><primary>Galápagos Islands</primary></indexterm> + <para> + Australia is not the only island to exhibit such levels of endemism. It was a visit to the Galápagos + Islands in 1835 that started Charles Darwin on the path to formulating his theory of evolution. Darwin + noticed the pronounced differences between the species of mocking birds and tortoises present on the + different islands of the archipelago and began to speculate on how such variations might have occurred. + </para> + <para> + In the world of evolutionary computation we can mimic this idea of having multiple isolated populations + evolving in parallel. Having additional populations would increase the likelihood of finding a solution that + is close to the global optimum. However, it is not just a question of having a larger global population. + A system of 10 islands each with a population of 50 individuals is not equivalent to a single island with a + population of 500. The reason for this is that the island system partitions the search. If one island + prematurely converges on a sub-optimal solution it does not affect the evolution happening on the other + islands; they are following their own paths. A single large population does not have this in-built + resilience. + </para> + <section> + <title>Migration</title> + <indexterm><primary>migration</primary></indexterm> + <indexterm><primary>island models</primary><secondary>migration</secondary></indexterm> + <para> + There is of course no real difference between evolving 10 completely separate islands in parallel and running + the same single-population evolution 10 times in a row, other than how the computing resources are utilised. + In practice the populations are not kept permanently isolated from each other and there are occasional + opportunities for individuals to migrate between islands. + </para> + <para> + In nature external species have been introduced to foreign ecosystems in several ways. In an ice age the waters + that previously separated two land masses might freeze providing a route for land animals to migrate to + previously unreachable places. Microorganisms and insects have often strayed beyond their usual environment by + hitching a ride with larger species. + </para> + <indexterm><primary>rabbits</primary></indexterm> + <indexterm><primary>Austin, Thomas</primary></indexterm> + <para> + The effect of introducing a foreign species to a new environment can vary. The new species might be + ill-adapted to its new surroundings and quickly perish. Alternatively, a lack of natural predators + may cause it to flourish, often to the detriment of indigenous species. One such example is the + introduction of rabbits to Australia. Australia was a land without rabbits until the arrival of European + settlers. An Englishman named Thomas Austin released 24 rabbits into the wild of Victoria in October 1859 + with the intention of hunting them. If rabbits are famous for one thing it is for reproducing prodigiously. + The mild winters allowed year-round breeding and the absence of any natural rabbit predators, such as foxes, + allowed the Australian rabbit population to explode unchecked. Within 10 years an annual cull of two million + rabbits was having no noticeable effect on rabbit numbers and the habitats of some native animals were being + destroyed by the floppy-eared pests. Today there are hundreds of millions of rabbits in Australia, despite + efforts to reduce the population, and the name of Thomas Austin is widely cursed for his catastrophic lack + of foresight. + </para> + <para> + While such invasions of separate species provide a useful analogy for what can happen when we introduce migration + into island model evolutionary algorithms, we are specifically interested in the effects of migration involving + genetically different members of the same species. This is because, in our simplified model of evolution, + all individuals are compatible and can reproduce. The island model of evolution provides the isolation necessary + for diversity to thrive while still providing opportunities for diverse individuals to be combined to produce + potentially fitter offspring. + </para> + <para> + In an island model, the isolation of the separate populations often leads to different traits originating on + different islands. Migration brings these diverse individuals together occasionally to see what happens when + they are combined. Remember that, even if the immigrants are weak, cross-over can result in offspring that are + fitter than either of their parents. In this way, the introduction to the population of new genetic building + blocks may result in evolutionary progress even if the immigrants themselves are not viable in the new + population. + </para> + </section> + <section> + <title>Islands in the Watchmaker Framework</title> + <indexterm><primary><classname>IslandEvolution</classname></primary></indexterm> + <para> + The Watchmaker Framework for Evolutionary Computation supports islands models via the + <classname>IslandEvolution</classname> class. Each island is a self-contained + <classname>EvolutionEngine</classname> just like those we have been using previously for single-population + evolutionary algorithms. The evolution is divided into <emphasis>epochs</emphasis>. Each epoch consists + of a fixed number of generations that each island completes in isolation. At the end of an epoch migration + occurs. Then, if the termination conditions are not yet satisfied, a new epoch begins. + </para> + <para> + The <classname>IslandEvolution</classname> supports pluggable migration strategies via different implementations + of the <interfacename>Migration</interfacename> interface. An island version of the string evolution example + from <xref linkend="watchmaker_chapter" /> might look something like this: + </para> + <indexterm><primary><interfacename>Migration</interfacename></primary></indexterm> + <indexterm><primary><classname>RingMigration</classname></primary></indexterm> + <informalexample> + <programlisting language="java"> +<![CDATA[IslandEvolution<String> engine + = new IslandEvolution<String>(5, // Number of islands. + new RingMigration(), + candidateFactory, + evolutionaryOperator, + fitnessEvaluator, + selectionStrategy, + rng); + +engine.evolve(100, // Population size per island. + 5, // Elitism for each island. + 50, // Epoch length (no. generations). + 3, // Migrations from each island at each epoch. + new TargetFitness(0, false));]]> + </programlisting> + </informalexample> + <indexterm><primary><interfacename>IslandEvolutionObserver</interfacename></primary></indexterm> + <indexterm><primary><methodname>populationUpdate</methodname></primary></indexterm> + <indexterm><primary><methodname>islandPopulationUpdate</methodname></primary></indexterm> + <para> + We can add listeners to an <classname>IslandEvolution</classname> object, just as we can with individual + <interfacename>EvolutionEngine</interfacename>s. We use a different interface for this though, + <interfacename>IslandEvolutionObserver</interfacename>, which provides two call-backs. + The <methodname>populationUpdate</methodname> method reports the global state of the combined population + of all islands at the end of each epoch. The <methodname>islandPopulationUpdate</methodname> method reports + the state of individual island populations at the end of each generation. + </para> + <section> + <title>Advanced Usage</title> + <indexterm><primary><classname>GenerationalEvolutionEngine</classname></primary><secondary>island evolution</secondary></indexterm> + <para> + In the example code above we specified how many islands we wanted to use and the + <classname>IslandEvolution</classname> class created one <classname>GenerationalEvolutionEngine</classname> + for each island. Using this approach all of the islands have the same configuration; they use the same + candidate factory, evolutionary operator(s) and selection strategy. This is the easiest way to create an + island system but it is also possible to construct each island individually for ultimate flexibility. + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[List<EvolutionEngine<String>> islands + = new ArrayList<EvolutionEngine<String>>(); + +// Create individual islands here and add them to the list. +// ... + +IslandEvolution<String> engine + = new IslandEvolution<String>(islands, + new RingMigration(), + false, // Natural fitness? + rng);]]> + </programlisting> + </informalexample> + <para> + One reason you might choose to construct the islands explicitly is that it makes it possible to configure + individual islands differently. You may choose to have different islands use different parameters + for evolutionary operators, or even to use different evolutionary operators all together. Alternatively, + you could use the same evolutionary operators and parameters but have different selection strategies so that + some islands have stronger selection pressure than others. You should generally use the same fitness function + for all islands though, otherwise you might get some strange results. + </para> + <indexterm><primary><classname>SteadyStateEvolutionEngine</classname></primary><secondary>island evolution</secondary></indexterm> + <indexterm><primary><classname>EvolutionStrategyEngine</classname></primary><secondary>island evolution</secondary></indexterm> + <para> + Another possible reason for creating the islands explicitly is so you don't have to use the standard + <classname>GenerationalEvolutionEngine</classname> for the islands. You can choose to use any implementation + of the <interfacename>EvolutionEngine</interfacename> interface, such as the + <classname>SteadyStateEvolutionEngine</classname> class or the <classname>EvolutionStrategyEngine</classname> + class. You can even use a mixture of different island types with the same + <classname>IslandEvolution</classname> object. + </para> + </section> + </section> +</chapter> diff --git a/watchmaker/book/src/xml/multiobjective.xml b/watchmaker/book/src/xml/multiobjective.xml new file mode 100644 index 0000000..d453c0f --- /dev/null +++ b/watchmaker/book/src/xml/multiobjective.xml @@ -0,0 +1,6 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Multi-Objective Optimisations</title> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/performance.xml b/watchmaker/book/src/xml/performance.xml new file mode 100644 index 0000000..6f050bb --- /dev/null +++ b/watchmaker/book/src/xml/performance.xml @@ -0,0 +1,157 @@ +<appendix xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Optimising for Performance</title> + <para> + This appendix lists some suggestions on how to get the best possible performance from your + evolutionary Java programs. Much of the advice here applies whether or not you are using the + Watchmaker Framework to develop your evolutionary programs. + </para> + <para> + As with all optimisations in software development, the golden rule is don't do it unless you have + a demonstrable need for improved performance. Optimisations often introduce complexity and make + code harder to maintain. Before starting on any optimisations, always use a profiler to identify + the bottlenecks in your application. This will pinpoint the areas where optimisations are most + likely to beneficial. It is pointless to expend effort to try to speed up a routine that accounts + for only 0.1% of the CPU time. + </para> + <section> + <title>Optimising the Fitness Evaluator</title> + <indexterm><primary>fitness function</primary><secondary>optimisation of</secondary></indexterm> + <para> + For most non-trivial evolutionary algorithms, the bulk of the work is the evaluation of + candidate solutions. For this reason the fitness function is often the obvious place to + make improvements. A fitness evaluator should do no more work than is absolutely + necessary on each invocation. If there is some initialisation that is repeated unnecessarily, + consider moving it to the constructor. If similar calculations are performed every time, + consider pre-computing the possible results and using a look-up table. When you consider + that the evaluator may be invoked millions of times in a single run, it is clear that even + small optimisations to the fitness function may add up to substantial reductions in running + time. + </para> + <section> + <title>The Caching Fitness Evaluator</title> + <indexterm><primary>CachingFitnessEvaluator</primary></indexterm> + <indexterm><primary>fitness function</primary><secondary>caching</secondary></indexterm> + <indexterm><primary>elitism</primary></indexterm> + <para> + In some evolutionary programs individuals can survive from generation to generation unmodified. + The most obvious example of this is elitism. Individuals that are preserved through elitism + will appear unaltered in the next generation and may survive for many generations. Individuals + may also survive without modification if the evolutionary operators in use are probabilistic and + don't always affect every candidate. + </para> + <para> + If fitness evaluations are expensive, it is wasteful to repeatedly recalculate fitness values + for unaltered individuals. The Watchmaker Framework provides the + <classname>org.uncommons.watchmaker.framework.CachingFitnessEvaluator</classname> class to + address this problem. It acts as a wrapper for your fitness evaluator and caches the results + of fitness calculations. If the same candidate is evaluated twice, the cached value is returned + the second time thus avoiding the cost of recalculating the fitness score. The cache uses Java's + weak references to avoid memory leakage (if the candidate does not survive, the associated cache + entry will also be garbage collected). + </para> + <note> + <para> + 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 + <emphasis>isolated</emphasis> and <emphasis>repeatable</emphasis>. 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. + Caching should not be used if it is possible for multiple evaluations of the same candidate + to return different scores. + </para> + </note> + </section> + </section> + <section> + <title>Minimising the Search Space</title> + <para> + An evolutionary algorithm is a type of non-deterministic search. The algorithm is + searching the space of all possible solutions to find one that is good enough. The + larger the search space, the longer it is likely to take to converge on a + satisfactory solution. + For this reason, anything we can do to constrain the search space, without + handicapping the algorithm, is likely to be beneficial. This includes choosing + an efficient candidate representation and using evolutionary operators that + avoid generating useless or invalid solutions. + A little intelligent design can go a long way. + </para> + </section> + <section> + <title>Random Number Generators</title> + <indexterm><primary>random number generator</primary></indexterm> + <indexterm><primary>Random</primary></indexterm> + <indexterm><primary>RNG</primary></indexterm> + <indexterm><primary>SecureRandom</primary></indexterm> + <para> + The random number generator (RNG) is a core component of any evolutionary simulation. It is + used for selection, for cross-over and for mutation. A slow random number generator can be + a bottleneck. Most programming languages provide a mechanism to generate random numbers. + Unfortunately, few of them are ideal. The Java standard library includes two RNGs, + <classname>java.util.Random</classname> and <classname>java.security.SecureRandom</classname>. + These should be avoided for statistical and performance reasons respectively. + </para> + <indexterm><primary>MersenneTwisterRNG</primary></indexterm> + <para> + The Watchmaker Framework comes bundled with three high-quality RNGs provided by the Uncommons + Maths project. Of these, the <classname>org.uncommons.maths.random.MersenneTwisterRNG</classname> + is the most suitable for the majority of evolutionary programs. Alternatively, you can use any + third party RNG that is a sub-class of <classname>java.util.Random</classname>. + </para> + </section> + <section> + <title>JVM Options</title> + <indexterm><primary>Java Virtual Machine</primary></indexterm> + <indexterm><primary>JVM</primary></indexterm> + <para> + The Java Virtual Machine (JVM) is a complex piece of software. It is designed to run a huge + variety different programs. As such, its default configuration is not optimised for the + particular needs of evolutionary computation. This section lists some of the JVM options + that you can tweak to try to achieve better performance. + </para> + <section> + <title>Server VM</title> + <indexterm><primary>server VM</primary></indexterm> + <para> + The Sun JVM provides two modes of operation, one optimised for client applications (the + default) and one for server applications. The server VM takes marginally longer to start + up but provides substantially better performance for long-running processes and is therefore + a better choice for most evolutionary algorithms. The server VM is enabled using the + <literal>-server</literal> switch. + </para> + </section> + <section> + <title>Garbage Collection</title> + <indexterm><primary>garbage collection</primary></indexterm> + <para> + Evolutionary algorithms create many short-lived objects. Modern JVMs, with their generational + garbage collectors, are typically well tuned for this usage pattern. However, you may find + that by modifying the settings you are able to improve throughput. + </para> + <para> + Garbage collectors make a trade-off between overall throughput and pause time. For + evolutionary algorithms we typically want to maximise throughput, even at the expense of + introducing noticeable pauses in the program's execution. What is most important is how soon + the program completes, not how smoothly it runs. + </para> + <para> + You can get information on what the garbage collector is doing by starting the JVM with the + <literal>-verbosegc</literal> switch. If you find that the program is spending a lot of time + collecting garbage, it may be because it is short of memory. If you have sufficient RAM, + increasing the maximum size of the Java heap (using the <literal>-Xmx</literal> switch) may + improve things. + </para> + </section> + <section> + <title>Alternative JVMs</title> + <para> + Sun Microsystems is not the only provider of virtual machines for Java. If your platform is + supported, you may also have the option of using a JVM from BEA, IBM or some other third party. + These virtual machines have different performance characteristics and different garbage + collector implementations. If you have tried everything else and still need something faster, + you may find that a different JVM will perform better. Then again, it may not. + </para> + </section> + </section> +</appendix> diff --git a/watchmaker/book/src/xml/preface.xml b/watchmaker/book/src/xml/preface.xml new file mode 100644 index 0000000..9d2e1ec --- /dev/null +++ b/watchmaker/book/src/xml/preface.xml @@ -0,0 +1,29 @@ +<preface xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Preface</title> + <para> + This book is intended to be a pragmatic, hands-on guide to implementing evolutionary + algorithms. The emphasis is on writing useful evolutionary programs and solving interesting + problems. This is not intended to be a thorough theoretical guide. I will introduce the + key evolutionary computation concepts and demonstrate their practical application, but I will + endeavour to keep abstract theory and mathematics to the minimum necessary. + For a more academic treatment of the material, I recommend Melanie Mitchell's + <emphasis>An Introduction to Genetic Algorithms</emphasis> and A.E. Eiben and J.E. Smith's + <emphasis>Introduction to Evolutionary Computing</emphasis>. + </para> + <para> + Though evolutionary algorithms can be implemented in any general purpose programming language, + all of the examples in this book are presented using Java. Java is one of the most widely + used programming languages in the world today. As such, many software developers are already + familiar with Java and able to understand programs written in it, even if they usually + develop in other languages. Java provides a good balance of performance, ease-of-use and + a rich standard library. + </para> + <para> + The concepts discussed in this book are not tied to a particular programming language, + so there is no reason why you couldn't implement the ideas in another language if you + preferred. + </para> +</preface> + diff --git a/watchmaker/book/src/xml/salesman.xml b/watchmaker/book/src/xml/salesman.xml new file mode 100644 index 0000000..9eac594 --- /dev/null +++ b/watchmaker/book/src/xml/salesman.xml @@ -0,0 +1,17 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>The Travelling Salesman</title> + <mediaobject> + <imageobject> + <imagedata fileref="travelling_salesman_problem.png" + format="PNG" scalefit="1" width="100%" /> + </imageobject> + <caption> + <para> + <link href="http://xkcd.com/399">http://xkcd.com/399</link> + </para> + </caption> + </mediaobject> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/selection.xml b/watchmaker/book/src/xml/selection.xml new file mode 100644 index 0000000..95525b1 --- /dev/null +++ b/watchmaker/book/src/xml/selection.xml @@ -0,0 +1,160 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd" + id="selection_chapter"> + <title>Selection Strategies & Elitism</title> + <para> + Selection is an important part of an evolutionary algorithm. Without selection directing the algorithm towards + fitter solutions there would be no progress. Selection must favour fitter candidates over weaker candidates but + beyond that there are no fixed rules. Furthermore, there is no one strategy that is best for all problems. Some + strategies result in fast convergence, others will tend to produce a more thorough exploration of the search space. + An evolutionary algorithm that appears ineffective with one selection strategy may be transformed by switching to + a strategy with different characteristics. This chapter describes the most commonly used selection strategies + (all of these strategies are supported in the Watchmaker Framework for Evolutionary Computation via different + implementations of the <classname>SelectionStrategy</classname> interface). + </para> + <section> + <title>Truncation Selection</title> + <indexterm><primary>truncation selection</primary></indexterm> + <indexterm><primary>selection</primary><secondary>truncation</secondary></indexterm> + <para> + Truncation selection is the simplest and arguably least useful selection strategy. Truncation selection + simply retains the fittest <varname>x</varname>% of the population. These fittest individuals are duplicated so + that the population size is maintained. For example, we might select the fittest 25% from a population of 100 + individuals. In this case we would create four copies of each of the 25 candidates in order to maintain a population + of 100 individuals. This is an easy selection strategy to implement but it can result in premature convergence as + less fit candidates are ruthlessly culled without being given the opportunity to evolve into something better. + Nevertheless, truncation selection can be an effective strategy for certain problems. + </para> + </section> + <section> + <title>Fitness-Proportionate Selection</title> + <indexterm><primary>fitness-proportionate selection</primary></indexterm> + <indexterm><primary>selection</primary><secondary>fitness-proportionate</secondary></indexterm> + <para> + A better approach to selection is to give every individual a chance of being selected to breed but to make + fitter candidates more likely to be chosen than weaker individuals. This is achieved by making an individual's + survival probability a function of its fitness score. Such strategies are known as + <emphasis>fitness-proportionate selection</emphasis>. + </para> + <section> + <title>Roulette Wheel Selection</title> + <indexterm><primary>roulette wheel selection</primary></indexterm> + <indexterm><primary>selection</primary><secondary>roulette wheel</secondary></indexterm> + <para> + The most common fitness-proportionate selection technique is called <emphasis>Roulette Wheel + Selection</emphasis>. Conceptually, each member of the population is allocated a section of an imaginary + roulette wheel. Unlike a real roulette wheel the sections are different sizes, proportional to the + individual's fitness, such that the fittest candidate has the biggest slice of the wheel and the weakest + candidate has the smallest. The wheel is then spun and the individual associated with the winning section + is selected. The wheel is spun as many times as is necessary to select the full set of parents for the next + generation. + </para> + <para> + Using this technique it is possible (probable) that one or more individuals is selected multiple times. + That's OK, it's what we want to happen. Remember that we are not selecting the members of the next + generation, we are selecting their parents and it is possible for an individual to be a parent multiple times. + If there is a particularly fit member of the population we would expect it to be more successful at producing + offspring than a weaker rival. + </para> + </section> + <section> + <title>Stochastic Universal Sampling</title> + <indexterm><primary>stochastic universal sampling</primary></indexterm> + <para> + <emphasis>Stochastic Universal Sampling</emphasis> is an elaborately-named variation of roulette wheel + selection. Stochastic Universal Sampling ensures that the observed selection frequencies of each individual + are in line with the expected frequencies. So if we have an individual that occupies 4.5% of the wheel + and we select 100 individuals, we would expect on average for that individual to be selected between four + and five times. Stochastic Universal Sampling guarantees this. The individual will be selected either four + times or five times, not three times, not zero times and not 100 times. Standard roulette wheel selection + does not make this guarantee. + </para> + <para> + Stochastic Universal Sampling works by making a single spin of the roulette wheel. This provides a starting + position and the first selected individual. The selection process then proceeds by advancing all the way + around the wheel in equal sized steps, where the step size is determined by the number of individuals to be + selected. So if we are selecting 30 individuals we will advance by + <inlineequation><mathphrase>1/30 x 360 degrees</mathphrase></inlineequation> for each selection. Note that + this does not mean that every candidate on the wheel will be selected. Some weak individuals will have very + thin slices of the wheel and these might be stepped over completely depending on the random starting position. + </para> + </section> + </section> + <section> + <title>Rank Selection</title> + <indexterm><primary>rank selection</primary></indexterm> + <indexterm><primary>selection</primary><secondary>rank</secondary></indexterm> + <para> + <emphasis>Rank Selection</emphasis> is similar to fitness-proportionate selection except that selection + probability is proportional to relative fitness rather than absolute fitness. In other words, it doesn't make + any difference whether the fittest candidate is ten times fitter than the next fittest or 0.001% fitter. In + both cases the selection probabilities would be the same; all that matters is the ranking relative to other + individuals. + </para> + <para> + Rank selection will tend to avoid premature convergence by tempering selection + pressure for large fitness differentials that occur in early generations. Conversely, by amplifying small + fitness differences in later generations, selection pressure is increased compared to alternative selection + strategies. + </para> + </section> + <section> + <title>Tournament Selection</title> + <indexterm><primary>selection</primary><secondary>tournament</secondary></indexterm> + <indexterm><primary>tournament selection</primary></indexterm> + <para> + <emphasis>Tournament Selection</emphasis> is among the most widely used selection strategies in evolutionary + algorithms. It works well for a wide range of problems, it can be implemented efficiently, and it is amenable to + parallelisation. + </para> + <para> + At its simplest tournament selection involves randomly picking two individuals from the population and staging + a tournament to determine which one gets selected. The "tournament" isn't much of a tournament at all, it + just involves generating a random value between zero and one and comparing it to a pre-determined selection + probability. If the random value is less than or equal to the selection probability, the fitter candidate is + selected, otherwise the weaker candidate is chosen. The probability parameter provides a convenient mechanism + for adjusting the selection pressure. In practise it is always set to be greater than 0.5 in order to favour + fitter candidates. The tournament can be extended to involve more than two individuals if desired. + </para> + </section> + <section> + <title>Sigma Scaling</title> + <indexterm><primary>selection</primary><secondary>sigma scaling</secondary></indexterm> + <indexterm><primary>sigma scaling</primary></indexterm> + <para> + Like rank selection, <emphasis>Sigma Scaling</emphasis> attempts to moderate selection pressure over time + so that it is not too strong in early generations and not too weak once the population has stabilised and + fitness differences are smaller. The Greek letter Sigma is used in statistics to denote standard deviation + and that's what it means here too. The standard deviation of the population fitness is used to scale the + fitness scores so that selection pressure is relatively constant over the lifetime of the evolutionary + program. + </para> + </section> + <section> + <title>Elitism</title> + <indexterm><primary>elitism</primary></indexterm> + <para> + Sometimes good candidates can be lost when cross-over or mutation results in offspring + that are weaker than the parents. Often the EA will re-discover these lost improvements + in a subsequent generation but there is no guarantee. To combat this we can use a + feature known as <emphasis>elitism</emphasis>. Elitism involves copying a small + propotion of the fittest candidates, unchanged, into the next generation. This can + sometimes have a dramatic impact on performance by ensuring that the EA does not waste + time re-discovering previously discarded partial solutions. + Candidate solutions that are preserved unchanged through elitism remain eligible for + selection as parents when breeding the remainder of the next generation. + </para> + <tip> + <para> + The Watchmaker Framework supports elitism via the second parameter to the + <methodname>evolve</methodname> method of an <interfacename>EvolutionEngine</interfacename>. + This elite count is the number of candidates in a generation that should be copied + unchanged from the previous generation, rather than created via evolution. Collectively + these candidates are the <emphasis>elite</emphasis>. So for a population size of 100, + setting the elite count to 5 will result in the fittest 5% of each generation being copied, + without modification, into the next generation. + </para> + </tip> + </section> +</chapter> diff --git a/watchmaker/book/src/xml/steadystate.xml b/watchmaker/book/src/xml/steadystate.xml new file mode 100644 index 0000000..2e032c8 --- /dev/null +++ b/watchmaker/book/src/xml/steadystate.xml @@ -0,0 +1,6 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>Steady-State Evolutionary Algorithms</title> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/sudoku.xml b/watchmaker/book/src/xml/sudoku.xml new file mode 100644 index 0000000..9c86740 --- /dev/null +++ b/watchmaker/book/src/xml/sudoku.xml @@ -0,0 +1,6 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <title>An Evolutionary Sudoku Solver</title> + <para>TODO</para> +</chapter> diff --git a/watchmaker/book/src/xml/watchmaker.xml b/watchmaker/book/src/xml/watchmaker.xml new file mode 100644 index 0000000..b18cb39 --- /dev/null +++ b/watchmaker/book/src/xml/watchmaker.xml @@ -0,0 +1,478 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd" + id="watchmaker_chapter"> + <title>The Watchmaker Framework</title> + <indexterm significance="preferred"><primary>Watchmaker Framework</primary></indexterm> + <para> + The Watchmaker Framework for Evolutionary Computation is an extensible, high-performance, + object-oriented framework for implementing platform-independent evolutionary algorithms + in Java. + It is freely available under a permissive Open Source licence. It can be downloaded from + <link xlink:href="http://watchmaker.uncommons.org">http://watchmaker.uncommons.org</link>. + </para> + <para> + This chapter introduces the core components of the Watchmaker Framework and shows how + they can be used to implement simple evolutionary algorithms such as the "Hello World" + example outlined in the previous chapter. + </para> + <section> + <title>The Evolution Engine</title> + <indexterm><primary><classname>GenerationalEvolutionEngine</classname></primary></indexterm> + <indexterm><primary><interfacename>EvolutionEngine</interfacename></primary></indexterm> + <para> + The central object of an evolutionary program built with the Watchmaker Framework is + the evolution engine. + </para> + <para> + The framework provides multiple implementations of the + <interfacename>EvolutionEngine</interfacename> interface, but the one that you will + usually want to use is <classname>GenerationalEvolutionEngine</classname>. This is a + general-purpose implementation of the evolutionary algorithm outline from chapter 1. + </para> + <para> + An <interfacename>EvolutionEngine</interfacename> has a single generic type parameter + that indicates the type of object that it can evolve. + For the "Hello World" program we need to be able to evolve Java strings. + Code that creates an engine that can evolve strings would look something like this: + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[EvolutionEngine<String> engine + = new GenerationalEvolutionEngine<String>(candidateFactory, + evolutionaryOperator, + fitnessEvaluator, + selectionStrategy, + rng);]]> + </programlisting> + </informalexample> + <para> + Once you have created an <interfacename>EvolutionEngine</interfacename>, your program + is as simple as calling the <methodname>evolve</methodname> method with appropriate + arguments. + However, as you can see from the code snippet above, there is a little bit of work to + be done first in order to create an <interfacename>EvolutionEngine</interfacename> that + is configured appropriately for the given problem. + The constructor of the <classname>GenerationalEvolutionEngine</classname> class requires + five objects. These are: + </para> + <itemizedlist> + <listitem>A Candidate Factory</listitem> + <listitem>An Evolutionary Operator</listitem> + <listitem>A Fitness Evaluator</listitem> + <listitem>A Selection Strategy</listitem> + <listitem>A Random Number Generator</listitem> + </itemizedlist> + </section> + <section> + <title>The Candidate Factory</title> + <indexterm significance="preferred"><primary><interfacename>CandidateFactory</interfacename></primary></indexterm> + <para> + The first object that needs to be plugged into the evolution engine is a candidate + factory. Every evolutionary simulation must start with an initial population of + candidate solutions and the <interfacename>CandidateFactory</interfacename> interface + is the mechanism by which the evolution engine creates this population. + </para> + <para> + A candidate factory implementation has an associated type. It can only create + objects of that type. The type must match the type of the evolution engine that + it is plugged into. + You can write your own implementation of <interfacename>CandidateFactory</interfacename> + for your program or, if you are using a common type such as strings, lists or + arrays, you may be able to use a ready-made factory from the + <package>org.uncommons.watchmaker.framework.factories</package> package. + </para> + <indexterm><primary><classname>StringFactory</classname></primary></indexterm> + <para> + For our "Hello World" program, we can use the provided + <classname>StringFactory</classname>: + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[// Define the set of permitted characters (A-Z plus space). +char[] chars = new char[27]; +for (char c = 'A'; c <= 'Z'; c++) +{ + chars[c - 'A'] = c; +} +chars[26] = ' '; + +// Factory for random 11-character Strings. +CandidateFactory<String> factory = new StringFactory(chars, 11);]]> + </programlisting> + </informalexample> + <tip> + <indexterm significance="preferred"><primary><classname>AbstractCandidateFactory</classname></primary></indexterm> + <para> + When writing your own <interfacename>CandidateFactory</interfacename> implementations, + it is easiest to extend the provided <classname>AbstractCandidateFactory</classname> + base class since there is then only a single method that must be implemented. + </para> + </tip> + </section> + <section> + <title>Evolutionary Operators</title> + <indexterm significance="preferred"><primary><interfacename>EvolutionaryOperator</interfacename></primary></indexterm> + <para> + Evolutionary operators are the components that perform the actual evolution of a + population. Cross-over is an evolutionary operator, as is mutation. + </para> + <para> + In the Watchmaker Framework, evolutionary operators are defined in terms of the + <interfacename>EvolutionaryOperator</interfacename> interface. This interface + declares a single method that takes a list of selected individuals and returns a + list of evolved individuals. Some operators (i.e. mutation) will process one + individual at a time, whereas others will process individuals in groups + (cross-over processes two individuals at a time). + </para> + <indexterm><primary><classname>StringCrossover</classname></primary></indexterm> + <indexterm><primary><classname>StringMutation</classname></primary></indexterm> + <para> + As with candidate factories, evolutionary operators have associated types that + must be compatible with the type of the evolution engine that they are used with. + And, as with candidate factories, the framework provides several ready-made operators + for common types. These can be found in the + <package>org.uncommons.watchmaker.framework.operators</package> package. The + cross-over and mutation operators that we need for our "Hello World" program are + provided by the <classname>StringCrossover</classname> and + <classname>StringMutation</classname> classes. + </para> + <section> + <title>The Evolution Pipeline</title> + <indexterm significance="preferred"><primary><classname>EvolutionPipeline</classname></primary></indexterm> + <para> + Alert readers will have noticed that the evolution engine constructor only accepts + a single evolutionary operator. So how can we use both cross-over and mutation? + The answer is provided by the <classname>EvolutionPipeline</classname> operator. + This is a compound evolutionary operator that chains together multiple operators of + the same type. + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[List<EvolutionaryOperator<String>> operators + = new LinkedList<EvolutionaryOperator<String>>(); +operators.add(new StringCrossover()); +operators.add(new StringMutation(chars, new Probability(0.02))); + +EvolutionaryOperator<String> pipeline + = new EvolutionPipeline<String>(operators);]]> + </programlisting> + </informalexample> + <note> + <para> + The evolution pipeline is just one of many useful operators included + in the <package>org.uncommons.watchmaker.framework.operators</package> package. + Elaborate evolution schemes can be constructed from combinations of these + operators. + Users of the Watchmaker Framework should take a few minutes to browse the API + documentation and familiarise themselves with the available classes. + </para> + </note> + </section> + </section> + <section> + <title>The Fitness Evaluator</title> + <indexterm significance="preferred"><primary><interfacename>FitnessEvaluator</interfacename></primary></indexterm> + <para> + So far we've been able to build our evolutionary program by simply combining instances + of classes provided by the framework. There is one part of the program that we will + always have to write for ourselves though and that is the fitness function, which is + necessarily different for every program. + </para> + <para> + In the Watchmaker Framework, a fitness function is written by implementing the + <interfacename>FitnessEvaluator</interfacename> interface. The + <methodname>getFitness</methodname> method of this interface takes a candidate solution + and returns its fitness score as a Java double. The method actually takes two + arguments, but we can ignore the second for now. + </para> + <para> + The listing below is a fitness evaluator for the "Hello World" program. It + simply assigns one point for each character in the candidate string that + matches the corresponding position in the target string. + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[public class StringEvaluator implements FitnessEvaluator<String> +{ + private final String targetString = "HELLO WORLD"; + + /** + * Assigns one "fitness point" for every character in the + * candidate String that matches the corresponding position in + * the target string. + */ + public double getFitness(String candidate, + List<? extends String> population) + { + int matches = 0; + for (int i = 0; i < candidate.length(); i++) + { + if (candidate.charAt(i) == targetString.charAt(i)) + { + ++matches; + } + } + return matches; + } + + public boolean isNatural() + { + return true; + } +}]]> + </programlisting> + </informalexample> + <indexterm><primary>fitness function</primary><secondary>natural</secondary></indexterm> + <indexterm><primary>natural fitness</primary></indexterm> + <para> + By some fitness measures, a higher value indicates a fitter solution. In other + cases a lower value is better. The <methodname>isNatural</methodname> method + of a fitness evaluator simply specifies which scenario applies. In Watchmaker + Framework terminology, a <emphasis>natural</emphasis> fitness function is one that + returns higher values for fitter individuals. + </para> + </section> + <section> + <title>Selection Strategy</title> + <indexterm><primary>selection</primary></indexterm> + <indexterm><primary>SelectionStrategy</primary></indexterm> + <para> + Selection is a key ingredient in any evolutionary algorithm. It's what determines + which individuals survive to reproduce and which are discarded. All we've said about + selection so far is that it should favour fitter individuals. This definition permits + several different implementations. The Watchmaker Framework includes all of the most + common selection strategies in the + <package>org.uncommons.watchmaker.framework.selection</package> package. These are + sufficient for most evolutionary algorithms but, if necessary, it is straightforward + to write your own implementation of the <interfacename>SelectionStrategy</interfacename> + interface. + </para> + <indexterm><primary>RouletteWheelSelection</primary></indexterm> + <para> + Some selection strategies work better than others for certain problems. Often a little + trial-and-error is required to pick the best option. We will delve into the details of + various selection strategies in <xref linkend="selection_chapter" />, but for now we will + just create an instance of the <classname>RouletteWheelSelection</classname> class and use + that for our "Hello World" application. + </para> + <indexterm><primary>fitness-proportionate selection</primary></indexterm> + <indexterm><primary>roulette wheel selection</primary></indexterm> + <indexterm><primary>selection</primary><secondary>fitness-proportionate</secondary></indexterm> + <indexterm><primary>selection</primary><secondary>roulette wheel</secondary></indexterm> + <para> + <emphasis>Roulette wheel selection</emphasis> is the most common type of + <emphasis>fitness-proportionate selection</emphasis>. + It gives all individuals a chance of being selected but favours the fitter + individuals since an individual's selection probability is derived from its + fitness score. + </para> + </section> + <section> + <title>Random Number Generator</title> + <indexterm><primary>random number generator</primary></indexterm> + <indexterm><primary>Random</primary></indexterm> + <indexterm><primary>RNG</primary></indexterm> + <indexterm><primary>SecureRandom</primary></indexterm> + <para> + The final dependency that must be satisfied in order to create an evolution engine + is the random number generator (RNG). An evolution engine has a single RNG that it + passes to its candidate factory, evolutionary operator and selection strategy. + An ideal RNG is both fast and statistically random. We <emphasis>could</emphasis> + use the standard Java RNG, <classname>java.util.Random</classname>, but its output + is not as random as it should be. The other RNG in the standard library, + <classname>java.security.SecureRandom</classname> is much better in this respect + but can be slow. + </para> + <indexterm><primary>MersenneTwisterRNG</primary></indexterm> + <para> + Fortunately, the Watchmaker Framework provides alternatives. The + <classname>org.uncommons.maths.random.MersenneTwisterRNG</classname> random number + generator is both fast and statistically sound. It is usually the best choice + when creating an evolution engine. + </para> + </section> + <section> + <title>Completing the Jigsaw</title> + <para> + We've now got all of the necessary pieces to complete the "Hello World" example + application. Assuming that you've already created the + <classname>StringEvaluator</classname> class (defined above) in a separate file, + the code needed to create the evolution engine looks like this: + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[// Create a factory to generate random 11-character Strings. +char[] chars = new char[27]; +for (char c = 'A'; c <= 'Z'; c++) +{ + chars[c - 'A'] = c; +} +chars[26] = ' '; +CandidateFactory<String> factory = new StringFactory(chars, 11); + +// Create a pipeline that applies cross-over then mutation. +List<EvolutionaryOperator<String>> operators + = new LinkedList<EvolutionaryOperator<String>>(); +operators.add(new StringCrossover()) +operators.add(new StringMutation(chars, new Probability(0.02))); +EvolutionaryOperator<String> pipeline + = new EvolutionPipeline<String>(operators); + +FitnessEvaluator<String> fitnessEvaluator = new StringEvaluator(); +SelectionStrategy<Object> selection = new RouletteWheelSelection(); +Random rng = new MersenneTwisterRNG(); + +EvolutionEngine<String> engine + = new GenerationalEvolutionEngine<String>(factory, + pipeline, + fitnessEvaluator, + selection, + rng);]]> + </programlisting> + </informalexample> + <indexterm><primary>evolve method</primary></indexterm> + <indexterm><primary>population</primary><secondary>size of</secondary></indexterm> + <para> + The listing above only creates the evolution engine, it does not perform any + evolution. For that we need to call the <methodname>evolve</methodname> method. + The <methodname>evolve</methodname> method takes three parameters. The first + is the size of the population. This is the number of candidate solutions that + exist at any time. A bigger population will often result in a satisfactory + solution being found in fewer generations. On the other hand, the processing + of each generation will take longer because there are more individuals to deal + with. For the "Hello World" program, a population size of 10 is fine. + </para> + <para> + The second parameter is concerned with <emphasis>elitism</emphasis>. Elitism + is explained in <xref linkend="selection_chapter" />. For now, just use a value of zero. + The final varargs parameter specifies one or more termination conditions. + </para> + <section> + <title>Termination Conditions</title> + <indexterm><primary>TerminationCondition</primary></indexterm> + <indexterm><primary>TargetFitness</primary></indexterm> + <para> + Termination conditions make the evolution stop. There are a few reasons why + we would like the evolution to stop. The most obvious is because we have found the + solution that we are looking for. In the case of the "Hello World" program, that + is when we have found the target string. The target string has a fitness score of + 11 so we use the <classname>TargetFitness</classname> condition. + </para> + <para> + To complete the evolutionary "Hello World" application, add the following two lines: + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[String result = engine.evolve(10, 0, new TargetFitness(11, true)); +System.out.println(result);]]> + </programlisting> + </informalexample> + <note> + <indexterm><primary>ElapsedTime</primary></indexterm> + <indexterm><primary>GenerationCount</primary></indexterm> + <indexterm><primary>Stagnation</primary></indexterm> + <para> + When we move on to less trivial evolutionary programs, we will rarely be able to + specify the outcome so precisely. The + <package>org.uncommons.watchmaker.framework.termination</package> package includes + other termination conditions that can be used. For example, we may want the program + to run for a certain period of time, or a certain number of generations, and then + return the best solution it has found up until that point. The + <classname>ElapsedTime</classname> and <classname>GenerationCount</classname> + conditions provide this functionality. Alternatively, we may want the program to + continue as long as it is finding progressively better solutions. The + <classname>Stagnation</classname> condition will terminate the evolution after a + set number of generations pass without any improvement in the fitness of the fittest + candidate. + If multiple termination conditions are specified, the evolution will stop as soon + as any one of them is satisfied. + </para> + </note> + </section> + <section> + <title>Evolution Observers</title> + <para> + Compile and run the above code and, perhaps after a brief pause, you'll see the + following output: + </para> + <informalexample> + <programlisting> +<![CDATA[ HELLO WORLD]]> + </programlisting> + </informalexample> + <indexterm><primary>EvolutionObserver</primary></indexterm> + <para> + This is quite probably the most convoluted "Hello World" program you'll ever write. + It also gives no hints as to its evolutionary nature. We can make the program more + interesting by adding an <interfacename>EvolutionObserver</interfacename> to report + on the progress of the evolution at the end of each generation. Add the following + code to your program before the call to the <methodname>evolve</methodname> method: + </para> + <informalexample> + <programlisting language="java"> +<![CDATA[engine.addEvolutionObserver(new EvolutionObserver<String>() +{ + public void populationUpdate(PopulationData<? extends String> data) + { + System.out.printf("Generation %d: %s\n", + data.getGenerationNumber(), + data.getBestCandidate()); + } +});]]> + </programlisting> + </informalexample> + <para> + Re-compile the program and run it again. This time you'll see all of the steps + taken to arrive at the target string: + </para> + <informalexample> + <programlisting> + Generation 0: JIKDORHOQZJ + Generation 1: ULLLFQWZPXG + Generation 2: UEULKFVFZLS + Generation 3: KLLLFKZGRLS + Generation 4: HLLLFKZGRLS + Generation 5: HEDPOYWOZLS + Generation 6: HEULKIWWZLD + Generation 7: HPRLOYWOZLS + Generation 8: HEULOYWOZLS + Generation 9: HEULOYWORLS + Generation 10: HEULOYWORLS + Generation 11: HPLLK WQRLH + Generation 12: HEBLOYWQRLS + Generation 13: HEULOYWOBLA + Generation 14: HEBLOIWMRLD + Generation 15: HEBLOIWMRLD + Generation 16: HEYLFNWQRLD + Generation 17: HEBLOIWORLS + Generation 18: HEBLOIWORLT + Generation 19: HEBLOKWGRLD + Generation 20: HELLAYWORLS + Generation 21: HELHOIWORLT + Generation 22: HEWLOIWORLS + Generation 23: HEBLOYCORLD + Generation 24: HELLKQWORLD + Generation 25: HELLOIWORLT + Generation 26: HELLOIWORLS + Generation 27: HELLKQWORLD + Generation 28: HELLFYWORLD + Generation 29: HELLOIWORLD + Generation 30: HELLOIWORLD + Generation 31: HELLOIWORLD + Generation 32: HELLOIWORLD + Generation 33: HELLOIWORLD + Generation 34: HELLOIWORLD + Generation 35: HELLOIWDRLD + Generation 36: HELLOIWORLD + Generation 37: HELLOIWORLD + Generation 38: HELLOPWORLD + Generation 39: HELLOIWORLD + Generation 40: HELLO WORLD + HELLO WORLD + </programlisting> + </informalexample> + </section> + </section> +</chapter> diff --git a/watchmaker/book/src/xml/website.xml b/watchmaker/book/src/xml/website.xml new file mode 100644 index 0000000..724e586 --- /dev/null +++ b/watchmaker/book/src/xml/website.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8" ?> +<book xmlns="http://docbook.org/ns/docbook" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://docbook.org/ns/docbook http://www.docbook.org/xml/5.0/xsd/docbook.xsd"> + <info> + <title>Evolutionary Computation in Java</title> + <subtitle>A Practical Guide to the Watchmaker Framework</subtitle> + <author> + <personname> + <firstname>Daniel</firstname> + <surname>Dyer</surname> + <othername>W.</othername> + </personname> + <email>dan@uncommons.org</email> + <uri>http://www.dandyer.co.uk</uri> + </author> + <copyright> + <year>2008</year> + <year>2009</year> + <year>2010</year> + </copyright> + <keywordset> + <keyword>algorithms</keyword> + <keyword>evolution</keyword> + <keyword>evolutionary algorithms</keyword> + <keyword>evolutionary computation</keyword> + <keyword>genetic algorithms</keyword> + <keyword>Java</keyword> + <keyword>programming</keyword> + <keyword>software</keyword> + </keywordset> + </info> + + <!-- Chapters --> + <xi:include href="evolution.xml" /> + <xi:include href="watchmaker.xml" /> + <xi:include href="selection.xml" /> + <xi:include href="islands.xml" /> + + <!-- Appendices --> + <xi:include href="performance.xml" /> + +</book> diff --git a/watchmaker/build.xml b/watchmaker/build.xml new file mode 100644 index 0000000..6292b47 --- /dev/null +++ b/watchmaker/build.xml @@ -0,0 +1,298 @@ +<!--=========================================================================== + Copyright 2006-2010 Daniel W. Dyer + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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 name="watchmaker" + xmlns:uncommons="antlib:org.uncommons.antlib" + default="dist" + basedir="."> + <description>Ant build file for the Watchmaker framework.</description> + + +<!-- ================================================================== + GLOBAL BUILD PROPERTIES +=================================================================== --> + + <!-- Project-global locations. --> + <property name="conf.dir" value="etc" /> + <property name="lib.dir" value="lib" /> + <property name="lib.compiletime" value="${lib.dir}/compiletime" /> + <property name="lib.runtime" value="${lib.dir}/runtime" /> + <property name="dist.dir" value="dist" /> + <property name="docs.dir" value="docs" /> + <property name="checkstyle-results.dir" value="${docs.dir}/checkstyle" /> + <property name="release.dir" value="release" /> + <!-- The "website" directory is a symlink to a copy of the Git repository + at http://github.com/dwdyer/watchmaker-website. --> + <property name="web.dir" value="website" /> + <property name="temp.dir" value="temp" /> + + <!-- Per-module locations. --> + <property name="src.dir" value="src" /> + <property name="java.dir" value="${src.dir}/java" /> + <property name="build.dir" value="build"/> + + + <!-- Classpath for compilation and tests. --> + <path id="tool.path"> + <fileset dir="${lib.dir}" includes="**/*.jar" /> + </path> + + <taskdef uri="antlib:org.uncommons.antlib" + resource="org/uncommons/antlib/antlib.xml" + classpathref="tool.path"/> + + + <property name="version" value="0.7.2"/> + <property name="artifact.identifier" value="watchmaker-framework-${version}"/> + + <!-- This is the minimum coverage percentage (for both lines and + branches) that will be tolerated. This is used to prevent + regressions in coverage. The threshold will be raised as + test coverage improves. --> + <property name="minimum.coverage" value="80" /> + + +<!-- ================================================================== + TARGETS FOR BUILDING THE SOFTWARE +=================================================================== --> + + <!-- Builds everything from scratch. --> + <target name="all" + depends="clean, dist, test, docs" + description="Builds everything, excluding docs, from scratch."/> + + + <!-- Deletes all directories and files created by the build process. --> + <target name="clean" + description="Remove all files created by the build process." > + <delete dir="${docs.dir}" /> + <delete dir="${dist.dir}" /> + <delete dir="${release.dir}" /> + <delete dir="${temp.dir}" /> + <uncommons:clean module="examples" /> + <uncommons:clean module="framework" /> + <uncommons:clean module="swing" /> + <uncommons:clean module="book" /> + </target> + + + <!-- Builds the core framework JAR. --> + <target name="framework" + description="Build the framework module."> + <uncommons:compile module="framework" /> + <uncommons:jar module="framework" jarfile="${artifact.identifier}.jar" /> + </target> + + + <!-- Builds the GUI module. --> + <target name="swing" + depends="framework" + description="Build the GUI module."> + <uncommons:compile module="swing" /> + <uncommons:jar module="swing" jarfile="watchmaker-swing-${version}.jar" /> + </target> + + + <!-- Builds the examples JAR, which depends on each of the other modules. --> + <target name="examples" + depends="framework, swing" + description="Build the examples."> + <uncommons:compile module="examples" /> + <uncommons:jar module="examples" + jarfile="watchmaker-examples-${version}.jar" + classpath="${artifact.identifier}.jar lib/uncommons-maths-1.2.2.jar lib/google-collect-1.0.jar watchmaker-swing-${version}.jar lib/jfreechart-1.0.13.jar lib/jcommon-1.0.16.jar" + mainclass="org.uncommons.watchmaker.examples.Launcher" /> + </target> + + + <!-- Copy all necessary files to distribution directory. --> + <target name="dist" + depends="framework, swing, examples" + description="Generate the project distribution." > + <uncommons:dist /> + <mkdir dir="${dist.dir}/src" /> + <copy todir="${dist.dir}/src" flatten="true"> + <fileset dir="." includes="**/${build.dir}/*-src.jar"/> + </copy> + </target> + + + <!-- Build source JAR files for inclusion in the release. --> + <target name="source" description="Build source JARs."> + <uncommons:source module="framework" jarfile="${artifact.identifier}-src.jar" /> + <uncommons:source module="swing" jarfile="watchmaker-swing-${version}-src.jar" /> + </target> + + + <!-- Create the release artifacts. --> + <target name="release" + depends="clean, source, dist, test, checkstyle, docs" + description="Creates the release archives."> + <uncommons:release name="${artifact.identifier}"> + <additionalcontents> + <tarfileset dir="examples/${java.dir}/main" + prefix="${artifact.identifier}/examples/src" + includes="**/*" /> + </additionalcontents> + </uncommons:release> + </target> + + + <target name="release-maven" + depends="clean, dist" + description="Deploys the software to the Java.net Maven repository."> + <uncommons:maven-deploy module="framework" + version="${version}" + username="${maven.user}" + password="${maven.password}"/> + <uncommons:maven-deploy module="swing" + version="${version}" + username="${maven.user}" + password="${maven.password}"/> + </target> + + + +<!-- ================================================================== + TARGETS FOR GENERATING TEST REPORTS & DOCUMENTATION + =================================================================== --> + + <!-- Runs unit tests for all modules. --> + <target name="test" + depends="dist" + description="Run the unit test suite."> + <!-- Don't run FEST tests in a headless environment (they will fail) --> + <condition property="tests.file" value="testng-headless.xml" else="testng.xml"> + <isset property="headless" /> + </condition> + <uncommons:test suites="${conf.dir}/${tests.file}" + headless="${headless}" + title="Watchmaker Framework Unit Test Report" + mincoverage="${minimum.coverage}" /> + </target> + + + <target name="checkstyle" + depends="dist" + description="Check that coding standard are adhered to."> + <taskdef resource="checkstyletask.properties" classpathref="tool.path"/> + <mkdir dir="${checkstyle-results.dir}" /> + + <!-- Compiled classes must be available on the classpath to work-around this + bug (http://jira.codehaus.org/browse/MPCHECKSTYLE-20). That is why this + target depends on the 'dist' target. --> + <path id="checkstyle.path"> + <fileset dir="${dist.dir}" includes="**/*.jar" /> + </path> + + <checkstyle config="${conf.dir}/checks.xml" + failonviolation="false" + classpathref="checkstyle.path"> + <fileset dir="." defaultexcludes="yes"> + <include name="**/${java.dir}/main/org/uncommons/**/*.java"/> + </fileset> + <formatter type="xml" tofile="${checkstyle-results.dir}/checkstyle_report.xml"/> + </checkstyle> + <xslt in="${checkstyle-results.dir}/checkstyle_report.xml" + out="${checkstyle-results.dir}/checkstyle_report.html" + style="${conf.dir}/checkstyle-noframes-sorted.xsl" /> + </target> + + + <!-- Generates API documentation for all modules. --> + <target name="docs" + description="Generates Javadoc API documentation for all modules."> + <uncommons:javadoc title="Watchmaker Framework for Evolutionary Computation API" + version="${version}" + excludes="examples/**/*,framework/${java.dir}/main/org/uncommons/util/**/*"> + <additionalconfig> + <group title="Watchmaker Evolution Framework" packages="org.uncommons.watchmaker.framework:org.uncommons.watchmaker.framework.*"/> + <group title="Watchmaker Swing Classes" packages="org.uncommons.swing:org.uncommons.swing.*:org.uncommons.watchmaker.swing:org.uncommons.watchmaker.swing.*"/> + <link href="http://maths.uncommons.org/api/"/> + </additionalconfig> + </uncommons:javadoc> + </target> + + + <target name="book" description="Generates the PDF user guide."> + <mkdir dir="book/${build.dir}" /> + <uncommons:docbook classpathref="tool.path" + source="book/${src.dir}/xml/book.xml" + format="pdf" + outputDir="book/${build.dir}" > + <parameter name="paper.type" value="A4" /> + <parameter name="highlight.source" value="1" /> + <parameter name="img.src.path" value="./book/src/resources/" /> + </uncommons:docbook> + </target> + + +<!-- ================================================================== + TARGETS FOR UPDATING THE PROJECT WEBSITE + =================================================================== --> + + <target name="website-docs" + description="Re-builds the website Javadocs." + depends="dist"> + <!-- Delete all existing HTML files and then regenerate the docs over the top. --> + <delete> + <fileset dir="${web.dir}"> + <include name="api/**/*.html" /> + <include name="manual/**/*" /> + </fileset> + </delete> + + <!-- Refresh the API documentation tree for the Watchmaker Framework website. --> + <uncommons:javadoc dir="${web.dir}/api" + title="Watchmaker Framework for Evolutionary Computation API" + version="${version}" + excludes="examples/**/*,framework/${java.dir}/main/org/uncommons/util/**/*"> + <additionalconfig> + <group title="Watchmaker Evolution Framework" packages="org.uncommons.watchmaker.framework:org.uncommons.watchmaker.framework.*"/> + <group title="Watchmaker Swing Classes" packages="org.uncommons.swing:org.uncommons.swing.*:org.uncommons.watchmaker.swing:org.uncommons.watchmaker.swing.*"/> + <link href="http://maths.uncommons.org/api/"/> + </additionalconfig> + </uncommons:javadoc> + + <copy todir="${web.dir}" file="./CHANGELOG.txt"> + <filterset> + <filter token="VERSION" value="${version}"/> + </filterset> + </copy> + + <!-- Copy latest jars into website examples directory. --> + <copy todir="${web.dir}/examples"> + <fileset dir="${dist.dir}" includes="*.jar" /> + </copy> + <copy todir="${web.dir}/examples/lib"> + <fileset dir="${dist.dir}/${lib.dir}" includes="*.jar" /> + </copy> + + <!-- Generate user manual from DocBook source. --> + <uncommons:docbook classpathref="tool.path" + source="book/${src.dir}/xml/website.xml" + format="html" + chunked="true" + outputDir="${web.dir}/manual" > + <parameter name="highlight.source" value="1" /> + <parameter name="img.src.path" value="./" /> + <parameter name="html.stylesheet" value="docbook.css" /> + </uncommons:docbook> + <copy todir="${web.dir}/manual"> + <fileset dir="book/${src.dir}/resources" includes="**/*"/> + </copy> + </target> + +</project> diff --git a/watchmaker/etc/checks.xml b/watchmaker/etc/checks.xml new file mode 100644 index 0000000..b9e5e82 --- /dev/null +++ b/watchmaker/etc/checks.xml @@ -0,0 +1,164 @@ +<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!-- Checkstyle configuration for uncommons.org Coding Standards. -->
+<module name="Checker">
+
+ <property name="severity" value="error"/>
+
+ <module name="NewlineAtEndOfFile"/>
+
+ <module name="TreeWalker">
+
+ <!-- Documentation. -->
+ <module name="Header">
+ <property name="headerFile" value="${conf.dir}/java.header"/>
+ <property name="ignoreLines" value="2"/>
+ </module>
+ <module name="JavadocMethod">
+ <property name="severity" value="warning"/>
+ <property name="scope" value="protected"/>
+ <property name="allowUndeclaredRTE" value="true" />
+ <property name="allowThrowsTagsForSubclasses" value="true" />
+ <property name="allowMissingPropertyJavadoc" value="true" />
+ </module>
+ <module name="JavadocStyle"/>
+ <module name="JavadocType"/>
+ <module name="JavadocVariable">
+ <property name="severity" value="warning"/>
+ <property name="scope" value="package"/>
+ </module>
+
+
+ <!-- Imports. -->
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/>
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+ <module name="ImportOrder" />
+
+
+ <!-- Length. -->
+ <module name="AnonInnerLength">
+ <property name="severity" value="warning"/>
+ </module>
+ <module name="FileLength"/>
+ <module name="LineLength">
+ <property name="max" value="120"/>
+ <property name="severity" value="warning"/>
+ </module>
+ <module name="MethodLength"/>
+ <module name="ParameterNumber">
+ <!-- Constuctors can have as many as needed, only check methods. -->
+ <property name="tokens" value="METHOD_DEF"/>
+ <property name="severity" value="warning"/>
+ </module>
+
+
+ <!-- Naming conventions. -->
+ <module name="ConstantName"/>
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+
+ <!-- Whitespace. -->
+ <module name="EmptyForIteratorPad"/>
+ <module name="Indentation">
+ <!-- This should be an error rather than a warning but it won't accept single-line case statements. -->
+ <property name="severity" value="warning"/>
+ </module>
+ <module name="MethodParamPad"/>
+ <module name="NoWhitespaceAfter"/>
+ <module name="NoWhitespaceBefore"/>
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="TypecastParenPad"/>
+ <module name="TabCharacter"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround">
+ <property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_ASSERT, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, TYPE_EXTENSION_AND"/>
+ </module>
+
+
+ <!-- Modifiers. -->
+ <module name="ModifierOrder"/>
+ <module name="RedundantModifier"/>
+
+
+ <!-- Blocks. -->
+ <module name="AvoidNestedBlocks">
+ <property name="allowInSwitchCase" value="true"/>
+ </module>
+ <module name="EmptyBlock">
+ <property name="option" value="text"/>
+ </module>
+ <module name="LeftCurly">
+ <property name="option" value="nl"/>
+ </module>
+ <module name="NeedBraces"/>
+ <module name="RightCurly">
+ <property name="option" value="alone"/>
+ </module>
+
+
+ <!-- Exceptions. -->
+ <module name="IllegalCatch">
+ <property name="severity" value="warning"/>
+ </module>
+
+
+ <!-- Inheritance -->
+ <module name="SuperClone"/>
+ <module name="SuperFinalize"/>
+
+
+ <!-- Probable bugs or confusing constructs. -->
+ <module name="BooleanExpressionComplexity"/>
+ <module name="CovariantEquals"/>
+ <module name="DefaultComesLast"/>
+ <module name="DoubleCheckedLocking"/>
+ <module name="EmptyStatement"/>
+ <module name="EqualsHashCode"/>
+ <module name="HiddenField">
+ <property name="ignoreConstructorParameter" value="true"/>
+ <property name="ignoreSetter" value="true"/>
+ </module>
+ <module name="InnerAssignment"/>
+ <module name="MissingSwitchDefault"/>
+ <module name="ModifiedControlVariable"/>
+ <module name="RedundantThrows"/>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+ <module name="StringLiteralEquality"/>
+ <module name="UpperEll"/>
+
+
+ <!-- Class design. -->
+ <module name="FinalClass"/>
+ <module name="HideUtilityClassConstructor">
+ <property name="severity" value="warning"/>
+ </module>
+ <module name="InterfaceIsType">
+ <property name="allowMarkerInterfaces" value="false" />
+ </module>
+ <module name="PackageDeclaration"/>
+ <module name="VisibilityModifier">
+ <property name="protectedAllowed" value="true" />
+ </module>
+
+
+ <!-- Miscellaneous style issues. -->
+ <module name="ArrayTypeStyle"/>
+ <module name="DeclarationOrder"/>
+ <module name="MultipleVariableDeclarations"/>
+
+ </module>
+
+</module>
diff --git a/watchmaker/etc/checkstyle-noframes-sorted.xsl b/watchmaker/etc/checkstyle-noframes-sorted.xsl new file mode 100644 index 0000000..e1aff38 --- /dev/null +++ b/watchmaker/etc/checkstyle-noframes-sorted.xsl @@ -0,0 +1,178 @@ +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+<xsl:output method="html" indent="yes"/>
+<xsl:decimal-format decimal-separator="." grouping-separator="," />
+
+<xsl:key name="files" match="file" use="@name" />
+
+<!-- Checkstyle XML Style Sheet by Stephane Bailliez <sbailliez@apache.org> -->
+<!-- Part of the Checkstyle distribution found at http://checkstyle.sourceforge.net -->
+<!-- Usage (generates checkstyle_report.html): -->
+<!-- <checkstyle failonviolation="false" config="${check.config}"> -->
+<!-- <fileset dir="${src.dir}" includes="**/*.java"/> -->
+<!-- <formatter type="xml" toFile="${doc.dir}/checkstyle_report.xml"/> -->
+<!-- </checkstyle> -->
+<!-- <style basedir="${doc.dir}" destdir="${doc.dir}" -->
+<!-- includes="checkstyle_report.xml" -->
+<!-- style="${doc.dir}/checkstyle-noframes-sorted.xsl"/> -->
+
+<xsl:template match="checkstyle">
+ <html>
+ <head>
+ <style type="text/css">
+ .bannercell {
+ border: 0px;
+ padding: 0px;
+ }
+ body {
+ margin-left: 10;
+ margin-right: 10;
+ font:normal 80% arial,helvetica,sanserif;
+ background-color:#FFFFFF;
+ color:#000000;
+ }
+ .a td {
+ background: #efefef;
+ }
+ .b td {
+ background: #fff;
+ }
+ th, td {
+ text-align: left;
+ vertical-align: top;
+ }
+ th {
+ font-weight:bold;
+ background: #ccc;
+ color: black;
+ }
+ table, th, td {
+ font-size:100%;
+ border: none
+ }
+ table.log tr td, tr th {
+
+ }
+ h2 {
+ font-weight:bold;
+ font-size:140%;
+ margin-bottom: 5;
+ }
+ h3 {
+ font-size:100%;
+ font-weight:bold;
+ background: #525D76;
+ color: white;
+ text-decoration: none;
+ padding: 5px;
+ margin-right: 2px;
+ margin-left: 2px;
+ margin-bottom: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <a name="top"></a>
+ <!-- jakarta logo -->
+ <table border="0" cellpadding="0" cellspacing="0" width="100%">
+ <tr>
+ <td class="bannercell" rowspan="2">
+ <!--a href="http://jakarta.apache.org/">
+ <img src="http://jakarta.apache.org/images/jakarta-logo.gif" alt="http://jakarta.apache.org" align="left" border="0"/>
+ </a-->
+ </td>
+ <td class="text-align:right"><h2>CheckStyle Audit</h2></td>
+ </tr>
+ <tr>
+ <td class="text-align:right">Designed for use with <a href='http://checkstyle.sourceforge.net/'>CheckStyle</a> and <a href='http://jakarta.apache.org'>Ant</a>.</td>
+ </tr>
+ </table>
+ <hr size="1"/>
+
+ <!-- Summary part -->
+ <xsl:apply-templates select="." mode="summary"/>
+ <hr size="1" width="100%" align="left"/>
+
+ <!-- Package List part -->
+ <xsl:apply-templates select="." mode="filelist"/>
+ <hr size="1" width="100%" align="left"/>
+
+ <!-- For each package create its part -->
+ <xsl:apply-templates select="file[@name and generate-id(.) = generate-id(key('files', @name))]" />
+
+ <hr size="1" width="100%" align="left"/>
+
+
+ </body>
+ </html>
+</xsl:template>
+
+
+
+ <xsl:template match="checkstyle" mode="filelist">
+ <h3>Files</h3>
+ <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
+ <tr>
+ <th>Name</th>
+ <th>Errors</th>
+ </tr>
+ <xsl:for-each select="file[@name and generate-id(.) = generate-id(key('files', @name))]">
+ <xsl:sort data-type="number" order="descending" select="count(key('files', @name)/error)"/>
+ <xsl:variable name="errorCount" select="count(error)"/>
+ <tr>
+ <xsl:call-template name="alternated-row"/>
+ <td><a href="#f-{@name}"><xsl:value-of select="@name"/></a></td>
+ <td><xsl:value-of select="$errorCount"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </xsl:template>
+
+
+ <xsl:template match="file">
+ <a name="f-{@name}"></a>
+ <h3>File <xsl:value-of select="@name"/></h3>
+
+ <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
+ <tr>
+ <th>Error Description</th>
+ <th>Line</th>
+ </tr>
+ <xsl:for-each select="key('files', @name)/error">
+ <xsl:sort data-type="number" order="ascending" select="@line"/>
+ <tr>
+ <xsl:call-template name="alternated-row"/>
+ <td><xsl:value-of select="@message"/></td>
+ <td><xsl:value-of select="@line"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ <a href="#top">Back to top</a>
+ </xsl:template>
+
+
+ <xsl:template match="checkstyle" mode="summary">
+ <h3>Summary</h3>
+ <xsl:variable name="fileCount" select="count(file[@name and generate-id(.) = generate-id(key('files', @name))])"/>
+ <xsl:variable name="errorCount" select="count(file/error)"/>
+ <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
+ <tr>
+ <th>Files</th>
+ <th>Errors</th>
+ </tr>
+ <tr>
+ <xsl:call-template name="alternated-row"/>
+ <td><xsl:value-of select="$fileCount"/></td>
+ <td><xsl:value-of select="$errorCount"/></td>
+ </tr>
+ </table>
+ </xsl:template>
+
+ <xsl:template name="alternated-row">
+ <xsl:attribute name="class">
+ <xsl:if test="position() mod 2 = 1">a</xsl:if>
+ <xsl:if test="position() mod 2 = 0">b</xsl:if>
+ </xsl:attribute>
+ </xsl:template>
+</xsl:stylesheet>
+
+
diff --git a/watchmaker/etc/java.header b/watchmaker/etc/java.header new file mode 100644 index 0000000..61bdfc5 --- /dev/null +++ b/watchmaker/etc/java.header @@ -0,0 +1,15 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= diff --git a/watchmaker/etc/testng-headless.xml b/watchmaker/etc/testng-headless.xml new file mode 100644 index 0000000..262a7a8 --- /dev/null +++ b/watchmaker/etc/testng-headless.xml @@ -0,0 +1,34 @@ +<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > + +<suite name="Unit Tests" verbose="1" > + + <test name="Evolution Framework" > + <packages> + <package name="org.uncommons.watchmaker.framework.*" /> + </packages> + </test> + + <test name="Swing Module" > + <!-- Don't run FEST tests in a headless environment. --> + <groups><run><exclude name="display-required" /></run></groups> + <packages> + <package name="org.uncommons.swing.*" /> + <package name="org.uncommons.watchmaker.swing.*" /> + </packages> + </test> + + <test name="Examples" > + <!-- Don't run FEST tests in a headless environment. --> + <groups><run><exclude name="display-required" /></run></groups> + <packages> + <package name="org.uncommons.watchmaker.examples.*" /> + </packages> + </test> + + <test name="Utilities Module" > + <packages> + <package name="org.uncommons.util.*" /> + </packages> + </test> + +</suite> diff --git a/watchmaker/etc/testng.xml b/watchmaker/etc/testng.xml new file mode 100644 index 0000000..70a2bfb --- /dev/null +++ b/watchmaker/etc/testng.xml @@ -0,0 +1,31 @@ +<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > + +<suite name="Unit Tests" verbose="1" > + + <test name="Evolution Framework" > + <packages> + <package name="org.uncommons.watchmaker.framework.*" /> + </packages> + </test> + + <test name="Swing Module" > + <packages> + <package name="org.uncommons.swing.*" /> + <package name="org.uncommons.watchmaker.swing.*" /> + </packages> + </test> + + <test name="Examples" > + <packages> + <package name="org.uncommons.watchmaker.examples.*" /> + </packages> + </test> + + <test name="Utilities Module" > + <packages> + <package name="org.uncommons.util.*" /> + </packages> + </test> + +</suite> +
\ No newline at end of file diff --git a/watchmaker/examples/examples.iml b/watchmaker/examples/examples.iml new file mode 100644 index 0000000..6f94484 --- /dev/null +++ b/watchmaker/examples/examples.iml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_6" inherit-compiler-output="false"> + <output url="file://$MODULE_DIR$/target/classes" /> + <output-test url="file://$MODULE_DIR$/target/test-classes" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src/java/main" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/java/resources" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/java/test" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="swing" /> + <orderEntry type="module" module-name="framework" /> + <orderEntry type="library" name="Maven: org.uncommons.maths:uncommons-maths:1.2.2" level="project" /> + <orderEntry type="library" name="Maven: jfree:jcommon:1.0.12" level="project" /> + <orderEntry type="library" name="Maven: jfree:jfreechart:1.0.13" level="project" /> + <orderEntry type="library" name="Maven: com.google.collections:google-collections:1.0" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.testng:testng:6.2.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: junit:junit:3.8.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.beanshell:bsh:2.0b4" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: com.beust:jcommander:1.12" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.yaml:snakeyaml:1.6" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-swing:1.2.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-assert:1.2" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-util:1.1.3" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-reflect:1.2" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: net.jcip:jcip-annotations:1.0" level="project" /> + </component> +</module> + diff --git a/watchmaker/examples/nb-configuration.xml b/watchmaker/examples/nb-configuration.xml new file mode 100644 index 0000000..ae35717 --- /dev/null +++ b/watchmaker/examples/nb-configuration.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<project-shared-configuration>
+ <!-- +This file contains additional configuration written by modules in the NetBeans IDE. +The configuration is intended to be shared among all the users of project and +therefore it is assumed to be part of version control checkout. +Without this configuration present, some functionality in the IDE may be limited or fail altogether. +-->
+ <properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
+ <!-- +Properties that influence various parts of the IDE, especially code formatting and the like. +You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up. +That way multiple projects can share the same settings (useful for formatting rules for example). +Any value defined here will override the pom.xml file value but is only applicable to the current project. +-->
+ <org-netbeans-modules-editor-indent.CodeStyle.usedProfile>project</org-netbeans-modules-editor-indent.CodeStyle.usedProfile>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab>2</org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.tab-size>2</org-netbeans-modules-editor-indent.CodeStyle.project.tab-size>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width>2</org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs>false</org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width>100</org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width>
+ <org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap>words</org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.blankLinesAfterClassHeader>0</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.blankLinesAfterClassHeader>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantIfBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantIfBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantForBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantForBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceBeforeColon>false</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceBeforeColon>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTryResources>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTryResources>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTernaryOps>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapTernaryOps>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaces-per-tab>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaces-per-tab>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssignOps>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssignOps>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodCallArgs>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodCallArgs>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapChainedMethodCalls>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapChainedMethodCalls>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapArrayInit>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapArrayInit>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapBinaryOps>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapBinaryOps>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.indent-shift-width>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.indent-shift-width>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeWhileOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeWhileOnNewLine>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.text-limit-width>100</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.text-limit-width>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.generateParagraphTagOnBlankLines>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.generateParagraphTagOnBlankLines>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantWhileBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantWhileBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.tab-size>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.tab-size>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize>2</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.expand-tabs>false</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.expand-tabs>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantDoWhileBraces>LEAVE_ALONE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantDoWhileBraces>
+ <org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList>WRAP_IF_LONG</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList>
+ </properties>
+</project-shared-configuration>
diff --git a/watchmaker/examples/pom.xml b/watchmaker/examples/pom.xml new file mode 100644 index 0000000..773b7d2 --- /dev/null +++ b/watchmaker/examples/pom.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!--=========================================================================== + Copyright 2006-2010 Daniel W. Dyer + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ==========================================================================--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.uncommons.watchmaker</groupId> + <artifactId>watchmaker</artifactId> + <version>0.7.2</version> + </parent> + <artifactId>watchmaker-examples</artifactId> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>watchmaker-swing</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <version>6.2.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.easytesting</groupId> + <artifactId>fest-swing</artifactId> + <version>1.2.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <sourceDirectory>src/java/main</sourceDirectory> + <testSourceDirectory>src/java/test</testSourceDirectory> + <resources> + <resource> + <directory>src/java/resources</directory> + </resource> + </resources> + </build> +</project> diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/AbstractExampleApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/AbstractExampleApplet.java new file mode 100644 index 0000000..8bf600a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/AbstractExampleApplet.java @@ -0,0 +1,102 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.awt.Container; +import java.lang.reflect.InvocationTargetException; +import javax.swing.JApplet; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +/** + * Base class for examples that run as applets. + * @author Daniel Dyer + */ +public abstract class AbstractExampleApplet extends JApplet +{ + /** + * {@inheritDoc} + */ + @Override + public void init() + { + configure(this); + } + + + /** + * Configure the program to display its GUI in the specified container. + * @param container The container to place the GUI components in. + */ + private void configure(final Container container) + { + try + { + // Use invokeAndWait so that we can be sure that initialisation is complete + // before continuing. + SwingUtilities.invokeAndWait(new Runnable() + { + public void run() + { + try + { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception ex) + { + // This should never happen as we are installing a known look-and-feel. + System.err.println("Failed to load System look-and-feel."); + } + prepareGUI(container); + } + }); + } + catch (InterruptedException ex) + { + // Restore interrupt flag. + Thread.currentThread().interrupt(); + } + catch (InvocationTargetException ex) + { + ex.getCause().printStackTrace(); + JOptionPane.showMessageDialog(container, ex.getCause(), "Error Occurred", JOptionPane.ERROR_MESSAGE); + } + } + + + /** + * Implemented in sub-classes to initialise and layout the GUI. + * @param container The container that this method should add components to. + */ + protected abstract void prepareGUI(Container container); + + + /** + * Display this example program using a JFrame as the top-level GUI container (rather + * than running the example as an applet). + * @param title The text to use for the frame's title bar. + */ + protected void displayInFrame(String title) + { + JFrame frame = new JFrame(title); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + configure(frame); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/EvolutionLogger.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/EvolutionLogger.java new file mode 100644 index 0000000..5c47146 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/EvolutionLogger.java @@ -0,0 +1,39 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * Trivial evolution observer for displaying information at the end + * of each generation. + * @param <T> The type of entity being evolved. + * @author Daniel Dyer + */ +public class EvolutionLogger<T> implements IslandEvolutionObserver<T> +{ + public void populationUpdate(PopulationData<? extends T> data) + { + System.out.println("Generation " + data.getGenerationNumber() + ": " + data.getBestCandidateFitness()); + } + + + public void islandPopulationUpdate(int islandIndex, PopulationData<? extends T> populationData) + { + // Do nothing. + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/Launcher.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/Launcher.java new file mode 100644 index 0000000..b4b5b7e --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/Launcher.java @@ -0,0 +1,79 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.uncommons.util.reflection.ReflectionUtils; +import org.uncommons.watchmaker.examples.biomorphs.BiomorphApplet; +import org.uncommons.watchmaker.examples.bits.BitsExample; +import org.uncommons.watchmaker.examples.geneticprogramming.GeneticProgrammingExample; +import org.uncommons.watchmaker.examples.monalisa.MonaLisaApplet; +import org.uncommons.watchmaker.examples.strings.StringsExample; +import org.uncommons.watchmaker.examples.sudoku.SudokuApplet; +import org.uncommons.watchmaker.examples.travellingsalesman.TravellingSalesmanApplet; + +/** + * Launcher for Watchmaker example applications. + * @author Daniel Dyer + */ +public class Launcher +{ + private static final Map<String, Class<?>> EXAMPLES = new LinkedHashMap<String, Class<?>>(); + static + { + EXAMPLES.put("biomorphs", BiomorphApplet.class); + EXAMPLES.put("bits", BitsExample.class); + EXAMPLES.put("gp", GeneticProgrammingExample.class); + EXAMPLES.put("monalisa", MonaLisaApplet.class); + EXAMPLES.put("salesman", TravellingSalesmanApplet.class); + EXAMPLES.put("strings", StringsExample.class); + EXAMPLES.put("sudoku", SudokuApplet.class); + } + + + private Launcher() + { + // Prevents instantiation of launcher class. + } + + + /** + * Launch the specified example application from the command-line. + * @param args First item is the name of the example to run. Any subsequent arguments are passed + * on to the specific example. + */ + public static void main(String[] args) + { + Class<?> exampleClass = args.length > 0 ? EXAMPLES.get(args[0]) : null; + if (exampleClass == null) + { + System.err.println("First argument must be the name of an example, i.e. one of " + + Arrays.toString(EXAMPLES.keySet().toArray())); + System.exit(1); + } + + // All args except the first one should be passed to the example application. + String[] appArgs = new String[args.length - 1]; + System.arraycopy(args, 1, appArgs, 0, appArgs.length); + + // Invoke the main method for the selected example application. + Method main = ReflectionUtils.findKnownMethod(exampleClass, "main", String[].class); + ReflectionUtils.invokeUnchecked(main, exampleClass, new Object[]{appArgs}); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/Biomorph.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/Biomorph.java new file mode 100644 index 0000000..7fe6fd5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/Biomorph.java @@ -0,0 +1,160 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.Arrays; + +/** + * <p>Candidate representation for a biomorph. We could just as easily have + * used an array of integers or a bit string representation with Gray codes + * but for clarity we will use this more object-oriented representation that + * conveniently combines state and related logic.</p> + * + * <p>The Biomporph class encapsulates 9 genes as described by Dawkins in his + * "The Evolution of Evolvability" paper.</p> + * + * @author Daniel Dyer + */ +public final class Biomorph +{ + /** The total number of genes that make up a biomorph. */ + public static final int GENE_COUNT = 9; + /** The minimum permitted value for most genes. */ + public static final int GENE_MIN = -5; + /** The maximum permitted value for most genes. */ + public static final int GENE_MAX = 5; + /** The index of the gene that controls biomporph size. */ + public static final int LENGTH_GENE_INDEX = 8; + /** The minimum permitted value for the length gene. */ + public static final int LENGTH_GENE_MIN = 1; + /** The maximum permitted value for the length gene. */ + public static final int LENGTH_GENE_MAX = 7; + + private final int[] genes; + + private int[][] phenotype; + + + /** + * Creates a biomorph with the specified genes. + * @param genes A 9-element array. The final element must be a positive + * value. + */ + public Biomorph(int[] genes) + { + if (genes.length != GENE_COUNT) + { + throw new IllegalArgumentException("Biomorph must have " + GENE_COUNT + " genes."); + } + this.genes = genes.clone(); + } + + + /** + * Returns an array of integers that represent the graphical pattern + * determined by the biomorph's genes. + * @return A 2-dimensional array containing the 8-element dx and dy + * arrays required to draw the biomorph. + */ + public int[][] getPatternPhenotype() + { + if (phenotype == null) + { + // Decode the genes as per Dawkins' rules. + int[] dx = new int[GENE_COUNT - 1]; + dx[3] = genes[0]; + dx[4] = genes[1]; + dx[5] = genes[2]; + + dx[1] = -dx[3]; + dx[0] = -dx[4]; + dx[7] = -dx[5]; + + dx[2] = 0; + dx[6] = 0; + + int[] dy = new int[GENE_COUNT - 1]; + dy[2] = genes[3]; + dy[3] = genes[4]; + dy[4] = genes[5]; + dy[5] = genes[6]; + dy[6] = genes[7]; + + dy[0] = dy[4]; + dy[1] = dy[3]; + dy[7] = dy[5]; + + phenotype = new int[][]{dx, dy}; + } + return phenotype; + } + + + /** + * @return The value of the gene that controls the size of this biomorph. + */ + public int getLengthPhenotype() + { + return genes[LENGTH_GENE_INDEX]; + } + + + /** + * Returns the 9 genes that make up the biomorph's genotype. + * @return A 9-element array containing this biomorph's genes. + */ + public int[] getGenotype() + { + return genes.clone(); + } + + + /** + * Compares the genes of two Biomporph objects and returns true if they are + * identical. + * @param obj The object to compare with this one. + * @return True if the argument is a Biomoprh instance and the 2 + * biomorphs have the same genes, false otherwise. + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass()) + { + return false; + } + + Biomorph biomorph = (Biomorph) obj; + + return Arrays.equals(genes, biomorph.genes); + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. Biomorphs + * with identical genes return identical hash codes. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + return Arrays.hashCode(genes); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphApplet.java new file mode 100644 index 0000000..0766422 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphApplet.java @@ -0,0 +1,241 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SpringLayout; +import javax.swing.SwingUtilities; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.interactive.InteractiveSelection; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.framework.termination.GenerationCount; +import org.uncommons.watchmaker.swing.SwingConsole; + +/** + * Watchmaker Framework implementation of Dawkin's biomorph program. + * @author Daniel Dyer + */ +public class BiomorphApplet extends AbstractExampleApplet +{ + private Renderer<Biomorph, JComponent> renderer; + private SwingConsole console; + private JDialog selectionDialog; + private JPanel biomorphHolder; + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + renderer = new SwingBiomorphRenderer(); + console = new SwingConsole(5); + selectionDialog = new JDialog((JFrame) null, "Biomorph Selection", true); + biomorphHolder = new JPanel(new GridLayout(1, 1)); + + container.add(new ControlPanel(), BorderLayout.WEST); + container.add(biomorphHolder, BorderLayout.CENTER); + biomorphHolder.setBorder(BorderFactory.createTitledBorder("Last Evolved Biomorph")); + biomorphHolder.add(new JLabel("Nothing generated yet.", JLabel.CENTER)); + selectionDialog.add(console, BorderLayout.CENTER); + selectionDialog.setSize(800, 600); + selectionDialog.validate(); + } + + + /** + * Helper method to create a background task for running the interactive evolutionary + * algorithm. + * @param populationSize How big the population used by the created evolution engine + * should be. + * @param generationCount How many generations to use when the evolution engine is + * invoked. + * @param random If true use random mutation, otherwise use Dawkins mutation. + * @return A Swing task that will execute on a background thread and update + * the GUI when it is done. + */ + private SwingBackgroundTask<Biomorph> createTask(final int populationSize, + final int generationCount, + final boolean random) + { + return new SwingBackgroundTask<Biomorph>() + { + @Override + protected Biomorph performTask() + { + EvolutionaryOperator<Biomorph> mutation = random ? new RandomBiomorphMutation(new Probability(0.12d)) + : new DawkinsBiomorphMutation(); + InteractiveSelection<Biomorph> selection = new InteractiveSelection<Biomorph>(console, + renderer, + populationSize, + 1); + EvolutionEngine<Biomorph> engine = new GenerationalEvolutionEngine<Biomorph>(new BiomorphFactory(), + mutation, + selection, + new MersenneTwisterRNG()); + engine.addEvolutionObserver(new GenerationTracker()); + return engine.evolve(populationSize, + 0, + new GenerationCount(generationCount)); + } + + @Override + protected void postProcessing(Biomorph result) + { + selectionDialog.setVisible(false); + biomorphHolder.removeAll(); + biomorphHolder.add(renderer.render(result)); + biomorphHolder.revalidate(); + } + }; + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + */ + public static void main(String[] args) + { + new BiomorphApplet().displayInFrame("Watchmaker Framework - Biomporphs Example"); + } + + + /** + * Simple observer to update the dialog title every time the evolution advances + * to a new generation. + */ + private final class GenerationTracker implements EvolutionObserver<Biomorph> + { + public void populationUpdate(final PopulationData<? extends Biomorph> populationData) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + selectionDialog.setTitle("Biomorph Selection - Generation " + + (populationData.getGenerationNumber() + 1)); + } + }); + } + } + + + /** + * Panel for controlling the evolutionary algorithm parameters. + */ + private final class ControlPanel extends JPanel + { + private JSpinner populationSpinner; + private JSpinner generationsSpinner; + private JComboBox mutationCombo; + + ControlPanel() + { + super(new BorderLayout()); + add(createInputPanel(), BorderLayout.NORTH); + add(createButtonPanel(), BorderLayout.SOUTH); + setBorder(BorderFactory.createTitledBorder("Evolution Controls")); + } + + + private JComponent createInputPanel() + { + JPanel inputPanel = new JPanel(new SpringLayout()); + JLabel populationLabel = new JLabel("Population Size: "); + populationSpinner = new JSpinner(new SpinnerNumberModel(18, 2, 25, 1)); + populationSpinner.setEnabled(false); + populationLabel.setLabelFor(populationSpinner); + inputPanel.add(populationLabel); + inputPanel.add(populationSpinner); + JLabel generationsLabel = new JLabel("Number of Generations: "); + generationsSpinner = new JSpinner(new SpinnerNumberModel(20, 1, 100, 1)); + generationsLabel.setLabelFor(generationsSpinner); + inputPanel.add(generationsLabel); + inputPanel.add(generationsSpinner); + JLabel mutationLabel = new JLabel("Mutation Type: "); + mutationCombo = new JComboBox(new String[]{"Dawkins (Non-random)", "Random"}); + mutationCombo.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + if (mutationCombo.getSelectedIndex() == 0) + { + populationSpinner.setValue(18); + populationSpinner.setEnabled(false); + } + else + { + populationSpinner.setEnabled(true); + } + } + }); + inputPanel.add(mutationLabel); + inputPanel.add(mutationCombo); + + SpringUtilities.makeCompactGrid(inputPanel, 3, 2, 30, 6, 6, 6); + + return inputPanel; + } + + + private JComponent createButtonPanel() + { + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton startButton = new JButton("Start"); + startButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent actionEvent) + { + createTask((Integer) populationSpinner.getValue(), + (Integer) generationsSpinner.getValue(), + mutationCombo.getSelectedIndex() == 1).execute(); + selectionDialog.setVisible(true); + } + }); + buttonPanel.add(startButton); + return buttonPanel; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactory.java new file mode 100644 index 0000000..d081e40 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactory.java @@ -0,0 +1,44 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.Random; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Candidate factory for creating random biomorphs. + * @author Daniel Dyer + */ +public class BiomorphFactory extends AbstractCandidateFactory<Biomorph> +{ + /** + * Generates a random biomorph by providing a random value for each gene. + * @param rng The source of randomness used to generate the biomoprh. + * @return A randomly-generated biomorph. + */ + public Biomorph generateRandomCandidate(Random rng) + { + int[] genes = new int[Biomorph.GENE_COUNT]; + for (int i = 0; i < Biomorph.GENE_COUNT - 1; i++) + { + // First 8 genes have values between -5 and 5. + genes[i] = rng.nextInt(11) - 5; + } + // Last genes ha a value between 1 and 7. + genes[Biomorph.LENGTH_GENE_INDEX] = rng.nextInt(Biomorph.LENGTH_GENE_MAX) + 1; + return new Biomorph(genes); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutation.java new file mode 100644 index 0000000..f718a76 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutation.java @@ -0,0 +1,67 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Non-random mutation of a population of biomorphs. This ensures that each selected candidate + * is mutated differently. This is the mutation used by Dawkins in his original experiment. + * @author Daniel Dyer + */ +public class DawkinsBiomorphMutation implements EvolutionaryOperator<Biomorph> +{ + /** + * Mutate a population of biomorphs non-randomly, ensuring that each selected + * candidate is mutated differently. + * @param selectedCandidates {@inheritDoc} + * @param rng A source of randomness (not used since this mutation is non-random). + * @return {@inheritDoc} + */ + public List<Biomorph> apply(List<Biomorph> selectedCandidates, Random rng) + { + List<Biomorph> mutatedPopulation = new ArrayList<Biomorph>(selectedCandidates.size()); + int mutatedGene = 0; + int mutation = 1; + for (Biomorph b : selectedCandidates) + { + int[] genes = b.getGenotype(); + + mutation *= -1; // Alternate between incrementing and decrementing. + if (mutation == 1) // After gene has been both incremented and decremented, move to next one. + { + mutatedGene = (mutatedGene + 1) % Biomorph.GENE_COUNT; + } + genes[mutatedGene] += mutation; + int min = mutatedGene == Biomorph.LENGTH_GENE_INDEX ? Biomorph.LENGTH_GENE_MIN : Biomorph.GENE_MIN; + int max = mutatedGene == Biomorph.LENGTH_GENE_INDEX ? Biomorph.LENGTH_GENE_MAX : Biomorph.GENE_MAX; + if (genes[mutatedGene] > max) + { + genes[mutatedGene] = min; + } + else if (genes[mutatedGene] < min) + { + genes[mutatedGene] = max; + } + + mutatedPopulation.add(new Biomorph(genes)); + } + return mutatedPopulation; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutation.java new file mode 100644 index 0000000..a98cb55 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutation.java @@ -0,0 +1,98 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutation operator for biomorphs. Mutates each individual gene + * according to some mutation probability. + * @author Daniel Dyer + */ +public class RandomBiomorphMutation implements EvolutionaryOperator<Biomorph> +{ + private final Probability mutationProbability; + + /** + * @param mutationProbability The probability that a given gene + * is changed. + */ + public RandomBiomorphMutation(Probability mutationProbability) + { + this.mutationProbability = mutationProbability; + } + + + /** + * Randomly mutate each selected candidate. + * @param selectedCandidates {@inheritDoc} + * @param rng {@inheritDoc} + * @return {@inheritDoc} + */ + public List<Biomorph> apply(List<Biomorph> selectedCandidates, Random rng) + { + List<Biomorph> mutatedPopulation = new ArrayList<Biomorph>(selectedCandidates.size()); + for (Biomorph biomorph : selectedCandidates) + { + mutatedPopulation.add(mutateBiomorph(biomorph, rng)); + } + return mutatedPopulation; + } + + + /** + * Mutates a single biomorph. + * @param biomorph The biomorph to mutate. + * @param rng The source of randomness to use for mutation. + * @return A mutated version of the biomorph. + */ + private Biomorph mutateBiomorph(Biomorph biomorph, Random rng) + { + int[] genes = biomorph.getGenotype(); + assert genes.length == Biomorph.GENE_COUNT : "Biomorphs must have " + Biomorph.GENE_COUNT + " genes."; + for (int i = 0; i < Biomorph.GENE_COUNT - 1; i++) + { + if (mutationProbability.nextEvent(rng)) + { + boolean increase = rng.nextBoolean(); + genes[i] += (increase ? 1 : -1); + if (genes[i] > Biomorph.GENE_MAX) + { + genes[i] = Biomorph.GENE_MIN; + } + else if (genes[i] < Biomorph.GENE_MIN) + { + genes[i] = Biomorph.GENE_MAX; + } + } + } + boolean increase = rng.nextBoolean(); + genes[Biomorph.LENGTH_GENE_INDEX] += (increase ? 1 : -1); + if (genes[Biomorph.LENGTH_GENE_INDEX] > Biomorph.LENGTH_GENE_MAX) + { + genes[Biomorph.LENGTH_GENE_INDEX] = Biomorph.LENGTH_GENE_MIN; + } + else if (genes[Biomorph.LENGTH_GENE_INDEX] < Biomorph.LENGTH_GENE_MIN) + { + genes[Biomorph.LENGTH_GENE_INDEX] = Biomorph.LENGTH_GENE_MAX; + } + return new Biomorph(genes); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/SwingBiomorphRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/SwingBiomorphRenderer.java new file mode 100644 index 0000000..d6d8e82 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/SwingBiomorphRenderer.java @@ -0,0 +1,111 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import javax.swing.JComponent; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Renders Biomorphs as Swing components. + * @author Daniel Dyer + */ +public class SwingBiomorphRenderer implements Renderer<Biomorph, JComponent> +{ + /** + * Renders an evolved biomorph as a component that can be displayed + * in a Swing GUI. + * @param biomorph The biomorph to render. + * @return A component that displays a visual representation of the + * biomorph. + */ + public JComponent render(Biomorph biomorph) + { + return new BiomorphView(biomorph); + } + + + /** + * A Swing component that can display a visual representation of a + * biomorph. + */ + private static final class BiomorphView extends JComponent + { + private final Biomorph biomorph; + + BiomorphView(Biomorph biomorph) + { + this.biomorph = biomorph; + Dimension size = new Dimension(200, 200); + setMinimumSize(size); + setPreferredSize(size); + } + + + @Override + protected void paintComponent(Graphics graphics) + { + super.paintComponent(graphics); + if (graphics instanceof Graphics2D) + { + ((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + + int[][] pattern = biomorph.getPatternPhenotype(); + int depth = biomorph.getLengthPhenotype(); + + drawTree(graphics, + getSize().width / 2, + getSize().height / 2, + depth, + 2, // Initial direction should be 2 or 6 to ensure horizontal symmetry. + pattern[0], // dx + pattern[1]); // dy + } + + + /** + * Recursive method for drawing tree branches. + */ + private void drawTree(Graphics graphics, + int x, + int y, + int length, + int direction, + int[] dx, + int[] dy) + { + // Make sure direction wraps round in the range 0 - 7. + direction = (direction + 8) % 8; + + int x2 = x + length * dx[direction]; + int y2 = y + length * dy[direction]; + + graphics.drawLine(x, y, x2, y2); + + if (length > 0) + { + // Recursively draw the left and right branches of the tree. + drawTree(graphics, x2, y2, length - 1, direction - 1, dx, dy); + drawTree(graphics, x2, y2, length - 1, direction + 1, dx, dy); + } + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/package-info.java new file mode 100644 index 0000000..1f73010 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/biomorphs/package-info.java @@ -0,0 +1,21 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary simulation along the lines of the Biomporph program described + * by Richard Dawkins in his book, The Blind Watchmaker. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.biomorphs; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitStringEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitStringEvaluator.java new file mode 100644 index 0000000..3a6421c --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitStringEvaluator.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.bits; + +import java.util.List; +import org.uncommons.maths.binary.BitString; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * A fitness evaluator that simply counts the number of ones in a bit + * string. + * @see BitString + * @author Daniel Dyer + */ +public class BitStringEvaluator implements FitnessEvaluator<BitString> +{ + /** + * Calculates a fitness score for the candidate bit string. + * @param candidate The evolved bit string to evaluate. + * @param population {@inheritDoc} + * @return How many bits in the string are set to 1. + */ + public double getFitness(BitString candidate, + List<? extends BitString> population) + { + return candidate.countSetBits(); + } + + + /** + * Always returns true. A higher score indicates a fitter individual. + * @return True. + */ + public boolean isNatural() + { + return true; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitsExample.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitsExample.java new file mode 100644 index 0000000..4b6a1c8 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/BitsExample.java @@ -0,0 +1,67 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.bits; + +import java.util.ArrayList; +import java.util.List; +import org.uncommons.maths.binary.BitString; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.EvolutionLogger; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.factories.BitStringFactory; +import org.uncommons.watchmaker.framework.operators.BitStringCrossover; +import org.uncommons.watchmaker.framework.operators.BitStringMutation; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; + +/** + * An implementation of the first exercise (page 31) from the book An Introduction to + * Genetic Algorithms, by Melanie Mitchell. The algorithm evolves bit strings and the + * fitness function simply counts the number of ones in the bit string. The evolution + * should therefore converge on strings that consist only of ones. + * @author Daniel Dyer + */ +public class BitsExample +{ + private static final int BITS = 20; + + public static void main(String[] args) + { + evolveBits(BITS); + } + + + public static BitString evolveBits(int length) + { + List<EvolutionaryOperator<BitString>> operators = new ArrayList<EvolutionaryOperator<BitString>>(2); + operators.add(new BitStringCrossover(1, new Probability(0.7d))); + operators.add(new BitStringMutation(new Probability(0.01d))); + EvolutionaryOperator<BitString> pipeline = new EvolutionPipeline<BitString>(operators); + GenerationalEvolutionEngine<BitString> engine = new GenerationalEvolutionEngine<BitString>(new BitStringFactory(length), + pipeline, + new BitStringEvaluator(), + new RouletteWheelSelection(), + new MersenneTwisterRNG()); + engine.setSingleThreaded(true); // Performs better for very trivial fitness evaluations. + engine.addEvolutionObserver(new EvolutionLogger<BitString>()); + return engine.evolve(100, // 100 individuals in each generation. + 0, // Don't use elitism. + new TargetFitness(length, true)); // Continue until a perfect match is found. + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/package-info.java new file mode 100644 index 0000000..058458b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/bits/package-info.java @@ -0,0 +1,23 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An implementation of the first exercise from the book An Introduction to Genetic + * Algorithms, by Melanie Mitchell. The algorithm evolves bit strings and the + * fitness function simply counts the number of ones in the bit string. The evolution + * should therefore converge on strings that consist only of ones. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.bits; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Addition.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Addition.java new file mode 100644 index 0000000..641510c --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Addition.java @@ -0,0 +1,79 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * Simple addition operator {@link Node}. + * @author Daniel Dyer + */ +public class Addition extends BinaryNode +{ + /** + * Creates a node that evaluates to the sum of the values of its two + * child nodes ({@literal left} and {@literal right}). + * @param left The first operand. + * @param right The second operand. + */ + public Addition(Node left, Node right) + { + super(left, right, '+'); + } + + + /** + * Evaluates the two sub-trees and returns the sum of these two values. + * @param programParameters Program parameters (ignored by the addition operator + * but may be used in evaluating the sub-trees). + * @return The sum of the values of both child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) + right.evaluate(programParameters); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // Adding zero is pointless, the expression can be reduced to its other argument. + if (simplifiedRight instanceof Constant && simplifiedRight.evaluate(NO_ARGS) == 0) + { + return simplifiedLeft; + } + else if (simplifiedLeft instanceof Constant && simplifiedLeft.evaluate(NO_ARGS) == 0) + { + return simplifiedRight; + } + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + else if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) + simplifiedRight.evaluate(NO_ARGS)); + } + else if (simplifiedLeft != left || simplifiedRight != right) + { + return new Addition(simplifiedLeft, simplifiedRight); + } + else + { + return this; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/BinaryNode.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/BinaryNode.java new file mode 100644 index 0000000..2273f74 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/BinaryNode.java @@ -0,0 +1,214 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.lang.reflect.Constructor; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.util.reflection.ReflectionUtils; + +/** + * Convenient base class for {@link Node}s that have two sub-trees. + * @author Daniel Dyer + */ +abstract class BinaryNode implements Node +{ + protected static final double[] NO_ARGS = new double[0]; + + /** The first argument to the binary function. */ + protected final Node left; + /** The second argument to the binary function. */ + protected final Node right; + + private final char symbol; + + + /** + * @param left The first argument to the binary function. + * @param right The second argument to the binary function. + * @param symbol A single character that indicates the type of function. + */ + protected BinaryNode(Node left, Node right, char symbol) + { + this.left = left; + this.right = right; + this.symbol = symbol; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return String.valueOf(symbol); + } + + + /** + * The arity of a binary node is two. + * @return 2 + */ + public int getArity() + { + return 2; + } + + + /** + * The depth of a binary node is the depth of its deepest sub-tree plus one. + * @return The depth of the tree rooted at this node. + */ + public int getDepth() + { + return 1 + Math.max(left.getDepth(), right.getDepth()); + } + + + /** + * The width of a binary node is the sum of the widths of its two sub-trees. + * @return The width of the tree rooted at this node. + */ + public int getWidth() + { + return left.getWidth() + right.getWidth(); + } + + + /** + * {@inheritDoc} + */ + public int countNodes() + { + return 1 + left.countNodes() + right.countNodes(); + } + + + /** + * {@inheritDoc} + */ + public Node getNode(int index) + { + if (index == 0) + { + return this; + } + int leftNodes = left.countNodes(); + if (index <= leftNodes) + { + return left.getNode(index - 1); + } + else + { + return right.getNode(index - leftNodes - 1); + } + } + + + /** + * {@inheritDoc} + */ + public Node getChild(int index) + { + switch (index) + { + case 0: return left; + case 1: return right; + default: throw new IndexOutOfBoundsException("Invalid child index: " + index); + } + } + + + /** + * {@inheritDoc} + */ + public Node replaceNode(int index, Node newNode) + { + if (index == 0) + { + return newNode; + } + + int leftNodes = left.countNodes(); + if (index <= leftNodes) + { + return newInstance(left.replaceNode(index - 1, newNode), right); + } + else + { + return newInstance(left, right.replaceNode(index - leftNodes - 1, newNode)); + } + } + + + + /** + * {@inheritDoc} + */ + public String print() + { + StringBuilder buffer = new StringBuilder("("); + buffer.append(left.print()); + buffer.append(' '); + buffer.append(symbol); + buffer.append(' '); + buffer.append(right.print()); + buffer.append(')'); + return buffer.toString(); + } + + + /** + * {@inheritDoc} + */ + public Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory) + { + if (mutationProbability.nextEvent(rng)) + { + return treeFactory.generateRandomCandidate(rng); + } + else + { + Node newLeft = left.mutate(rng, mutationProbability, treeFactory); + Node newRight = right.mutate(rng, mutationProbability, treeFactory); + if (newLeft != left && newRight != right) + { + return newInstance(newLeft, newRight); + } + else + { + // Tree has not changed. + return this; + } + } + } + + + private Node newInstance(Node newLeft, Node newRight) + { + Constructor<? extends BinaryNode> constructor = ReflectionUtils.findKnownConstructor(this.getClass(), + Node.class, + Node.class); + return ReflectionUtils.invokeUnchecked(constructor, newLeft, newRight); + } + + + @Override + public String toString() + { + return print(); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Constant.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Constant.java new file mode 100644 index 0000000..2a07538 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Constant.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.text.DecimalFormat; + +/** + * A program node that evaluates to a constant value. + * @author Daniel Dyer + */ +public class Constant extends LeafNode +{ + private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("######0.##"); + + private final double constant; + private final String label; + + + /** + * Creates a constant-valued node. + * @param constant The value that this node will always evaluate to. + */ + public Constant(double constant) + { + this.constant = constant; + this.label = NUMBER_FORMAT.format(constant); + } + + + /** + * @param programParameters The parameters passed to the program (ignored by this node). + * @return The numeric value of this constant. + */ + public double evaluate(double[] programParameters) + { + return constant; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return label; + } + + + /** + * @return The String representation of this constant. + */ + public String print() + { + return String.valueOf(constant); + } + + + /** + * Two constants are equal if they have the same numeric value. + * @param other The object that this object is compared to. + * @return True if the constants are equivalent, false otherwise. + */ + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (other == null || getClass() != other.getClass()) + { + return false; + } + return Double.compare(((Constant) other).constant, constant) == 0; + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + long temp = constant != +0.0d ? Double.doubleToLongBits(constant) : 0L; + return (int) (temp ^ (temp >>> 32)); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExample.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExample.java new file mode 100644 index 0000000..c060d49 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExample.java @@ -0,0 +1,85 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.EvolutionLogger; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; + +/** + * Simple tree-based genetic programming application based on the first example + * in Chapter 11 of Toby Segaran's Programming Collective Intelligence. + * @author Daniel Dyer + */ +public class GeneticProgrammingExample +{ + // This data describes the problem. For each pair of inputs, the generated program + // should return the associated output. The goal of this appliction is to generalise + // the examples into an equation. + private static final Map<double[], Double> TEST_DATA = new HashMap<double[], Double>(); + static + { + TEST_DATA.put(new double[]{26, 35}, 829.0d); + TEST_DATA.put(new double[]{8, 24}, 141.0d); + TEST_DATA.put(new double[]{20, 1}, 467.0d); + TEST_DATA.put(new double[]{33, 11}, 1215.0d); + TEST_DATA.put(new double[]{37, 16}, 1517.0d); + } + + + public static void main(String[] args) + { + Node program = evolveProgram(TEST_DATA); + System.out.println(program.print()); + } + + + /** + * Evolve a function to fit the specified data. + * @param data A map from input values to expected output values. + * @return A program that generates the correct outputs for all specified + * sets of input. + */ + public static Node evolveProgram(Map<double[], Double> data) + { + TreeFactory factory = new TreeFactory(2, // Number of parameters passed into each program. + 4, // Maximum depth of generated trees. + Probability.EVENS, // Probability that a node is a function node. + new Probability(0.6d)); // Probability that other nodes are params rather than constants. + List<EvolutionaryOperator<Node>> operators = new ArrayList<EvolutionaryOperator<Node>>(3); + operators.add(new TreeMutation(factory, new Probability(0.4d))); + operators.add(new TreeCrossover()); + operators.add(new Simplification()); + TreeEvaluator evaluator = new TreeEvaluator(data); + EvolutionEngine<Node> engine = new GenerationalEvolutionEngine<Node>(factory, + new EvolutionPipeline<Node>(operators), + evaluator, + new RouletteWheelSelection(), + new MersenneTwisterRNG()); + engine.addEvolutionObserver(new EvolutionLogger<Node>()); + return engine.evolve(1000, 5, new TargetFitness(0d, evaluator.isNatural())); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElse.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElse.java new file mode 100644 index 0000000..54abc4c --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElse.java @@ -0,0 +1,268 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; + +/** + * Simple conditional program {@link Node}. + * @author Daniel Dyer + */ +public class IfThenElse implements Node +{ + private final Node condition; + private final Node then; + private final Node otherwise; + + /** + * @param condition If this node evaluates to a value greater than zero then + * the value of the {@literal then} node is returned. Otherwise the value of + * the {@literal otherwise} node is returned. + * @param then This node is evaluated if the {@literal condition} node has a + * value greater than zero. + * @param otherwise This node is evaluated if the {@literal condition} node has a + * value less than or equal to zero. + */ + public IfThenElse(Node condition, Node then, Node otherwise) + { + this.condition = condition; + this.then = then; + this.otherwise = otherwise; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return "if"; + } + + + /** + * The arity of a ternary node is three. + * @return 3 + */ + public int getArity() + { + return 3; + } + + + /** + * {@inheritDoc} + */ + public int getDepth() + { + return 1 + Math.max(condition.getDepth(), Math.max(then.getDepth(), otherwise.getDepth())); + } + + + /** + * {@inheritDoc} + */ + public int getWidth() + { + return condition.getWidth() + then.getWidth() + otherwise.getWidth(); + } + + + /** + * {@inheritDoc} + */ + public int countNodes() + { + return 1 + condition.countNodes() + then.countNodes() + otherwise.countNodes(); + } + + + /** + * {@inheritDoc} + */ + public Node getNode(int index) + { + if (index == 0) + { + return this; + } + int conditionNodes = condition.countNodes(); + if (index <= conditionNodes) + { + return condition.getNode(index - 1); + } + else + { + int thenNodes = then.countNodes(); + if (index <= conditionNodes + thenNodes) + { + return then.getNode(index - conditionNodes - 1); + } + else + { + return otherwise.getNode(index - conditionNodes - thenNodes - 1); + } + } + } + + + /** + * {@inheritDoc} + */ + public Node getChild(int index) + { + switch (index) + { + case 0: return condition; + case 1: return then; + case 2 : return otherwise; + default: throw new IndexOutOfBoundsException("Invalid child index: " + index); + } + } + + + + /** + * {@inheritDoc} + */ + public Node replaceNode(int index, Node newNode) + { + if (index == 0) + { + return newNode; + } + + int conditionNodes = condition.countNodes(); + if (index <= conditionNodes) + { + return new IfThenElse(condition.replaceNode(index - 1, newNode), then, otherwise); + } + else + { + int thenNodes = then.countNodes(); + if (index <= conditionNodes + thenNodes) + { + return new IfThenElse(condition, then.replaceNode(index - conditionNodes - 1, newNode), otherwise); + } + else + { + return new IfThenElse(condition, + then, + otherwise.replaceNode(index - conditionNodes - thenNodes - 1, newNode)); + } + } + } + + + + /** + * {@inheritDoc} + * Operates on three other nodes. The first is an expression to evaluate. + * Which of the other two nodes is evaluated and returned depends on whether + * this node evaluates to greater than zero or not. + * @param programParameters Program parameters (ignored by the conditional operator + * but may be used in evaluating child nodes). + */ + public double evaluate(double[] programParameters) + { + return condition.evaluate(programParameters) > 0 // If... + ? then.evaluate(programParameters) // Then... + : otherwise.evaluate(programParameters); // Else... + } + + + /** + * {@inheritDoc} + */ + public String print() + { + return "(" + condition.print() + " ? " + then.print() + " : " + otherwise.print() + ")"; + } + + + /** + * {@inheritDoc} + */ + public Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory) + { + if (mutationProbability.nextEvent(rng)) + { + return treeFactory.generateRandomCandidate(rng); + } + else + { + Node newCondition = condition.mutate(rng, mutationProbability, treeFactory); + Node newThen = then.mutate(rng, mutationProbability, treeFactory); + Node newOtherwise = otherwise.mutate(rng, mutationProbability, treeFactory); + if (newCondition != condition || newThen != then || newOtherwise != otherwise) + { + return new IfThenElse(newCondition, newThen, newOtherwise); + } + else + { + // Tree has not changed. + return this; + } + } + + } + + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + return print(); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedCondition = condition.simplify(); + + // If the condition is constant then the expression can be replaced by the branch that + // always gets evaluated. + if (simplifiedCondition instanceof Constant) + { + return simplifiedCondition.evaluate(null) > 0 ? then.simplify() : otherwise.simplify(); + } + else + { + Node simplifiedThen = then.simplify(); + Node simplifiedOtherwise = otherwise.simplify(); + // If both branches are identical, the condition is irrelevant. + if (simplifiedThen.equals(simplifiedOtherwise)) + { + return simplifiedThen; + } + // Only create a new node if something has actually changed, otherwise return the existing node. + if (simplifiedCondition != condition || simplifiedThen != then || simplifiedOtherwise != otherwise) + { + return new IfThenElse(simplifiedCondition, simplifiedThen, simplifiedOtherwise); + } + else + { + return this; + } + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IsGreater.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IsGreater.java new file mode 100644 index 0000000..bd23472 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/IsGreater.java @@ -0,0 +1,78 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * A program {@link Node} that evaluates to a one if the value of its first + * argument is greater than the value of its second, or evaluates to zero otherwise. + * @author Daniel Dyer + */ +public class IsGreater extends BinaryNode +{ + /** + * Creates a node that evaluates to one if the value of the first child node + * is greater than the value of the second child node. Otherwise it evaluates + * to zero. + * @param left The first operand. + * @param right The second operand. + */ + public IsGreater(Node left, Node right) + { + super(left, right, '>'); + } + + + /** + * Returns a value of one if the value of the first node is greater than the value of + * the second node. Returns a value of zero otherwise. + * @param programParameters The parameters passed to this program (ignored by the + * IsGreater node but may be used in the evaluation of child nodes). + * @return One or zero depending on the relative values of the two child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) > right.evaluate(programParameters) ? 1 : 0; + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // If the two arguments are exactly equivalent, one cannot be greater than the other. + if (simplifiedLeft.equals(simplifiedRight)) + { + return new Constant(0); + } + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + else if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) > simplifiedRight.evaluate(NO_ARGS) ? 1 : 0); + } + else if (simplifiedLeft != left || simplifiedRight != right) + { + return new IsGreater(simplifiedLeft, simplifiedRight); + } + else + { + return this; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/LeafNode.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/LeafNode.java new file mode 100644 index 0000000..cf8718a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/LeafNode.java @@ -0,0 +1,127 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; + +/** + * Convenient base class for {@link Node}s that have no sub-trees. + * @author Daniel Dyer + */ +abstract class LeafNode implements Node +{ + /** + * The arity of a non-function node is always zero. + * @return 0 + */ + public int getArity() + { + return 0; + } + + + /** + * Leaf nodes always have a depth of 1 since they have no child nodes. + * @return 1 + */ + public int getDepth() + { + return 1; + } + + + /** + * Leaf nodes always have a width of 1 since they have no child nodes. + * @return 1 + */ + public int getWidth() + { + return 1; + } + + + /** + * {@inheritDoc} + */ + public int countNodes() + { + return 1; + } + + + public Node getNode(int index) + { + if (index != 0) + { + throw new IndexOutOfBoundsException("Invalid node index: " + index); + } + return this; + } + + + /** + * {@inheritDoc} + */ + public Node getChild(int index) + { + throw new IndexOutOfBoundsException("Leaf nodes have no children."); + } + + + public Node replaceNode(int index, Node newNode) + { + if (index != 0) + { + throw new IndexOutOfBoundsException("Invalid node index: " + index); + } + return newNode; + } + + + /** + * {@inheritDoc} + */ + public Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory) + { + if (mutationProbability.nextEvent(rng)) + { + return treeFactory.generateRandomCandidate(rng); + } + else + { + // Node is unchanged. + return this; + } + } + + + @Override + public String toString() + { + return print(); + } + + + /** + * Returns this node (leaf nodes cannot be simplified). + * @return This node, unmodified. + */ + public Node simplify() + { + return this; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Multiplication.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Multiplication.java new file mode 100644 index 0000000..bd5dd60 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Multiplication.java @@ -0,0 +1,90 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * Simple multiplication operator {@link Node}. + * @author Daniel Dyer + */ +public class Multiplication extends BinaryNode +{ + /** + * Creates a node that evaluates to the product of the values of its two + * child nodes ({@literal left} and {@literal right}). + * @param left The first operand. + * @param right The second operand. + */ + public Multiplication(Node left, Node right) + { + super(left, right, '*'); + } + + + /** + * Evaluates the two sub-trees and returns the product of these two values. + * @param programParameters Program parameters (ignored by the multiplication operator + * but may be used in evaluating the sub-trees). + * @return The sum of the values of both child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) * right.evaluate(programParameters); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) * simplifiedRight.evaluate(NO_ARGS)); + } + // Multiplying by one is pointless, the expression can be reduced to its other argument. + else if (simplifiedRight instanceof Constant) + { + double constant = simplifiedRight.evaluate(NO_ARGS); + if (constant == 1) + { + return simplifiedLeft; + } + else if (constant == 0) + { + return new Constant(0); + } + } + else if (simplifiedLeft instanceof Constant) + { + double constant = simplifiedLeft.evaluate(NO_ARGS); + if (constant == 1) + { + return simplifiedRight; + } + else if (constant == 0) + { + return new Constant(0); + } + } + return simplifiedLeft != left || simplifiedRight != right + ? new Multiplication(simplifiedLeft, simplifiedRight) + : this; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Node.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Node.java new file mode 100644 index 0000000..7df1685 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Node.java @@ -0,0 +1,120 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; + +/** + * Operations supported by the different types of nodes that make up + * genetic program trees. + * @author Daniel Dyer + */ +public interface Node +{ + /** + * Recursively evaluates the (sub-)tree represented by this node (including any + * child nodes) and returns a numeric value. + * @param programParameters Program parameters (possibly used by this node and/or + * in the evaluation of child nodes). + * @return The result of evaluating this node and all of its children. + */ + double evaluate(double[] programParameters); + + /** + * Recursively builds a string representation of the tree rooted at this node. + * @return A string representation of this tree. + */ + String print(); + + /** + * @return A short String that represents the function or value represented by this node. + */ + String getLabel(); + + /** + * If this is a function (non-leaf) node, how many arguments does it take? For + * leaf nodes the answer is zero. + * @return The arity of this function, or zero if this node is a leaf node. + * @see #countNodes() + */ + int getArity(); + + /** + * @return The number of levels of nodes that make up this tree. + * @see #getWidth() + */ + int getDepth(); + + /** + * Work out how wide (in nodes) this tree is. Used primarily for laying out a + * visual representation. A leaf node has a width of 1. A binary node's width + * is the sum of the widths of its sub-trees. + * @return The maximum width of this tree. + * @see #getDepth() + * @see #getArity() + */ + int getWidth(); + + /** + * @return The total number of nodes in this tree (recursively counts the nodes + * for each sub-node of this node). + * @see #getArity() + */ + int countNodes(); + + /** + * Retrieves a sub-node from this tree. + * @param index The index of a node. Index 0 is the root node. Nodes are numbered + * depth-first, right-to-left. + * @return The node at the specified position. + */ + Node getNode(int index); + + /** + * Retrieves a direct sub-node from this tree. + * @param index The index of a child node. Index 0 is the first child. Nodes are numbered + * right-to-left, grandchild nodes are not included. + * @return The node at the specified position. + */ + Node getChild(int index); + + /** + * Returns a new tree that is identical to this tree except with the specified node + * replaced. + * @param index The index of the node to replace. + * @param newNode The replacement node. + * @return A new tree with the node at the specified index replaced by + * {@code newNode}. + */ + Node replaceNode(int index, Node newNode); + + /** + * Helper method for the {@link TreeMutation} evolutionary operator. + * @param rng A source of randomness. + * @param mutationProbability The probability that a given node will be mutated. + * @param treeFactory A factory for creating the new sub-trees needed for mutation. + * @return The mutated node (or the same node if no mutation occurred). + */ + Node mutate(Random rng, Probability mutationProbability, TreeFactory treeFactory); + + /** + * Reduce this program tree to its simplest equivalent form. + * @return A simplification of this program tree, or this program tree unodified if it + * cannot be simplified. + */ + Node simplify(); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Parameter.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Parameter.java new file mode 100644 index 0000000..575bd1d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Parameter.java @@ -0,0 +1,102 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * A program {@link Node} that simply returns the value of one of the + * program's parameters. + * @author Daniel Dyer + */ +public class Parameter extends LeafNode +{ + private final int parameterIndex; + + /** + * @param parameterIndex Which of the program's (zero-indexed) parameter + * values should be returned upon evaluation of this node. + */ + public Parameter(int parameterIndex) + { + this.parameterIndex = parameterIndex; + } + + + /** + * Returns the value of one of the program parameters. + * @param programParameters The parameters to this program. + * @return The program parameter at the index condigured for this node. + */ + public double evaluate(double[] programParameters) + { + if (parameterIndex >= programParameters.length) + { + throw new IllegalArgumentException("Invalid parameter index: " + parameterIndex); + } + return programParameters[parameterIndex]; + } + + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return "P" + parameterIndex; + } + + + /** + * {@inheritDoc} + * For a parameter node the String representation is simply "arg0", "arg1", etc. + * depending on which program parameter it refers to. + */ + public String print() + { + return "arg" + parameterIndex; + } + + + /** + * Two parameters are equal if they evaluate to the same program argument + * (i.e. they have the same parameter index). + * @param other The object that this object is compared to. + * @return True if the parameters are equivalent, false otherwise. + */ + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (other == null || getClass() != other.getClass()) + { + return false; + } + return parameterIndex == ((Parameter) other).parameterIndex; + } + + + /** + * Over-ridden to be consistent with {@link #equals(Object)}. + * @return This object's hash code. + */ + @Override + public int hashCode() + { + return parameterIndex; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Simplification.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Simplification.java new file mode 100644 index 0000000..f44864b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Simplification.java @@ -0,0 +1,71 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Evolutionary operator for GP trees that reduces them to their simplest form. + * @author Daniel Dyer + */ +public class Simplification implements EvolutionaryOperator<Node> +{ + private final Probability probability; + + + /** + * Creates a simplification operator with a probability of 1 (i.e. all + * canidates will be simplified). + */ + public Simplification() + { + this(Probability.ONE); + } + + + /** + * Creates a simplfication operator that has the specified probability of being + * applied to any individual candidate. + * @param probability The probability that this operator will attempt to simplify + * any single expression. + */ + public Simplification(Probability probability) + { + this.probability = probability; + } + + + /** + * Simplify the expressions represented by the candidates. Each expression + * is simplified according to the configured probability. + * @param selectedCandidates The individuals to evolve. + * @param rng A source of randomness. + * @return The (possibly) simplified candidates. + */ + public List<Node> apply(List<Node> selectedCandidates, Random rng) + { + List<Node> evolved = new ArrayList<Node>(selectedCandidates.size()); + for (Node node : selectedCandidates) + { + evolved.add(probability.nextEvent(rng) ? node.simplify() : node); + } + return evolved; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Subtraction.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Subtraction.java new file mode 100644 index 0000000..97083af --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/Subtraction.java @@ -0,0 +1,80 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +/** + * Simple subtraction operator {@link Node}. + * @author Daniel Dyer + */ +public class Subtraction extends BinaryNode +{ + /** + * Creates a node that evaluates the the value of {@literal left} + * minus the value of {@literal right}. + * @param left The first operand. + * @param right The second operand. + */ + public Subtraction(Node left, Node right) + { + super(left, right, '-'); + } + + + /** + * Evaluates the two sub-trees and returns the difference between these two values. + * @param programParameters Program parameters (ignored by the subtraction operator + * but may be used in evaluating the sub-trees). + * @return The difference between the values of the two child nodes. + */ + public double evaluate(double[] programParameters) + { + return left.evaluate(programParameters) - right.evaluate(programParameters); + } + + + /** + * {@inheritDoc} + */ + public Node simplify() + { + Node simplifiedLeft = left.simplify(); + Node simplifiedRight = right.simplify(); + // If the two arguments are identical then the result will always be zero. + if (simplifiedLeft.equals(simplifiedRight)) + { + return new Constant(0); + } + // Subtracting zero is pointless, the expression can be reduced to its lefthand side. + else if (simplifiedRight instanceof Constant && simplifiedRight.evaluate(NO_ARGS) == 0) + { + return simplifiedLeft; + } + // If the two arguments are constants, we can simplify by calculating the result, it won't + // ever change. + else if (simplifiedLeft instanceof Constant && simplifiedRight instanceof Constant) + { + return new Constant(simplifiedLeft.evaluate(NO_ARGS) - simplifiedRight.evaluate(NO_ARGS)); + } + else if (simplifiedLeft != left || simplifiedRight != right) + { + return new Subtraction(simplifiedLeft, simplifiedRight); + } + else + { + return this; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRenderer.java new file mode 100644 index 0000000..fd16238 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRenderer.java @@ -0,0 +1,141 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import javax.swing.JComponent; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * A Swing renderer for genetic programming trees. + * @author Daniel Dyer + */ +public class SwingGPTreeRenderer implements Renderer<Node, JComponent> +{ + /** + * Renders a GP tree as a Swing component. + * @param tree The root node of the GP tree to render. + * @return A {@link JComponent} that displays a graphical representation of the tree. + */ + public JComponent render(Node tree) + { + return new GPTreeView(tree); + } + + + private static final class GPTreeView extends JComponent + { + // Allow 30 pixels for each node horizontally. + private static final int NODE_WIDTH = 30; + // Allow 50 pixels for each node vertically. + private static final int NODE_HEIGHT = 50; + private static final int CIRCLE_RADIUS = 9; + private static final int CIRCLE_DIAMETER = CIRCLE_RADIUS * 2; + + private final Node rootNode; + + + GPTreeView(Node rootNode) + { + this.rootNode = rootNode; + int minHeight = rootNode.getDepth() * NODE_HEIGHT; + int minWidth = rootNode.getWidth() * NODE_WIDTH; + Dimension size = new Dimension(minWidth, minHeight); + setMinimumSize(size); + setPreferredSize(size); + } + + + @Override + protected void paintComponent(Graphics graphics) + { + super.paintComponent(graphics); + if (graphics instanceof Graphics2D) + { + ((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + // Center the tree if the component is bigger than required. + int offset = (getSize().width - getMinimumSize().width) / 2; + drawNode(rootNode, offset, 0, graphics); + } + + + /** + * Recursively draw the specified node and its children. + * @param node The sub-tree to draw. + * @param x The left edge of the area in which this tree is drawn. + * @param y The top edge of the area in which this tree is drawn. + * @param graphics The target for drawing. + */ + private void drawNode(Node node, int x, int y, Graphics graphics) + { + int start = x + node.getWidth() * NODE_WIDTH / 2; + if (node instanceof Constant) + { + graphics.setColor(Color.YELLOW); + graphics.fillRoundRect(start - (NODE_WIDTH / 2 - 2), + y, + NODE_WIDTH - 4, + CIRCLE_DIAMETER, + CIRCLE_RADIUS, + CIRCLE_RADIUS); + graphics.setColor(Color.BLACK); + graphics.drawRoundRect(start - (NODE_WIDTH / 2 - 2), + y, + NODE_WIDTH - 4, + CIRCLE_DIAMETER, + CIRCLE_RADIUS, + CIRCLE_RADIUS); + } + else if (node instanceof Parameter) + { + graphics.setColor(Color.GREEN); + graphics.fillRect(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + graphics.setColor(Color.BLACK); + graphics.drawRect(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + } + else + { + graphics.setColor(Color.WHITE); + graphics.fillOval(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + graphics.setColor(Color.BLACK); + graphics.drawOval(start - CIRCLE_RADIUS, y, CIRCLE_DIAMETER, CIRCLE_DIAMETER); + } + FontMetrics metrics = graphics.getFontMetrics(); + int stringWidth = metrics.stringWidth(node.getLabel()); + graphics.drawString(node.getLabel(), + (int) Math.round(start - (double) stringWidth / 2), + (int) Math.round(y + CIRCLE_RADIUS + (double) metrics.getHeight() / 2 - metrics.getDescent())); + + int xOffset = x; + for (int i = 0; i < node.getArity(); i++) + { + Node child = node.getChild(i); + drawNode(child, xOffset, y + NODE_HEIGHT, graphics); + graphics.drawLine(start, + y + CIRCLE_DIAMETER, xOffset + (child.getWidth() * NODE_WIDTH / 2), + y + NODE_HEIGHT); + xOffset += child.getWidth() * NODE_WIDTH; + } + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossover.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossover.java new file mode 100644 index 0000000..8fe711e --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossover.java @@ -0,0 +1,72 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.operators.AbstractCrossover; + +/** + * Cross-over operator for the trees of {@link Node}s used in the genetic + * programming example application. + * @author Daniel Dyer + */ +public class TreeCrossover extends AbstractCrossover<Node> +{ + /** + * Creates a single-point cross-over operator. + */ + public TreeCrossover() + { + super(1); + } + + + /** + * Swaps randomly selected sub-trees between the two parents. + * @param parent1 The first parent. + * @param parent2 The second parent. + * @param numberOfCrossoverPoints The number of cross-overs to perform. + * @param rng A source of randomness. + * @return A list of two offspring, generated by swapping sub-trees + * between the two parents. + */ + @Override + protected List<Node> mate(Node parent1, + Node parent2, + int numberOfCrossoverPoints, + Random rng) + { + List<Node> offspring = new ArrayList<Node>(2); + Node offspring1 = parent1; + Node offspring2 = parent2; + + for (int i = 0; i < numberOfCrossoverPoints; i++) + { + int crossoverPoint1 = rng.nextInt(parent1.countNodes()); + Node subTree1 = parent1.getNode(crossoverPoint1); + int crossoverPoint2 = rng.nextInt(parent2.countNodes()); + Node subTree2 = parent2.getNode(crossoverPoint2); + offspring1 = parent1.replaceNode(crossoverPoint1, subTree2); + offspring2 = parent2.replaceNode(crossoverPoint2, subTree1); + } + + offspring.add(offspring1); + offspring.add(offspring2); + return offspring; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluator.java new file mode 100644 index 0000000..57e718b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluator.java @@ -0,0 +1,79 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.List; +import java.util.Map; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Fitness function for the genetic programming example application. + * An evolved program tree is tested against a table of inputs and associated + * expected outputs. If the evolved program correctly calculates the right answer + * for all sets of inputs then it has a fitness of zero. Otherwise, its fitness + * is an error value that indicates how accurate it was (the larger the combined + * error value, the less accurate the function is). + * @author Daniel Dyer + */ +public class TreeEvaluator implements FitnessEvaluator<Node> +{ + private final Map<double[], Double> data; + + + /** + * @param data Each row is consists of a set of inputs and an expected output (the + * last item in the row is the output). + */ + public TreeEvaluator(Map<double[], Double> data) + { + this.data = data; + } + + + /** + * If the evolved program correctly calculates the right answer + * for all sets of inputs then it has a fitness of zero. Otherwise, its fitness + * is an error value that indicates how accurate it was (the larger the combined + * error value, the less accurate the function is). + * The combined error value is calculated by summing the squares of each individual + * error (the difference between the expected output and the actual output). + * @param candidate The program tree to evaluate. + * @param population Ignored by this implementation. + * @return The fitness score for the specified candidate. + */ + public double getFitness(Node candidate, List<? extends Node> population) + { + double error = 0; + for (Map.Entry<double[], Double> entry : data.entrySet()) + { + double actualValue = candidate.evaluate(entry.getKey()); + double diff = actualValue - entry.getValue(); + error += (diff * diff); + } + return error; + } + + + /** + * This fitness evaluator is a minimising function. A fitness of zero + * indicates a perfect solution. + * @return false + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactory.java new file mode 100644 index 0000000..abb935d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactory.java @@ -0,0 +1,115 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * {@link org.uncommons.watchmaker.framework.CandidateFactory} for generating + * trees of {@link Node}s for the genetic programming example application. + * @author Daniel Dyer + */ +public class TreeFactory extends AbstractCandidateFactory<Node> +{ + // The number of program parameters that each program tree will be provided. + private final int parameterCount; + + // The maximum depth of a program tree. No function nodes will be created below + // this depth (branches will be terminated with parameters or constants). + private final int maximumDepth; + + // Probability that a created node is a function node rather + // than a value node. + private final Probability functionProbability; + + // Probability that a value (non-function) node is a parameter + // node rather than a constant node. + private final Probability parameterProbability; + + + /** + * @param parameterCount The number of program parameters that each + * generated program tree can will be provided when executed. + * @param maxDepth The maximum depth of generated trees. + * @param functionProbability The probability (between 0 and 1) that a + * randomly-generated node will be a function node rather than a value + * (parameter or constant) node. + * @param parameterProbability The probability that a randomly-generated + * non-function node will be a parameter node rather than a constant node. + */ + public TreeFactory(int parameterCount, + int maxDepth, + Probability functionProbability, + Probability parameterProbability) + { + if (parameterCount < 0) + { + throw new IllegalArgumentException("Parameter count must be greater than or equal to 0."); + } + if (maxDepth < 1) + { + throw new IllegalArgumentException("Max depth must be at least 1."); + } + + this.parameterCount = parameterCount; + this.maximumDepth = maxDepth; + this.functionProbability = functionProbability; + this.parameterProbability = parameterProbability; + } + + + /** + * {@inheritDoc} + */ + public Node generateRandomCandidate(Random rng) + { + return makeNode(rng, maximumDepth); + } + + + /** + * Recursively constructs a tree of Nodes, up to the specified maximum depth. + * @param rng The RNG used to random create nodes. + * @param maxDepth The maximum depth of the generated tree. + * @return A tree of nodes. + */ + private Node makeNode(Random rng, int maxDepth) + { + if (functionProbability.nextEvent(rng) && maxDepth > 1) + { + // Max depth for sub-trees is one less than max depth for this node. + int depth = maxDepth - 1; + switch (rng.nextInt(5)) + { + case 0: return new Addition(makeNode(rng, depth), makeNode(rng, depth)); + case 1: return new Subtraction(makeNode(rng, depth), makeNode(rng, depth)); + case 2: return new Multiplication(makeNode(rng, depth), makeNode(rng, depth)); + case 3: return new IfThenElse(makeNode(rng, depth), makeNode(rng, depth), makeNode(rng, depth)); + default: return new IsGreater(makeNode(rng, depth), makeNode(rng, depth)); + } + } + else if (parameterProbability.nextEvent(rng)) + { + return new Parameter(rng.nextInt(parameterCount)); + } + else + { + return new Constant(rng.nextInt(11)); + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutation.java new file mode 100644 index 0000000..75f64b5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutation.java @@ -0,0 +1,61 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutation operator for the trees of {@link Node}s used in the genetic + * programming example application. + * @author Daniel Dyer + */ +public class TreeMutation implements EvolutionaryOperator<Node> +{ + private final TreeFactory treeFactory; + + private final Probability mutationProbability; + + /** + * The tree mutation operator requires a {@link TreeFactory} because + * the process of mutation involves creating new sub-trees. The same + * TreeFactory that is used to create the initial population should be + * used. + * @param treeFactory Used to generate the new sub-trees required for mutation. + * @param mutationProbability The probability that any given node in a tree is + * mutated by this operator. + */ + public TreeMutation(TreeFactory treeFactory, + Probability mutationProbability) + { + this.treeFactory = treeFactory; + this.mutationProbability = mutationProbability; + } + + + public List<Node> apply(List<Node> selectedCandidates, Random rng) + { + List<Node> mutatedPopulation = new ArrayList<Node>(selectedCandidates.size()); + for (Node tree : selectedCandidates) + { + mutatedPopulation.add(tree.mutate(rng, mutationProbability, treeFactory)); + } + return mutatedPopulation; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/package-info.java new file mode 100644 index 0000000..2aa8b11 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/geneticprogramming/package-info.java @@ -0,0 +1,20 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * A simple example of tree-based genetic programming. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.geneticprogramming; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AbstractVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AbstractVertexMutation.java new file mode 100644 index 0000000..490ed92 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AbstractVertexMutation.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Base class for mutation operators that modify the points of polygons in an + * image. + * @author Daniel Dyer + */ +abstract class AbstractVertexMutation implements EvolutionaryOperator<ColouredPolygon> +{ + private final Dimension canvasSize; + private final NumberGenerator<Probability> mutationProbability; + + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the probability + * that a polygon's points will be mutated. + * @param canvasSize The size of the canvas. Used to constrain the positions of the points. + */ + protected AbstractVertexMutation(NumberGenerator<Probability> mutationProbability, + Dimension canvasSize) + { + this.mutationProbability = mutationProbability; + this.canvasSize = canvasSize; + } + + + /** + * @return The dimensions of the target image. + */ + protected Dimension getCanvasSize() + { + return canvasSize; + } + + + /** + * @return The {@link NumberGenerator} that provides the mutation probability. + */ + protected NumberGenerator<Probability> getMutationProbability() + { + return mutationProbability; + } + + + /** + * Applies the mutation to each polygon in the list provided according to the + * pre-configured mutation probability. If the probability is 0.1, approximately + * 10% of the individuals will be mutated. The actual mutation operation is + * defined in the sub-class implementation of the + * {@link #mutateVertices(java.util.List, java.util.Random)} method. + * @param polygons The list of polygons to be mutated. + * @param rng A source of randomness. + * @return The polygons after mutation. None, some or all will have been + * modified. + */ + public List<ColouredPolygon> apply(List<ColouredPolygon> polygons, Random rng) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(polygons.size()); + for (ColouredPolygon polygon : polygons) + { + List<Point> newVertices = mutateVertices(polygon.getVertices(), rng); + newPolygons.add(newVertices == polygon.getVertices() + ? polygon + : new ColouredPolygon(polygon.getColour(), newVertices)); + } + return newPolygons; + } + + + /** + * Implemented in sub-classes to perform the mutation of the vertices. + * @param vertices A list of the points that make up the polygon. + * @param rng A source of randomness. + * @return A mutated list of points. + */ + protected abstract List<Point> mutateVertices(List<Point> vertices, Random rng); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutation.java new file mode 100644 index 0000000..183a8b7 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutation.java @@ -0,0 +1,97 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Randomly mutates the polygons that make up an image by adding a polygon + * according to some probability. + * @author Daniel Dyer + */ +public class AddPolygonMutation implements EvolutionaryOperator<List<ColouredPolygon>> +{ + private final NumberGenerator<Probability> addPolygonProbability; + private final PolygonImageFactory factory; + private final int maxPolygons; + + + + /** + * @param addPolygonProbability A {@link NumberGenerator} that controls the probability + * that a polygon will be added. + * @param factory Used to create new polygons. + * @param maxPolygons The maximum number of polygons permitted in an image (must be at least 2). + */ + public AddPolygonMutation(NumberGenerator<Probability> addPolygonProbability, + PolygonImageFactory factory, + int maxPolygons) + { + if (maxPolygons < 2) + { + throw new IllegalArgumentException("Max polygons must be > 1."); + } + this.addPolygonProbability = addPolygonProbability; + this.factory = factory; + this.maxPolygons = maxPolygons; + } + + + /** + * @param addPolygonProbability The probability that a polygon will be removed. + * @param factory Used to create new polygons. + * @param maxPolygons The maximum number of polygons permitted in an image (must be at least 2). + */ + public AddPolygonMutation(Probability addPolygonProbability, + PolygonImageFactory factory, + int maxPolygons) + { + this(new ConstantGenerator<Probability>(addPolygonProbability), + factory, + maxPolygons); + } + + + public List<List<ColouredPolygon>> apply(List<List<ColouredPolygon>> selectedCandidates, Random rng) + { + List<List<ColouredPolygon>> mutatedCandidates = new ArrayList<List<ColouredPolygon>>(selectedCandidates.size()); + for (List<ColouredPolygon> candidate : selectedCandidates) + { + // A single polygon is added with the configured probability, unless + // we already have the maximum permitted number of polygons. + if (candidate.size() < maxPolygons && addPolygonProbability.nextValue().nextEvent(rng)) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(candidate); + newPolygons.add(rng.nextInt(newPolygons.size() + 1), + factory.createRandomPolygon(rng)); + mutatedCandidates.add(newPolygons); + } + else // Nothing changed. + { + mutatedCandidates.add(candidate); + } + } + return mutatedCandidates; + } + + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddVertexMutation.java new file mode 100644 index 0000000..decebdd --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AddVertexMutation.java @@ -0,0 +1,84 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by adding a point, according to some probability. + * @author Daniel Dyer + */ +public class AddVertexMutation extends AbstractVertexMutation +{ + static final int MAX_VERTEX_COUNT = 10; + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the probability + * that a point will be added. + * @param canvasSize The size of the canvas. Used to constrain the positions of the points. + */ + public AddVertexMutation(Dimension canvasSize, + NumberGenerator<Probability> mutationProbability) + { + super(mutationProbability, canvasSize); + } + + + /** + * @param mutationProbability The probability that a point will be added. + * @param canvasSize The size of the canvas. Used to constrain the positions of the points. + */ + public AddVertexMutation(Dimension canvasSize, + Probability mutationProbability) + { + this(canvasSize, new ConstantGenerator<Probability>(mutationProbability)); + } + + + /** + * Mutates the list of vertices for a given polygon by adding a new random point. + * Whether or not a point is actually added is determined by the configured mutation probability. + * @param vertices A list of the points that make up the polygon. + * @param rng A source of randomness. + * @return A mutated list of points. + */ + @Override + protected List<Point> mutateVertices(List<Point> vertices, Random rng) + { + // A single point is added with the configured probability, unless + // we already have the maximum permitted number of points. + if (vertices.size() < MAX_VERTEX_COUNT && getMutationProbability().nextValue().nextEvent(rng)) + { + List<Point> newVertices = new ArrayList<Point>(vertices); + newVertices.add(rng.nextInt(newVertices.size()), + new Point(rng.nextInt(getCanvasSize().width), + rng.nextInt(getCanvasSize().height))); + return newVertices; + } + else // Nothing changed. + { + return vertices; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutation.java new file mode 100644 index 0000000..0549cc6 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutation.java @@ -0,0 +1,94 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.Maths; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by moving a point, according to some probability. + * @author Daniel Dyer + */ +public class AdjustVertexMutation extends AbstractVertexMutation +{ + private final NumberGenerator<? extends Number> changeAmount; + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the + * probability that a point will be moved. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + * @param changeAmount A {@link NumberGenerator} that controls the distance + * that points are moved (in pixels). Should generate both positive and + * negative values. + */ + public AdjustVertexMutation(Dimension canvasSize, + NumberGenerator<Probability> mutationProbability, + NumberGenerator<? extends Number> changeAmount) + { + super(mutationProbability, canvasSize); + this.changeAmount = changeAmount; + } + + + /** + * @param mutationProbability The probability that a point will be moved. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + * @param changeAmount A {@link NumberGenerator} that controls the distance + * that points are moved (in pixels). Should generate both positive and + * negative values. + */ + public AdjustVertexMutation(Dimension canvasSize, + Probability mutationProbability, + NumberGenerator<? extends Number> changeAmount) + { + this(canvasSize, new ConstantGenerator<Probability>(mutationProbability), changeAmount); + } + + + @Override + protected List<Point> mutateVertices(List<Point> vertices, Random rng) + { + // A single point is modified with the configured probability. + if (getMutationProbability().nextValue().nextEvent(rng)) + { + List<Point> newVertices = new ArrayList<Point>(vertices); + int xDelta = (int) Math.round(changeAmount.nextValue().doubleValue()); + int yDelta = (int) Math.round(changeAmount.nextValue().doubleValue()); + int index = rng.nextInt(newVertices.size()); + Point oldPoint = newVertices.get(index); + int newX = oldPoint.x + xDelta; + int newY = oldPoint.y + yDelta; + newX = Maths.restrictRange(newX, 0, getCanvasSize().width - 1); + newY = Maths.restrictRange(newY, 0, getCanvasSize().height - 1); + newVertices.set(index, new Point(newX, newY)); + return newVertices; + } + else // Nothing changed. + { + return vertices; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ColouredPolygon.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ColouredPolygon.java new file mode 100644 index 0000000..eb8c481 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ColouredPolygon.java @@ -0,0 +1,78 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Polygon; +import java.util.List; + +/** + * A coloured polygon consists of a set of vertices and a colour that is used to + * fill the polygon when it is rendered. + * @author Daniel Dyer + */ +public class ColouredPolygon +{ + private final Color colour; + private final List<Point> vertices; + private final Polygon polygon; + + + /** + * Creates a new filled polygon with the specified points and colour. + * @param colour The colour of this polygon. The alpha channel is used + * to specify the polygons translucency. + * @param vertices The points that define the polygon's outline. + */ + public ColouredPolygon(Color colour, List<Point> vertices) + { + this.colour = colour; + this.vertices = vertices; + this.polygon = new Polygon(); + for (Point point : vertices) + { + polygon.addPoint(point.x, point.y); + } + } + + + /** + * @return The colour that this polygon should be rendered. + */ + public Color getColour() + { + return colour; + } + + + /** + * @return A list of this polygon's vertices. + */ + public List<Point> getVertices() + { + return vertices; + } + + + /** + * @return The AWT shape used for rendering. + */ + public Polygon getPolygon() + { + return polygon; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MonaLisaApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MonaLisaApplet.java new file mode 100644 index 0000000..4a0db99 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MonaLisaApplet.java @@ -0,0 +1,249 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Random; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import org.uncommons.maths.random.Probability; +import org.uncommons.maths.random.XORShiftRNG; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.CachingFitnessEvaluator; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.FitnessEvaluator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.TerminationCondition; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.framework.selection.TournamentSelection; +import org.uncommons.watchmaker.framework.termination.Stagnation; +import org.uncommons.watchmaker.swing.AbortControl; +import org.uncommons.watchmaker.swing.ProbabilityParameterControl; +import org.uncommons.watchmaker.swing.evolutionmonitor.EvolutionMonitor; + +/** + * This program is inspired by Roger Alsing's evolution of the Mona Lisa + * (http://rogeralsing.com/2008/12/07/genetic-programming-evolution-of-mona-lisa/). + * It attempts to find the combination of 50 translucent polygons that most closely + * resembles Leonardo da Vinci's Mona Lisa. + * @author Daniel Dyer + */ +public class MonaLisaApplet extends AbstractExampleApplet +{ + private static final String IMAGE_PATH = "org/uncommons/watchmaker/examples/monalisa/monalisa.jpg"; + + private ProbabilitiesPanel probabilitiesPanel; + private EvolutionMonitor<List<ColouredPolygon>> monitor; + private JButton startButton; + private AbortControl abort; + private JSpinner populationSpinner; + private JSpinner elitismSpinner; + private ProbabilityParameterControl selectionPressureControl; + private BufferedImage targetImage; + + + @Override + public void init() + { + try + { + URL imageURL = MonaLisaApplet.class.getClassLoader().getResource(IMAGE_PATH); + targetImage = ImageIO.read(imageURL); + super.init(); + } + catch (IOException ex) + { + ex.printStackTrace(); + JOptionPane.showMessageDialog(this, ex, "Failed to Load Image", JOptionPane.ERROR_MESSAGE); + + } + } + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + probabilitiesPanel = new ProbabilitiesPanel(); + probabilitiesPanel.setBorder(BorderFactory.createTitledBorder("Evolution Probabilities")); + JPanel controls = new JPanel(new BorderLayout()); + controls.add(createParametersPanel(), BorderLayout.NORTH); + controls.add(probabilitiesPanel, BorderLayout.SOUTH); + container.add(controls, BorderLayout.NORTH); + + Renderer<List<ColouredPolygon>, JComponent> renderer = new PolygonImageSwingRenderer(targetImage); + monitor = new EvolutionMonitor<List<ColouredPolygon>>(renderer, false); + container.add(monitor.getGUIComponent(), BorderLayout.CENTER); + } + + + private JComponent createParametersPanel() + { + Box parameters = Box.createHorizontalBox(); + parameters.add(Box.createHorizontalStrut(10)); + final JLabel populationLabel = new JLabel("Population Size: "); + parameters.add(populationLabel); + parameters.add(Box.createHorizontalStrut(10)); + populationSpinner = new JSpinner(new SpinnerNumberModel(10, 2, 1000, 1)); + populationSpinner.setMaximumSize(populationSpinner.getMinimumSize()); + parameters.add(populationSpinner); + parameters.add(Box.createHorizontalStrut(10)); + final JLabel elitismLabel = new JLabel("Elitism: "); + parameters.add(elitismLabel); + parameters.add(Box.createHorizontalStrut(10)); + elitismSpinner = new JSpinner(new SpinnerNumberModel(2, 1, 1000, 1)); + elitismSpinner.setMaximumSize(elitismSpinner.getMinimumSize()); + parameters.add(elitismSpinner); + parameters.add(Box.createHorizontalStrut(10)); + + parameters.add(new JLabel("Selection Pressure: ")); + parameters.add(Box.createHorizontalStrut(10)); + selectionPressureControl = new ProbabilityParameterControl(Probability.EVENS, + Probability.ONE, + 2, + new Probability(0.7)); + parameters.add(selectionPressureControl.getControl()); + parameters.add(Box.createHorizontalStrut(10)); + + startButton = new JButton("Start"); + abort = new AbortControl(); + startButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent ev) + { + abort.getControl().setEnabled(true); + populationLabel.setEnabled(false); + populationSpinner.setEnabled(false); + elitismLabel.setEnabled(false); + elitismSpinner.setEnabled(false); + startButton.setEnabled(false); + new EvolutionTask((Integer) populationSpinner.getValue(), + (Integer) elitismSpinner.getValue(), + abort.getTerminationCondition(), + new Stagnation(1000, false)).execute(); + } + }); + abort.getControl().setEnabled(false); + parameters.add(startButton); + parameters.add(abort.getControl()); + parameters.add(Box.createHorizontalStrut(10)); + + parameters.setBorder(BorderFactory.createTitledBorder("Parameters")); + return parameters; + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + * @throws IOException If there is a problem loading the target image. + */ + public static void main(String[] args) throws IOException + { + MonaLisaApplet gui = new MonaLisaApplet(); + // If a URL is specified as an argument, use that image. Otherwise use the default Mona Lisa picture. + URL imageURL = args.length > 0 + ? new URL(args[0]) + : MonaLisaApplet.class.getClassLoader().getResource(IMAGE_PATH); + gui.targetImage = ImageIO.read(imageURL); + gui.displayInFrame("Watchmaker Framework - Mona Lisa Example"); + } + + + /** + * The task that acutally performs the evolution. + */ + private class EvolutionTask extends SwingBackgroundTask<List<ColouredPolygon>> + { + private final int populationSize; + private final int eliteCount; + private final TerminationCondition[] terminationConditions; + + + EvolutionTask(int populationSize, int eliteCount, TerminationCondition... terminationConditions) + { + this.populationSize = populationSize; + this.eliteCount = eliteCount; + this.terminationConditions = terminationConditions; + } + + + @Override + protected List<ColouredPolygon> performTask() throws Exception + { + Dimension canvasSize = new Dimension(targetImage.getWidth(), targetImage.getHeight()); + + Random rng = new XORShiftRNG(); + FitnessEvaluator<List<ColouredPolygon>> evaluator + = new CachingFitnessEvaluator<List<ColouredPolygon>>(new PolygonImageEvaluator(targetImage)); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + EvolutionaryOperator<List<ColouredPolygon>> pipeline + = probabilitiesPanel.createEvolutionPipeline(factory, canvasSize, rng); + + SelectionStrategy<Object> selection = new TournamentSelection(selectionPressureControl.getNumberGenerator()); + EvolutionEngine<List<ColouredPolygon>> engine + = new GenerationalEvolutionEngine<List<ColouredPolygon>>(factory, + pipeline, + evaluator, + selection, + rng); + engine.addEvolutionObserver(monitor); + + return engine.evolve(populationSize, eliteCount, terminationConditions); + } + + + @Override + protected void postProcessing(List<ColouredPolygon> result) + { + abort.reset(); + abort.getControl().setEnabled(false); + populationSpinner.setEnabled(true); + elitismSpinner.setEnabled(true); + startButton.setEnabled(true); + } + + + @Override + protected void onError(Throwable throwable) + { + super.onError(throwable); + postProcessing(null); + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutation.java new file mode 100644 index 0000000..1b91f73 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutation.java @@ -0,0 +1,74 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Randomly mutates an image by swapping the z-order of two of its polygons + * according to some probability. + * @author Daniel Dyer + */ +public class MovePolygonMutation implements EvolutionaryOperator<List<ColouredPolygon>> +{ + private final NumberGenerator<Probability> movePolygonProbability; + + + /** + * @param movePolygonProbability A {@link NumberGenerator} that controls the probability + * that a polygon will be replaced. + */ + public MovePolygonMutation(NumberGenerator<Probability> movePolygonProbability) + { + this.movePolygonProbability = movePolygonProbability; + } + + + /** + * @param replacePolygonProbability The probability that a polygon will be replaced. + */ + public MovePolygonMutation(Probability replacePolygonProbability) + { + this(new ConstantGenerator<Probability>(replacePolygonProbability)); + } + + + public List<List<ColouredPolygon>> apply(List<List<ColouredPolygon>> selectedCandidates, Random rng) + { + List<List<ColouredPolygon>> mutatedCandidates = new ArrayList<List<ColouredPolygon>>(selectedCandidates.size()); + for (List<ColouredPolygon> candidate : selectedCandidates) + { + if (movePolygonProbability.nextValue().nextEvent(rng)) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(candidate); + ColouredPolygon polygon = newPolygons.remove(rng.nextInt(newPolygons.size())); + newPolygons.add(rng.nextInt(newPolygons.size()) + 1, polygon); + mutatedCandidates.add(newPolygons); + } + else // Nothing changed. + { + mutatedCandidates.add(candidate); + } + } + return mutatedCandidates; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutation.java new file mode 100644 index 0000000..0434646 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutation.java @@ -0,0 +1,113 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.Maths; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by changing their colour and/or either adding a point, removing a point or + * changing the position of a point. + * @author Daniel Dyer + */ +public class PolygonColourMutation implements EvolutionaryOperator<ColouredPolygon> +{ + private final NumberGenerator<Probability> mutationProbability; + private final NumberGenerator<Double> mutationAmount; + + + /** + * @param mutationProbability A {@link NumberGenerator} that controls the + * probability that the colour will be modified. + * @param mutationAmount A {@link NumberGenerator} that controls the amount + * that the colour's components are adjusted by. + */ + public PolygonColourMutation(NumberGenerator<Probability> mutationProbability, + NumberGenerator<Double> mutationAmount) + { + this.mutationProbability = mutationProbability; + this.mutationAmount = mutationAmount; + } + + + /** + * @param mutationProbability The probability that the colour will be modified. + * @param mutationAmount A {@link NumberGenerator} that controls the amount + * that the colour's components are adjusted by. + */ + public PolygonColourMutation(Probability mutationProbability, + NumberGenerator<Double> mutationAmount) + { + this(new ConstantGenerator<Probability>(mutationProbability), mutationAmount); + } + + + public List<ColouredPolygon> apply(List<ColouredPolygon> polygons, Random rng) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(polygons.size()); + for (ColouredPolygon polygon : polygons) + { + Color newColour = mutateColour(polygon.getColour(), rng); + newPolygons.add(newColour == polygon.getColour() + ? polygon + : new ColouredPolygon(newColour, polygon.getVertices())); + } + return newPolygons; + } + + + /** + * Mutate the specified colour. + * @param colour The colour to mutate. + * @param rng A source of randomness. + * @return The (possibly) mutated colour. + */ + private Color mutateColour(Color colour, Random rng) + { + if (mutationProbability.nextValue().nextEvent(rng)) + { + return new Color(mutateColourComponent(colour.getRed()), + mutateColourComponent(colour.getGreen()), + mutateColourComponent(colour.getBlue()), + mutateColourComponent(colour.getAlpha())); + } + else + { + return colour; + } + } + + + /** + * Adjust a single component (red, green, blue or alpha) of a colour. + * @param component The value to mutate. + * @return The mutated component value. + */ + private int mutateColourComponent(int component) + { + int mutatedComponent = (int) Math.round(component + mutationAmount.nextValue()); + mutatedComponent = Maths.restrictRange(mutatedComponent, 0, 255); + return mutatedComponent; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluator.java new file mode 100644 index 0000000..acc3e1b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluator.java @@ -0,0 +1,158 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.util.List; +import org.uncommons.watchmaker.framework.FitnessEvaluator; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Compares the generated polygon-based images to the target bitmap. The polygon images + * are rendered the same size as the target image and then each pixel is compared. The + * fitness value is a combination of the differences for each pixel. Lower fitness is better. + * @author Daniel Dyer + */ +public class PolygonImageEvaluator implements FitnessEvaluator<List<ColouredPolygon>> +{ + // This field is marked as transient, even though the class is not Serializable, because + // Terracotta will respect the fact it is transient and not try to share it. + private final transient ThreadLocal<Renderer<List<ColouredPolygon>, BufferedImage>> threadLocalRenderer + = new ThreadLocal<Renderer<List<ColouredPolygon>, BufferedImage>>(); + + private final int width; + private final int height; + private final AffineTransform transform; + private final int[] targetPixels; + + + /** + * Creates an evaluator that assigns fitness scores to images based on how + * close they are to the specified target image. + * @param targetImage The image that all others are compared to. + */ + public PolygonImageEvaluator(BufferedImage targetImage) + { + // Scale the image down so that its smallest dimension is 100 pixels. For large images this drastically + // reduces the number of pixels that we need to check for fitness evaluation. + Raster targetImageData; + if (targetImage.getWidth() > 100 && targetImage.getHeight() > 100) + { + double ratio = 100.0d / (targetImage.getWidth() > targetImage.getHeight() ? targetImage.getHeight() : targetImage.getWidth()); + transform = AffineTransform.getScaleInstance(ratio, ratio); + AffineTransformOp transformOp = new AffineTransformOp(transform, + AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + targetImageData = convertImage(transformOp.filter(targetImage, null)).getData(); + } + else + { + targetImageData = convertImage(targetImage).getData(); + transform = null; + } + + this.width = targetImageData.getWidth(); + this.height = targetImageData.getHeight(); + int[] pixelArray = new int[targetImageData.getWidth() * targetImageData.getHeight()]; + targetPixels = (int[]) targetImageData.getDataElements(0, + 0, + targetImageData.getWidth(), + targetImageData.getHeight(), + pixelArray); + } + + + /** + * Make sure that the image is in the most efficient format for reading from. + * This avoids having to convert pixels every time we access them. + * @param image The image to convert. + * @return The image converted to INT_RGB format. + */ + private BufferedImage convertImage(BufferedImage image) + { + if (image.getType() == BufferedImage.TYPE_INT_RGB) + { + return image; + } + else + { + BufferedImage newImage = new BufferedImage(image.getWidth(), + image.getHeight(), + BufferedImage.TYPE_INT_RGB); + newImage.getGraphics().drawImage(image, 0, 0, null); + return newImage; + } + } + + + /** + * Render the polygons as an image and then do a pixel-by-pixel comparison + * against the target image. The fitness score is the total error. A lower + * score means a closer match. + * @param candidate The image to evaluate. + * @param population Not used. + * @return A number indicating how close the candidate image is to the target image + * (lower is better). + */ + public double getFitness(List<ColouredPolygon> candidate, + List<? extends List<ColouredPolygon>> population) + { + // Use one renderer per thread because they are not thread safe. + Renderer<List<ColouredPolygon>, BufferedImage> renderer = threadLocalRenderer.get(); + if (renderer == null) + { + renderer = new PolygonImageRenderer(new Dimension(width, height), + false, + transform); + threadLocalRenderer.set(renderer); + } + + BufferedImage candidateImage = renderer.render(candidate); + Raster candidateImageData = candidateImage.getData(); + + int[] candidatePixelValues = new int[targetPixels.length]; + candidatePixelValues = (int[]) candidateImageData.getDataElements(0, + 0, + candidateImageData.getWidth(), + candidateImageData.getHeight(), + candidatePixelValues); + double fitness = 0; + for (int i = 0; i < targetPixels.length; i++) + { + fitness += comparePixels(targetPixels[i], candidatePixelValues[i]); + } + + return fitness; + } + + + private double comparePixels(int p1, int p2) + { + int deltaR = ((p1 >>> 16) & 0xFF) - ((p2 >>> 16) & 0xFF); + int deltaG = ((p1 >>> 8) & 0xFF) - ((p2 >>> 8) & 0xFF); + int deltaB = (p1 & 0xFF) - (p2 & 0xFF); + return Math.sqrt(deltaR * deltaR + deltaG * deltaG + deltaB * deltaB); + } + + + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactory.java new file mode 100644 index 0000000..9613172 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactory.java @@ -0,0 +1,75 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Creates random polygon-based images. + * @author Daniel Dyer + */ +public class PolygonImageFactory extends AbstractCandidateFactory<List<ColouredPolygon>> +{ + /** + * Each image must have at least 2 polygons. + */ + static final int MINIMUM_POLYGON_COUNT = 2; + + /** + * Each polygon must have at least 3 points. + */ + static final int MINIMUM_VERTEX_COUNT = 3; + + private final Dimension canvasSize; + + /** + * @param canvasSize The size of the canvas on which the image will be rendered. + * All polygons must fit within its bounds. + */ + public PolygonImageFactory(Dimension canvasSize) + { + this.canvasSize = canvasSize; + } + + + public List<ColouredPolygon> generateRandomCandidate(Random rng) + { + List<ColouredPolygon> polygons = new ArrayList<ColouredPolygon>(MINIMUM_POLYGON_COUNT); + for (int i = 0; i < MINIMUM_POLYGON_COUNT; i++) + { + polygons.add(createRandomPolygon(rng)); + } + return polygons; + } + + + ColouredPolygon createRandomPolygon(Random rng) + { + List<Point> vertices = new ArrayList<Point>(MINIMUM_VERTEX_COUNT); + for (int j = 0; j < MINIMUM_VERTEX_COUNT; j++) + { + vertices.add(new Point(rng.nextInt(canvasSize.width), rng.nextInt(canvasSize.height))); + } + Color colour = new Color(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256), rng.nextInt(256)); + return new ColouredPolygon(colour, vertices); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageRenderer.java new file mode 100644 index 0000000..876c9f0 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageRenderer.java @@ -0,0 +1,91 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.util.List; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Renders a polygon-based image to a {@link BufferedImage}. For efficiency reasons, this + * renderer returns the same image object (with different data) for subsequent invocations. + * This means that invoking code should not expect returned images to be unaltered following + * subsequent invocations. It also means that a renderer is not thread-safe. + * @author Daniel Dyer + */ +public class PolygonImageRenderer implements Renderer<List<ColouredPolygon>, BufferedImage> +{ + private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); + + private final Dimension targetSize; + private final AffineTransform transform; + private final BufferedImage image; + private final Graphics2D graphics; + + + /** + * @param targetSize The size of the canvas on which the polygons will be rendered. + * @param antialias Whether or not to enable anti-aliasing for the rendered image. + * @param transform A transformation applied to the vertices of an image's polygons + * before drawing to the destination image. This transformation adjusts the image + * so that it fits on a canvas of the specified {@code targetSize}. + */ + public PolygonImageRenderer(Dimension targetSize, + boolean antialias, + AffineTransform transform) + { + this.targetSize = targetSize; + this.transform = transform; + this.image = new BufferedImage(targetSize.width, + targetSize.height, + BufferedImage.TYPE_INT_RGB); + this.graphics = image.createGraphics(); + if (antialias) + { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + } + + + /** + * Renders the specified polygons as an image. + * @param entity A collection of coloured polygons. + * @return An image object displaying the polygons. + */ + public BufferedImage render(List<ColouredPolygon> entity) + { + // Need to set the background before applying the transform. + graphics.setTransform(IDENTITY_TRANSFORM); + graphics.setColor(Color.GRAY); + graphics.fillRect(0, 0, targetSize.width, targetSize.height); + if (transform != null) + { + graphics.setTransform(transform); + } + for (ColouredPolygon polygon : entity) + { + graphics.setColor(polygon.getColour()); + graphics.fillPolygon(polygon.getPolygon()); + } + return image; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageSwingRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageSwingRenderer.java new file mode 100644 index 0000000..1ce7e05 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/PolygonImageSwingRenderer.java @@ -0,0 +1,143 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.util.List; +import javax.swing.JComponent; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Converts a {@link BufferedImage} to a {@link JComponent} that displays that image + * alongside a pre-specified target image. + * @author Daniel Dyer + */ +public class PolygonImageSwingRenderer implements Renderer<List<ColouredPolygon>, JComponent> +{ + private final BufferedImage targetImage; + private final Renderer<List<ColouredPolygon>, BufferedImage> delegate; + + + /** + * @param targetImage This image is displayed on the left side of the + * JComponent. + */ + public PolygonImageSwingRenderer(BufferedImage targetImage) + { + // Convert the target image into the most efficient format for rendering. + this.targetImage = new BufferedImage(targetImage.getWidth(), + targetImage.getHeight(), + BufferedImage.TYPE_INT_RGB); + this.targetImage.getGraphics().drawImage(targetImage, 0, 0, null); + this.targetImage.setAccelerationPriority(1); + + this.delegate = new PolygonImageRenderer(new Dimension(targetImage.getWidth(), + targetImage.getHeight()), + true, // Anti-alias. + null); + } + + + /** + * Renders the specified image as a JComponent with the image on the + * right and the pre-specified target image on the left. + * @param entity The image to render on the right side of the component. + * @return A Swing component that displays the rendered image. + */ + public JComponent render(List<ColouredPolygon> entity) + { + return new ImageComponent(entity); + } + + + /** + * Swing component for rendering a fixed size image. If the image is smaller + * than the component, it is centered. + */ + private final class ImageComponent extends JComponent + { + private static final int GAP = 10; + private static final int FOOTER = 20; + + private final List<ColouredPolygon> candidate; + private final Dimension minimumSize; + + + ImageComponent(List<ColouredPolygon> candidate) + { + this.candidate = candidate; + this.minimumSize = new Dimension(targetImage.getWidth() * 2 + GAP, + targetImage.getHeight() + FOOTER); + } + + @Override + public Dimension getPreferredSize() + { + return minimumSize; + } + + @Override + public Dimension getMinimumSize() + { + return minimumSize; + } + + + @Override + protected void paintComponent(Graphics graphics) + { + int x = Math.max(0, (getWidth() - minimumSize.width) / 2); + int y = Math.max(0, (getHeight() - minimumSize.height) / 2); + graphics.drawImage(targetImage, x, y, this); + BufferedImage candidateImage = delegate.render(candidate); + candidateImage.setAccelerationPriority(1); + Graphics clip = graphics.create(x + targetImage.getWidth() + GAP, + y, + candidateImage.getWidth(), + candidateImage.getHeight() + FOOTER); + clip.drawImage(candidateImage, 0, 0, this); + + clip.setColor(getForeground()); + String info = candidate.size() + " polygons, " + countVertices(candidate) + " vertices"; + FontMetrics fontMetrics = clip.getFontMetrics(); + int width = fontMetrics.stringWidth(info); + int height = Math.round(fontMetrics.getLineMetrics(info, clip).getHeight()); + clip.drawString(info, + candidateImage.getWidth() - width, + candidateImage.getHeight() + height); + } + + + /** + * Count the number of vertices in each polygon in the image and return + * the total. + * @param image The image to inspect. + * @return The total number of vertices in all polygons in the image. + */ + private int countVertices(List<ColouredPolygon> image) + { + int count = 0; + for (ColouredPolygon polygon : image) + { + count += polygon.getVertices().size(); + } + return count; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanel.java new file mode 100644 index 0000000..5267be3 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanel.java @@ -0,0 +1,173 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SpringLayout; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.GaussianGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.operators.ListCrossover; +import org.uncommons.watchmaker.framework.operators.ListOperator; +import org.uncommons.watchmaker.swing.ProbabilityParameterControl; + +/** + * Panel that displays controls for the Mona Lisa example program. These + * controls allow the evolution parameters to be tweaked. + * @author Daniel Dyer + */ +class ProbabilitiesPanel extends JPanel +{ + private static final Probability ONE_TENTH = new Probability(0.1d); + + private final ProbabilityParameterControl addPolygonControl; + private final ProbabilityParameterControl removePolygonControl; + private final ProbabilityParameterControl movePolygonControl; + private final ProbabilityParameterControl crossOverControl; + private final ProbabilityParameterControl addVertexControl; + private final ProbabilityParameterControl removeVertexControl; + private final ProbabilityParameterControl moveVertexControl; + private final ProbabilityParameterControl changeColourControl; + + + ProbabilitiesPanel() + { + super(new SpringLayout()); + addPolygonControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.02)); + add(new JLabel("Add Polygon: ")); + add(addPolygonControl.getControl()); + addPolygonControl.setDescription("For each IMAGE, the probability that a new " + + "randomly-generated polygon will be added."); + + addVertexControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Add Vertex: ")); + add(addVertexControl.getControl()); + addVertexControl.setDescription("For each POLYGON, the probability that a new " + + "randomly-generated vertex will be added."); + + removePolygonControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.02)); + add(new JLabel("Remove Polygon: ")); + add(removePolygonControl.getControl()); + removePolygonControl.setDescription("For each IMAGE, the probability that a " + + "randomly-selected polygon will be discarded."); + + removeVertexControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Remove Vertex: ")); + add(removeVertexControl.getControl()); + removeVertexControl.setDescription("For each POLYGON, the probability that a " + + "randomly-selected vertex will be discarded."); + + movePolygonControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Reorder Polygons: ")); + add(movePolygonControl.getControl()); + movePolygonControl.setDescription("For each IMAGE, the probability that the z-positions " + + "of two randomly-selected polygons will be swapped."); + + moveVertexControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.03)); + add(new JLabel("Move Vertex: ")); + add(moveVertexControl.getControl()); + moveVertexControl.setDescription("For each POLYGON, the probability that a randomly-selected " + + "vertex will be displaced."); + + crossOverControl = new ProbabilityParameterControl(Probability.ZERO, + Probability.ONE, + 2, + Probability.ONE); + add(new JLabel("Cross-over: ")); + add(crossOverControl.getControl()); + crossOverControl.setDescription("For each PAIR of parent IMAGES, the probability that " + + "2-point cross-over is applied."); + + changeColourControl = new ProbabilityParameterControl(Probability.ZERO, + ONE_TENTH, + 3, + new Probability(0.01)); + add(new JLabel("Change Colour: ")); + add(changeColourControl.getControl()); + changeColourControl.setDescription("For each POLYGON, the probability that its colour will be mutated."); + + // Set component names for easy look-up from tests. + addPolygonControl.getControl().setName("AddPolygon"); + removePolygonControl.getControl().setName("RemovePolygon"); + movePolygonControl.getControl().setName("MovePolygon"); + crossOverControl.getControl().setName("Cross-over"); + addVertexControl.getControl().setName("AddVertex"); + removeVertexControl.getControl().setName("RemoveVertex"); + moveVertexControl.getControl().setName("MoveVertex"); + changeColourControl.getControl().setName("ChangeColour"); + + SpringUtilities.makeCompactGrid(this, 4, 4, 10, 0, 10, 0); + } + + + /** + * Construct the combination of evolutionary operators that will be used to evolve the + * polygon-based images. + * @param factory A source of polygons. + * @param canvasSize The size of the target image. + * @param rng A source of randomness. + * @return A complex evolutionary operator constructed from simpler operators. + */ + public EvolutionaryOperator<List<ColouredPolygon>> createEvolutionPipeline(PolygonImageFactory factory, + Dimension canvasSize, + Random rng) + { + List<EvolutionaryOperator<List<ColouredPolygon>>> operators + = new LinkedList<EvolutionaryOperator<List<ColouredPolygon>>>(); + operators.add(new ListCrossover<ColouredPolygon>(new ConstantGenerator<Integer>(2), + crossOverControl.getNumberGenerator())); + operators.add(new RemovePolygonMutation(removePolygonControl.getNumberGenerator())); + operators.add(new MovePolygonMutation(movePolygonControl.getNumberGenerator())); + operators.add(new ListOperator<ColouredPolygon>(new RemoveVertexMutation(canvasSize, + removeVertexControl.getNumberGenerator()))); + operators.add(new ListOperator<ColouredPolygon>(new AdjustVertexMutation(canvasSize, + moveVertexControl.getNumberGenerator(), + new GaussianGenerator(0, 3, rng)))); + operators.add(new ListOperator<ColouredPolygon>(new AddVertexMutation(canvasSize, + addVertexControl.getNumberGenerator()))); + operators.add(new ListOperator<ColouredPolygon>(new PolygonColourMutation(changeColourControl.getNumberGenerator(), + new GaussianGenerator(0, 20, rng)))); + operators.add(new AddPolygonMutation(addPolygonControl.getNumberGenerator(), factory, 50)); + return new EvolutionPipeline<List<ColouredPolygon>>(operators); + } + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutation.java new file mode 100644 index 0000000..d1d33bb --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutation.java @@ -0,0 +1,75 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Randomly mutates the polygons that make up an image by removing a polygon + * according to some probability. + * @author Daniel Dyer + */ +public class RemovePolygonMutation implements EvolutionaryOperator<List<ColouredPolygon>> +{ + private final NumberGenerator<Probability> removePolygonProbability; + + /** + * @param removePolygonProbability A {@link NumberGenerator} that controls the probability + * that a polygon will be removed. + */ + public RemovePolygonMutation(NumberGenerator<Probability> removePolygonProbability) + { + this.removePolygonProbability = removePolygonProbability; + } + + + /** + * @param removePolygonProbability The probability that a polygon will be removed. + */ + public RemovePolygonMutation(Probability removePolygonProbability) + { + this(new ConstantGenerator<Probability>(removePolygonProbability)); + } + + + public List<List<ColouredPolygon>> apply(List<List<ColouredPolygon>> selectedCandidates, Random rng) + { + List<List<ColouredPolygon>> mutatedCandidates = new ArrayList<List<ColouredPolygon>>(selectedCandidates.size()); + for (List<ColouredPolygon> candidate : selectedCandidates) + { + // A single polygon is removed with the configured probability, unless + // we already have the minimum permitted number of polygons. + if (candidate.size() > PolygonImageFactory.MINIMUM_POLYGON_COUNT + && removePolygonProbability.nextValue().nextEvent(rng)) + { + List<ColouredPolygon> newPolygons = new ArrayList<ColouredPolygon>(candidate); + newPolygons.remove(rng.nextInt(newPolygons.size())); + mutatedCandidates.add(newPolygons); + } + else // Nothing changed. + { + mutatedCandidates.add(candidate); + } + } + return mutatedCandidates; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutation.java new file mode 100644 index 0000000..4ec530d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutation.java @@ -0,0 +1,76 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * Evolutionary operator for mutating individual polygons. Polygons are mutated + * by removing a point, according to some probability. + * @author Daniel Dyer + */ +public class RemoveVertexMutation extends AbstractVertexMutation +{ + /** + * @param mutationProbability A {@link NumberGenerator} that controls the + * probability that a point will be removed. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + */ + public RemoveVertexMutation(Dimension canvasSize, + NumberGenerator<Probability> mutationProbability) + { + super(mutationProbability, canvasSize); + } + + + /** + * @param mutationProbability The probability that a point will be removed. + * @param canvasSize The size of the canvas. Used to constrain the positions + * of the points. + */ + public RemoveVertexMutation(Dimension canvasSize, + Probability mutationProbability) + { + this(canvasSize, new ConstantGenerator<Probability>(mutationProbability)); + } + + + @Override + protected List<Point> mutateVertices(List<Point> vertices, Random rng) + { + // A single point is removed with the configured probability, unless + // we already have the minimum permitted number of points. + if (vertices.size() > PolygonImageFactory.MINIMUM_VERTEX_COUNT + && getMutationProbability().nextValue().nextEvent(rng)) + { + List<Point> newVertices = new ArrayList<Point>(vertices); + newVertices.remove(rng.nextInt(newVertices.size())); + return newVertices; + } + else // Nothing changed. + { + return vertices; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/package-info.java new file mode 100644 index 0000000..1602c91 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/monalisa/package-info.java @@ -0,0 +1,23 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary art example application inspired by Roger Alsing's evolution of the + * Mona Lisa (http://rogeralsing.com/2008/12/07/genetic-programming-evolution-of-mona-lisa/). + * It attempts to find the combination of 50 translucent polygons that most closely + * resembles Leonardo da Vinci's Mona Lisa. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.monalisa; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringEvaluator.java new file mode 100644 index 0000000..ac56c9f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringEvaluator.java @@ -0,0 +1,73 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import java.util.List; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Evaluates strings and assigns a fitness score based on how many characters + * differ from the equivalent positions in a given target string. + * @author Daniel Dyer + */ +public class StringEvaluator implements FitnessEvaluator<String> +{ + private final String targetString; + + + /** + * Creates a {@link FitnessEvaluator} that calculates scores + * for Strings based on how close they are to a target String. + * @param targetString The target of the evolution. + */ + public StringEvaluator(String targetString) + { + this.targetString = targetString; + } + + + /** + * Assigns one "penalty point" for every character in the candidate + * string that differs from the corresponding position in the target + * string. + * @param candidate The evolved string to evaluate. + * @param population {@inheritDoc} + * @return The fitness score (how many characters are wrong) of the + * specified string. + */ + public double getFitness(String candidate, + List<? extends String> population) + { + int errors = 0; + for (int i = 0; i < candidate.length(); i++) + { + if (candidate.charAt(i) != targetString.charAt(i)) + { + ++errors; + } + } + return errors; + } + + + /** + * {@inheritDoc} + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringsExample.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringsExample.java new file mode 100644 index 0000000..d8425fa --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/StringsExample.java @@ -0,0 +1,119 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import java.util.ArrayList; +import java.util.List; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.factories.StringFactory; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.operators.StringCrossover; +import org.uncommons.watchmaker.framework.operators.StringMutation; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; + +/** + * Simple evolutionary algorithm that evolves a population of randomly-generated + * strings until at least one matches a specified target string. + * @author Daniel Dyer + */ +public final class StringsExample +{ + private static final char[] ALPHABET = new char[27]; + static + { + for (char c = 'A'; c <= 'Z'; c++) + { + ALPHABET[c - 'A'] = c; + } + ALPHABET[26] = ' '; + } + + + /** + * Entry point for the sample application. Any data specified on the + * command line is considered to be the target String. If no target is + * specified, a default of "HELLOW WORLD" is used instead. + * @param args The target String (as an array of words). + */ + public static void main(String[] args) + { + String target = args.length == 0 ? "HELLO WORLD" : convertArgs(args); + String result = evolveString(target); + System.out.println("Evolution result: " + result); + } + + + public static String evolveString(String target) + { + StringFactory factory = new StringFactory(ALPHABET, target.length()); + List<EvolutionaryOperator<String>> operators = new ArrayList<EvolutionaryOperator<String>>(2); + operators.add(new StringMutation(ALPHABET, new Probability(0.02d))); + operators.add(new StringCrossover()); + EvolutionaryOperator<String> pipeline = new EvolutionPipeline<String>(operators); + EvolutionEngine<String> engine = new GenerationalEvolutionEngine<String>(factory, + pipeline, + new StringEvaluator(target), + new RouletteWheelSelection(), + new MersenneTwisterRNG()); + engine.addEvolutionObserver(new EvolutionLogger()); + return engine.evolve(100, // 100 individuals in the population. + 5, // 5% elitism. + new TargetFitness(0, false)); + } + + + /** + * Converts an arguments array into a single String of words + * separated by spaces. + * @param args The command-line arguments. + * @return A single String made from the command line data. + */ + private static String convertArgs(String[] args) + { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < args.length; i++) + { + result.append(args[i]); + if (i < args.length - 1) + { + result.append(' '); + } + } + return result.toString().toUpperCase(); + } + + + /** + * Trivial evolution observer for displaying information at the end + * of each generation. + */ + private static class EvolutionLogger implements EvolutionObserver<String> + { + public void populationUpdate(PopulationData<? extends String> data) + { + System.out.printf("Generation %d: %s\n", + data.getGenerationNumber(), + data.getBestCandidate()); + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/package-info.java new file mode 100644 index 0000000..f5f4489 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/strings/package-info.java @@ -0,0 +1,22 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * Simple example that demonstrates how to use the evolution framework. + * Evolves candidates of type String, attempting to find a particular + * target phrase. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.strings; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/Sudoku.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/Sudoku.java new file mode 100644 index 0000000..9319177 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/Sudoku.java @@ -0,0 +1,150 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +/** + * A potential solution for a Sudoku puzzle. + * @author Daniel Dyer + */ +public final class Sudoku +{ + /** The dimensions (in cells) of the puzzle square. */ + public static final int SIZE = 9; + private static final int MIN_VALUE = 1; + private static final int MAX_VALUE = SIZE; + + private final Cell[][] cells; + + + /** + * Creates a sudoku solution from a 2-dimensional array of cells. + * @param cells The cells that make-up this Sudoku grid. + */ + public Sudoku(Cell[][] cells) + { + if (cells.length != SIZE) + { + throw new IllegalArgumentException("Sudoku must have 9 rows."); + } + if (cells[0].length != SIZE) // Should really check all rows because the array may not be square. + { + throw new IllegalArgumentException("Sudoku must have 9 columns."); + } + this.cells = cells; + } + + + /** + * Queries the value of a particular cell. + * @param row The row index of the cell. + * @param column The column index of the cell. + * @return The value (1 - 9) of the specified cell. + */ + public int getValue(int row, int column) + { + return cells[row][column].getValue(); + } + + + /** + * Checks whether a particular cell is a 'given' or not. + * @param row The row index of the cell. + * @param column The column index of the cell. + * @return True if the value in the identified cell is fixed (a 'given' cell), + * false otherwise. + */ + public boolean isFixed(int row, int column) + { + return cells[row][column].isFixed(); + } + + + /** + * Returns an array of cells that make up a row. The array + * returned is a clone of the underlying data structure and + * therefore can be modified without affecting this object. + * @param row The index of the row to return. + * @return A row of cells from this Sudoku grid. + */ + public Cell[] getRow(int row) + { + return cells[row].clone(); + } + + + /** + * Renders the Sudoku grid as a multi-line String. + * @return The String representation of this grid. + */ + @Override + public String toString() + { + StringBuilder buffer = new StringBuilder(); + for (Cell[] row : cells) + { + for (Cell cell : row) + { + buffer.append(' '); + buffer.append(cell.getValue()); + } + buffer.append('\n'); + } + return buffer.toString(); + } + + + /** + * A single cell in a sudoku grid. Contains a value and may be fixed (i.e. it is + * one of the given cells at the start). + */ + public static final class Cell + { + private final int value; + private final boolean fixed; + + /** + * @param value The value (1 - 9) contained in this cell. + * @param fixed Whether or not this cell's value is fixed (a 'given'). + */ + public Cell(int value, boolean fixed) + { + if (value < MIN_VALUE || value > MAX_VALUE) + { + throw new IllegalArgumentException("Value must be between 1 and 9."); + } + this.value = value; + this.fixed = fixed; + } + + + /** + * @return The value contained in this cell. + */ + public int getValue() + { + return value; + } + + + /** + * @return True if this cell is a 'given', false otherwise. + */ + public boolean isFixed() + { + return fixed; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuApplet.java new file mode 100644 index 0000000..2a856e9 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuApplet.java @@ -0,0 +1,283 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SpringLayout; +import javax.swing.SwingUtilities; +import org.uncommons.maths.random.DiscreteUniformGenerator; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.PoissonGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.selection.TournamentSelection; +import org.uncommons.watchmaker.framework.termination.TargetFitness; +import org.uncommons.watchmaker.swing.AbortControl; +import org.uncommons.watchmaker.swing.ProbabilityParameterControl; +import org.uncommons.watchmaker.swing.SwingEvolutionObserver; +import org.uncommons.watchmaker.swing.evolutionmonitor.StatusBar; + +/** + * An evolutionary Sudoku solver. + * @author Daniel Dyer + */ +public class SudokuApplet extends AbstractExampleApplet +{ + private static final String[] BLANK_PUZZLE = {".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + "........."}; + + private static final String[] EASY_PUZZLE = {"4.5...9.7", + ".2..9..6.", + "39.6.7.28", + "9..3.2..6", + "7..9.6..3", + "5..4.8..1", + "28.1.5.49", + ".7..3..8.", + "6.4...3.2"}; + + private static final String[] MEDIUM_PUZZLE = {"....3....", + ".....6293", + ".2.9.48..", + ".754...38", + "..46.71..", + "91...547.", + "..38.9.1.", + "1567.....", + "....1...."}; + + private static final String[] HARD_PUZZLE = {"...891...", + "....5.8..", + ".....6.2.", + "5....4..8", + "49....67.", + "8.13....5", + ".6..8..9.", + "..5.4.2.7", + "...1.3.8."}; + + private static final String[][] PUZZLES = {EASY_PUZZLE, + MEDIUM_PUZZLE, + HARD_PUZZLE, + BLANK_PUZZLE}; + + private SelectionStrategy<Object> selectionStrategy; + + private SudokuView sudokuView; + private JButton solveButton; + private JComboBox puzzleCombo; + private JSpinner populationSizeSpinner; + private AbortControl abortControl; + private StatusBar statusBar; + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + sudokuView = new SudokuView(); + container.add(createControls(), BorderLayout.NORTH); + container.add(sudokuView, BorderLayout.CENTER); + statusBar = new StatusBar(); + container.add(statusBar, BorderLayout.SOUTH); + sudokuView.setPuzzle(EASY_PUZZLE); + } + + + private JComponent createControls() + { + JPanel controls = new JPanel(new BorderLayout()); + JPanel innerPanel = new JPanel(new SpringLayout()); + innerPanel.add(new JLabel("Puzzle: ")); + puzzleCombo = new JComboBox(new String[]{"Easy Demo (38 givens)", + "Medium Demo (32 givens)", + "Hard Demo (28 givens)", + "Custom"}); + innerPanel.add(puzzleCombo); + puzzleCombo.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent ev) + { + sudokuView.setPuzzle(PUZZLES[puzzleCombo.getSelectedIndex()]); + } + }); + innerPanel.add(new JLabel("Selection Pressure: ")); + ProbabilityParameterControl selectionPressure = new ProbabilityParameterControl(Probability.EVENS, + Probability.ONE, + 2, + new Probability(0.85d)); + + selectionStrategy = new TournamentSelection(selectionPressure.getNumberGenerator()); + innerPanel.add(selectionPressure.getControl()); + innerPanel.add(new JLabel("Population Size: ")); + populationSizeSpinner = new JSpinner(new SpinnerNumberModel(500, 10, 50000, 1)); + innerPanel.add(populationSizeSpinner); + SpringUtilities.makeCompactGrid(innerPanel, 3, 2, 0, 6, 6, 6); + innerPanel.setBorder(BorderFactory.createTitledBorder("Configuration")); + controls.add(innerPanel, BorderLayout.CENTER); + controls.add(createButtonPanel(), BorderLayout.SOUTH); + return controls; + } + + + private JComponent createButtonPanel() + { + JPanel buttonPanel = new JPanel(new FlowLayout()); + solveButton = new JButton("Solve"); + solveButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent ev) + { + int populationSize = (Integer) populationSizeSpinner.getValue(); + puzzleCombo.setEnabled(false); + populationSizeSpinner.setEnabled(false); + solveButton.setEnabled(false); + abortControl.reset(); + createTask(sudokuView.getPuzzle(), + populationSize, + (int) Math.round(populationSize * 0.05)).execute(); // Elite count is 5%. + } + }); + + buttonPanel.add(solveButton); + abortControl = new AbortControl(); + buttonPanel.add(abortControl.getControl()); + abortControl.getControl().setEnabled(false); + return buttonPanel; + } + + + /** + * Helper method to create a background task for running the interactive evolutionary + * algorithm. + * @return A Swing task that will execute on a background thread and update + * the GUI when it is done. + */ + private SwingBackgroundTask<Sudoku> createTask(final String[] puzzle, + final int populationSize, + final int eliteCount) + { + return new SwingBackgroundTask<Sudoku>() + { + @Override + protected Sudoku performTask() + { + Random rng = new MersenneTwisterRNG(); + List<EvolutionaryOperator<Sudoku>> operators = new ArrayList<EvolutionaryOperator<Sudoku>>(2); + // Cross-over rows between parents (so offspring is x rows from parent1 and + // y rows from parent2). + operators.add(new SudokuVerticalCrossover()); + // Mutate the order of cells within individual rows. + operators.add(new SudokuRowMutation(new PoissonGenerator(2, rng), + new DiscreteUniformGenerator(1, 8, rng))); + + EvolutionaryOperator<Sudoku> pipeline = new EvolutionPipeline<Sudoku>(operators); + + EvolutionEngine<Sudoku> engine = new GenerationalEvolutionEngine<Sudoku>(new SudokuFactory(puzzle), + pipeline, + new SudokuEvaluator(), + selectionStrategy, + rng); + engine.addEvolutionObserver(new SwingEvolutionObserver<Sudoku>(new GridViewUpdater(), + 100, + TimeUnit.MILLISECONDS)); + engine.addEvolutionObserver(statusBar); + return engine.evolve(populationSize, + eliteCount, + new TargetFitness(0, false), // Continue until a perfect solution is found... + abortControl.getTerminationCondition()); // ...or the user aborts. + } + + + @Override + protected void postProcessing(Sudoku result) + { + puzzleCombo.setEnabled(true); + populationSizeSpinner.setEnabled(true); + solveButton.setEnabled(true); + abortControl.getControl().setEnabled(false); + } + }; + } + + + + /** + * Evolution observer for displaying information at the end of + * each generation. + */ + private class GridViewUpdater implements EvolutionObserver<Sudoku> + { + public void populationUpdate(final PopulationData<? extends Sudoku> data) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + sudokuView.setSolution(data.getBestCandidate()); + } + }); + } + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + */ + public static void main(String[] args) + { + new SudokuApplet().displayInFrame("Watchmaker Framework - Sudoku Example"); + } + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuCellRenderer.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuCellRenderer.java new file mode 100644 index 0000000..ebfa15a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuCellRenderer.java @@ -0,0 +1,162 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import javax.swing.table.DefaultTableCellRenderer; +import org.uncommons.swing.ConfigurableLineBorder; + +/** + * Renders Sudoku cells in a JTable. As well as displaying the cell value, + * grid lines are drawn to conform to normal Sudoku conventions and incorrect + * solutions have their invalid cells colour-coded. + * @author Daniel Dyer + */ +class SudokuCellRenderer extends DefaultTableCellRenderer +{ + private static final Font VARIABLE_CELL_FONT = new Font("Monospaced", Font.PLAIN, 32); + private static final Font FIXED_CELL_FONT = new Font("Monospaced", Font.BOLD, 34); + private static final Color VARIABLE_TEXT_COLOUR = Color.DARK_GRAY; + private static final Color FIXED_TEXT_COLOUR = Color.BLACK; + + private static final Color[] CONFLICT_COLOURS = {Color.WHITE, Color.YELLOW, Color.ORANGE, Color.RED}; + + private static final Border TOP_BORDER = new ConfigurableLineBorder(true, false, false, false, 1); + private static final Border BOTTOM_BORDER = new ConfigurableLineBorder(false, false, true, false, 1); + private static final Border LEFT_BORDER = new ConfigurableLineBorder(false, true, false, false, 1); + private static final Border RIGHT_BORDER = new ConfigurableLineBorder(false, false, false, true, 1); + private static final Border TOP_LEFT_BORDER = new ConfigurableLineBorder(true, true, false, false, 1); + private static final Border TOP_RIGHT_BORDER = new ConfigurableLineBorder(true, false, false, true, 1); + private static final Border BOTTOM_LEFT_BORDER = new ConfigurableLineBorder(false, true, true, false, 1); + private static final Border BOTTOM_RIGHT_BORDER = new ConfigurableLineBorder(false, false, true, true, 1); + + + SudokuCellRenderer() + { + setHorizontalAlignment(JLabel.CENTER); + } + + + @Override + public Component getTableCellRendererComponent(JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column) + { + Component component = super.getTableCellRendererComponent(table, + value, + false, + false, + row, + column); + SudokuTableModel model = (SudokuTableModel) table.getModel(); + Sudoku sudoku = model.getSudoku(); + if (sudoku != null) + { + if (sudoku.isFixed(row, column)) + { + component.setFont(FIXED_CELL_FONT); + component.setForeground(FIXED_TEXT_COLOUR); + } + else + { + component.setFont(VARIABLE_CELL_FONT); + component.setForeground(VARIABLE_TEXT_COLOUR); + } + + int conflicts = 0; + // Calculate conflicts in columns (there should be no conflicts in rows + // because of the constraints enforced by the evolutionary operators). + for (int i = 0; i < Sudoku.SIZE; i++) + { + if (i != row && model.getValueAt(i, column).equals(value)) + { + ++conflicts; + } + } + // Calculate conflicts in sub-grid. + int band = row / 3; + int bandStart = band * 3; + int stack = column / 3; + int stackStart = stack * 3; + for (int i = bandStart; i < bandStart + 3; i++) + { + for (int j = stackStart; j < stackStart + 3; j++) + { + if (i != row && j != column && model.getValueAt(i, j).equals(value)) + { + ++conflicts; + } + } + } + + // Color the cell based on how "wrong" it is. + component.setBackground(CONFLICT_COLOURS[Math.min(conflicts, CONFLICT_COLOURS.length - 1)]); + } + else + { + // If the model is in puzzle mode, then all non-null cells are 'givens'. + component.setFont(FIXED_CELL_FONT); + component.setForeground(FIXED_TEXT_COLOUR); + } + + ((JComponent) component).setBorder(getBorder(row, column)); + + + return component; + } + + + /** + * Get appropriate border for cell based on its position in the grid. + */ + private Border getBorder(int row, int column) + { + if (row % 3 == 2) + { + switch (column % 3) + { + case 2: return BOTTOM_RIGHT_BORDER; + case 0: return BOTTOM_LEFT_BORDER; + default: return BOTTOM_BORDER; + } + } + else if (row % 3 == 0) + { + switch (column % 3) + { + case 2: return TOP_RIGHT_BORDER; + case 0: return TOP_LEFT_BORDER; + default: return TOP_BORDER; + } + } + + switch (column % 3) + { + case 2: return RIGHT_BORDER; + case 0: return LEFT_BORDER; + default: return null; + } + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluator.java new file mode 100644 index 0000000..ca44394 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluator.java @@ -0,0 +1,89 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * {@link org.uncommons.watchmaker.framework.FitnessEvaluator} for potential Sudoku + * solutions. Counts the number of duplicate values in rows, columns and sub-grids. + * The fitness score is the total number of duplicate values. Therefore, a fitness + * score of zero indicates a perfect solution. + * @author Daniel Dyer + */ +public class SudokuEvaluator implements FitnessEvaluator<Sudoku> +{ + /** + * The fitness score for a potential Sudoku solution is the number of + * cells that conflict with other cells in the grid (i.e. if there are + * two 7s in the same column, both of these cells are conflicting). A + * lower score indicates a fitter individual. + * @param candidate The Sudoku grid to evaluate. + * @param population {@inheritDoc} + * @return The fitness score for the specified individual. + */ + public double getFitness(Sudoku candidate, + List<? extends Sudoku> population) + { + // We can assume that there are no duplicates in any rows because + // the candidate factory and evolutionary operators that we use do + // not permit rows to contain duplicates. + + int fitness = 0; + + // Check columns for duplicates. + Set<Integer> values = new HashSet<Integer>(Sudoku.SIZE * 2); // Big enough to avoid re-hashing. + for (int column = 0; column < Sudoku.SIZE; column++) + { + for (int row = 0; row < Sudoku.SIZE; row++) + { + values.add(candidate.getValue(row, column)); + } + fitness += (Sudoku.SIZE - values.size()); + values.clear(); + } + + // Check sub-grids for duplicates. + for (int band = 0; band < Sudoku.SIZE; band += 3) + { + for (int stack = 0; stack < Sudoku.SIZE; stack += 3) + { + for (int row = band; row < band + 3; row++) + { + for (int column = stack; column < stack + 3; column++) + { + values.add(candidate.getValue(row, column)); + } + } + fitness += (Sudoku.SIZE - values.size()); + values.clear(); + } + } + return fitness; + } + + + /** + * @return false + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuFactory.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuFactory.java new file mode 100644 index 0000000..d96ed1a --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuFactory.java @@ -0,0 +1,125 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.uncommons.watchmaker.framework.factories.AbstractCandidateFactory; + +/** + * Factory that generates potential Sudoku solutions from a list of "givens". + * The rows of the generated solutions will all be valid (i.e. no duplicate values) + * but there are no constraints on the columns or sub-grids (these will be refined + * by the evolutionary algorithm). + * @author Daniel Dyer + */ +public class SudokuFactory extends AbstractCandidateFactory<Sudoku> +{ + private static final List<Integer> VALUES = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); + + private final Sudoku.Cell[][] template; + private final List<List<Integer>> nonFixedValues = new ArrayList<List<Integer>>(Sudoku.SIZE); + + + /** + * Creates a factory for generating random candidate solutions + * for a specified Sudoku puzzle. + * @param pattern An array of Strings. Each element represents + * one row in the puzzle. Each character represents a single + * cell. Permitted characters are the digits '1' to '9' (each + * of which represents a fixed cell in the pattern) or the '.' + * character, which represents an empty cell. + * @throws IllegalArgumentException If {@literal pattern} does not + * consist of nine Strings with nine characters ('0' to '9', or '.') + * in each. + */ + public SudokuFactory(String... pattern) + { + if (pattern.length != Sudoku.SIZE) + { + throw new IllegalArgumentException("Sudoku layout must have " + Sudoku.SIZE + " rows."); + } + + this.template = new Sudoku.Cell[Sudoku.SIZE][Sudoku.SIZE]; + + // Keep track of which values in each row are not 'givens'. + for (int i = 0; i < Sudoku.SIZE; i++) + { + nonFixedValues.add(new ArrayList<Integer>(VALUES)); + } + + for (int i = 0; i < pattern.length; i++) + { + char[] rowPattern = pattern[i].toCharArray(); + if (rowPattern.length != Sudoku.SIZE) + { + throw new IllegalArgumentException("Sudoku layout must have " + Sudoku.SIZE + " cells in each row."); + } + for (int j = 0; j < rowPattern.length; j++) + { + char c = rowPattern[j]; + if (c >= '1' && c <= '9') // Cell is a 'given'. + { + int value = c - '0'; // Convert char to in that it represents.. + template[i][j] = new Sudoku.Cell(value, true); + List<Integer> rowValues = nonFixedValues.get(i); + int index = Collections.binarySearch(rowValues, value); + rowValues.remove(index); + } + else if (c != '.') + { + throw new IllegalArgumentException("Unexpected character at (" + i + ", " + j + "): " + c); + } + } + } + } + + + /** + * {@inheritDoc} + * The generated potential solution is guaranteed to have no + * duplicates in any row but could have duplicates in a column or sub-grid. + */ + public Sudoku generateRandomCandidate(Random rng) + { + // Clone the template as the basis for this grid. + Sudoku.Cell[][] rows = template.clone(); + for (int i = 0; i < rows.length; i++) + { + rows[i] = template[i].clone(); + } + + // Fill-in the non-fixed cells. + for (int i = 0; i < rows.length; i++) + { + List<Integer> rowValues = nonFixedValues.get(i); + Collections.shuffle(rowValues); + int index = 0; + for (int j = 0; j < rows[i].length; j++) + { + if (rows[i][j] == null) + { + rows[i][j] = new Sudoku.Cell(rowValues.get(index), false); + ++index; + } + } + } + return new Sudoku(rows); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutation.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutation.java new file mode 100644 index 0000000..029ae2b --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutation.java @@ -0,0 +1,206 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Mutates rows in a potential Sudoku solution by manipulating the order + * of non-fixed cells in much the same way as the + * {@link org.uncommons.watchmaker.framework.operators.ListOrderMutation} + * operator does in the Travelling Salesman example. + * @author Daniel Dyer + */ +public class SudokuRowMutation implements EvolutionaryOperator<Sudoku> +{ + private final NumberGenerator<Integer> mutationCountVariable; + private final NumberGenerator<Integer> mutationAmountVariable; + + // These look-up tables keep track of which values are fixed in which columns + // and sub-grids. Because the values are fixed, they are the same for all + // potential solutions, so we cache the information here to minimise the amount + // of processing that needs to be done for each mutation. There is no need to + // worry about rows since the mutation ensures that rows are always valid. + private final boolean[][] columnFixedValues = new boolean[Sudoku.SIZE][Sudoku.SIZE]; + private final boolean[][] subGridFixedValues = new boolean[Sudoku.SIZE][Sudoku.SIZE]; + private boolean cached = false; + + /** + * Default is one mutation per candidate. + */ + public SudokuRowMutation() + { + this(1, 1); + } + + /** + * @param mutationCount The constant number of mutations + * to apply to each row in a Sudoku solution. + * @param mutationAmount The constant number of positions by + * which a list element will be displaced as a result of mutation. + */ + public SudokuRowMutation(int mutationCount, int mutationAmount) + { + this(new ConstantGenerator<Integer>(mutationCount), + new ConstantGenerator<Integer>(mutationAmount)); + if (mutationCount < 1) + { + throw new IllegalArgumentException("Mutation count must be at least 1."); + } + else if (mutationAmount < 1) + { + throw new IllegalArgumentException("Mutation amount must be at least 1."); + } + } + + + /** + * Typically the mutation count will be from a Poisson distribution. + * The mutation amount can be from any discrete probability distribution + * and can include negative values. + * @param mutationCount A random variable that provides a number + * of mutations that will be applied to each row in an individual. + * @param mutationAmount A random variable that provides a number + * of positions by which to displace an element when mutating. + */ + public SudokuRowMutation(NumberGenerator<Integer> mutationCount, + NumberGenerator<Integer> mutationAmount) + { + this.mutationCountVariable = mutationCount; + this.mutationAmountVariable = mutationAmount; + } + + + public List<Sudoku> apply(List<Sudoku> selectedCandidates, Random rng) + { + if (!cached) + { + buildCache(selectedCandidates.get(0)); + } + + List<Sudoku> mutatedCandidates = new ArrayList<Sudoku>(selectedCandidates.size()); + for (Sudoku sudoku : selectedCandidates) + { + mutatedCandidates.add(mutate(sudoku, rng)); + } + return mutatedCandidates; + } + + + private Sudoku mutate(Sudoku sudoku, Random rng) + { + Sudoku.Cell[][] newRows = new Sudoku.Cell[Sudoku.SIZE][]; + // Mutate each row in turn. + for (int i = 0; i < Sudoku.SIZE; i++) + { + newRows[i] = new Sudoku.Cell[Sudoku.SIZE]; + System.arraycopy(sudoku.getRow(i), 0, newRows[i], 0, Sudoku.SIZE); + } + + int mutationCount = Math.abs(mutationCountVariable.nextValue()); + while (mutationCount > 0) + { + int row = rng.nextInt(Sudoku.SIZE); + int fromIndex = rng.nextInt(Sudoku.SIZE); + int mutationAmount = mutationAmountVariable.nextValue(); + int toIndex = (fromIndex + mutationAmount) % Sudoku.SIZE; + + // Make sure we're not trying to mutate a 'given'. + if (!newRows[row][fromIndex].isFixed() + && !newRows[row][toIndex].isFixed() + // ...or trying to introduce a duplicate of a given value. + && (!isIntroducingFixedConflict(sudoku, row, fromIndex, toIndex) + || isRemovingFixedConflict(sudoku, row, fromIndex, toIndex))) + { + // Swap the randomly selected element with the one that is the + // specified displacement distance away. + Sudoku.Cell temp = newRows[row][fromIndex]; + newRows[row][fromIndex] = newRows[row][toIndex]; + newRows[row][toIndex] = temp; + --mutationCount; + } + } + + return new Sudoku(newRows); + } + + + private void buildCache(Sudoku sudoku) + { + for (int row = 0; row < Sudoku.SIZE; row++) + { + for (int column = 0; column < Sudoku.SIZE; column++) + { + if (sudoku.isFixed(row, column)) + { + columnFixedValues[column][sudoku.getValue(row, column) - 1] = true; + subGridFixedValues[convertToSubGrid(row, column)][sudoku.getValue(row, column) - 1] = true; + } + } + } + cached = true; + } + + + /** + * Checks whether the proposed mutation would introduce a duplicate of a fixed value + * into a column or sub-grid. + */ + private boolean isIntroducingFixedConflict(Sudoku sudoku, + int row, + int fromIndex, + int toIndex) + { + return columnFixedValues[fromIndex][sudoku.getValue(row, toIndex) - 1] + || columnFixedValues[toIndex][sudoku.getValue(row, fromIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, fromIndex)][sudoku.getValue(row, toIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, toIndex)][sudoku.getValue(row, fromIndex) - 1]; + } + + + /** + * Checks whether the proposed mutation would remove a duplicate of a fixed value + * from a column or sub-grid. + */ + private boolean isRemovingFixedConflict(Sudoku sudoku, + int row, + int fromIndex, + int toIndex) + { + return columnFixedValues[fromIndex][sudoku.getValue(row, fromIndex) - 1] + || columnFixedValues[toIndex][sudoku.getValue(row, toIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, fromIndex)][sudoku.getValue(row, fromIndex) - 1] + || subGridFixedValues[convertToSubGrid(row, toIndex)][sudoku.getValue(row, toIndex) - 1]; + } + + + + /** + * Returns the index of the sub-grid that the specified cells belongs to. + * @return A number between 0 (top left) and 8 (bottom right). + */ + private int convertToSubGrid(int row, int column) + { + int band = row / 3; + int stack = column / 3; + return band * 3 + stack; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuTableModel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuTableModel.java new file mode 100644 index 0000000..0249f7f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuTableModel.java @@ -0,0 +1,156 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import javax.swing.table.AbstractTableModel; + +/** + * {@link javax.swing.table.TableModel} for displaying a Sudoku + * grid in a {@link javax.swing.JTable}. + * @author Daniel Dyer + */ +class SudokuTableModel extends AbstractTableModel +{ + // In puzzle mode, the user can edit the given cells and all other + // cells are blank. In solution mode, all cells have values and are + // uneditable. + private boolean puzzleMode = true; + + // Solution mode data. + private Sudoku sudoku; + + // Puzzle mode data. + private final Character[][] cells = new Character[Sudoku.SIZE][Sudoku.SIZE]; + + + /** + * Sets the Sudoku grid represented by this table model. + */ + public void setSudoku(Sudoku sudoku) + { + this.sudoku = sudoku; + puzzleMode = false; + fireTableRowsUpdated(0, getRowCount() - 1); + } + + + /** + * @return The Sudoku grid represented by this table model. + */ + public Sudoku getSudoku() + { + return sudoku; + } + + + public int getRowCount() + { + return Sudoku.SIZE; + } + + + public int getColumnCount() + { + return Sudoku.SIZE; + } + + + public Object getValueAt(int row, int column) + { + if (puzzleMode) + { + return cells[row][column]; + } + else + { + return sudoku == null ? null : sudoku.getValue(row, column); + } + } + + + @Override + public boolean isCellEditable(int row, int column) + { + return puzzleMode; + } + + + @Override + public void setValueAt(Object object, int row, int column) + { + Character value = (Character) object; + if (!(value == null || (value >= '1' && value <= '9'))) + { + throw new IllegalArgumentException("Invalid character: " + value); + } + cells[row][column] = value; + fireTableCellUpdated(row, column); + } + + + /** + * Sets all cells at once using the same patterns as supported by + * {@link SudokuFactory}. + * @param pattern A String representation of a Sudoku puzzle. Each element + * in the array represents a single row. There are 9 elements and each has 9 + * characters, one per cell. Number cells are represented by the characters + * '0' to '9' and blank cells are represented by dots. + */ + public void setPattern(String[] pattern) + { + if (pattern.length != Sudoku.SIZE) + { + throw new IllegalArgumentException("Pattern must have " + Sudoku.SIZE + " rows."); + } + for (int row = 0; row < pattern.length; row++) + { + String patternRow = pattern[row]; + if (patternRow.length() != Sudoku.SIZE) + { + throw new IllegalArgumentException("Row must have " + Sudoku.SIZE + " columns."); + } + for (int column = 0; column < patternRow.toCharArray().length; column++) + { + char c = patternRow.toCharArray()[column]; + cells[row][column] = c == '.' ? null : c; + } + } + sudoku = null; + puzzleMode = true; + fireTableRowsUpdated(0, getRowCount() - 1); + } + + + /** + * @return A Sudoku puzzle in the pattern format used by {@link SudokuFactory}. + */ + public String[] getPattern() + { + String[] pattern = new String[Sudoku.SIZE]; + for (int i = 0; i < cells.length; i++) + { + Character[] row = cells[i]; + StringBuilder rowString = new StringBuilder(); + for (Character c : row) + { + rowString.append(c == null ? '.' : c); + } + pattern[i] = rowString.toString(); + } + return pattern; + } + +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossover.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossover.java new file mode 100644 index 0000000..e5c790f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossover.java @@ -0,0 +1,105 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.watchmaker.framework.operators.AbstractCrossover; + +/** + * Performs cross-over between Sudoku grids by re-combining rows from parents + * to form new offspring. Rows are copied intact, only columns are disrupted + * by this cross-over. + * @author Daniel Dyer + */ +public class SudokuVerticalCrossover extends AbstractCrossover<Sudoku> +{ + /** + * Single-point cross-over. + */ + public SudokuVerticalCrossover() + { + this(1); + } + + + /** + * Multiple-point cross-over (fixed number of points). + * @param crossoverPoints The fixed number of cross-overs applied to each + * pair of parents. + */ + public SudokuVerticalCrossover(int crossoverPoints) + { + super(crossoverPoints); + } + + + /** + * Multiple-point cross-over (variable number of points). + * @param crossoverPointsVariable Provides the (possibly variable) number of + * cross-overs applied to each pair of parents. + */ + public SudokuVerticalCrossover(NumberGenerator<Integer> crossoverPointsVariable) + { + super(crossoverPointsVariable); + } + + + /** + * Applies cross-over to a pair of parents. Cross-over is performed vertically + * (each offspring consists of some rows from {@literal parent1} and some rows + * from {@literal parent2}). + * @param parent1 The first parent. + * @param parent2 The second parent. + * @param numberOfCrossoverPoints The number of cross-overs to perform. + * @param rng The RNG used to select the cross-over points. + * @return A list containing a pair of offspring. + */ + @Override + protected List<Sudoku> mate(Sudoku parent1, + Sudoku parent2, + int numberOfCrossoverPoints, + Random rng) + { + Sudoku.Cell[][] offspring1 = new Sudoku.Cell[Sudoku.SIZE][]; + Sudoku.Cell[][] offspring2 = new Sudoku.Cell[Sudoku.SIZE][]; + for (int i = 0; i < Sudoku.SIZE; i++) + { + offspring1[i] = parent1.getRow(i); + offspring2[i] = parent2.getRow(i); + } + + // Apply as many cross-overs as required. + Sudoku.Cell[][] temp = new Sudoku.Cell[Sudoku.SIZE][]; + for (int i = 0; i < numberOfCrossoverPoints; i++) + { + // Cross-over index is always greater than zero and less than + // the length of the parent so that we always pick a point that + // will result in a meaningful cross-over. + int crossoverIndex = (1 + rng.nextInt(Sudoku.SIZE - 1)); + System.arraycopy(offspring1, 0, temp, 0, crossoverIndex); + System.arraycopy(offspring2, 0, offspring1, 0, crossoverIndex); + System.arraycopy(temp, 0, offspring2, 0, crossoverIndex); + } + + List<Sudoku> result = new ArrayList<Sudoku>(2); + result.add(new Sudoku(offspring1)); + result.add(new Sudoku(offspring2)); + return result; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuView.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuView.java new file mode 100644 index 0000000..1f8f269 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/SudokuView.java @@ -0,0 +1,76 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.BorderLayout; +import java.awt.Color; +import javax.swing.BorderFactory; +import javax.swing.DefaultCellEditor; +import javax.swing.JComboBox; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +/** + * A component for displaying Sudoku puzzles and solutions. + * @author Daniel Dyer + */ +class SudokuView extends JPanel +{ + private final SudokuTableModel sudokuTableModel = new SudokuTableModel(); + + SudokuView() + { + super(new BorderLayout()); + JTable sudokuTable = new JTable(sudokuTableModel); + sudokuTable.setRowHeight(40); + sudokuTable.setGridColor(Color.GRAY); + sudokuTable.setShowGrid(true); + TableColumnModel columnModel = sudokuTable.getColumnModel(); + TableCellRenderer renderer = new SudokuCellRenderer(); + JComboBox valueCombo = new JComboBox(new Object[]{null, '1', '2', '3', '4', '5', '6', '7', '8', '9'}); + TableCellEditor editor = new DefaultCellEditor(valueCombo); + for (int i = 0; i < columnModel.getColumnCount(); i++) + { + TableColumn column = columnModel.getColumn(i); + column.setCellRenderer(renderer); + column.setCellEditor(editor); + } + add(sudokuTable, BorderLayout.CENTER); + setBorder(BorderFactory.createTitledBorder("Puzzle/Solution")); + } + + + public void setSolution(Sudoku sudoku) + { + sudokuTableModel.setSudoku(sudoku); + } + + + public void setPuzzle(String[] pattern) + { + sudokuTableModel.setPattern(pattern); + } + + + public String[] getPuzzle() + { + return sudokuTableModel.getPattern(); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/package-info.java new file mode 100644 index 0000000..bd69653 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/sudoku/package-info.java @@ -0,0 +1,20 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary Sudoku solver. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.sudoku; diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesman.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesman.java new file mode 100644 index 0000000..312e6d4 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesman.java @@ -0,0 +1,107 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import org.uncommons.maths.combinatorics.PermutationGenerator; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Naive brute-force solution to the travelling salesman problem. It would take about + * a day and a half to brute-force the 15-city travelling salesman problem on a home + * computer using this implementation. However, this is a not the best possible + * implementation that is guaranteed to find a the shortest route (for example there + * is no branch-and-bound optimisation). + * @author Daniel Dyer + */ +public class BruteForceTravellingSalesman implements TravellingSalesmanStrategy +{ + private final DistanceLookup distances; + + + /** + * @param distances Information about the distances between cities. + */ + public BruteForceTravellingSalesman(DistanceLookup distances) + { + this.distances = distances; + } + + + /** + * {@inheritDoc} + */ + public String getDescription() + { + return "Brute Force"; + } + + + /** + * To reduce the search space we will only consider routes that start + * and end at one city (whichever is first in the collection). All other + * possible routes are equivalent to one of these routes since start city + * is irrelevant in determining the shortest cycle. + * @param cities The list of destinations, each of which must be visited + * once. + * @param progressListener Call-back for receiving the status of the + * algorithm as it progresses. May be null. + * @return The shortest route that visits each of the specified cities once. + */ + public List<String> calculateShortestRoute(Collection<String> cities, + ProgressListener progressListener) + { + Iterator<String> iterator = cities.iterator(); + String startCity = iterator.next(); + Collection<String> destinations = new ArrayList<String>(cities.size() - 1); + while (iterator.hasNext()) + { + destinations.add(iterator.next()); + } + + FitnessEvaluator<List<String>> evaluator = new RouteEvaluator(distances); + PermutationGenerator<String> generator = new PermutationGenerator<String>(destinations); + long totalPermutations = generator.getTotalPermutations(); + long count = 0; + List<String> shortestRoute = null; + double shortestDistance = Double.POSITIVE_INFINITY; + List<String> currentRoute = new ArrayList<String>(cities.size()); + while (generator.hasMore()) + { + List<String> route = generator.nextPermutationAsList(currentRoute); + route.add(0, startCity); + double distance = evaluator.getFitness(route, null); + if (distance < shortestDistance) + { + shortestDistance = distance; + shortestRoute = new ArrayList<String>(route); + } + ++count; + if (count % 1000 == 0 && progressListener != null) + { + progressListener.updateProgress(((double) count) / totalPermutations * 100); + } + } + if (progressListener != null) + { + progressListener.updateProgress(100); // Finished. + } + return shortestRoute; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/DistanceLookup.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/DistanceLookup.java new file mode 100644 index 0000000..9013d75 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/DistanceLookup.java @@ -0,0 +1,39 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.List; + +/** + * Strategy interface for providing distances between cities in the + * Travelling Salesman problem. + * @author Daniel Dyer + */ +public interface DistanceLookup +{ + /** + * @return The list of cities that this object knows about. + */ + List<String> getKnownCities(); + + /** + * Looks-up the distance between two cities. + * @param startingCity The city to start from. + * @param destinationCity The city to end in. + * @return The distance (in kilometres) between the two cities. + */ + int getDistance(String startingCity, String destinationCity); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookup.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookup.java new file mode 100644 index 0000000..547347f --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookup.java @@ -0,0 +1,326 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class contains data about cities in Europe and the distances + * between them. + * @author Daniel Dyer + */ +public final class EuropeanDistanceLookup implements DistanceLookup +{ + private static final Map<String, Map<String, Integer>> DISTANCES = new HashMap<String, Map<String, Integer>>(15); + static + { + // Distances are in km as the crow flies (from http://www.indo.com/distance/) + + Map<String, Integer> amsterdam = new HashMap<String, Integer>(20); + amsterdam.put("Amsterdam", 0); + amsterdam.put("Athens", 2162); + amsterdam.put("Berlin", 576); + amsterdam.put("Brussels", 171); + amsterdam.put("Copenhagen", 622); + amsterdam.put("Dublin", 757); + amsterdam.put("Helsinki", 1506); + amsterdam.put("Lisbon", 1861); + amsterdam.put("London", 356); + amsterdam.put("Luxembourg", 318); + amsterdam.put("Madrid", 1477); + amsterdam.put("Paris", 429); + amsterdam.put("Rome", 1304); + amsterdam.put("Stockholm", 1132); + amsterdam.put("Vienna", 938); + DISTANCES.put("Amsterdam", amsterdam); + + Map<String, Integer> athens = new HashMap<String, Integer>(20); + athens.put("Amsterdam", 2162); + athens.put("Athens", 0); + athens.put("Berlin", 1801); + athens.put("Brussels", 2089); + athens.put("Copenhagen", 2140); + athens.put("Dublin", 2860); + athens.put("Helsinki", 2464); + athens.put("Lisbon", 2854); + athens.put("London", 2391); + athens.put("Luxembourg", 1901); + athens.put("Madrid", 2374); + athens.put("Paris", 2097); + athens.put("Rome", 1040); + athens.put("Stockholm", 2410); + athens.put("Vienna", 1280); + DISTANCES.put("Athens", athens); + + Map<String, Integer> berlin = new HashMap<String, Integer>(20); + berlin.put("Amsterdam", 576); + berlin.put("Athens", 1801); + berlin.put("Berlin", 0); + berlin.put("Brussels", 648); + berlin.put("Copenhagen", 361); + berlin.put("Dublin", 1315); + berlin.put("Helsinki", 1108); + berlin.put("Lisbon", 2310); + berlin.put("London", 929); + berlin.put("Luxembourg", 595); + berlin.put("Madrid", 1866); + berlin.put("Paris", 877); + berlin.put("Rome", 1185); + berlin.put("Stockholm", 818); + berlin.put("Vienna", 525); + DISTANCES.put("Berlin", berlin); + + Map<String, Integer> brussels = new HashMap<String, Integer>(20); + brussels.put("Amsterdam", 171); + brussels.put("Athens", 2089); + brussels.put("Berlin", 648); + brussels.put("Brussels", 0); + brussels.put("Copenhagen", 764); + brussels.put("Dublin", 780); + brussels.put("Helsinki", 1649); + brussels.put("Lisbon", 1713); + brussels.put("London", 321); + brussels.put("Luxembourg", 190); + brussels.put("Madrid", 1315); + brussels.put("Paris", 266); + brussels.put("Rome", 1182); + brussels.put("Stockholm", 1284); + brussels.put("Vienna", 917); + DISTANCES.put("Brussels", brussels); + + Map<String, Integer> copenhagen = new HashMap<String, Integer>(20); + copenhagen.put("Amsterdam", 622); + copenhagen.put("Athens", 2140); + copenhagen.put("Berlin", 361); + copenhagen.put("Brussels", 764); + copenhagen.put("Copenhagen", 0); + copenhagen.put("Dublin", 1232); + copenhagen.put("Helsinki", 885); + copenhagen.put("Lisbon", 2477); + copenhagen.put("London", 953); + copenhagen.put("Luxembourg", 799); + copenhagen.put("Madrid", 2071); + copenhagen.put("Paris", 1028); + copenhagen.put("Rome", 1540); + copenhagen.put("Stockholm", 526); + copenhagen.put("Vienna", 876); + DISTANCES.put("Copenhagen", copenhagen); + + Map<String, Integer> dublin = new HashMap<String, Integer>(20); + dublin.put("Amsterdam", 757); + dublin.put("Athens", 2860); + dublin.put("Berlin", 1315); + dublin.put("Brussels", 780); + dublin.put("Copenhagen", 1232); + dublin.put("Dublin", 0); + dublin.put("Helsinki", 2021); + dublin.put("Lisbon", 1652); + dublin.put("London", 469); + dublin.put("Luxembourg", 961); + dublin.put("Madrid", 1458); + dublin.put("Paris", 787); + dublin.put("Rome", 1903); + dublin.put("Stockholm", 1625); + dublin.put("Vienna", 1687); + DISTANCES.put("Dublin", dublin); + + Map<String, Integer> helsinki = new HashMap<String, Integer>(20); + helsinki.put("Amsterdam", 1506); + helsinki.put("Athens", 2464); + helsinki.put("Berlin", 1108); + helsinki.put("Brussels", 1649); + helsinki.put("Copenhagen", 885); + helsinki.put("Dublin", 2021); + helsinki.put("Helsinki", 0); + helsinki.put("Lisbon", 3362); + helsinki.put("London", 1823); + helsinki.put("Luxembourg", 1667); + helsinki.put("Madrid", 2949); + helsinki.put("Paris", 1912); + helsinki.put("Rome", 2202); + helsinki.put("Stockholm", 396); + helsinki.put("Vienna", 1439); + DISTANCES.put("Helsinki", helsinki); + + Map<String, Integer> lisbon = new HashMap<String, Integer>(20); + lisbon.put("Amsterdam", 1861); + lisbon.put("Athens", 2854); + lisbon.put("Berlin", 2310); + lisbon.put("Brussels", 1713); + lisbon.put("Copenhagen", 2477); + lisbon.put("Dublin", 1652); + lisbon.put("Helsinki", 3362); + lisbon.put("Lisbon", 0); + lisbon.put("London", 1585); + lisbon.put("Luxembourg", 1716); + lisbon.put("Madrid", 501); + lisbon.put("Paris", 1452); + lisbon.put("Rome", 1873); + lisbon.put("Stockholm", 2993); + lisbon.put("Vienna", 2300); + DISTANCES.put("Lisbon", lisbon); + + Map<String, Integer> london = new HashMap<String, Integer>(20); + london.put("Amsterdam", 356); + london.put("Athens", 2391); + london.put("Berlin", 929); + london.put("Brussels", 321); + london.put("Copenhagen", 953); + london.put("Dublin", 469); + london.put("Helsinki", 1823); + london.put("Lisbon", 1585); + london.put("London", 0); + london.put("Luxembourg", 494); + london.put("Madrid", 1261); + london.put("Paris", 343); + london.put("Rome", 1444); + london.put("Stockholm", 1436); + london.put("Vienna", 1237); + DISTANCES.put("London", london); + + Map<String, Integer> luxembourg = new HashMap<String, Integer>(20); + luxembourg.put("Amsterdam", 318); + luxembourg.put("Athens", 1901); + luxembourg.put("Berlin", 595); + luxembourg.put("Brussels", 190); + luxembourg.put("Copenhagen", 799); + luxembourg.put("Dublin", 961); + luxembourg.put("Helsinki", 1667); + luxembourg.put("Lisbon", 1716); + luxembourg.put("London", 494); + luxembourg.put("Luxembourg", 0); + luxembourg.put("Madrid", 1282); + luxembourg.put("Paris", 294); + luxembourg.put("Rome", 995); + luxembourg.put("Stockholm", 1325); + luxembourg.put("Vienna", 761); + DISTANCES.put("Luxembourg", luxembourg); + + Map<String, Integer> madrid = new HashMap<String, Integer>(20); + madrid.put("Amsterdam", 1477); + madrid.put("Athens", 2374); + madrid.put("Berlin", 1866); + madrid.put("Brussels", 1315); + madrid.put("Copenhagen", 2071); + madrid.put("Dublin", 1458); + madrid.put("Helsinki", 2949); + madrid.put("Lisbon", 501); + madrid.put("London", 1261); + madrid.put("Luxembourg", 1282); + madrid.put("Madrid", 0); + madrid.put("Paris", 1050); + madrid.put("Rome", 1377); + madrid.put("Stockholm", 2596); + madrid.put("Vienna", 1812); + DISTANCES.put("Madrid", madrid); + + Map<String, Integer> paris = new HashMap<String, Integer>(20); + paris.put("Amsterdam", 429); + paris.put("Athens", 2097); + paris.put("Berlin", 877); + paris.put("Brussels", 266); + paris.put("Copenhagen", 1028); + paris.put("Dublin", 787); + paris.put("Helsinki", 1912); + paris.put("Lisbon", 1452); + paris.put("London", 343); + paris.put("Luxembourg", 294); + paris.put("Madrid", 1050); + paris.put("Paris", 0); + paris.put("Rome", 1117); + paris.put("Stockholm", 1549); + paris.put("Vienna", 1037); + DISTANCES.put("Paris", paris); + + Map<String, Integer> rome = new HashMap<String, Integer>(20); + rome.put("Amsterdam", 1304); + rome.put("Athens", 1040); + rome.put("Berlin", 1185); + rome.put("Brussels", 1182); + rome.put("Copenhagen", 1540); + rome.put("Dublin", 1903); + rome.put("Helsinki", 2202); + rome.put("Lisbon", 1873); + rome.put("London", 1444); + rome.put("Luxembourg", 995); + rome.put("Madrid", 1377); + rome.put("Paris", 1117); + rome.put("Rome", 0); + rome.put("Stockholm", 1984); + rome.put("Vienna", 765); + DISTANCES.put("Rome", rome); + + Map<String, Integer> stockholm = new HashMap<String, Integer>(20); + stockholm.put("Amsterdam", 1132); + stockholm.put("Athens", 2410); + stockholm.put("Berlin", 818); + stockholm.put("Brussels", 1284); + stockholm.put("Copenhagen", 526); + stockholm.put("Dublin", 1625); + stockholm.put("Helsinki", 396); + stockholm.put("Lisbon", 2993); + stockholm.put("London", 1436); + stockholm.put("Luxembourg", 1325); + stockholm.put("Madrid", 2596); + stockholm.put("Paris", 1549); + stockholm.put("Rome", 1984); + stockholm.put("Stockholm", 0); + stockholm.put("Vienna", 1247); + DISTANCES.put("Stockholm", stockholm); + + Map<String, Integer> vienna = new HashMap<String, Integer>(20); + vienna.put("Amsterdam", 938); + vienna.put("Athens", 1280); + vienna.put("Berlin", 525); + vienna.put("Brussels", 917); + vienna.put("Copenhagen", 876); + vienna.put("Dublin", 1687); + vienna.put("Helsinki", 1439); + vienna.put("Lisbon", 2300); + vienna.put("London", 1237); + vienna.put("Luxembourg", 761); + vienna.put("Madrid", 1812); + vienna.put("Paris", 1037); + vienna.put("Rome", 765); + vienna.put("Stockholm", 1247); + vienna.put("Vienna", 0); + DISTANCES.put("Vienna", vienna); + } + + + /** + * {@inheritDoc} + */ + public List<String> getKnownCities() + { + List<String> cities = new ArrayList<String>(DISTANCES.keySet()); + Collections.sort(cities); + return cities; + } + + + /** + * {@inheritDoc} + */ + public int getDistance(String startingCity, String destinationCity) + { + return DISTANCES.get(startingCity).get(destinationCity); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionPanel.java new file mode 100644 index 0000000..82089d5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionPanel.java @@ -0,0 +1,119 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.FlowLayout; +import java.util.List; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SpringLayout; +import org.uncommons.maths.random.Probability; +import org.uncommons.swing.SpringUtilities; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.swing.SelectionStrategyControl; + +/** + * Controls for configuring an {@link EvolutionaryTravellingSalesman} object. + * @author Daniel Dyer + */ +final class EvolutionPanel extends JPanel +{ + private final JLabel populationLabel; + private final JSpinner populationSpinner; + private final JLabel elitismLabel; + private final JSpinner elitismSpinner; + private final JLabel generationsLabel; + private final JSpinner generationsSpinner; + private final JLabel selectionLabel; + private final SelectionStrategyControl<List<String>> selectionControl; + private final JCheckBox crossoverCheckbox; + private final JCheckBox mutationCheckbox; + private final DistanceLookup distances; + + EvolutionPanel(DistanceLookup distances) + { + super(new FlowLayout(FlowLayout.LEFT, 0, 0)); + this.distances = distances; + JPanel innerPanel = new JPanel(new SpringLayout()); + + populationLabel = new JLabel("Population Size: "); + populationSpinner = new JSpinner(new SpinnerNumberModel(300, 2, 10000, 1)); + populationLabel.setLabelFor(populationSpinner); + innerPanel.add(populationLabel); + innerPanel.add(populationSpinner); + + elitismLabel = new JLabel("Elitism: "); + elitismSpinner = new JSpinner(new SpinnerNumberModel(3, 0, 10000, 1)); + elitismLabel.setLabelFor(elitismSpinner); + innerPanel.add(elitismLabel); + innerPanel.add(elitismSpinner); + + generationsLabel = new JLabel("Number of Generations: "); + generationsSpinner = new JSpinner(new SpinnerNumberModel(100, 1, 10000, 1)); + generationsLabel.setLabelFor(generationsSpinner); + innerPanel.add(generationsLabel); + innerPanel.add(generationsSpinner); + + selectionLabel = new JLabel("Selection Strategy: "); + List<SelectionStrategy<? super List<String>>> strategies + = SelectionStrategyControl.createDefaultOptions(new Probability(0.95d), 0.5d); + this.selectionControl = new SelectionStrategyControl<List<String>>(strategies); + innerPanel.add(selectionLabel); + selectionControl.getControl().setSelectedIndex(selectionControl.getControl().getItemCount() - 1); + innerPanel.add(selectionControl.getControl()); + + crossoverCheckbox = new JCheckBox("Cross-over", true); + mutationCheckbox = new JCheckBox("Mutation", true); + + innerPanel.add(crossoverCheckbox); + innerPanel.add(mutationCheckbox); + + SpringUtilities.makeCompactGrid(innerPanel, 5, 2, 30, 6, 6, 6); + add(innerPanel); + } + + + @Override + public void setEnabled(boolean b) + { + populationLabel.setEnabled(b); + populationSpinner.setEnabled(b); + elitismLabel.setEnabled(b); + elitismSpinner.setEnabled(b); + generationsLabel.setEnabled(b); + generationsSpinner.setEnabled(b); + selectionLabel.setEnabled(b); + selectionControl.getControl().setEnabled(b); + crossoverCheckbox.setEnabled(b); + mutationCheckbox.setEnabled(b); + super.setEnabled(b); + } + + + public TravellingSalesmanStrategy getStrategy() + { + return new EvolutionaryTravellingSalesman(distances, + selectionControl.getSelectionStrategy(), + (Integer) populationSpinner.getValue(), + (Integer) elitismSpinner.getValue(), + (Integer) generationsSpinner.getValue(), + crossoverCheckbox.isSelected(), + mutationCheckbox.isSelected()); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesman.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesman.java new file mode 100644 index 0000000..6814101 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesman.java @@ -0,0 +1,152 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import org.uncommons.maths.random.MersenneTwisterRNG; +import org.uncommons.maths.random.PoissonGenerator; +import org.uncommons.watchmaker.framework.CandidateFactory; +import org.uncommons.watchmaker.framework.EvolutionEngine; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; +import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.factories.ListPermutationFactory; +import org.uncommons.watchmaker.framework.operators.EvolutionPipeline; +import org.uncommons.watchmaker.framework.operators.ListOrderCrossover; +import org.uncommons.watchmaker.framework.operators.ListOrderMutation; +import org.uncommons.watchmaker.framework.termination.GenerationCount; + +/** + * Evolutionary algorithm for finding (approximate) solutions to the + * travelling salesman problem. + * @author Daniel Dyer + */ +public class EvolutionaryTravellingSalesman implements TravellingSalesmanStrategy +{ + private final DistanceLookup distances; + private final SelectionStrategy<? super List<String>> selectionStrategy; + private final int populationSize; + private final int eliteCount; + private final int generationCount; + private final boolean crossover; + private final boolean mutation; + + /** + * Creates an evolutionary Travelling Salesman solver with the + * specified configuration. + * @param distances Information about the distances between cities. + * @param selectionStrategy The selection implementation to use for + * the evolutionary algorithm. + * @param populationSize The number of candidates in the population + * of evolved routes. + * @param eliteCount The number of candidates to preserve via elitism + * at each generation. + * @param generationCount The number of iterations of evolution to perform. + * @param crossover Whether or not to use a cross-over operator in the evolution. + * @param mutation Whether or not to use a mutation operator in the evolution. + */ + public EvolutionaryTravellingSalesman(DistanceLookup distances, + SelectionStrategy<? super List<String>> selectionStrategy, + int populationSize, + int eliteCount, + int generationCount, + boolean crossover, + boolean mutation) + { + if (!crossover && !mutation) + { + throw new IllegalArgumentException("At least one of cross-over or mutation must be selected."); + } + this.distances = distances; + this.selectionStrategy = selectionStrategy; + this.populationSize = populationSize; + this.eliteCount = eliteCount; + this.generationCount = generationCount; + this.crossover = crossover; + this.mutation = mutation; + } + + + /** + * {@inheritDoc} + */ + public String getDescription() + { + String selectionName = selectionStrategy.toString(); + return "Evolution (pop: " + populationSize + ", gen: " + generationCount + + ", elite: " + eliteCount + ", " + selectionName + ")"; + } + + + /** + * Calculates the shortest route using a generational evolutionary + * algorithm with a single ordered mutation operator and truncation + * selection. + * @param cities The list of destinations, each of which must be visited + * once. + * @param progressListener Call-back for receiving the status of the + * algorithm as it progresses. + * @return The (approximate) shortest route that visits each of the + * specified cities once. + */ + public List<String> calculateShortestRoute(Collection<String> cities, + final ProgressListener progressListener) + { + Random rng = new MersenneTwisterRNG(); + + // Set-up evolution pipeline (cross-over followed by mutation). + List<EvolutionaryOperator<List<String>>> operators = new ArrayList<EvolutionaryOperator<List<String>>>(2); + if (crossover) + { + operators.add(new ListOrderCrossover<String>()); + } + if (mutation) + { + operators.add(new ListOrderMutation<String>(new PoissonGenerator(1.5, rng), + new PoissonGenerator(1.5, rng))); + } + + EvolutionaryOperator<List<String>> pipeline = new EvolutionPipeline<List<String>>(operators); + + CandidateFactory<List<String>> candidateFactory + = new ListPermutationFactory<String>(new LinkedList<String>(cities)); + EvolutionEngine<List<String>> engine + = new GenerationalEvolutionEngine<List<String>>(candidateFactory, + pipeline, + new RouteEvaluator(distances), + selectionStrategy, + rng); + if (progressListener != null) + { + engine.addEvolutionObserver(new EvolutionObserver<List<String>>() + { + public void populationUpdate(PopulationData<? extends List<String>> data) + { + progressListener.updateProgress(((double) data.getGenerationNumber() + 1) / generationCount * 100); + } + }); + } + return engine.evolve(populationSize, + eliteCount, + new GenerationCount(generationCount)); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ExecutionPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ExecutionPanel.java new file mode 100644 index 0000000..e4d5be0 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ExecutionPanel.java @@ -0,0 +1,107 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.event.ActionListener; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +/** + * Panel for controlling the execution of the Travelling Salesman applet. + * Contains controls for starting and stopping the route-finding algorithms + * and for displaying progress and results. + * @author Daniel Dyer + */ +final class ExecutionPanel extends JPanel implements ProgressListener +{ + private final JButton startButton; + private final JTextArea output; + private final JScrollPane scroller; + private final JProgressBar progressBar; + + + ExecutionPanel() + { + super(new BorderLayout()); + JPanel controlPanel = new JPanel(new BorderLayout()); + startButton = new JButton("Start"); + controlPanel.add(startButton, BorderLayout.WEST); + progressBar = new JProgressBar(0, 100); + controlPanel.add(progressBar, BorderLayout.CENTER); + add(controlPanel, BorderLayout.NORTH); + output = new JTextArea(); + output.setEditable(false); + output.setLineWrap(true); + output.setWrapStyleWord(true); + output.setFont(new Font("Monospaced", Font.PLAIN, 12)); + scroller = new JScrollPane(output); + scroller.setBorder(BorderFactory.createTitledBorder("Results")); + add(scroller, BorderLayout.CENTER); + } + + + /** + * Adds logic to the start button so that something happens when + * it is clicked. + * @param actionListener The action to perform when the button is + * clicked. + */ + public void addActionListener(ActionListener actionListener) + { + startButton.addActionListener(actionListener); + } + + + /** + * Updates the position of the progress bar. + */ + public void updateProgress(final double percentComplete) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + progressBar.setValue((int) percentComplete); + } + }); + } + + + /** + * Appends the specified text to this panel's text area. + * @param text The text to append. + */ + public void appendOutput(String text) + { + output.append(text); + } + + + @Override + public void setEnabled(boolean b) + { + startButton.setEnabled(b); + scroller.setEnabled(b); + super.setEnabled(b); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanel.java new file mode 100644 index 0000000..0e5c154 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanel.java @@ -0,0 +1,114 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JPanel; + +/** + * Component for selecting which cities are to be visited by the + * travelling salesman. + * @author Daniel Dyer + */ +final class ItineraryPanel extends JPanel +{ + private final Collection<JCheckBox> checkBoxes; + private final JButton selectAllButton; + private final JButton clearButton; + + ItineraryPanel(List<String> cities) + { + super(new BorderLayout()); + + JPanel checkBoxPanel = new JPanel(new GridLayout(0, 1)); + checkBoxes = new ArrayList<JCheckBox>(cities.size()); + for (String city : cities) + { + JCheckBox checkBox = new JCheckBox(city, false); + checkBox.setName(city); // Helps to find the checkbox from a unit test. + checkBoxes.add(checkBox); + checkBoxPanel.add(checkBox); + } + add(checkBoxPanel, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new GridLayout(2, 1)); + selectAllButton = new JButton("Select All"); + selectAllButton.setName("All"); + buttonPanel.add(selectAllButton); + clearButton = new JButton("Clear Selection"); + clearButton.setName("None"); + buttonPanel.add(clearButton); + ActionListener buttonListener = new ActionListener() + { + + public void actionPerformed(ActionEvent actionEvent) + { + boolean select = actionEvent.getSource() == selectAllButton; + for (JCheckBox checkBox : checkBoxes) + { + checkBox.setSelected(select); + } + } + }; + selectAllButton.addActionListener(buttonListener); + clearButton.addActionListener(buttonListener); + add(buttonPanel, BorderLayout.SOUTH); + + setBorder(BorderFactory.createTitledBorder("Itinerary")); + } + + + /** + * Returns the cities that have been selected as part of the itinerary. + * @return A list of cities. + */ + public Collection<String> getSelectedCities() + { + Set<String> cities = new TreeSet<String>(); + for (JCheckBox checkBox : checkBoxes) + { + if (checkBox.isSelected()) + { + cities.add(checkBox.getText()); + } + } + return cities; + } + + + @Override + public void setEnabled(boolean b) + { + for (JCheckBox checkBox : checkBoxes) + { + checkBox.setEnabled(b); + } + selectAllButton.setEnabled(b); + clearButton.setEnabled(b); + super.setEnabled(b); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ProgressListener.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ProgressListener.java new file mode 100644 index 0000000..661a5e5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/ProgressListener.java @@ -0,0 +1,31 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +/** + * Call-back interface for keeping track of the progress of a + * {@link TravellingSalesmanStrategy} implementation. + * @author Daniel Dyer + */ +public interface ProgressListener +{ + /** + * Call-back method that informs the implementing object + * of the current completion percentage. + * @param percentComplete A percentage between 0 and 100. + */ + void updateProgress(double percentComplete); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluator.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluator.java new file mode 100644 index 0000000..144d593 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluator.java @@ -0,0 +1,74 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.List; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Fitness evalator that measures the total distance of a route in the travelling salesman + * problem. The fitness score of a route is the total distance (in km). A route + * is represented as a list of cities in the order that they will be visited. + * The last leg of the journey is from the last city in the list back to the + * first. + * @author Daniel Dyer + */ +public class RouteEvaluator implements FitnessEvaluator<List<String>> +{ + private final DistanceLookup distances; + + + /** + * @param distances Provides distances between a set of cities. + */ + public RouteEvaluator(DistanceLookup distances) + { + this.distances = distances; + } + + + /** + * Calculates the length of an evolved route. + * @param candidate The route to evaluate. + * @param population {@inheritDoc} + * @return The total distance (in kilometres) of a journey that visits + * each city in order and returns to the starting point. + */ + public double getFitness(List<String> candidate, + List<? extends List<String>> population) + { + int totalDistance = 0; + int cityCount = candidate.size(); + for (int i = 0; i < cityCount; i++) + { + int nextIndex = i < cityCount - 1 ? i + 1 : 0; + totalDistance += distances.getDistance(candidate.get(i), + candidate.get(nextIndex)); + } + return totalDistance; + } + + + /** + * {@inheritDoc} + * Returns false since shorter distances represent fitter candidates. + * @return false + */ + public boolean isNatural() + { + return false; + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanel.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanel.java new file mode 100644 index 0000000..99f727d --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanel.java @@ -0,0 +1,92 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JPanel; +import javax.swing.JRadioButton; + +/** + * Panel for configuring a route-finding strategy for the travelling + * salesman problem. + * @author Daniel Dyer + */ +final class StrategyPanel extends JPanel +{ + private final DistanceLookup distances; + private final JRadioButton evolutionOption; + private final JRadioButton bruteForceOption; + private final EvolutionPanel evolutionPanel; + + /** + * Creates a panel with components for controlling the route-finding + * strategy. + * @param distances Data used by the strategy in order to calculate + * shortest routes. + */ + StrategyPanel(DistanceLookup distances) + { + super(new BorderLayout()); + this.distances = distances; + evolutionOption = new JRadioButton("Evolution", true); + evolutionOption.setName("EvolutionOption"); // Helps to find the radio button from a unit test. + bruteForceOption = new JRadioButton("Brute Force", false); + bruteForceOption.setName("BruteForceOption"); // Helps to find the radio button from a unit test. + evolutionOption.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + evolutionPanel.setEnabled(evolutionOption.isSelected()); + } + }); + ButtonGroup strategyGroup = new ButtonGroup(); + strategyGroup.add(evolutionOption); + strategyGroup.add(bruteForceOption); + evolutionPanel = new EvolutionPanel(distances); + evolutionPanel.setName("EvolutionPanel"); // Helps to find the panel from a unit test. + add(evolutionOption, BorderLayout.NORTH); + add(evolutionPanel, BorderLayout.CENTER); + add(bruteForceOption, BorderLayout.SOUTH); + setBorder(BorderFactory.createTitledBorder("Route-Finding Strategy")); + } + + + public TravellingSalesmanStrategy getStrategy() + { + if (bruteForceOption.isSelected()) + { + return new BruteForceTravellingSalesman(distances); + } + else + { + return evolutionPanel.getStrategy(); + } + } + + + @Override + public void setEnabled(boolean b) + { + evolutionOption.setEnabled(b); + bruteForceOption.setEnabled(b); + evolutionPanel.setEnabled(b && evolutionOption.isSelected()); + super.setEnabled(b); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanApplet.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanApplet.java new file mode 100644 index 0000000..96490b2 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanApplet.java @@ -0,0 +1,185 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collection; +import java.util.List; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import org.uncommons.swing.SwingBackgroundTask; +import org.uncommons.watchmaker.examples.AbstractExampleApplet; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Applet for comparing evolutionary and brute force approaches to the + * Travelling Salesman problem. + * @author Daniel Dyer + */ +public final class TravellingSalesmanApplet extends AbstractExampleApplet +{ + private final DistanceLookup distances = new EuropeanDistanceLookup(); + private final FitnessEvaluator<List<String>> evaluator = new RouteEvaluator(distances); + + private ItineraryPanel itineraryPanel; + private StrategyPanel strategyPanel; + private ExecutionPanel executionPanel; + + + /** + * Initialise and layout the GUI. + * @param container The Swing component that will contain the GUI controls. + */ + @Override + protected void prepareGUI(Container container) + { + itineraryPanel = new ItineraryPanel(distances.getKnownCities()); + strategyPanel = new StrategyPanel(distances); + executionPanel = new ExecutionPanel(); + + container.add(itineraryPanel, BorderLayout.WEST); + JPanel innerPanel = new JPanel(new BorderLayout()); + innerPanel.add(strategyPanel, BorderLayout.NORTH); + innerPanel.add(executionPanel, BorderLayout.CENTER); + container.add(innerPanel, BorderLayout.CENTER); + + executionPanel.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent actionEvent) + { + Collection<String> cities = itineraryPanel.getSelectedCities(); + if (cities.size() < 4) + { + JOptionPane.showMessageDialog(TravellingSalesmanApplet.this, + "Itinerary must include at least 4 cities.", + "Error", + JOptionPane.ERROR_MESSAGE); + } + else + { + try + { + setEnabled(false); + createTask(cities).execute(); + } + catch (IllegalArgumentException ex) + { + JOptionPane.showMessageDialog(TravellingSalesmanApplet.this, + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + setEnabled(true); + } + } + } + }); + container.validate(); + } + + + /** + * Helper method to create a background task for running the travelling + * salesman algorithm. + * @param cities The set of cities to generate a route for. + * @return A Swing task that will execute on a background thread and update + * the GUI when it is done. + */ + private SwingBackgroundTask<List<String>> createTask(final Collection<String> cities) + { + final TravellingSalesmanStrategy strategy = strategyPanel.getStrategy(); + return new SwingBackgroundTask<List<String>>() + { + private long elapsedTime = 0; + + @Override + protected List<String> performTask() + { + long startTime = System.currentTimeMillis(); + List<String> result = strategy.calculateShortestRoute(cities, executionPanel); + elapsedTime = System.currentTimeMillis() - startTime; + return result; + } + + @Override + protected void postProcessing(List<String> result) + { + executionPanel.appendOutput(createResultString(strategy.getDescription(), + result, + evaluator.getFitness(result, null), + elapsedTime)); + setEnabled(true); + } + }; + } + + + /** + * Helper method for formatting a result as a string for display. + */ + private String createResultString(String strategyDescription, + List<String> shortestRoute, + double distance, + long elapsedTime) + { + StringBuilder buffer = new StringBuilder(); + buffer.append('['); + buffer.append(strategyDescription); + buffer.append("]\n"); + buffer.append("ROUTE: "); + for (String s : shortestRoute) + { + buffer.append(s); + buffer.append(" -> "); + } + buffer.append(shortestRoute.get(0)); + buffer.append('\n'); + buffer.append("TOTAL DISTANCE: "); + buffer.append(String.valueOf(distance)); + buffer.append("km\n"); + buffer.append("(Search Time: "); + double seconds = (double) elapsedTime / 1000; + buffer.append(String.valueOf(seconds)); + buffer.append(" seconds)\n\n"); + return buffer.toString(); + } + + + /** + * Toggles whether the controls are enabled for input or not. + * @param b Enables the controls if this flag is true, disables them otherwise. + */ + @Override + public void setEnabled(boolean b) + { + itineraryPanel.setEnabled(b); + strategyPanel.setEnabled(b); + executionPanel.setEnabled(b); + super.setEnabled(b); + } + + + /** + * Entry point for running this example as an application rather than an applet. + * @param args Program arguments (ignored). + */ + public static void main(String[] args) + { + new TravellingSalesmanApplet().displayInFrame("Watchmaker Framework - Travelling Salesman Example"); + } +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanStrategy.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanStrategy.java new file mode 100644 index 0000000..82f75c8 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/TravellingSalesmanStrategy.java @@ -0,0 +1,44 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Collection; +import java.util.List; + +/** + * Defines methods that must be implemented by classes that provide + * solutions to the Travelling Salesman problem. + * @author Daniel Dyer + */ +public interface TravellingSalesmanStrategy +{ + /** + * @return A description of the strategy. + */ + String getDescription(); + + /** + * Calculates the shortest round trip distance that visits each + * of the specified cities once and returns to the starting point. + * @param cities The destination that must each be visited for the route + * to be valid. + * @param progressListener A call-back for keeping track of the route-finding + * algorithm's progress. + * @return The shortest route found for the given list of destinations. + */ + List<String> calculateShortestRoute(Collection<String> cities, + ProgressListener progressListener); +} diff --git a/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/package-info.java b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/package-info.java new file mode 100644 index 0000000..59f25f5 --- /dev/null +++ b/watchmaker/examples/src/java/main/org/uncommons/watchmaker/examples/travellingsalesman/package-info.java @@ -0,0 +1,21 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * An evolutionary approach to tackling the well-known Travelling Salesman + * problem. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.examples.travellingsalesman; diff --git a/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/lighthouse.jpg b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/lighthouse.jpg Binary files differnew file mode 100644 index 0000000..fcd57f6 --- /dev/null +++ b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/lighthouse.jpg diff --git a/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/monalisa.jpg b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/monalisa.jpg Binary files differnew file mode 100644 index 0000000..61ae626 --- /dev/null +++ b/watchmaker/examples/src/java/resources/org/uncommons/watchmaker/examples/monalisa/monalisa.jpg diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/AbstractExampleAppletTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/AbstractExampleAppletTest.java new file mode 100644 index 0000000..f762147 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/AbstractExampleAppletTest.java @@ -0,0 +1,94 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.awt.BorderLayout; +import java.awt.Container; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.FrameMatcher; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link AbstractExampleAppletTest} class. + * @author Daniel Dyer + */ +public class AbstractExampleAppletTest +{ + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testPreparationOnEDT() + { + final boolean[] onEDT = new boolean[1]; + AbstractExampleApplet applet = new AbstractExampleApplet() + { + @Override + protected void prepareGUI(Container container) + { + onEDT[0] = SwingUtilities.isEventDispatchThread(); + } + }; + applet.init(); + assert onEDT[0] : "Prepare method was not called on Event Dispatch Thread."; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testDisplayInFrame() + { + AbstractExampleApplet applet = new AbstractExampleApplet() + { + @Override + protected void prepareGUI(Container container) + { + JLabel label = new JLabel("Test"); + label.setName("Test"); + container.add(label, BorderLayout.CENTER); + } + }; + applet.displayInFrame("ExampleFrame"); + robot.waitForIdle(); + // There ought to be a visible frame containing the example GUI. + JFrame frame = (JFrame) robot.finder().find(FrameMatcher.withTitle("ExampleFrame").andShowing()); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + FrameFixture frameFixture = new FrameFixture(robot, frame); + assert frameFixture.label("Test").component().isShowing() : "GUI not displayed correctly."; + frameFixture.close(); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/ExamplesTestUtils.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/ExamplesTestUtils.java new file mode 100644 index 0000000..5affc17 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/ExamplesTestUtils.java @@ -0,0 +1,48 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples; + +import java.util.Random; +import org.uncommons.maths.random.XORShiftRNG; + +/** + * Utility methods for Watchmaker examples unit tests. Provides + * access to shared resources used by tests. + * @author Daniel Dyer + */ +public final class ExamplesTestUtils +{ + private static final Random RNG = new XORShiftRNG(); + + private ExamplesTestUtils() + { + // Prevent instantiation. + } + + + /** + * Returns the singleton RNG shared by all tests. It might be preferable + * to have a separate RNG for each test (for true separation) but this + * causes problems. Seeding dozens of RNGs can exhaust the system's + * available entropy (the Uncommons Maths RNGs seed themselves from + * /dev/random by default). + * @return A random number generator. + */ + public static Random getRNG() + { + return RNG; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactoryTest.java new file mode 100644 index 0000000..c1d1cce --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphFactoryTest.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for the biomorph candidate factory. + * @author Daniel Dyer + */ +public class BiomorphFactoryTest +{ + /** + * Ensures that biomorphs created by the factory are valid. + */ + @Test + public void testValidity() + { + CandidateFactory<Biomorph> factory = new BiomorphFactory(); + List<Biomorph> biomorphs = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (Biomorph biomorph : biomorphs) + { + // Returns 9 genes, last one is the length gene. + int[] genes = biomorph.getGenotype(); + for (int i = 0; i < Biomorph.GENE_COUNT - 1; i++) + { + assert genes[i] >= Biomorph.GENE_MIN && genes[i] <= Biomorph.GENE_MAX + : "Gene " + i + " is out of range: " + genes[i]; + } + int length = biomorph.getLengthPhenotype(); + assert length >= Biomorph.LENGTH_GENE_MIN && length <= Biomorph.LENGTH_GENE_MAX + : "Length gene is out of range: " + length; + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphTest.java new file mode 100644 index 0000000..b62c8a2 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/BiomorphTest.java @@ -0,0 +1,59 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.Arrays; +import org.testng.annotations.Test; + +/** + * Some basic sanity checks for the {@link Biomorph} type used in the + * interactive evolution example program. + * @author Daniel Dyer + */ +public class BiomorphTest +{ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInsufficientGenes() + { + new Biomorph(new int[Biomorph.GENE_COUNT - 1]); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManyGenes() + { + new Biomorph(new int[Biomorph.GENE_COUNT + 1]); + } + + + @Test + public void testEquality() + { + Biomorph biomorph1 = new Biomorph(new int[Biomorph.GENE_COUNT]); + Biomorph biomorph2 = new Biomorph(new int[Biomorph.GENE_COUNT]); + int[] genes = new int[Biomorph.GENE_COUNT]; + Arrays.fill(genes, 2); + Biomorph biomorph3 = new Biomorph(genes); + + assert biomorph1.equals(biomorph1) : "Equality must be reflexive."; + assert biomorph1.equals(biomorph2) : "Biomorphs with identical genes should be considered equal."; + assert biomorph2.equals(biomorph1) : "Equality must be reflective."; + assert biomorph1.hashCode() == biomorph2.hashCode() : "Equal objects must have identical hash codes."; + assert !biomorph1.equals(biomorph3) : "Biomorphs with different genes should not be considered equal."; + + assert !biomorph1.equals(null) : "No object should be considered equal to a null reference."; + assert !biomorph3.equals(genes) : "Biomorphs should not be considered equal to objects of different types."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutationTest.java new file mode 100644 index 0000000..09efe3e --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/DawkinsBiomorphMutationTest.java @@ -0,0 +1,72 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for non-random mutation used by Biomorph example application. + * @author Daniel Dyer + */ +public class DawkinsBiomorphMutationTest +{ + /** + * Ensure that each possible mutation occurs exactly once. + */ + @Test + public void testMutations() + { + Biomorph source = new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 2, 8}); + List<Biomorph> originalPopulation = new ArrayList<Biomorph>(18); + for (int i = 0; i < 18; i++) + { + originalPopulation.add(source); + } + EvolutionaryOperator<Biomorph> mutation = new DawkinsBiomorphMutation(); + List<Biomorph> mutatedPopulation = mutation.apply(originalPopulation, + ExamplesTestUtils.getRNG()); // RNG should be ignored. + assert mutatedPopulation.size() == originalPopulation.size() : "Mutated population is wrong size."; + // Lazy way of checking for duplicates. Add all mutations to a set. If there are any + // duplicates, the size of the set will be shorter than the list. + Set<Biomorph> distinctBiomorphs = new HashSet<Biomorph>(mutatedPopulation); + assert distinctBiomorphs.size() == mutatedPopulation.size() : "Mutated population contains duplicates."; + // Check for each of the expected mutations (mutations should differ from the original in only one gene). + assert distinctBiomorphs.contains(new Biomorph(new int[]{5, -4, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-4, -4, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -5, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -3, -3, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -4, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -2, -2, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -3, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -1, -1, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -2, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, 0, 0, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, -1, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 1, 1, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 0, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 2, 2, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 1, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 3, 8})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 2, 7})) : "Missing mutation."; + assert distinctBiomorphs.contains(new Biomorph(new int[]{-5, -4, -3, -2, -1, 0, 1, 2, 1})) : "Missing mutation."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutationTest.java new file mode 100644 index 0000000..5c0890d --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/biomorphs/RandomBiomorphMutationTest.java @@ -0,0 +1,59 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.biomorphs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for random mutation operator for biomorphs. + * @author Daniel Dyer + */ +public class RandomBiomorphMutationTest +{ + @Test + public void testValidity() + { + EvolutionaryOperator<Biomorph> mutation = new RandomBiomorphMutation(Probability.ONE); // Mutate every gene. + List<Biomorph> population = new ArrayList<Biomorph>(3); + population.add(new Biomorph(new int[]{5, -4, -3, -2, -1, 0, 1, 2, 8})); + population.add(new Biomorph(new int[]{-5, 4, 4, -5, 5, 3, 0, 2, 2})); + population.add(new Biomorph(new int[]{-4, -1, 0, 0, 3, 0, 4, 5, 4})); + Random rng = ExamplesTestUtils.getRNG(); + for (int i = 0; i < 20; i++) // Perform several mutations to cover more possibilties. + { + population = mutation.apply(population, rng); + for (Biomorph biomorph : population) + { + // Returns 9 genes, last one is the length gene. + int[] genes = biomorph.getGenotype(); + for (int j = 0; j < Biomorph.GENE_COUNT - 1; j++) + { + assert genes[j] >= Biomorph.GENE_MIN && genes[j] <= Biomorph.GENE_MAX + : "Gene " + j + " is out of range: " + genes[j]; + } + int length = biomorph.getLengthPhenotype(); + assert length >= Biomorph.LENGTH_GENE_MIN && length <= Biomorph.LENGTH_GENE_MAX + : "Length gene is out of range: " + length; + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/bits/BitsExampleTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/bits/BitsExampleTest.java new file mode 100644 index 0000000..737ed55 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/bits/BitsExampleTest.java @@ -0,0 +1,34 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.bits; + +import org.testng.annotations.Test; +import org.uncommons.maths.binary.BitString; + +/** + * Simple unit test for the bits example. Makes sure that the evolution engine + * eventually returns the expected result. + * @author Daniel Dyer + */ +public class BitsExampleTest +{ + @Test + public void testEvolution() + { + BitString result = BitsExample.evolveBits(8); + assert result.toString().equals("11111111") : "Wrong result returned: " + result.toString(); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/AdditionTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/AdditionTest.java new file mode 100644 index 0000000..1ad9a46 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/AdditionTest.java @@ -0,0 +1,112 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Addition} node type. + * @author Daniel Dyer + */ +public class AdditionTest +{ + @Test + public void testEvaluation() + { + Node node = new Addition(new Constant(1), new Parameter(0)); + double value = node.evaluate(new double[]{3}); // 1 + 3 + assert value == 4 : "Wrong result: " + value; + } + + @Test + public void testStringRepresentation() + { + Node node = new Addition(new Constant(1), new Constant(3)); + assert node.print().equals("(1.0 + 3.0)") : "Wrong string representation: " + node.print(); + } + + + /** + * If the arguments to the add function are both constants then the addition node + * should be replaced by a constant node containing the evaluation of this sum. + */ + @Test + public void testSimplifyConstants() + { + Node node = new Addition(new Constant(7), new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 12; + + } + + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new Addition(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new Addition(new Parameter(0), + new Addition(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof Addition + : "Simplified node should be Addition, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } + + + @Test + public void testSimplifyAddZero() + { + Node node = new Addition(new Parameter(0), new Constant(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + } + + + @Test + public void testSimplifyAddToZero() + { + Node node = new Addition(new Constant(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ConstantTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ConstantTest.java new file mode 100644 index 0000000..1a5eb36 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ConstantTest.java @@ -0,0 +1,55 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Unit test for the {@link Constant} node type. + * @author Daniel Dyer + */ +public class ConstantTest +{ + @Test + public void testEquality() + { + Constant zero = new Constant(0); + Constant one = new Constant(1); + Constant anotherOne = new Constant(1); + + assert zero.equals(zero) : "Equality must be reflexive."; + assert one.equals(anotherOne) : "Same-valued constants must be equal."; + assert anotherOne.equals(one) : "Equality must be symmetric."; + assert one.hashCode() == anotherOne.hashCode() : "Equal objects must have equal hash codes."; + assert !zero.equals(one) : "Different valued constants must be non-equal."; + assert !zero.equals(null) : "No non-null object should not be considered equal to null."; + assert !zero.equals(Double.valueOf(0)) : "Objects of different types should not be equal."; + } + + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testGetInvalidNodeIndex() + { + new Constant(1).getNode(1); // Should throw an exception. + } + + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testReplaceInvalidNodeIndex() + { + new Constant(1).replaceNode(1, new Constant(2)); // Should throw an exception. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExampleTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExampleTest.java new file mode 100644 index 0000000..3d254fd --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/GeneticProgrammingExampleTest.java @@ -0,0 +1,50 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.HashMap; +import java.util.Map; +import org.testng.annotations.Test; + +/** + * Test for the {@link GeneticProgrammingExample} example application. + * @author Daniel Dyer + */ +public class GeneticProgrammingExampleTest +{ + @Test + public void testApplication() + { + Map<double[], Double> testData = new HashMap<double[], Double>(); + testData.put(new double[]{26, 35}, 165.0d); + testData.put(new double[]{8, 24}, 64.0d); + testData.put(new double[]{20, 1}, 101.0d); + testData.put(new double[]{33, 11}, 176.0d); + testData.put(new double[]{37, 16}, 201.0d); + + Node evolvedProgram = GeneticProgrammingExample.evolveProgram(testData); + + // Check that evolved program works for test data. + double result1 = evolvedProgram.evaluate(new double[]{8, 24}); + assert result1 == 64.0d : "Incorrect result: " + result1; + + // Check that the evolved program generalises. + double result2 = evolvedProgram.evaluate(new double[]{10, 7}); + assert result2 == 57.0d : "Incorrect result: " + result2; + double result3 = evolvedProgram.evaluate(new double[]{13, 22}); + assert result3 == 87.0d : "Incorrect result: " + result3; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElseTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElseTest.java new file mode 100644 index 0000000..13ab813 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IfThenElseTest.java @@ -0,0 +1,248 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link IfThenElse} node type. + * @author Daniel Dyer + */ +public class IfThenElseTest +{ + @Test + public void testIfBranch() + { + Node node = new IfThenElse(new Constant(1), + new Constant(5), + new Constant(10)); + // Condition (1) is true, so result should be 5. + double value = node.evaluate(new double[0]); + assert value == 5 : "Wrong answer: " + value; + } + + + @Test + public void testElseBranch() + { + Node node = new IfThenElse(new Constant(0), + new Constant(5), + new Constant(10)); + // Condition (0) is true, so result should be 10. + double value = node.evaluate(new double[0]); + assert value == 10 : "Wrong answer: " + value; + } + + + /** + * Count nodes when the sub-trees each consist of only a single leaf node. + */ + @Test + public void testCountNodesSimple() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + int nodeCount = node.countNodes(); + assert nodeCount == 4 : "Tree has 4 nodes (root + 3 children), not " + nodeCount; + } + + + /** + * Count nodes when the sub-trees consist of non-leaf nodes. + */ + @Test + public void testCountNodesComplex() + { + Node node = new IfThenElse(new Addition(new Constant(1), new Constant(2)), + new Subtraction(new Constant(3), new Constant(4)), + new Multiplication(new Constant(5), new Constant(6))); + int nodeCount = node.countNodes(); + assert nodeCount == 10 : "Tree has 10 nodes (root + 3 children and 6 grand-children), not " + nodeCount; + } + + + @Test + public void testGetNode() + { + Node condition = new Constant(1); + Node then = new Constant(2); + Constant sub1 = new Constant(3); + Constant sub2 = new Constant(4); + Node otherwise = new Addition(sub1, sub2); + Node node = new IfThenElse(condition, then, otherwise); + + assert node.getNode(0) == node : "Node zero should be root node."; + assert node.getNode(1) == condition : "Node one should be condition node."; + assert node.getNode(2) == then : "Node one should be then node."; + assert node.getNode(3) == otherwise : "Node one should be else node."; + assert node.getNode(4) == sub1 : "Node 4 should be first sub-node of else node."; + assert node.getNode(5) == sub2 : "Node 5 should be second sub-node of else node."; + } + + + @Test + public void testReplaceRootNode() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + Constant constant = new Constant(0); + Node newNode = node.replaceNode(0, constant); + assert newNode == constant : "Root node should be replaced by new node."; + } + + + @Test(dependsOnMethods = "testIfBranch") + public void testReplaceConditionNode() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + Node newNode = node.replaceNode(1, new Constant(0)); + assert newNode instanceof IfThenElse : "Replacing condition node should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 3d : "Changing condition node should change evaluation."; + } + + + @Test(dependsOnMethods = "testIfBranch") + public void testReplaceThenNode() + { + Node node = new IfThenElse(new Constant(1), new Constant(2), new Constant(3)); + Node newNode = node.replaceNode(2, new Constant(4)); + assert newNode instanceof IfThenElse : "Replacing then node should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 4d : "Changing then node should change evaluation."; + } + + + @Test(dependsOnMethods = "testElseBranch") + public void testReplaceElseNode() + { + Node node = new IfThenElse(new Constant(0), new Constant(2), new Constant(3)); + Node newNode = node.replaceNode(3, new Constant(5)); + assert newNode instanceof IfThenElse : "Replacing else node should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 5d : "Changing then node should change evaluation."; + } + + + /** + * Test replacing of nodes which are not direct sub-nodes of this node but are further down + * the tree. + */ + @Test(dependsOnMethods = "testReplaceElseNode") + public void testReplaceSubNodes() + { + Node node = new IfThenElse(new Constant(0), new Constant(2), new Addition(new Constant(3), new Constant(4))); + // Replace both of the constants summed by the addition node on the else branch. + Node newNode = node.replaceNode(4, new Constant(5)); + newNode = newNode.replaceNode(5, new Constant(6)); + assert newNode instanceof IfThenElse : "Replacing sub-nodes should not change type of root node."; + assert newNode.evaluate(BinaryNode.NO_ARGS) == 11d : "Changing sub-nodes should change evaluation."; + } + + + @Test + public void testZeroProbabilityMutation() + { + Node node = new IfThenElse(new Constant(0), new Constant(2), new Addition(new Constant(3), new Constant(4))); + double value = node.evaluate(BinaryNode.NO_ARGS); + String string = node.print(); + + Node mutated = node.mutate(ExamplesTestUtils.getRNG(), Probability.ZERO, null); + assert mutated == node : "Node should not have changed."; + assert value == mutated.evaluate(BinaryNode.NO_ARGS) : "Node should not have been altered."; + assert string.equals(mutated.print()) : "Node should not have been altered."; + } + + + @Test + public void testSimplify() + { + Node node = new IfThenElse(new Constant(0), new Constant(1), new Constant(2)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new IfThenElse(new Constant(4), + new IfThenElse(new Constant(1), new Constant(2), new Constant(3)), + new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 2; + } + + + /** + * Make sure that sub-nodes are simplified and that these simplfications are taken into + * account when optimising the parent node (in other words the child nodes should be + * simplified first). + */ + @Test + public void testSimplifyComplex() + { + Node node = new IfThenElse(new IfThenElse(new Constant(1), new Constant(2), new Constant(3)), + new Constant(4), + new Constant(5)); + Node simplified = node.simplify(); + // It is not sufficient to simplify this to an IfThenElse with a constant for each node, + // the fact that the condition node can be replaced by a Constant should result in the whole + // tree being reduced to a single constant (with a value of 4). + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 4; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new IfThenElse(new Parameter(0), new Constant(1), new Constant(2)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * If the two branches (then and else) are identical, it doesn't matter what the condition is, + * the tree can be replaced by either one of the branches. + */ + @Test + public void testSimplifyIdenticalBranches() + { + Node node = new IfThenElse(new Parameter(0), + new Constant(2), + new Constant(2)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{1}; // Need one argument for the parameter node to use. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 2; + + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IsGreaterTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IsGreaterTest.java new file mode 100644 index 0000000..91baeec --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/IsGreaterTest.java @@ -0,0 +1,110 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link IsGreater} node type. + * @author Daniel Dyer + */ +public class IsGreaterTest +{ + /** + * If two nodes are equal then neither is greater than the other. + */ + @Test + public void testBothNodesEqual() + { + Node node = new IsGreater(new Constant(1), new Constant(1)); + assert node.evaluate(new double[0]) == 0 : "First node should not be greater than second."; + } + + + @Test + public void testFirstNodeGreater() + { + Node node = new IsGreater(new Constant(2), new Constant(1)); + assert node.evaluate(new double[0]) > 0 : "First node should be greater than second."; + } + + + @Test + public void testSecondNodeGreater() + { + Node node = new IsGreater(new Constant(-1), new Constant(1)); + assert node.evaluate(new double[0]) == 0 : "First node should be less than second."; + } + + + /** + * If the arguments to the IsGreater function are both constants then the node + * should be replaced by a constant node containing the evaluation of this expression. + */ + @Test + public void testSimplifyConstants() + { + Node node = new IsGreater(new Constant(7), new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 1; + + } + + + @Test + public void testSimplifyIdenticalArguments() + { + Node node = new IsGreater(new Parameter(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new IsGreater(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new IsGreater(new Parameter(0), + new IsGreater(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof IsGreater + : "Simplified node should be IsGreater, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/MultiplicationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/MultiplicationTest.java new file mode 100644 index 0000000..f81c367 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/MultiplicationTest.java @@ -0,0 +1,157 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Multiplication} node type. + * @author Daniel Dyer + */ +public class MultiplicationTest +{ + @Test + public void testEvaluation() + { + Node node = new Multiplication(new Constant(5), new Constant(2)); + double value = node.evaluate(new double[0]); + assert value == 10 : "Wrong result: " + value; + } + + + @Test + public void testStringRepresentation() + { + Node node = new Multiplication(new Constant(5), new Parameter(0)); + assert node.print().equals("(5.0 * arg0)") : "Wrong string representation: " + node.print(); + } + + + /** + * If the arguments to the multiply function are both constants then the multiplication node + * should be replaced by a constant node containing the evaluation of this product. + */ + @Test + public void testSimplifyConstants() + { + Node node = new Multiplication(new Constant(3), new Constant(4)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 12; + + } + + + /** + * If the lefthand argument is zero, the result will always be zero regardless of the righthand + * argument. + */ + @Test + public void testSimplifyMultiplyZero() + { + Node node = new Multiplication(new Constant(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 0; + + } + + + /** + * If the righthand argument is zero, the result will always be zero regardless of the lefthand + * argument. + */ + @Test + public void testSimplifyMultiplyByZero() + { + Node node = new Multiplication(new Parameter(0), new Constant(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 0; + } + + + /** + * If the lefthand argument is one, the result can be reduced to the righthand argument. + */ + @Test + public void testSimplifyMultiplyOne() + { + Node node = new Multiplication(new Constant(1), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 5; + + } + + + /** + * If the righthand argument is one, the result can be reduced to the lefthand argument. + */ + @Test + public void testSimplifyMultiplyByOne() + { + Node node = new Multiplication(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 5; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new Multiplication(new Parameter(0), new Constant(2)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new Multiplication(new Parameter(0), + new Multiplication(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof Multiplication + : "Simplified node should be Multiplication, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ParameterTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ParameterTest.java new file mode 100644 index 0000000..7324242 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/ParameterTest.java @@ -0,0 +1,60 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Parameter} node type. + * @author Daniel Dyer + */ +public class ParameterTest +{ + @Test + public void testParameterSelection() + { + Parameter parameter = new Parameter(2); + double value = parameter.evaluate(new double[]{0, 1, 2, 3}); + assert value == 2 : "Incorect argument selected: " + value; + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidParameterSelection() + { + Parameter parameter = new Parameter(2); + // There is only one argument, so trying to select the 3rd one should + // result in an IllegalArgumentException. + parameter.evaluate(new double[]{0}); + } + + + @Test + public void testEquality() + { + Parameter zero = new Parameter(0); + Parameter one = new Parameter(1); + Parameter anotherOne = new Parameter(1); + + assert zero.equals(zero) : "Equality must be reflexive."; + assert one.equals(anotherOne) : "Same-index parameters must be equal."; + assert anotherOne.equals(one) : "Equality must be symmetric."; + assert one.hashCode() == anotherOne.hashCode() : "Equal objects must have equal hash codes."; + assert !zero.equals(one) : "Different index parameters must be non-equal."; + assert !zero.equals(null) : "No non-null object should not be considered equal to null."; + assert !zero.equals(Integer.valueOf(0)) : "Objects of different types should not be equal."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SimplificationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SimplificationTest.java new file mode 100644 index 0000000..f439021 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SimplificationTest.java @@ -0,0 +1,70 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link Simplification} evolutionary operator. + * @author Daniel Dyer + */ +public class SimplificationTest +{ + /** + * When the probability is 1, all candidates should be processed. + */ + @Test + public void testProbabilityOne() + { + Node node1 = new Addition(new Constant(1), new Constant(1)); + Node node2 = new Subtraction(new Constant(5), new Constant(4)); + Node node3 = new Multiplication(new Constant(3), new Constant(3)); + List<Node> population = Arrays.asList(node1, node2, node3); + + Simplification simplification = new Simplification(); + List<Node> evolved = simplification.apply(population, ExamplesTestUtils.getRNG()); + assert evolved.size() == population.size() : "Output should be same size as input."; + for (Node node : evolved) + { + assert node instanceof Constant : "Node was not simplified."; + } + } + + + /** + * When the probability is 0, no candidates should be processed. + */ + @Test + public void testProbabilityZero() + { + Node node1 = new Addition(new Constant(1), new Constant(1)); + Node node2 = new Subtraction(new Constant(5), new Constant(4)); + Node node3 = new Multiplication(new Constant(3), new Constant(3)); + List<Node> population = Arrays.asList(node1, node2, node3); + + Simplification simplification = new Simplification(Probability.ZERO); + List<Node> evolved = simplification.apply(population, ExamplesTestUtils.getRNG()); + assert evolved.size() == population.size() : "Output should be same size as input."; + for (Node node : evolved) + { + assert !(node instanceof Constant) : "Node should not have been simplified."; + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SubtractionTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SubtractionTest.java new file mode 100644 index 0000000..1682b25 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SubtractionTest.java @@ -0,0 +1,133 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the {@link Subtraction} node type. + * @author Daniel Dyer + */ +public class SubtractionTest +{ + @Test + public void testEvaluation() + { + Node node = new Subtraction(new Constant(7), new Constant(3)); + double value = node.evaluate(new double[0]); + assert value == 4 : "Wrong result: " + value; + } + + + @Test + public void testStringRepresentation() + { + Node node = new Subtraction(new Constant(7), new Constant(3)); + assert node.print().equals("(7.0 - 3.0)") : "Wrong string representation: " + node.print(); + } + + + /** + * If the arguments to the subtract function are both constants then the subtract node + * should be replaced by a constant node containing the evaluation of this sum. + */ + @Test + public void testSimplifyConstants() + { + Node node = new Subtraction(new Constant(7), new Constant(5)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + assert simplified.evaluate(BinaryNode.NO_ARGS) == node.evaluate(BinaryNode.NO_ARGS) : "Simplified answer differs."; + assert simplified.evaluate(BinaryNode.NO_ARGS) == 2; + } + + + /** + * If the arguments to the subtract function are identical, even if they are not constant, + * then the answer will always be zero, so this node should be replaced by the constant zero. + */ + @Test + public void testSimplifyIdenticalArguments() + { + Node node = new Subtraction(new Parameter(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified instanceof Constant + : "Simplified node should be Constant, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 0; + } + + + /** + * Test that simplification doesn't cause any problems when the expression is already as simple + * as possible. + */ + @Test + public void testSimplifySimplest() + { + Node node = new Subtraction(new Parameter(0), new Constant(1)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * If the second argument is zero, the experession can be replaced by its lefthand side. + */ + @Test + public void testSimplifySubtractZero() + { + Node node = new Subtraction(new Parameter(0), new Constant(0)); + Node simplified = node.simplify(); + assert simplified instanceof Parameter + : "Simplified node should be Parameter, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.evaluate(args) == 5; + } + + + /** + * But if the first argument is zero, the experession should not be simplified as it has + * the effect of negating the second argument. + */ + @Test + public void testSimplifySubtractFromZero() + { + Node node = new Subtraction(new Constant(0), new Parameter(0)); + Node simplified = node.simplify(); + assert simplified == node : "Expression should not have been changed."; + } + + + /** + * Make sure that sub-nodes are simplified. + */ + @Test + public void testSimplifySubNode() + { + Node node = new Subtraction(new Parameter(0), + new Subtraction(new Constant(3), new Constant(2))); + Node simplified = node.simplify(); + assert simplified instanceof Subtraction + : "Simplified node should be Subtraction, is " + simplified.getClass().getSimpleName(); + double[] args = new double[]{5}; // Provides a value for the parameter nodes. + assert simplified.evaluate(args) == node.evaluate(args) : "Simplified answer differs."; + assert simplified.countNodes() < node.countNodes() : "Should be fewer nodes after simplification."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRendererTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRendererTest.java new file mode 100644 index 0000000..c0ac452 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/SwingGPTreeRendererTest.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.awt.Dimension; +import javax.swing.JComponent; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link SwingGPTreeRenderer} class. + * @author Daniel Dyer + */ +public class SwingGPTreeRendererTest +{ + // There's not much we can effectively test for this class, but we can at least + // make sure the generated component has the correct dimensions. + @Test + public void testSizes() + { + SwingGPTreeRenderer renderer = new SwingGPTreeRenderer(); + + // Simple case. + Node tree = new Constant(2); + JComponent component = renderer.render(tree); + Dimension minSize = component.getMinimumSize(); + assert minSize.width == 30 : "Wrong width: " + minSize.width; + assert minSize.height == 50 : "Wrong height: " + minSize.height; + + // More complicated case. + tree = new IfThenElse(new Parameter(0), + new Addition(new Constant(2), new Parameter(1)), + new Constant(5)); + component = renderer.render(tree); + minSize = component.getMinimumSize(); + assert minSize.width == 120 : "Wrong width: " + minSize.width; + assert minSize.height == 150 : "Wrong height: " + minSize.height; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossoverTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossoverTest.java new file mode 100644 index 0000000..5fdf777 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeCrossoverTest.java @@ -0,0 +1,47 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link TreeCrossover} evolutionary operator used by + * the genetic programming example application. + * @author Daniel Dyer + */ +public class TreeCrossoverTest +{ + /** + * This is just a simple sanity check. Cross-over should result in the same + * number of individuals, all constructed from the same set of nodes that existed + * in the parent generation (but perhaps connected differently). + */ + @Test + public void testCrossover() + { + TreeCrossover crossover = new TreeCrossover(); + Node tree1 = new Multiplication(new Constant(1), new Constant(2)); + Node tree2 = new Subtraction(new Parameter(0), new Parameter(1)); + + List<Node> offspring = crossover.apply(Arrays.asList(tree1, tree2), ExamplesTestUtils.getRNG()); + assert offspring.size() == 2 : "Should be 2 offspring after cross-over."; + int totalNodeCount = offspring.get(0).countNodes() + offspring.get(1).countNodes(); + assert totalNodeCount == 6 : "Should be exactly 6 nodes in total, is " + totalNodeCount; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluatorTest.java new file mode 100644 index 0000000..7aec257 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeEvaluatorTest.java @@ -0,0 +1,78 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Unit test for the {@link FitnessEvaluator} used + * in the genetic programming example applicaiton. + * @author Daniel Dyer + */ +public class TreeEvaluatorTest +{ + /** + * A function that perfectly generates the correct output for all inputs + * should have a fitness of zero. + */ + @Test + public void testPerfectFunction() + { + Map<double[], Double> data = new HashMap<double[], Double>(); + // Data for multiplication program. + data.put(new double[]{5d, 3d}, 15d); + data.put(new double[]{3d, 8d}, 24d); + data.put(new double[]{7d, 2d}, 14d); + + FitnessEvaluator<Node> evaluator = new TreeEvaluator(data); + + // Program that multiplies its two inputs together. + Node program = new Multiplication(new Parameter(0), new Parameter(1)); + + double fitness = evaluator.getFitness(program, Arrays.asList(program)); + assert fitness == 0 : "Correct program should have zero fitness."; + } + + + /** + * A function that doesn't generate the correct output for all inputs + * should have a non-zero fitness. + */ + @Test + public void testIncorrectFunction() + { + Map<double[], Double> data = new HashMap<double[], Double>(); + // Data for multiplication program. + data.put(new double[]{5d, 3d}, 15d); + data.put(new double[]{3d, 8d}, 24d); + data.put(new double[]{7d, 2d}, 14d); + + FitnessEvaluator<Node> evaluator = new TreeEvaluator(data); + + // Program that multiplies its first input by 3 (will give the correct answer + // for the first set of inputs but the wrong answer for the other two). + Node program = new Multiplication(new Parameter(0), new Constant(3d)); + + double fitness = evaluator.getFitness(program, Arrays.asList(program)); + // Error on second example is 15, error on third is 7. + // 15^2 + 7^2 = 225 + 49 = 274 + assert fitness == 274d : "Wrong fitness for incorrect program."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactoryTest.java new file mode 100644 index 0000000..d4ed9e6 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeFactoryTest.java @@ -0,0 +1,66 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for the {@link TreeFactory} used by the gentic programming + * example. + * @author Daniel Dyer + */ +public class TreeFactoryTest +{ + @Test + public void testMaxDepth() + { + final int maxDepth = 3; + CandidateFactory<Node> factory = new TreeFactory(2, + maxDepth, + new Probability(0.6), + Probability.EVENS); + List<Node> trees = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (Node tree : trees) + { + // Make sure that each tree is no bigger than the maximum permitted. + assert tree.getDepth() <= maxDepth : "Generated tree is too deep: " + tree.getDepth(); + } + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidParameterCount() + { + new TreeFactory(-1, + 1, + Probability.EVENS, + Probability.EVENS); // Should throw an exception, parameter count can't be negative. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMaxDepth() + { + new TreeFactory(1, + 0, + Probability.EVENS, + Probability.EVENS); // Should throw an exception, depth must be at least one. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutationTest.java new file mode 100644 index 0000000..3bf6b0f --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/geneticprogramming/TreeMutationTest.java @@ -0,0 +1,76 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.geneticprogramming; + +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link TreeMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class TreeMutationTest +{ + /** + * If the mutation doesn't happen, the candidates should be returned unaltered. + */ + @Test + public void testNoMutations() + { + TreeFactory treeFactory = new TreeFactory(0, 3, Probability.EVENS, Probability.EVENS); + EvolutionaryOperator<Node> mutation = new TreeMutation(treeFactory, + Probability.ZERO); // Zero probability means no mutations. + List<Node> candidates = Arrays.<Node>asList(new Addition(new Constant(3), new Constant(4))); + List<Node> result = mutation.apply(candidates, ExamplesTestUtils.getRNG()); + assert result.size() == 1 : "Wrong number of trees returned: " + result.size(); + assert candidates.get(0) == result.get(0) : "Tree should have been returned unmodified."; + } + + + /** + * If the mutation doesn't happen, the candidates should be returned unaltered. + */ + @Test + public void testSomeMutations() + { + TreeFactory treeFactory = new TreeFactory(1, 4, new Probability(0.6d), new Probability(0.2d)); + EvolutionaryOperator<Node> mutation = new TreeMutation(treeFactory, + Probability.ONE); // Probability of 1 means guaranteed mutations. + List<Node> candidates = treeFactory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + + Map<Node, Node> distinctTrees = new IdentityHashMap<Node, Node>(); + for (Node node : candidates) + { + distinctTrees.put(node, node); + } + + List<Node> result = mutation.apply(candidates, ExamplesTestUtils.getRNG()); + assert result.size() == 20 : "Wrong number of trees returned: " + result.size(); + for (Node node : result) + { + distinctTrees.put(node, node); + } + + // If none of the original trees are returned, we should have 40 distict trees. + assert distinctTrees.size() == 40 : "New trees should have been returned."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutationTest.java new file mode 100644 index 0000000..53c7aea --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddPolygonMutationTest.java @@ -0,0 +1,104 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link AddPolygonMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class AddPolygonMutationTest +{ + private final PolygonImageFactory factory = new PolygonImageFactory(new Dimension(200, 200)); + + @Test + public void testAddPolygon() + { + List<ColouredPolygon> image = factory.generateRandomCandidate(ExamplesTestUtils.getRNG()); + assert image.size() == 2 : "Image should have 2 polygons"; + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new AddPolygonMutation(Probability.ONE, + factory, + 5); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() + 1 : "Image should have 1 extra polygon after mutation."; + } + + + @Test + public void testZeroProbability() + { + List<ColouredPolygon> image = factory.generateRandomCandidate(ExamplesTestUtils.getRNG()); + assert image.size() == 2 : "Image should have 2 polygons"; + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new AddPolygonMutation(Probability.ZERO, + factory, + 5); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } + + + /** + * If the image already has the maximum permitted number of polygons, extra polygons + * should not be added by mutation. + */ + @Test + public void testAddMaxPolygons() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new AddPolygonMutation(Probability.ONE, + factory, + 3); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have no more than the maximum number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } + + + /** + * An image must have at least 2 polygons. The configured maximum must respect this. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMaximum() + { + new AddPolygonMutation(Probability.ONE, factory, 1); // Invalid, should throw IllegalArgumentException. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddVertexMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddVertexMutationTest.java new file mode 100644 index 0000000..50579a8 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AddVertexMutationTest.java @@ -0,0 +1,100 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link AddVertexMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class AddVertexMutationTest +{ + private final Dimension canvasSize = new Dimension(200, 200); + private final PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + + @Test + public void testAddVertex() + { + ColouredPolygon polygon = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AddVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + List<Point> vertices = evolved.get(0).getVertices(); + assert vertices.size() == polygon.getVertices().size() + 1 + : "Polygon should have 1 extra point after mutation."; + } + + + @Test + public void testZeroProbability() + { + ColouredPolygon polygon = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AddVertexMutation(canvasSize, + Probability.ZERO); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no extra points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } + + + /** + * If the image already has the maximum permitted number of points, extra points + * should not be added by mutation. + */ + @Test + public void testAddMaxPoints() + { + List<Point> points = new ArrayList<Point>(AddVertexMutation.MAX_VERTEX_COUNT); + for (int i = 0; i < AddVertexMutation.MAX_VERTEX_COUNT; i++) + { + points.add(new Point(i, i)); + } + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AddVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no extra points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutationTest.java new file mode 100644 index 0000000..a4b8142 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/AdjustVertexMutationTest.java @@ -0,0 +1,115 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link AdjustVertexMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class AdjustVertexMutationTest +{ + private final Dimension canvasSize = new Dimension(200, 200); + + @Test + public void testAdjustVertex() + { + final Point point1 = new Point(1, 1); + final Point point2 = new Point(2, 2); + final Point point3 = new Point(3, 3); + List<Point> points = Arrays.asList(point1, point2, point3); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + final int amount = 5; + EvolutionaryOperator<ColouredPolygon> mutation = new AdjustVertexMutation(canvasSize, + Probability.ONE, + new ConstantGenerator<Integer>(amount)); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have same number of points after mutation."; + + if (vertices.get(0) != point1) + { + Point newPoint = vertices.get(0); + assert newPoint.x == point1.x + 5 : "X-coordinate not mutated properly."; + assert newPoint.y == point1.y + 5 : "Y-coordinate not mutated properly."; + // If the first point is different the other two should be the same. + assert vertices.get(1) == point2 : "Second point should be unchanged."; + assert vertices.get(2) == point3 : "Third point should be unchanged."; + } + else if (vertices.get(1) != point2) + { + Point newPoint = vertices.get(1); + assert newPoint.x == point2.x + 5 : "X-coordinate not mutated properly."; + assert newPoint.y == point2.y + 5 : "Y-coordinate not mutated properly."; + // If the second point is different the other two should be the same. + assert vertices.get(0) == point1 : "First point should be unchanged."; + assert vertices.get(2) == point3 : "Third point should be unchanged."; + } + else if (vertices.get(2) != point3) + { + Point newPoint = vertices.get(2); + assert newPoint.x == point3.x + 5 : "X-coordinate not mutated properly."; + assert newPoint.y == point3.y + 5 : "Y-coordinate not mutated properly."; + // If the third point is different the other two should be the same. + assert vertices.get(0) == point1 : "Third point should be unchanged."; + assert vertices.get(1) == point2 : "Second point should be unchanged."; + } + } + + + @Test + public void testZeroProbability() + { + Point point1 = new Point(1, 1); + Point point2 = new Point(2, 2); + Point point3 = new Point(3, 3); + List<Point> points = Arrays.asList(point1, point2, point3); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new AdjustVertexMutation(canvasSize, + Probability.ZERO, + new ConstantGenerator<Integer>(1)); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have same number of points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + assert vertices.get(0) == point1 : "First point should be unchanged."; + assert vertices.get(1) == point2 : "Second point should be unchanged."; + assert vertices.get(2) == point3 : "Third point should be unchanged."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutationTest.java new file mode 100644 index 0000000..cf679e8 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/MovePolygonMutationTest.java @@ -0,0 +1,75 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link MovePolygonMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class MovePolygonMutationTest +{ + private final PolygonImageFactory factory = new PolygonImageFactory(new Dimension(200, 200)); + + @Test + public void testMovePolygon() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new MovePolygonMutation(Probability.ONE); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons after mutation."; + // Can't reliably test that the order was mutated because the random selection may have moved + // the polygon back to its original index. + } + + + @Test + public void testZeroProbability() + { + ColouredPolygon polygon1 = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + ColouredPolygon polygon2 = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + ColouredPolygon polygon3 = factory.createRandomPolygon(ExamplesTestUtils.getRNG()); + List<ColouredPolygon> image = Arrays.asList(polygon1, polygon2, polygon3); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new MovePolygonMutation(Probability.ZERO); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + assert evolved.get(0).get(0) == polygon1 : "First polygon should not have moved."; + assert evolved.get(0).get(1) == polygon2 : "Second polygon should not have moved."; + assert evolved.get(0).get(2) == polygon3 : "Third polygon should not have moved."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutationTest.java new file mode 100644 index 0000000..bbdb1f9 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonColourMutationTest.java @@ -0,0 +1,69 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Point; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.number.ConstantGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; + +/** + * Unit test for the {@link PolygonColourMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class PolygonColourMutationTest +{ + @Test + public void testColourMutation() + { + PolygonColourMutation mutation = new PolygonColourMutation(Probability.ONE, // Guaranteed mutation. + new ConstantGenerator<Double>(1d)); + // A grey triangle. + final ColouredPolygon polygon = new ColouredPolygon(new Color(128, 128, 128, 128), + Arrays.asList(new Point(0, 0), + new Point(50, 50), + new Point(0, 75))); + List<ColouredPolygon> image = Arrays.asList(polygon); + List<ColouredPolygon> mutatedImage = mutation.apply(image, ExamplesTestUtils.getRNG()); + Color mutatedColour = mutatedImage.get(0).getColour(); + assert mutatedColour.getRed() == 129 : "Red component should have been incremented, is " + mutatedColour.getRed(); + assert mutatedColour.getGreen() == 129 : "Green component should have been incremented, is " + mutatedColour.getGreen(); + assert mutatedColour.getBlue() == 129 : "Blue component should have been incremented, is " + mutatedColour.getBlue(); + assert mutatedColour.getAlpha() == 129 : "Alpha component should have been incremented, is " + mutatedColour.getAlpha(); + } + + + @Test + public void testZeroProbability() + { + PolygonColourMutation mutation = new PolygonColourMutation(Probability.ZERO, + new ConstantGenerator<Double>(1d)); + // A grey triangle. + Color originalColour = new Color(128, 128, 128, 128); + final ColouredPolygon polygon = new ColouredPolygon(originalColour, + Arrays.asList(new Point(0, 0), + new Point(50, 50), + new Point(0, 75))); + List<ColouredPolygon> image = Arrays.asList(polygon); + List<ColouredPolygon> mutatedImage = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert mutatedImage.get(0) == polygon : "Polygon should not have changed at all."; + assert mutatedImage.get(0).getColour() == originalColour : "Colour should not have changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluatorTest.java new file mode 100644 index 0000000..fbd94e8 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageEvaluatorTest.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.MersenneTwisterRNG; + +/** + * Unit test for {@link PolygonImageEvaluator}. + * @author Daniel Dyer + */ +public class PolygonImageEvaluatorTest +{ + /** + * An image that is identical to the target image should have a fitness + * of zero. + */ + @Test(groups = "display-required") + public void testPerfectMatch() + { + Dimension canvasSize = new Dimension(100, 100); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + List<ColouredPolygon> image = factory.generateRandomCandidate(new MersenneTwisterRNG()); + + BufferedImage targetImage = new PolygonImageRenderer(canvasSize, false, null).render(image); + PolygonImageEvaluator evaluator = new PolygonImageEvaluator(targetImage); + + double fitness = evaluator.getFitness(image, null); + assert fitness == 0 : "Fitness should be zero when image is an exact match."; + } + + + /** + * An image that is different to the target image should have a non-zero fitness. + */ + @Test(groups = "display-required") + public void testDifferentImages() + { + Dimension canvasSize = new Dimension(100, 100); + List<ColouredPolygon> targetImage = Arrays.asList(new ColouredPolygon(Color.BLACK, + Arrays.asList(new Point(0, 0), + new Point(99, 0), + new Point(99, 99), + new Point(0, 99)))); + List<ColouredPolygon> candidateImage = Arrays.asList(new ColouredPolygon(Color.WHITE, + Arrays.asList(new Point(0, 0), + new Point(99, 0), + new Point(99, 99), + new Point(0, 99)))); + + BufferedImage renderedTarget = new PolygonImageRenderer(canvasSize, false, null).render(targetImage); + PolygonImageEvaluator evaluator = new PolygonImageEvaluator(renderedTarget); + + double fitness = evaluator.getFitness(candidateImage, null); + assert fitness > 0 : "Fitness should be non-zero when image does not match target."; + } + + + /** + * If the image is not INT_RGB, it will be converted. This should not affect the results. + */ + @Test(groups = "display-required") + public void testImageConversion() + { + Dimension canvasSize = new Dimension(100, 100); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + List<ColouredPolygon> image = factory.generateRandomCandidate(new MersenneTwisterRNG()); + + BufferedImage targetImage = new PolygonImageRenderer(canvasSize, false, null).render(image); + // Convert target image to some format that will have to be converted to INT_RGB. + BufferedImage newImage = new BufferedImage(targetImage.getWidth(), + targetImage.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); // Sub-optimal image type. + newImage.getGraphics().drawImage(targetImage, 0, 0, null); + + PolygonImageEvaluator evaluator = new PolygonImageEvaluator(newImage); + + double fitness = evaluator.getFitness(image, null); + assert fitness == 0 : "Fitness should be zero when image is an exact match."; + } + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactoryTest.java new file mode 100644 index 0000000..15b2db4 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/PolygonImageFactoryTest.java @@ -0,0 +1,59 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.awt.Point; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for {@link PolygonImageFactory}. + * @author Daniel Dyer + */ +public class PolygonImageFactoryTest +{ + /** + * Make sure that the generated images have the correct number of polygons, + * that each polygon has the correct number of points and that all of the + * points fall within the bounds of the specified canvas. + */ + @Test + public void testConstraints() + { + final int width = 100; + final int height = 50; + CandidateFactory<List<ColouredPolygon>> factory = new PolygonImageFactory(new Dimension(width, height)); + List<List<ColouredPolygon>> candidates = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (List<ColouredPolygon> image : candidates) + { + assert image.size() == PolygonImageFactory.MINIMUM_POLYGON_COUNT + : "Wrong number of polygons: " + image.size(); + for (ColouredPolygon polygon : image) + { + assert polygon.getVertices().size() == PolygonImageFactory.MINIMUM_VERTEX_COUNT + : "Wrong number of vertices: " + polygon.getVertices().size(); + for (Point point : polygon.getVertices()) + { + assert point.x >= 0 && point.x < width : "X out-of-range: " + point.x; + assert point.y >= 0 && point.y < height : "Y out-of-range: " + point.y; + } + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanelTest.java new file mode 100644 index 0000000..3e44663 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/ProbabilitiesPanelTest.java @@ -0,0 +1,132 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link ProbabilitiesPanel} class. + * @author Daniel Dyer + */ +public class ProbabilitiesPanelTest +{ + private Robot robot; + + @BeforeMethod(groups = "display-required") + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod(groups = "display-required") + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + @Test(groups = "display-required") + public void testSetAllProbabilitiesToZero() + { + ProbabilitiesPanel panel = new ProbabilitiesPanel(); + JFrame frame = new JFrame(); + frame.add(panel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(700, 300); + frame.validate(); + + frameFixture.show(); + frameFixture.panel("AddPolygon").slider().slideToMinimum(); + frameFixture.panel("RemovePolygon").slider().slideToMinimum(); + frameFixture.panel("MovePolygon").slider().slideToMinimum(); + frameFixture.panel("Cross-over").slider().slideToMinimum(); + frameFixture.panel("AddVertex").slider().slideToMinimum(); + frameFixture.panel("RemoveVertex").slider().slideToMinimum(); + frameFixture.panel("MoveVertex").slider().slideToMinimum(); + frameFixture.panel("ChangeColour").slider().slideToMinimum(); + + Dimension canvasSize = new Dimension(100, 100); + PolygonImageFactory factory = new PolygonImageFactory(canvasSize); + EvolutionaryOperator<List<ColouredPolygon>> operator = panel.createEvolutionPipeline(factory, + canvasSize, + ExamplesTestUtils.getRNG()); + List<ColouredPolygon> candidate1 = Arrays.asList(new ColouredPolygon(Color.WHITE, + Arrays.asList(new Point(1, 1)))); + List<ColouredPolygon> candidate2 = Arrays.asList(new ColouredPolygon(Color.BLACK, + Arrays.asList(new Point(2, 2)))); + List<List<ColouredPolygon>> population = new ArrayList<List<ColouredPolygon>>(2); + population.add(candidate1); + population.add(candidate2); + + List<List<ColouredPolygon>> evolved = operator.apply(population, + ExamplesTestUtils.getRNG()); + // Candidate order may have changed, but individual candidates should remain unaltered. + assert (checkEquals(evolved.get(0), candidate1) && checkEquals(evolved.get(1), candidate2)) + || (checkEquals(evolved.get(0), candidate2) && checkEquals(evolved.get(1), candidate1)) + : "Candidates should be unaltered when all probabilities are zero."; + } + + + private boolean checkEquals(List<ColouredPolygon> candidate1, + List<ColouredPolygon> candidate2) + { + if (candidate1.size() != candidate2.size()) + { + return false; + } + for (int i = 0; i < candidate1.size(); i++) + { + ColouredPolygon polygon1 = candidate1.get(i); + ColouredPolygon polygon2 = candidate2.get(i); + if (!polygon1.getColour().equals(polygon2.getColour())) + { + return false; + } + List<Point> vertices1 = polygon1.getVertices(); + List<Point> vertices2 = polygon2.getVertices(); + if (vertices1.size() != vertices2.size()) + { + return false; + } + for (int j = 0; j < vertices1.size(); j++) + { + Point point1 = vertices1.get(j); + Point point2 = vertices2.get(j); + if (point1.x != point2.x || point1.y != point2.y) + { + return false; + } + } + } + return true; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutationTest.java new file mode 100644 index 0000000..bcd5f6c --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemovePolygonMutationTest.java @@ -0,0 +1,91 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link RemovePolygonMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class RemovePolygonMutationTest +{ + private final PolygonImageFactory factory = new PolygonImageFactory(new Dimension(200, 200)); + + @Test + public void testRemovePolygon() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new RemovePolygonMutation(Probability.ONE); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() - 1 : "Image should have 1 fewer polygon after mutation."; + } + + + @Test + public void testZeroProbability() + { + List<ColouredPolygon> image = Arrays.asList(factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG()), + factory.createRandomPolygon(ExamplesTestUtils.getRNG())); + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new RemovePolygonMutation(Probability.ZERO); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have same number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } + + + /** + * If the image already has the minimum permitted number of polygons, further polygons + * should not be removed by mutation. + */ + @Test + public void testRemoveMinPolygons() + { + List<ColouredPolygon> image = factory.generateRandomCandidate(ExamplesTestUtils.getRNG()); + assert image.size() == 2 : "Image should have 2 polygons"; + List<List<ColouredPolygon>> list = new ArrayList<List<ColouredPolygon>>(1); + list.add(image); + + EvolutionaryOperator<List<ColouredPolygon>> mutation = new RemovePolygonMutation(Probability.ONE); + + List<List<ColouredPolygon>> evolved = mutation.apply(list, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Population size should not be altered by mutation."; + assert evolved.get(0).size() == image.size() : "Image should have no fewer than the minimum number of polygons."; + assert evolved.get(0) == image : "Image should not have been changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutationTest.java new file mode 100644 index 0000000..e204d55 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/monalisa/RemoveVertexMutationTest.java @@ -0,0 +1,98 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.monalisa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for the {@link RemoveVertexMutation} evolutionary operator. + * @author Daniel Dyer + */ +public class RemoveVertexMutationTest +{ + private final Dimension canvasSize = new Dimension(200, 200); + + @Test + public void testRemoveVertex() + { + List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2), new Point(3, 3), new Point(4, 4)); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new RemoveVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + List<Point> vertices = evolved.get(0).getVertices(); + assert vertices.size() == polygon.getVertices().size() - 1 + : "Polygon should have 1 fewer point after mutation."; + } + + + @Test + public void testZeroProbability() + { + List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2), new Point(3, 3), new Point(4, 4)); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new RemoveVertexMutation(canvasSize, + Probability.ZERO); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no fewer points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } + + + /** + * If the image already has the minimum permitted number of points, further points + * should not be removed by mutation. + */ + @Test + public void testAddMaxPoints() + { + List<Point> points = Arrays.asList(new Point(1, 1), new Point(2, 2), new Point(3, 3)); + ColouredPolygon polygon = new ColouredPolygon(Color.RED, points); + List<ColouredPolygon> image = new ArrayList<ColouredPolygon>(1); + image.add(polygon); + + EvolutionaryOperator<ColouredPolygon> mutation = new RemoveVertexMutation(canvasSize, + Probability.ONE); + + List<ColouredPolygon> evolved = mutation.apply(image, ExamplesTestUtils.getRNG()); + assert evolved.size() == 1 : "Polygon count should not be altered by mutation."; + ColouredPolygon evolvedPolygon = evolved.get(0); + List<Point> vertices = evolvedPolygon.getVertices(); + assert vertices.size() == polygon.getVertices().size() : "Polygon should have no fewer points after mutation."; + assert evolvedPolygon == polygon : "Polygon should not have been changed at all."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringEvaluatorTest.java new file mode 100644 index 0000000..54a3dd4 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringEvaluatorTest.java @@ -0,0 +1,58 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Unit test for fitness function used by the strings example. + * @author Daniel Dyer + */ +public class StringEvaluatorTest +{ + @Test + public void testIdentical() + { + String target = "abcdefgh"; + String candidate = "abcdefgh"; + FitnessEvaluator<String> evaluator = new StringEvaluator(target); + int score = (int) evaluator.getFitness(candidate, null); + assert score == 0 : "Fitness should be zero for identical strings, is " + score; + } + + + @Test + public void testCompletelyDifferent() + { + String target = "abcdefgh"; + String candidate = "ijklmnop"; + FitnessEvaluator<String> evaluator = new StringEvaluator(target); + int score = (int) evaluator.getFitness(candidate, null); + assert score == target.length() : "Fitness should be " + target.length() + ", is " + score; + } + + + @Test + public void testPartialSolution() + { + String target = "abcdefgh"; + String candidate = "abcdxxxx"; + FitnessEvaluator<String> evaluator = new StringEvaluator(target); + int score = (int) evaluator.getFitness(candidate, null); + assert score == 4 : "Fitness score should be 4, is " + score; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringsExampleTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringsExampleTest.java new file mode 100644 index 0000000..ab54121 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/strings/StringsExampleTest.java @@ -0,0 +1,34 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.strings; + +import org.testng.annotations.Test; + +/** + * Simple unit test for the Strings example. Makes sure that the evolution engine + * eventually returns the target String. + * @author Daniel Dyer + */ +public class StringsExampleTest +{ + @Test + public void testEvolution() + { + String target = "WATCHMAKER"; + String result = StringsExample.evolveString(target); + assert result.equals(target) : "Evolution returned wrong result: " + result; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuCellRendererTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuCellRendererTest.java new file mode 100644 index 0000000..e1f3df3 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuCellRendererTest.java @@ -0,0 +1,112 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.Color; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.TableCellRenderer; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link SudokuCellRenderer} class. + * @author Daniel Dyer + */ +public class SudokuCellRendererTest +{ + private JTable table; + + + @Test + public void testRenderFixedCells() + { + SudokuTableModel model = new SudokuTableModel(); + // Set the 2 lefthand cells in the top row. + model.setValueAt('1', 0, 0); + model.setValueAt('2', 0, 1); + + JTable table = new JTable(model); + TableCellRenderer renderer = new SudokuCellRenderer(); + + JLabel cell1 = (JLabel) renderer.getTableCellRendererComponent(table, '1', false, false, 0, 0); + assert cell1.getText().equals("1") : "Wrong text at cell 1: " + cell1.getText(); + assert cell1.getFont().isBold() : "Fixed cells should be rendered in bold."; + JLabel cell2 = (JLabel) renderer.getTableCellRendererComponent(table, '2', false, false, 0, 1); + assert cell2.getText().equals("2") : "Wrong text at cell 2: " + cell2.getText(); + assert cell2.getFont().isBold() : "Fixed cells should be rendered in bold."; + // Check an empty cell, it should have no text. + JLabel cell3 = (JLabel) renderer.getTableCellRendererComponent(table, null, false, false, 0, 2); + assert cell3.getText().length() == 0 : "Wrong text at cell 3: " + cell3.getText(); + } + + + @BeforeClass + public void createTable() + { + SudokuTableModel model = new SudokuTableModel(); + model.setSudoku(SudokuTestUtils.createSudoku(new int[][]{{3, 2, 4, 8, 9, 1, 7, 5, 6}, + {6, 9, 7, 1, 5, 2, 8, 4, 3}, + {8, 1, 5, 7, 3, 6, 4, 2, 9}, + {5, 2, 6, 9, 7, 4, 3, 1, 8}, + {4, 9, 8, 1, 2, 5, 6, 7, 3}, + {8, 7, 1, 3, 4, 2, 9, 6, 5}, + {2, 6, 3, 4, 8, 7, 5, 9, 1}, + {1, 3, 5, 8, 4, 9, 2, 6, 7}, + {7, 4, 2, 1, 5, 3, 9, 8, 6}})); + table = new JTable(model); + } + + + @Test + public void testRenderCellWithNoConflicts() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 1, false, false, 2, 1); + assert cell.getText().equals("1") : "Wrong value at cell (2, 1): " + cell.getText(); + assert cell.getBackground() == Color.WHITE : "Cell without conflicts should be white."; + } + + + @Test + public void testRenderCellWithOneConflict() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 3, false, false, 7, 1); + assert cell.getText().equals("3") : "Wrong value at cell (7, 1): " + cell.getText(); + assert cell.getBackground() == Color.YELLOW : "Cell with one conflict should be yellow."; + } + + + @Test + public void testRenderCellWithTwoConflicts() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 6, false, false, 8, 8); + assert cell.getText().equals("6") : "Wrong value at cell (8, 8): " + cell.getText(); + assert cell.getBackground() == Color.ORANGE : "Cell with two conflicts should be orange."; + } + + + @Test + public void testRenderCellWithThreeConflicts() + { + TableCellRenderer renderer = new SudokuCellRenderer(); + JLabel cell = (JLabel) renderer.getTableCellRendererComponent(table, 1, false, false, 1, 3); + assert cell.getText().equals("1") : "Wrong value at cell (1, 3): " + cell.getText(); + assert cell.getBackground() == Color.RED : "Cell with two conflicts should be red."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluatorTest.java new file mode 100644 index 0000000..38864fd --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuEvaluatorTest.java @@ -0,0 +1,77 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.FitnessEvaluator; + +/** + * Unit test for sudoku fitness evaluator. + * @author Daniel Dyer + */ +public class SudokuEvaluatorTest +{ + /** + * Ensure that the evaluator returns zero for a correct solution. + */ + @Test + public void testCorrectSolution() + { + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][] + { + {1, 2, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 1, 2, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + }); + FitnessEvaluator<Sudoku> evaluator = new SudokuEvaluator(); + int fitness = (int) evaluator.getFitness(sudoku, null); + assert fitness == 0 : "Fitness should be zero for correct solution, is " + fitness; + } + + + /** + * Ensure that the evaluator returns zero for a correct solution. + */ + @Test + public void testDuplicates() + { + // This sudoku as 4 invalid columns (0, 1, 3 and 5) and 2 invalid + // sub-grids (bottom-left and bottom-center). + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][] + { + {2, 1, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 2, 1, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + }); + FitnessEvaluator<Sudoku> evaluator = new SudokuEvaluator(); + int fitness = (int) evaluator.getFitness(sudoku, null); + assert fitness == 6 : "Fitness should be 6, is " + fitness; + } + + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuFactoryTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuFactoryTest.java new file mode 100644 index 0000000..f81cfa6 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuFactoryTest.java @@ -0,0 +1,142 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.CandidateFactory; + +/** + * Unit test for sudoku candidate solution factory. + * @author Daniel Dyer + */ +public class SudokuFactoryTest +{ + /** + * Checks to make sure that the givens are correctly placed and that each row + * contains each value exactly once. + */ + @Test + public void testValidity() + { + CandidateFactory<Sudoku> factory = new SudokuFactory(".9.......", + ".........", + "........5", + "....2....", + ".........", + ".........", + ".........", + "...1.....", + "........9"); + + List<Sudoku> population = factory.generateInitialPopulation(20, ExamplesTestUtils.getRNG()); + for (Sudoku sudoku : population) + { + // Check givens are correctly placed. + assert sudoku.isFixed(2, 8) : "Cell (2, 8) should be fixed."; + assert sudoku.getValue(2, 8) == 5 : "Cell (2, 8) should contain 5."; + assert sudoku.isFixed(7, 3) : "Cell (7, 3) should be fixed."; + assert sudoku.getValue(7, 3) == 1 : "Cell (7, 3) should contain 1."; + assert sudoku.isFixed(3, 4) : "Cell (3, 4) should be fixed."; + assert sudoku.getValue(3, 4) == 2 : "Cell (3, 4) should contain 2."; + assert sudoku.isFixed(0, 1) : "Cell (0, 1) should be fixed."; + assert sudoku.getValue(0, 1) == 9 : "Cell (0, 1) should contain 9."; + assert sudoku.isFixed(8, 8) : "Cell (8, 8) should be fixed."; + assert sudoku.getValue(8, 8) == 9 : "Cell (8, 8) should contain 9."; + + // Check that each row has no duplicates. + Set<Integer> set = new HashSet<Integer>(); + for (int i = 0; i < 9; i++) + { + Sudoku.Cell[] row = sudoku.getRow(i); + for (Sudoku.Cell cell : row) + { + set.add(cell.getValue()); + } + if (set.size() < 9) + { + System.out.println(sudoku); + assert false : "Row " + i + " contains duplicates."; + } + } + } + } + + + /** + * If the pattern used to create a Sudoku factory contains any characters + * other than the values 1 - 9 or dots (which represent empty cells), then + * an appropriate exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidPatternChars() + { + // This pattern is the right size but contains an invalid character. + String[] pattern = {"....9....", + "2..3.....", + "........1", + "....a....", // Invalid character on this line. + "....4....", + ".........", + ".........", + ".........", + "........."}; + new SudokuFactory(pattern); + } + + + /** + * If the pattern used to create a Sudoku factory contains the wrong number + * of rows then an appropriate exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfRows() + { + // This pattern contains only valid characters and has the right number + // of columns but doesn't have enough rows. + String[] pattern = {"....9....", + "2..3.....", + "........1", + ".........", + "........."}; + new SudokuFactory(pattern); + } + + + /** + * If the pattern used to create a Sudoku factory contains the wrong number + * of columns then an appropriate exception should be thrown. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfColumns() + { + // This pattern contains only valid characters and has the right number + // of rows but has too many columns in some + String[] pattern = {"....9....", + "2..3.....", + "........1", + ".........", + ".........7", + ".........", + ".4.......6", + "..1.3....", + "........8"}; + new SudokuFactory(pattern); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutationTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutationTest.java new file mode 100644 index 0000000..9386e83 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuRowMutationTest.java @@ -0,0 +1,127 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for Sudoku mutation operator. + * @author Daniel Dyer + */ +public class SudokuRowMutationTest +{ + /** + * Tests to ensure that rows are still valid after mutation. Each row + * should contain each value 1-9 exactly once. + */ + @Test + public void testValidity() + { + EvolutionaryOperator<Sudoku> mutation = new SudokuRowMutation(8, 1); + List<Sudoku> population = Arrays.asList(SudokuTestUtils.createSudoku(new int[][] + { + {1, 2, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 1, 2, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + })); + final Set<Integer> counts = new HashSet<Integer>(Sudoku.SIZE); + for (int i = 0; i < 20; i++) + { + population = mutation.apply(population, ExamplesTestUtils.getRNG()); + assert population.size() == 1 : "Population size changed after mutation(s)."; + Sudoku mutatedSudoku = population.get(0); + for (int j = 0; j < Sudoku.SIZE; j++) + { + Sudoku.Cell[] row = mutatedSudoku.getRow(j); + assert row.length == Sudoku.SIZE : "Row length is invalid: " + row.length; + for (Sudoku.Cell cell : row) + { + int value = cell.getValue(); + assert value > 0 && value <= Sudoku.SIZE : "Cell value out of range: " + value; + counts.add(value); + } + assert counts.size() == Sudoku.SIZE : "Row contains duplicates."; + counts.clear(); + } + } + } + + + /** + * Check that the mutation never modifies the value of fixed cells. + */ + @Test(dependsOnMethods = "testValidity") + public void testFixedConstraints() + { + EvolutionaryOperator<Sudoku> mutation = new SudokuRowMutation(8, 1); + Sudoku.Cell[][] cells = new Sudoku.Cell[Sudoku.SIZE][Sudoku.SIZE]; + // One cell in each row is fixed (cell 1 in row 1, cell 2 in row 2, etc.) + for (int row = 0; row < Sudoku.SIZE; row++) + { + for (int column = 0; column < Sudoku.SIZE; column++) + { + cells[row][column] = new Sudoku.Cell(column + 1, column == row); + } + } + List<Sudoku> population = Arrays.asList(new Sudoku(cells)); + for (int i = 0; i < 100; i++) // 100 generations of mutation. + { + population = mutation.apply(population, ExamplesTestUtils.getRNG()); + Sudoku sudoku = population.get(0); + for (int row = 0; row < Sudoku.SIZE; row++) + { + for (int column = 0; column < Sudoku.SIZE; column++) + { + if (row == column) + { + assert sudoku.isFixed(row, column) : "Fixed cell has become unfixed."; + assert sudoku.getValue(row, column) == (row + 1) : "Fixed cell has changed value."; + } + else + { + assert !sudoku.isFixed(row, column) : "Unfixed cell has become fixed."; + } + } + } + } + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMutationCount() + { + new SudokuRowMutation(0, 1); // Should throw an IllegalArgumentException. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidMutationAmount() + { + new SudokuRowMutation(1, 0); // Should throw an IllegalArgumentException. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTableModelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTableModelTest.java new file mode 100644 index 0000000..30bbf18 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTableModelTest.java @@ -0,0 +1,113 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import org.testng.annotations.Test; + +/** + * Unit test for the table model used by the Sudoku example application. + * @author Daniel Dyer + */ +public class SudokuTableModelTest +{ + private static final String[] TEST_PUZZLE = {"4.5...9.7", + ".2..9..6.", + "39.6.7.28", + "9..3.2..6", + "7..9.6..3", + "5..4.8..1", + "28.1.5.49", + ".7..3..8.", + "6.4...3.2"}; + + /** + * Makes sure that the table model class correctly converts to and from the + * String patterns used by {@link SudokuFactory}. + */ + @Test + public void testPatternConversions() + { + SudokuTableModel model = new SudokuTableModel(); + model.setPattern(TEST_PUZZLE); + // Change a cell, to ensure we don't get exactly the same pattern back. + model.setValueAt('1', 8, 7); + String[] newPattern = model.getPattern(); + // Make sure the pattern is correct. + for (int i = 0; i < 8; i++) // Check the first 8 rows, which haven't changed. + { + assert newPattern[i].equals(TEST_PUZZLE[i]) : "Row " + i + " incorrect: " + newPattern[i]; + } + // Check modified row. + assert newPattern[8].equals("6.4...312") : "Row 8 incorrect: " + newPattern[8]; + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManyRowsInPattern() + { + SudokuTableModel model = new SudokuTableModel(); + model.setPattern(new String[]{".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + ".........", + "........."}); // 10 rows should trigger an IllegalArgumentException. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testTooManyColumnsInPattern() + { + SudokuTableModel model = new SudokuTableModel(); + model.setPattern(new String[]{"..........", + "..........", + "..........", + "..........", + "..........", + "..........", + "..........", + "..........", + ".........."}); // 10 columns in each row should trigger an IllegalArgumentException. + } + + + @Test + public void testGetValueAt() + { + SudokuTableModel model = new SudokuTableModel(); + model.setSudoku(SudokuTestUtils.createSudoku(new int[][]{{1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}})); + for (int row = 0; row < model.getRowCount(); row++) + { + for (int column = 0; column < model.getColumnCount(); column++) + { + int actualCell = (Integer) model.getValueAt(row, column); + assert actualCell == column + 1: "Wrong value at " + row + ", " + column + ": " + actualCell; + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTest.java new file mode 100644 index 0000000..798b0fa --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTest.java @@ -0,0 +1,97 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import org.testng.annotations.Test; + +/** + * Unit test for the {@link Sudoku} data structure. + * @author Daniel Dyer + */ +public class SudokuTest +{ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfRows() + { + // This grid has one row and nine columns. Constructing it should throw + // an IllegalArgumentException. + new Sudoku(new Sudoku.Cell[][]{{new Sudoku.Cell(1, false), + new Sudoku.Cell(2, false), + new Sudoku.Cell(3, false), + new Sudoku.Cell(4, false), + new Sudoku.Cell(5, false), + new Sudoku.Cell(6, false), + new Sudoku.Cell(7, false), + new Sudoku.Cell(8, false), + new Sudoku.Cell(9, false)}}); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testWrongNumberOfColumns() + { + // This grid has nine rows and one column. Constructing it should throw + // an IllegalArgumentException. + new Sudoku(new Sudoku.Cell[][]{{new Sudoku.Cell(1, false)}, + {new Sudoku.Cell(2, false)}, + {new Sudoku.Cell(3, false)}, + {new Sudoku.Cell(4, false)}, + {new Sudoku.Cell(5, false)}, + {new Sudoku.Cell(6, false)}, + {new Sudoku.Cell(7, false)}, + {new Sudoku.Cell(8, false)}, + {new Sudoku.Cell(9, false)}}); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testCellValueTooLow() + { + new Sudoku.Cell(0, false); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testCellValueTooHigh() + { + new Sudoku.Cell(10, false); + } + + + @Test + public void testToString() + { + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][]{{1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 5, 6, 7, 8, 9}}); + String text = sudoku.toString(); + assert text.equals(" 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n" + + " 1 2 3 4 5 6 7 8 9\n") : "Wrong string representation:\n" + text; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTestUtils.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTestUtils.java new file mode 100644 index 0000000..58dc4ea --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuTestUtils.java @@ -0,0 +1,48 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +/** + * Utility methods that help in unit tests. + * @author Daniel Dyer + */ +class SudokuTestUtils +{ + private SudokuTestUtils() + { + // Prevents instantiation. + } + + + /** + * Convenience method for creating a Sudoku object from a 2D array of ints. + * Ignores which cells are fixed and which are not. + * @param values A 9x9 2-dimensional array that contains values (1-9) for + * each of the cells in a Sudoku grid. + */ + public static Sudoku createSudoku(int[][] values) + { + Sudoku.Cell[][] cells = new Sudoku.Cell[Sudoku.SIZE][Sudoku.SIZE]; + for (int i = 0; i < Sudoku.SIZE; i++) + { + for (int j = 0; j < Sudoku.SIZE; j++) + { + cells[i][j] = new Sudoku.Cell(values[i][j], false); + } + } + return new Sudoku(cells); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossoverTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossoverTest.java new file mode 100644 index 0000000..4f430be --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuVerticalCrossoverTest.java @@ -0,0 +1,64 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.examples.ExamplesTestUtils; +import org.uncommons.watchmaker.framework.EvolutionaryOperator; + +/** + * Unit test for Sudoku cross-over operator. + * @author Daniel Dyer + */ +public class SudokuVerticalCrossoverTest +{ + /** + * Ensures that the simplest configuration (a single cross-over point) + * works as expected. + */ + @Test + public void testSinglePointCrossover() + { + EvolutionaryOperator<Sudoku> crossover = new SudokuVerticalCrossover(); + int[] forwards = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + int[] backwards = {9, 8, 7, 6, 5, 4, 3, 2, 1}; + int[][] parent1 = new int[Sudoku.SIZE][]; + int[][] parent2 = new int[Sudoku.SIZE][]; + for (int i = 0; i < Sudoku.SIZE; i++) + { + parent1[i] = forwards; + parent2[i] = backwards; + } + List<Sudoku> population = Arrays.asList(SudokuTestUtils.createSudoku(parent1), + SudokuTestUtils.createSudoku(parent2)); + population = crossover.apply(population, ExamplesTestUtils.getRNG()); + assert population.size() == 2 : "Population size changed after cross-over."; + Sudoku offspring1 = population.get(0); + Sudoku offspring2 = population.get(1); + // Two groups of rows should be different from each other (they come from different parents). + assert !Arrays.equals(offspring1.getRow(0), offspring1.getRow(8)) + : "Row 0 should not match row 8."; + assert !Arrays.equals(offspring2.getRow(0), offspring2.getRow(8)) + : "Row 0 should not match row 8."; + // The two offspring should have inherited different rows. + assert !Arrays.equals(offspring1.getRow(0), offspring2.getRow(0)) + : "Offspring 1 row 0 should not match offspring 2 row 0."; + assert !Arrays.equals(offspring1.getRow(8), offspring2.getRow(8)) + : "Offspring 1 row 8 should not match offspring 2 row 8."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuViewTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuViewTest.java new file mode 100644 index 0000000..999a92a --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/sudoku/SudokuViewTest.java @@ -0,0 +1,117 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.sudoku; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.data.TableCell; +import org.fest.swing.fixture.FrameFixture; +import org.fest.swing.fixture.JTableCellFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link SudokuView} class. + * @author Daniel Dyer + */ +public class SudokuViewTest +{ + private static final String[] TEST_PUZZLE = {"4.5...9.7", + ".2..9..6.", + "39.6.7.28", + "9..3.2..6", + "7..9.6..3", + "5..4.8..1", + "28.1.5.49", + ".7..3..8.", + "6.4...3.2"}; + + private Robot robot; + + @BeforeMethod(groups = "display-required") + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod(groups = "display-required") + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") + public void testDisplayPuzzle() + { + SudokuView view = new SudokuView(); + JFrame frame = new JFrame(); + frame.add(view, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 400); + frame.validate(); + + frameFixture.show(); + view.setPuzzle(TEST_PUZZLE); + + // Check a non-empty cell. + JTableCellFixture cell1 = frameFixture.table().cell(TableCell.row(0).column(0)); + cell1.requireEditable(); + cell1.requireValue("4"); + + // And an empty cell. + JTableCellFixture cell2 = frameFixture.table().cell(TableCell.row(0).column(1)); + cell2.requireEditable(); + cell2.requireValue(""); + } + + + @Test(groups = "display-required") + public void testDisplaySolution() + { + SudokuView view = new SudokuView(); + JFrame frame = new JFrame(); + frame.add(view, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 400); + frame.validate(); + + frameFixture.show(); + Sudoku sudoku = SudokuTestUtils.createSudoku(new int[][] + { + {1, 2, 8, 5, 4, 3, 9, 6, 7}, + {7, 6, 4, 9, 2, 8, 5, 1, 3}, + {3, 9, 5, 7, 6, 1, 2, 4, 8}, + {6, 1, 9, 4, 8, 5, 7, 3, 2}, + {5, 8, 3, 6, 7, 2, 1, 9, 4}, + {4, 7, 2, 3, 1, 9, 8, 5, 6}, + {8, 5, 1, 2, 3, 6, 4, 7, 9}, + {9, 4, 6, 8, 5, 7, 3, 2, 1}, + {2, 3, 7, 1, 9, 4, 6, 8, 5} + }); + view.setSolution(sudoku); + + JTableCellFixture cell1 = frameFixture.table().cell(TableCell.row(0).column(0)); + cell1.requireNotEditable(); + cell1.requireValue("1"); + } + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesmanTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesmanTest.java new file mode 100644 index 0000000..ee7a144 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/BruteForceTravellingSalesmanTest.java @@ -0,0 +1,51 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; + +/** + * Test for the brute force solution to the travelling salesman + * problem. + * @author Daniel Dyer + */ +public class BruteForceTravellingSalesmanTest +{ + /** + * Make sure that the brute force implementation returns the correct result. + */ + @Test + public void testEvaluation() + { + DistanceLookup data = new TestDistances(); + TravellingSalesmanStrategy strategy = new BruteForceTravellingSalesman(data); + List<String> cities = Arrays.asList("City1", "City2", "City3", "City4"); + List<String> route = strategy.calculateShortestRoute(cities, null); + assert route.size() == 4 : "Route is wrong length: " + route.size(); + assert route.contains("City1") : "Route does not contain City1."; + assert route.contains("City2") : "Route does not contain City2."; + assert route.contains("City3") : "Route does not contain City3."; + assert route.contains("City4") : "Route does not contain City4."; + double distance = 0; + distance += data.getDistance(route.get(0), route.get(1)); + distance += data.getDistance(route.get(1), route.get(2)); + distance += data.getDistance(route.get(2), route.get(3)); + distance += data.getDistance(route.get(3), route.get(0)); + assert (long) distance == 47 : "Incorrect shortest route: " + distance; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookupTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookupTest.java new file mode 100644 index 0000000..debdc96 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EuropeanDistanceLookupTest.java @@ -0,0 +1,65 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import org.testng.annotations.Test; + +/** + * Unit test for the distance look-ups for European cities that is used by + * the Travelling Salesman applet. + * @author Daniel Dyer + */ +public class EuropeanDistanceLookupTest +{ + /** + * Check to make sure that each city's distance to itself is zero. + */ + @Test + public void testSelfDistances() + { + DistanceLookup europe = new EuropeanDistanceLookup(); + List<String> cities = europe.getKnownCities(); + for (String city : cities) + { + assert europe.getDistance(city, city) == 0 : city + " distance to self is non-zero."; + } + } + + + /** + * Check to make sure that for every pair of cities, the distance in one + * direction is equal to the distance in the other direction. + */ + @Test + public void testReturnDistances() + { + DistanceLookup europe = new EuropeanDistanceLookup(); + Queue<String> cities = new LinkedList<String>(europe.getKnownCities()); + while (!cities.isEmpty()) + { + String startCity = cities.remove(); + for(String city : cities) + { + int outDistance = europe.getDistance(startCity, city); + int returnDistance = europe.getDistance(city, startCity); + assert outDistance == returnDistance : "Return distance mismatch for " + startCity + " and " + city; + } + } + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesmanTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesmanTest.java new file mode 100644 index 0000000..c2ece30 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/EvolutionaryTravellingSalesmanTest.java @@ -0,0 +1,95 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.selection.TruncationSelection; + +/** + * Unit test for the evolutionary approach to the travelling salesman problem. + * This test does not check that the correct result is returned (we would need + * to run the full evolutionary algorithm for that, and even then optimality + * is not guaranteed). This just ensures that nothing invalid happens and + * provides coverage for the classes involved. + * @author Daniel Dyer + */ +public class EvolutionaryTravellingSalesmanTest +{ + private final DistanceLookup data = new TestDistances(); + + /** + * Ensure that the algorithm behaves when configured to use mutation. + */ + @Test + public void testWithMutation() + { + TravellingSalesmanStrategy strategy = new EvolutionaryTravellingSalesman(data, + new TruncationSelection(0.5), + 10, // Small population. + 0, // No elitism. + 3, // Only a few generations. + false, // Cross-over. + true); // Mutation. + List<String> cities = Arrays.asList("City1", "City2", "City3", "City4"); + List<String> route = strategy.calculateShortestRoute(cities, null); + assert route.size() == 4 : "Route is wrong length: " + route.size(); + assert route.contains("City1") : "Route does not contain City1."; + assert route.contains("City2") : "Route does not contain City2."; + assert route.contains("City3") : "Route does not contain City3."; + assert route.contains("City4") : "Route does not contain City4."; + } + + + /** + * Ensure that the algorithm behaves when configured to use cross-over. + */ + @Test + public void testWithCrossover() + { + TravellingSalesmanStrategy strategy = new EvolutionaryTravellingSalesman(data, + new TruncationSelection(0.5), + 10, // Small population. + 0, // No elitism. + 3, // Only a few generations. + true, // Cross-over + false); // Mutation. + List<String> cities = Arrays.asList("City1", "City2", "City3", "City4"); + List<String> route = strategy.calculateShortestRoute(cities, null); + assert route.size() == 4 : "Route is wrong length: " + route.size(); + assert route.contains("City1") : "Route does not contain City1."; + assert route.contains("City2") : "Route does not contain City2."; + assert route.contains("City3") : "Route does not contain City3."; + assert route.contains("City4") : "Route does not contain City4."; + } + + + /** + * The strategy must ensure that at least one of cross-over or mutation is chosen. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testNoEvolution() + { + new EvolutionaryTravellingSalesman(data, + new TruncationSelection(0.5), + 10, + 0, + 3, + false, + false); // Should throw an IllegalArgumentException as no operators are enabled. + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanelTest.java new file mode 100644 index 0000000..9f2244d --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/ItineraryPanelTest.java @@ -0,0 +1,91 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import java.util.Collection; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link ItineraryPanel} class. Makes sure that the + * buttons operate as expected so that the right cities are returned. + * @author Daniel Dyer + */ +public class ItineraryPanelTest +{ + private static final TestDistances CITIES = new TestDistances(); + + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testSelectAll() + { + ItineraryPanel itineraryPanel = new ItineraryPanel(CITIES.getKnownCities()); + JFrame frame = new JFrame(); + frame.add(itineraryPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(100, 300); + frame.validate(); + frame.setVisible(true); + assert itineraryPanel.getSelectedCities().isEmpty() : "Should be no cities selected initially."; + frameFixture.button("All").click(); + Collection<String> selectedCities = itineraryPanel.getSelectedCities(); + assert selectedCities.size() == CITIES.getKnownCities().size() + : "All cities should be selected after button click."; + } + + + @Test(groups = "display-required", // Will fail if run in a headless environment. + dependsOnMethods = "testSelectAll") + public void testSelectNone() + { + ItineraryPanel itineraryPanel = new ItineraryPanel(CITIES.getKnownCities()); + JFrame frame = new JFrame(); + frame.add(itineraryPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(100, 300); + frame.validate(); + frame.setVisible(true); + frameFixture.button("All").click(); + Collection<String> selectedCities = itineraryPanel.getSelectedCities(); + assert selectedCities.size() == CITIES.getKnownCities().size() + : "All cities should be selected after all button click."; + frameFixture.button("None").click(); + assert itineraryPanel.getSelectedCities().isEmpty() + : "No cities should be selected after clear button is clicked."; + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluatorTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluatorTest.java new file mode 100644 index 0000000..b6ca0ee --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/RouteEvaluatorTest.java @@ -0,0 +1,70 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.Arrays; +import java.util.List; +import org.testng.annotations.Test; + +/** + * Unit test for the route evaluator used by both Travelling Salesman + * implementations. Checks to make sure that route distances are + * calculated correctly. + * @author Daniel Dyer + */ +public class RouteEvaluatorTest +{ + /** + * Test different routes to make sure the distances are calculated correctly. + */ + @Test + public void testDistanceCalculations() + { + RouteEvaluator evaluator = new RouteEvaluator(new TestDistances()); + + List<String> route1 = Arrays.asList("City4", "City1", "City3", "City2"); + // Expected distance is sum of distances between adjacent cities on route + // including returning to the start city. + int expectedDistance1 = 8 + 5 + 13 + 21; + int actualDistance1 = (int) evaluator.getFitness(route1, null); + assert actualDistance1 == expectedDistance1 : "Distance should be " + expectedDistance1 + ", was " + actualDistance1; + + List<String> route2 = Arrays.asList("City3", "City4", "City2", "City1"); + // Expected distance is sum of distances between adjacent cities on route + // including returning to the start city. + int expectedDistance2 = 34 + 21 + 3 + 5; + int actualDistance2 = (int) evaluator.getFitness(route2, null); + assert actualDistance2 == expectedDistance2 : "Distance should be " + expectedDistance2 + ", was " + actualDistance2; + } + + + /** + * Make sure the route evaluator works for end cases. + */ + @Test + public void testSingleCityRoute() + { + RouteEvaluator evaluator = new RouteEvaluator(new TestDistances()); + + List<String> route = Arrays.asList("City1"); + // Expected distance is sum of distances between adjacent cities on route + // including returning to the start city. + int distance = (int) evaluator.getFitness(route, null); + assert distance == 0 : "Distance should be 0, was " + distance; + } + + +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanelTest.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanelTest.java new file mode 100644 index 0000000..00519d9 --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/StrategyPanelTest.java @@ -0,0 +1,119 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link StrategyPanel} used by the Travelling + * Salesman example applet. Makes sure that it returns the correct type + * of solver strategy depending on the radio button settings. + * @author Daniel Dyer + */ +public class StrategyPanelTest +{ + private static final TestDistances CITIES = new TestDistances(); + + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") + public void testBruteForceOption() + { + StrategyPanel strategyPanel = new StrategyPanel(CITIES); + JFrame frame = new JFrame(); + frame.add(strategyPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(500, 300); + frame.validate(); + frame.setVisible(true); + robot.waitForIdle(); + + // Evolution controls should be enabled by default. + frameFixture.panel("EvolutionPanel").requireEnabled(); + + frameFixture.radioButton("BruteForceOption").click(); + + // Evolution controls should be disabled when brute force option is selected. + frameFixture.panel("EvolutionPanel").requireDisabled(); + + TravellingSalesmanStrategy strategy = strategyPanel.getStrategy(); + assert strategy instanceof BruteForceTravellingSalesman : "Wrong strategy class: " + strategy.getClass(); + } + + + @Test(groups = "display-required") + public void testEvolutionOption() + { + StrategyPanel strategyPanel = new StrategyPanel(CITIES); + JFrame frame = new JFrame(); + frame.add(strategyPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(500, 300); + frame.validate(); + frame.setVisible(true); + robot.waitForIdle(); + + frameFixture.radioButton("EvolutionOption").click(); + TravellingSalesmanStrategy strategy = strategyPanel.getStrategy(); + assert strategy instanceof EvolutionaryTravellingSalesman : "Wrong strategy class: " + strategy.getClass(); + } + + + @Test(groups = "display-required") + public void testDisablePanel() + { + StrategyPanel strategyPanel = new StrategyPanel(CITIES); + JFrame frame = new JFrame(); + frame.add(strategyPanel, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(500, 300); + frame.validate(); + frame.setVisible(true); + robot.waitForIdle(); + + // Components should be enabled initially. + frameFixture.radioButton("EvolutionOption").requireEnabled(); + frameFixture.panel("EvolutionPanel").requireEnabled(); + frameFixture.radioButton("BruteForceOption").requireEnabled(); + + strategyPanel.setEnabled(false); + frameFixture.radioButton("EvolutionOption").requireDisabled(); + frameFixture.panel("EvolutionPanel").requireDisabled(); + frameFixture.radioButton("BruteForceOption").requireDisabled(); + } +} diff --git a/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/TestDistances.java b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/TestDistances.java new file mode 100644 index 0000000..cc7da3a --- /dev/null +++ b/watchmaker/examples/src/java/test/org/uncommons/watchmaker/examples/travellingsalesman/TestDistances.java @@ -0,0 +1,70 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.examples.travellingsalesman; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Test data for travelling salesman unit tests. The shortest route between each + * of the four cities in this data set is 47km. + */ +final class TestDistances implements DistanceLookup +{ + private static final Map<String, Map<String, Integer>> distances = new TreeMap<String, Map<String, Integer>>(); + static + { + Map<String, Integer> city1 = new TreeMap<String, Integer>(); + city1.put("City1", 0); + city1.put("City2", 3); + city1.put("City3", 5); + city1.put("City4", 8); + distances.put("City1", city1); + + Map<String, Integer> city2 = new TreeMap<String, Integer>(); + city2.put("City1", 3); + city2.put("City2", 0); + city2.put("City3", 13); + city2.put("City4", 21); + distances.put("City2", city2); + + Map<String, Integer> city3 = new TreeMap<String, Integer>(); + city3.put("City1", 5); + city3.put("City2", 13); + city3.put("City3", 0); + city3.put("City4", 34); + distances.put("City3", city3); + + Map<String, Integer> city4 = new TreeMap<String, Integer>(); + city4.put("City1", 8); + city4.put("City2", 21); + city4.put("City3", 34); + city4.put("City4", 0); + distances.put("City4", city4); + } + + public List<String> getKnownCities() + { + return new ArrayList<String>(distances.keySet()); + } + + public int getDistance(String startingCity, String destinationCity) + { + return distances.get(startingCity).get(destinationCity); + } +} 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."; + } +} diff --git a/watchmaker/nb-configuration.xml b/watchmaker/nb-configuration.xml new file mode 100644 index 0000000..e958b93 --- /dev/null +++ b/watchmaker/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.placeElseOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine>
+ <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.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.placeCatchOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine>
+ <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.classDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>
+ <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.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.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.placeWhileOnNewLine>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeWhileOnNewLine>
+ <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.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.otherBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>
+ <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.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/pom.xml b/watchmaker/pom.xml new file mode 100644 index 0000000..6698f38 --- /dev/null +++ b/watchmaker/pom.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<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/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.sonatype.oss</groupId> + <artifactId>oss-parent</artifactId> + <version>7</version> + </parent> + <groupId>org.uncommons.watchmaker</groupId> + <artifactId>watchmaker</artifactId> + <version>0.7.2</version> + <packaging>pom</packaging> + <name>watchmaker</name> + <description>Watchmaker Framework - Root Project</description> + <url>http://watchmaker.uncommons.org</url> + + <licenses> + <license> + <name>Apache License, Version 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.html</url> + <distribution>repo</distribution> + </license> + </licenses> + + <scm> + <url>https://github.com/dwdyer/watchmaker</url> + <connection>scm:git://github.com/dwdyer/watchmaker.git</connection> + <developerConnection>scm:git:ssh://git@github.com:dwdyer/watchmaker.git</developerConnection> + </scm> + + <developers> + <developer> + <id>dwdyer</id> + <name>Dan Dyer</name> + <roles> + <role>committer</role> + </roles> + </developer> + <developer> + <id>cowwoc</id> + <name>Gili Tzabari</name> + <roles> + <role>contributor</role> + </roles> + </developer> + </developers> + + <modules> + <module>framework</module> + <module>swing</module> + <module>examples</module> + </modules> + + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.3.2</version> + <configuration> + <source>1.6</source> + <target>1.6</target> + <encoding>${project.build.sourceEncoding}</encoding> + </configuration> + </plugin> + <plugin> + <artifactId>maven-resources-plugin</artifactId> + <version>2.4.3</version> + <configuration> + <encoding>${project.build.sourceEncoding}</encoding> + </configuration> + </plugin> + </plugins> + </build> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> +</project>
\ No newline at end of file diff --git a/watchmaker/swing/lib/runtime/jfreechart-LICENCE.txt b/watchmaker/swing/lib/runtime/jfreechart-LICENCE.txt new file mode 100644 index 0000000..b1e3f5a --- /dev/null +++ b/watchmaker/swing/lib/runtime/jfreechart-LICENCE.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/watchmaker/swing/nb-configuration.xml b/watchmaker/swing/nb-configuration.xml new file mode 100644 index 0000000..ae35717 --- /dev/null +++ b/watchmaker/swing/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/swing/pom.xml b/watchmaker/swing/pom.xml new file mode 100644 index 0000000..690f0db --- /dev/null +++ b/watchmaker/swing/pom.xml @@ -0,0 +1,63 @@ +<?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-swing</artifactId> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>watchmaker-framework</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.uncommons.maths</groupId> + <artifactId>uncommons-maths</artifactId> + <version>1.2.2</version> + </dependency> + <dependency> + <groupId>jfree</groupId> + <artifactId>jfreechart</artifactId> + <version>1.0.13</version> + </dependency> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <version>6.2.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.easytesting</groupId> + <artifactId>fest-swing</artifactId> + <version>1.2.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <sourceDirectory>src/java/main</sourceDirectory> + <testSourceDirectory>src/java/test</testSourceDirectory> + </build> +</project>
\ No newline at end of file diff --git a/watchmaker/swing/src/java/main/org/uncommons/swing/ConfigurableLineBorder.java b/watchmaker/swing/src/java/main/org/uncommons/swing/ConfigurableLineBorder.java new file mode 100644 index 0000000..79bbdf2 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/swing/ConfigurableLineBorder.java @@ -0,0 +1,116 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Insets; +import javax.swing.border.Border; + +/** + * Border class that allows each of the four sides of the border to be enabled + * or disabled independently. + * @author Daniel Dyer + */ +public class ConfigurableLineBorder implements Border +{ + private final boolean top; + private final boolean left; + private final boolean bottom; + private final boolean right; + private final int thickness; + private final Insets insets; + + + /** + * @param top Whether or not to draw the border on the top edge. + * @param left Whether or not to draw the border on the left edge. + * @param bottom Whether or not to draw the border on the bottom edge. + * @param right Whether or not to draw the border on the right edge. + * @param thickness The width (in pixels) of each side of the border. + */ + public ConfigurableLineBorder(boolean top, + boolean left, + boolean bottom, + boolean right, + int thickness) + { + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + this.thickness = thickness; + this.insets = new Insets(top ? thickness : 0, + left ? thickness : 0, + bottom ? thickness : 0, + right ? thickness : 0); + } + + + /** + * Renders borders for the specified component based on the configuration + * of this border object. + * @param component The component for which the border is painted. + * @param graphics A {@link Graphics} object to use for painting. + * @param x The X-coordinate of the top left point of the border rectangle. + * @param y The Y-coordinate of the top left point of the border rectangle. + * @param width The width of the border rectangle. + * @param height The height of the border rectangle. + */ + public void paintBorder(Component component, + Graphics graphics, + int x, + int y, + int width, + int height) + { + if (top) + { + graphics.fillRect(x, y, width, thickness); + } + if (bottom) + { + graphics.fillRect(x, y + height - thickness, width, thickness); + } + if (left) + { + graphics.fillRect(x, y, thickness, height); + } + if (right) + { + graphics.fillRect(x + width - thickness, y, thickness, height); + } + } + + + /** + * @param component The component for which the border is painted. + * @return Insets for the current border configuration. + */ + public Insets getBorderInsets(Component component) + { + return insets; + } + + + /** + * @return false + */ + public boolean isBorderOpaque() + { + return false; + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/swing/SpringUtilities.java b/watchmaker/swing/src/java/main/org/uncommons/swing/SpringUtilities.java new file mode 100644 index 0000000..43add37 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/swing/SpringUtilities.java @@ -0,0 +1,121 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import java.awt.Component; +import java.awt.Container; +import javax.swing.Spring; +import javax.swing.SpringLayout; + +/** + * Utility methods for creating form-style or grid-style layouts with SpringLayout. + * Modified version of the class presented in the Sun Swing tutorial + * (http://java.sun.com/docs/books/tutorial/uiswing/layout/examples/SpringUtilities.java). + */ +public final class SpringUtilities +{ + private SpringUtilities() + { + // Private constructor prevents instantiation of utility class. + } + + + /** + * Aligns the first {@code rows} * {@code cols} components of {@code parent} + * in a grid. Each component in a column is as wide as the maximum preferred + * width of the components in that column; height is similarly determined for + * each row. The parent is made just big enough to fit them all. + * @param parent The container to layout. + * @param rows number of rows + * @param columns number of columns + * @param initialX x location to start the grid at + * @param initialY y location to start the grid at + * @param xPad x padding between cells + * @param yPad y padding between cells + */ + public static void makeCompactGrid(Container parent, + int rows, + int columns, + int initialX, + int initialY, + int xPad, + int yPad) + { + if (!(parent.getLayout() instanceof SpringLayout)) + { + throw new IllegalArgumentException("The first argument to makeCompactGrid must use SpringLayout."); + } + SpringLayout layout = (SpringLayout) parent.getLayout(); + + // Align all cells in each column and make them the same width. + Spring x = Spring.constant(initialX); + for (int c = 0; c < columns; c++) + { + Spring width = Spring.constant(0); + for (int r = 0; r < rows; r++) + { + width = Spring.max(width, + getConstraintsForCell(r, c, parent, columns).getWidth()); + } + for (int r = 0; r < rows; r++) + { + SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, columns); + constraints.setX(x); + constraints.setWidth(width); + } + x = Spring.sum(x, Spring.sum(width, Spring.constant(xPad))); + } + + // Align all cells in each row and make them the same height. + Spring y = Spring.constant(initialY); + for (int r = 0; r < rows; r++) + { + Spring height = Spring.constant(0); + for (int c = 0; c < columns; c++) + { + height = Spring.max(height, + getConstraintsForCell(r, c, parent, columns).getHeight()); + } + for (int c = 0; c < columns; c++) + { + SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, columns); + constraints.setY(y); + constraints.setHeight(height); + } + y = Spring.sum(y, Spring.sum(height, Spring.constant(yPad))); + } + + // Set the parent's size. + SpringLayout.Constraints parentConstraints = layout.getConstraints(parent); + parentConstraints.setConstraint(SpringLayout.SOUTH, y); + parentConstraints.setConstraint(SpringLayout.EAST, x); + } + + + /** + * Helper method for {@link #makeCompactGrid(Container, int, int, int, int, int, int)}. + * @return The constraints for the specified cell. + */ + private static SpringLayout.Constraints getConstraintsForCell(int row, + int col, + Container parent, + int cols) + { + SpringLayout layout = (SpringLayout) parent.getLayout(); + Component c = parent.getComponent(row * cols + col); + return layout.getConstraints(c); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/swing/SwingBackgroundTask.java b/watchmaker/swing/src/java/main/org/uncommons/swing/SwingBackgroundTask.java new file mode 100644 index 0000000..e8fc73c --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/swing/SwingBackgroundTask.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.swing; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +/** + * A task that is executed on a background thread and then updates + * a Swing GUI. A task may only be executed once. + * @author Daniel Dyer + * @param <V> Type of result generated by the task. + */ +public abstract class SwingBackgroundTask<V> +{ + // Used to assign thread IDs to make threads easier to identify when debugging. + private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger(0); + + private final CountDownLatch latch = new CountDownLatch(1); + private final int id; + + + protected SwingBackgroundTask() + { + this.id = INSTANCE_COUNT.getAndIncrement(); + } + + + /** + * Asynchronous call that begins execution of the task and returns immediately. + * The {@link #performTask()} method will be invoked on a background thread and, + * when it has completed, {@link #postProcessing(Object)} will be invoked on the + * Event Dispatch Thread (or, if there is an exception, {@link #onError(Throwable)} + * will be invoked instead - also on the EDT). + * @see #performTask() + * @see #postProcessing(Object) + * @see #onError(Throwable) + * @see #waitForCompletion() + */ + public void execute() + { + Runnable task = new Runnable() + { + public void run() + { + try + { + final V result = performTask(); + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + postProcessing(result); + latch.countDown(); + } + }); + } + // If an exception occurs performing the task, we need + // to handle it. + catch (final Throwable throwable) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + onError(throwable); + latch.countDown(); + } + }); + } + } + }; + new Thread(task, "SwingBackgroundTask-" + id).start(); + } + + + /** + * Waits for the execution of this task to complete. If the {@link #execute()} + * method has not yet been invoked, this method will block indefinitely. + * @throws InterruptedException If the thread executing the task + * is interrupted. + */ + public void waitForCompletion() throws InterruptedException + { + latch.await(); + } + + + /** + * Performs the processing of the task and returns a result. + * Implement in sub-classes to provide the task logic. This method will + * run on a background thread and not on the Event Dispatch Thread and + * therefore should not manipulate any Swing components. + * @return The result of executing this task. + * @throws Exception The task may throw an exception, in which case + * the {@link #onError(Throwable)} method will be invoked instead of + * {@link #postProcessing(Object)}. + */ + protected abstract V performTask() throws Exception; + + + /** + * This method is invoked, on the Event Dispatch Thread, after the task + * has been executed. + * This empty default implementation should be over-ridden in sub-classes + * in order to provide GUI updates that should occur following successful + * task completion. + * @param result The result from the {@link #performTask()} method. + */ + protected void postProcessing(V result) + { + // Over-ride in sub-class. + } + + + /** + * This method is invoked, on the Event Dispatch Thread, if there is an + * exception or error executing the {@link #performTask()} method. + * This default implementation displays a message dialog with details of the + * exception. It may be over-ridden in sub-classes. + * @param throwable The exception or error that was thrown while executing + * the task. + */ + protected void onError(Throwable throwable) + { + throwable.printStackTrace(); + JOptionPane.showMessageDialog(null, + throwable.getMessage(), + throwable.getClass().getName(), + JOptionPane.ERROR_MESSAGE); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/swing/package-info.java b/watchmaker/swing/src/java/main/org/uncommons/swing/package-info.java new file mode 100644 index 0000000..92ec475 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/swing/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. +//============================================================================= +/** + * Utility classes and generic components for building Swing graphical + * user interfaces. + * @author Daniel Dyer + */ +package org.uncommons.swing; diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/AbortControl.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/AbortControl.java new file mode 100644 index 0000000..fc8b49f --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/AbortControl.java @@ -0,0 +1,86 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JButton; +import org.uncommons.watchmaker.framework.TerminationCondition; +import org.uncommons.watchmaker.framework.termination.UserAbort; + +/** + * A GUI control that allows the user to abort an evolutionary program. + * @author Daniel Dyer + */ +public class AbortControl implements EvolutionControl +{ + private final JButton control = new JButton("Abort"); + private final UserAbort abortCondition = new UserAbort(); + + public AbortControl() + { + control.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent actionEvent) + { + control.setEnabled(false); + abortCondition.abort(); + } + }); + setDescription("Terminate the evolution on completion of the current generation."); + } + + + /** + * @return A button that, when pressed, will trigger the abort condition + * associated with this control. + */ + public JButton getControl() + { + return control; + } + + + /** + * Enables the GUI control and resets the abort condition ready + * for use. + */ + public void reset() + { + control.setEnabled(true); + abortCondition.reset(); + } + + + /** + * @return A {@link TerminationCondition} that is tied to this control. It can + * be passed to an {@link org.uncommons.watchmaker.framework.EvolutionEngine} + * so that the evolution is aborted when this control is invoked. + */ + public TerminationCondition getTerminationCondition() + { + return abortCondition; + } + + + /** + * {@inheritDoc} + */ + public final void setDescription(String description) + { + control.setToolTipText(description); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/EvolutionControl.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/EvolutionControl.java new file mode 100644 index 0000000..0a59534 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/EvolutionControl.java @@ -0,0 +1,42 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import javax.swing.JComponent; + +/** + * Common interface for GUI controls for evolutionary programs. + * @author Daniel Dyer + */ +public interface EvolutionControl +{ + /** + * @return The GUI component used by this control. + */ + JComponent getControl(); + + /** + * Resets the control to its initial configuration. + */ + void reset(); + + /** + * Provides a textual description of the purpose of the control. This may be displayed + * somewhere on the GUI component (typically as tooltip text). + * @param description The description of the control. + */ + void setDescription(String description); +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/NumericParameterControl.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/NumericParameterControl.java new file mode 100644 index 0000000..e304cd3 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/NumericParameterControl.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.swing; + +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.uncommons.maths.number.AdjustableNumberGenerator; +import org.uncommons.maths.number.NumberGenerator; + +/** + * A GUI control that allows the user to set/update the value of a + * numeric parameter. + * @param <T> The numeric type of this control (e.g. Integer, Double). + * @author Daniel Dyer + */ +public class NumericParameterControl<T extends Number & Comparable<T>> implements EvolutionControl +{ + private final T defaultValue; + private final JSpinner control; + private final AdjustableNumberGenerator<T> numberGenerator; + + public NumericParameterControl(T minimum, + T maximum, + T stepSize, + T initialValue) + { + this.defaultValue = initialValue; + this.numberGenerator = new AdjustableNumberGenerator<T>(this.defaultValue); + control = new JSpinner(new SpinnerNumberModel(initialValue, + minimum, + maximum, + stepSize)); + control.addChangeListener(new ChangeListener() + { + public void stateChanged(ChangeEvent changeEvent) + { + @SuppressWarnings("unchecked") + T value = (T) control.getValue(); + numberGenerator.setValue(value); + } + }); + } + + + /** + * {@inheritDoc} + */ + public JSpinner getControl() + { + return control; + } + + + /** + * {@inheritDoc} + */ + public void reset() + { + control.setValue(defaultValue); + control.setEnabled(true); + } + + + /** + * Returns a number generator that simply returns the current value contained + * in the spinner field. + * @return A number generator that can be used to control an evolutionary program. + */ + public NumberGenerator<T> getNumberGenerator() + { + return numberGenerator; + } + + + /** + * {@inheritDoc} + */ + public void setDescription(String description) + { + control.setToolTipText(description); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/ObjectSwingRenderer.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/ObjectSwingRenderer.java new file mode 100644 index 0000000..d915d87 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/ObjectSwingRenderer.java @@ -0,0 +1,46 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import javax.swing.JComponent; +import javax.swing.JTextArea; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * A default {@link Renderer} implementation that can display any object + * as a Swing component. It simply converts the object into its String + * representation (via the {@link Object#toString()} method) and shows + * that in a text area. + * @author Daniel Dyer + */ +public class ObjectSwingRenderer implements Renderer<Object, JComponent> +{ + /** + * Calls {@link Object#toString()} on the specified entity and creates + * a {@link JTextArea} containing that text. + * @param entity The evolved entity to render. + * @return A text area containing the string representation of the entity. + */ + public JComponent render(Object entity) + { + JTextArea text = new JTextArea(entity.toString()); + text.setEditable(false); + text.setBackground(null); + text.setLineWrap(true); + text.setWrapStyleWord(true); + return text; + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/ProbabilityParameterControl.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/ProbabilityParameterControl.java new file mode 100644 index 0000000..cc2c887 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/ProbabilityParameterControl.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.swing; + +import java.awt.BorderLayout; +import java.text.DecimalFormat; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.uncommons.maths.Maths; +import org.uncommons.maths.number.AdjustableNumberGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; + +/** + * A GUI control that allows the user to set/update the value of a + * {@link Probability} parameter. + * @author Daniel Dyer + */ +public class ProbabilityParameterControl implements EvolutionControl +{ + private final Probability defaultValue; + private final int range; + private final JComponent control; + private final JSlider probabilitySlider; + private final JLabel valueLabel = new JLabel(); + private final AdjustableNumberGenerator<Probability> numberGenerator; + private final DecimalFormat format; + + + /** + * Creates a control with a default range of 0..1 and a default granularity + * of 2 decimal places. + * @param defaultValue The default probability value. + */ + public ProbabilityParameterControl(Probability defaultValue) + { + this(Probability.ZERO, Probability.ONE, 2, defaultValue); + } + + + /** + * @param minimum The minimum probability that this control will permit. + * @param maximum The maximum probability that this control will permit. + * @param decimalPlaces The granularity of the control. + * @param initialValue The default probability. + */ + public ProbabilityParameterControl(Probability minimum, + Probability maximum, + int decimalPlaces, + Probability initialValue) + { + if (initialValue.compareTo(minimum) < 0 || initialValue.compareTo(maximum) > 0) + { + throw new IllegalArgumentException("Initial value must respect minimum and maximum."); + } + if (decimalPlaces < 1) + { + throw new IllegalArgumentException("Number of decimal places must be >= 1."); + } + this.format = createFormat(decimalPlaces); + this.defaultValue = initialValue; + this.numberGenerator = new AdjustableNumberGenerator<Probability>(this.defaultValue); + this.range = (int) Maths.raiseToPower(10, decimalPlaces); + this.probabilitySlider = createSlider(initialValue, minimum, maximum); + probabilitySlider.setName("Slider"); // For easy look-up in unit tests. + this.control = new JPanel(new BorderLayout()); + control.add(probabilitySlider, BorderLayout.CENTER); + valueLabel.setText(format.format(defaultValue)); + control.add(valueLabel, BorderLayout.WEST); + } + + + private DecimalFormat createFormat(int decimalPlaces) + { + StringBuilder formatString = new StringBuilder("0."); + for (int i = 0; i < decimalPlaces; i++) + { + formatString.append('0'); + } + return new DecimalFormat(formatString.toString()); + } + + + private JSlider createSlider(Probability initialValue, + Probability minimum, + Probability maximum) + { + int value = (int) Math.round(range * initialValue.doubleValue()); + int min = (int) Math.round(range * minimum.doubleValue()); + int max = (int) Math.round(range * maximum.doubleValue()); + final JSlider slider = new JSlider(min, max, value); + slider.addChangeListener(new ChangeListener() + { + public void stateChanged(ChangeEvent changeEvent) + { + Probability probability = new Probability((double) slider.getValue() / range); + numberGenerator.setValue(probability); + valueLabel.setText(format.format(probability)); + } + }); + slider.setMajorTickSpacing(10); + slider.setMinorTickSpacing(5); + slider.setPaintTicks(true); + return slider; + } + + + /** + * {@inheritDoc} + */ + public JComponent getControl() + { + return control; + } + + + /** + * {@inheritDoc} + */ + public void reset() + { + int value = (int) Math.round(range * defaultValue.doubleValue()); + probabilitySlider.setValue(value); + valueLabel.setText(format.format(defaultValue)); + numberGenerator.setValue(defaultValue); + } + + + /** + * Returns a number generator that simply returns the current probability value + * represented by the position of the slider control. + * @return A number generator that can be used to control an evolutionary program. + */ + public NumberGenerator<Probability> getNumberGenerator() + { + return numberGenerator; + } + + + /** + * {@inheritDoc} + */ + public void setDescription(String description) + { + probabilitySlider.setToolTipText(description); + valueLabel.setToolTipText(description); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SelectionStrategyControl.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SelectionStrategyControl.java new file mode 100644 index 0000000..d9b60b5 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SelectionStrategyControl.java @@ -0,0 +1,170 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Vector; +import javax.swing.JComboBox; +import org.uncommons.maths.random.Probability; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.selection.RankSelection; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.selection.SigmaScaling; +import org.uncommons.watchmaker.framework.selection.StochasticUniversalSampling; +import org.uncommons.watchmaker.framework.selection.TournamentSelection; +import org.uncommons.watchmaker.framework.selection.TruncationSelection; + +/** + * An evolution control for selecting between different {@link SelectionStrategy} implementations. + * This control provides a proxy selection strategy that delegates to the currently selected + * strategy. Using this proxy strategy with an {@link org.uncommons.watchmaker.framework.EvolutionEngine} + * means that any change to the combo-box selection is immediately reflected in the selection used + * by the running evolution engine. + * @param <T> A generic type that matches the type associated with the selection strategies. + * @author Daniel Dyer + */ +public class SelectionStrategyControl<T> implements EvolutionControl +{ + private final JComboBox control; + private final ProxySelectionStrategy selectionStrategy; + + + /** + * Creates a control for choosing between a specified set of selection strategies. + * @param options The selection strategies to choose from. + */ + public SelectionStrategyControl(List<SelectionStrategy<? super T>> options) + { + this.control = new JComboBox(new Vector<SelectionStrategy<? super T>>(options)); + this.selectionStrategy = new ProxySelectionStrategy(options.get(0)); + this.control.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent ev) + { + if (ev.getStateChange() == ItemEvent.SELECTED) + { + @SuppressWarnings("unchecked") + SelectionStrategy<? super T> delegate = (SelectionStrategy<? super T>) control.getSelectedItem(); + selectionStrategy.setDelegate(delegate); + } + } + }); + } + + + /** + * Creates a list containing one instance of each of the standard selection strategies. + * These strategies are {@link RankSelection}, {@link RouletteWheelSelection}, + * {@link StochasticUniversalSampling}, {@link TournamentSelection} and {@link TruncationSelection}. + * @param tournamentProbability The probability parameter for {@link TournamentSelection}. + * @param truncationRatio The ratio parameter for {@link TruncationSelection}. + * @return A list of selection strategies. + */ + public static <T> List<SelectionStrategy<? super T>> createDefaultOptions(Probability tournamentProbability, + double truncationRatio) + { + List<SelectionStrategy<? super T>> options = new LinkedList<SelectionStrategy<? super T>>(); + options.add(new RankSelection()); + options.add(new RouletteWheelSelection()); + options.add(new SigmaScaling()); + options.add(new StochasticUniversalSampling()); + options.add(new TournamentSelection(tournamentProbability)); + options.add(new TruncationSelection(truncationRatio)); + return options; + } + + + /** + * {@inheritDoc} + */ + public JComboBox getControl() + { + return control; + } + + + /** + * {@inheritDoc} + */ + public void reset() + { + control.setSelectedIndex(0); + } + + + /** + * {@inheritDoc} + */ + public void setDescription(String description) + { + control.setToolTipText(description); + } + + + /** + * @return A proxied {@link SelectionStrategy} that delegates to whichever + * concrete selection strategy is currently selected. + */ + public SelectionStrategy<T> getSelectionStrategy() + { + return selectionStrategy; + } + + + /** + * A {@link SelectionStrategy} implementation that simply delegates to the selection strategy + * currently selected by the combobox control. + */ + private class ProxySelectionStrategy implements SelectionStrategy<T> + { + private volatile SelectionStrategy<? super T> delegate; + + ProxySelectionStrategy(SelectionStrategy<? super T> delegate) + { + this.delegate = delegate; + } + + + public void setDelegate(SelectionStrategy<? super T> delegate) + { + this.delegate = delegate; + } + + + /** + * {@inheritDoc} + */ + public <S extends T> List<S> select(List<EvaluatedCandidate<S>> population, + boolean naturalFitnessScores, + int selectionSize, + Random rng) + { + return delegate.select(population, naturalFitnessScores, selectionSize, rng); + } + + + @Override + public String toString() + { + return delegate.toString(); + } + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingConsole.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingConsole.java new file mode 100644 index 0000000..0ce6db5 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingConsole.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.swing; + +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import org.uncommons.watchmaker.framework.interactive.Console; + +/** + * Swing-based console for interactive evolutionary algorithms. + * @author Daniel Dyer + */ +public class SwingConsole extends JPanel implements Console<JComponent> +{ + private final Lock lock = new ReentrantLock(); + private final Condition selected = lock.newCondition(); + private final AtomicInteger selectedIndex = new AtomicInteger(-1); + + /** + * Creates a console that displays candidates arranged in three columns + * (and as many rows as required). + */ + public SwingConsole() + { + this(3); + } + + + /** + * Creates a console with a configurable number of columns. + * @param columns The number of columns to use when displaying the + * candidates for selection. + */ + public SwingConsole(int columns) + { + super(new GridLayout(0, columns)); + } + + + /** + * This method blocks and therefore must not be invoked from the Event + * Dispatch Thread. + * {@inheritDoc} + */ + public int select(final List<? extends JComponent> renderedEntities) + { + selectedIndex.set(-1); + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + removeAll(); + int index = -1; + for (JComponent entity : renderedEntities) + { + add(new EntityPanel(entity, ++index)); + } + revalidate(); + } + }); + waitForSelection(); + return selectedIndex.get(); + } + + + /** + * Wait until the user has made a selection. + */ + private void waitForSelection() + { + lock.lock(); + try + { + while (selectedIndex.get() < 0) + { + selected.awaitUninterruptibly(); + } + } + finally + { + lock.unlock(); + } + } + + + /** + * Swing panel that wraps a rendered entity and a button for selecting + * that entity. + */ + private class EntityPanel extends JPanel + { + EntityPanel(JComponent entityComponent, final int index) + { + super(new BorderLayout()); + add(entityComponent, BorderLayout.CENTER); + JButton selectButton = new JButton("Select"); + selectButton.setName("Selection-" + index); // This helps to find the button from a unit test. + selectButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent actionEvent) + { + lock.lock(); + try + { + selectedIndex.set(index); + selected.signalAll(); + } + finally + { + lock.unlock(); + } + } + }); + add(selectButton, BorderLayout.SOUTH); + setBorder(BorderFactory.createEtchedBorder()); + } + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingEvolutionObserver.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingEvolutionObserver.java new file mode 100644 index 0000000..6e76daa --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingEvolutionObserver.java @@ -0,0 +1,78 @@ +package org.uncommons.watchmaker.swing; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.uncommons.util.concurrent.ConfigurableThreadFactory; +import org.uncommons.watchmaker.framework.EvolutionObserver; +import org.uncommons.watchmaker.framework.PopulationData; + +/** + * Limits the update rate of a Swing-based {@link EvolutionObserver}. + * + * @param <T> The population type. + * @author Gili Tzabari + */ +public class SwingEvolutionObserver<T> implements EvolutionObserver<T> +{ + private final EvolutionObserver<T> delegate; + private final long delay; + private final TimeUnit unit; + private final ConfigurableThreadFactory threadFactory = new ConfigurableThreadFactory("SwingEvolutionObserver", + Thread.NORM_PRIORITY, + true); + private final ScheduledExecutorService timer = Executors.newScheduledThreadPool(1, threadFactory); + + private final AtomicReference<PopulationData<? extends T>> latestPopulation + = new AtomicReference<PopulationData<? extends T>>(); + + + /** + * Creates a new SwingEvolutionObserver. + * + * @param delegate The underlying EvolutionObserver to update. + * @param delay The amount of time to wait before updating the underlying {@link EvolutionObserver}. + * @param unit The time unit of delay. + * @throws NullPointerException If delegate or unit are null. + * @throws IllegalArgumentException If delay is negative. + */ + public SwingEvolutionObserver(EvolutionObserver<T> delegate, long delay, TimeUnit unit) + { + if (delegate == null) + { + throw new NullPointerException("delegate may not be null"); + } + if (unit == null) + { + throw new NullPointerException("unit may not be null"); + } + if (delay < 0) + { + throw new IllegalArgumentException("delay may not be negative: " + delay); + } + + this.delegate = delegate; + this.delay = delay; + this.unit = unit; + } + + + public void populationUpdate(PopulationData<? extends T> populationData) + { + if (latestPopulation.getAndSet(populationData) != null) + { + // An update is already scheduled. + return; + } + + // Schedule an update in 300ms. + timer.schedule(new Runnable() + { + public void run() + { + delegate.populationUpdate(latestPopulation.getAndSet(null)); + } + }, delay, unit); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingIslandEvolutionObserver.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingIslandEvolutionObserver.java new file mode 100644 index 0000000..f840f82 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/SwingIslandEvolutionObserver.java @@ -0,0 +1,98 @@ +package org.uncommons.watchmaker.swing; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.uncommons.util.concurrent.ConfigurableThreadFactory; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * Limits the update rate of a Swing-based {@link IslandEvolutionObserver}. + * + * @param <T> The population type. + * @author Gili Tzabari + */ +public class SwingIslandEvolutionObserver<T> implements IslandEvolutionObserver<T> +{ + private final IslandEvolutionObserver<T> delegate; + private final long delay; + private final TimeUnit unit; + private final ConfigurableThreadFactory threadFactory = new ConfigurableThreadFactory("SwingIslandEvolutionObserver", + Thread.NORM_PRIORITY, + true); + private final ScheduledExecutorService timer = Executors.newScheduledThreadPool(1, threadFactory); + private final AtomicReference<PopulationData<? extends T>> latestPopulation + = new AtomicReference<PopulationData<? extends T>>(); + private final ConcurrentHashMap<Integer, PopulationData<? extends T>> latestIslandPopulation + = new ConcurrentHashMap<Integer, PopulationData<? extends T>>(); + + /** + * Creates a new SwingIslandEvolutionObserver. + * + * @param delegate The underlying {@link IslandEvolutionObserver} to update. + * @param delay The amount of time to wait before updating the underlying {@link IslandEvolutionObserver}. + * @param unit The time unit of delay. + * @throws NullPointerException If delegate or unit are null. + * @throws IllegalArgumentException If delay is negative. + */ + public SwingIslandEvolutionObserver(IslandEvolutionObserver<T> delegate, long delay, TimeUnit unit) + { + if (delegate == null) + { + throw new NullPointerException("delegate may not be null"); + } + if (unit == null) + { + throw new NullPointerException("unit may not be null"); + } + if (delay < 0) + { + throw new IllegalArgumentException("delay may not be negative: " + delay); + } + + this.delegate = delegate; + this.delay = delay; + this.unit = unit; + } + + + public void populationUpdate(PopulationData<? extends T> populationData) + { + if (latestPopulation.getAndSet(populationData) != null) + { + // An update is already scheduled. + return; + } + + // Schedule an update. + timer.schedule(new Runnable() + { + public void run() + { + delegate.populationUpdate(latestPopulation.getAndSet(null)); + } + }, delay, unit); + } + + + public void islandPopulationUpdate(final int islandIndex, PopulationData<? extends T> populationData) + { + if (latestIslandPopulation.put(islandIndex, populationData) != null) + { + // An update is already scheduled. + return; + } + + // Schedule an update. + timer.schedule(new Runnable() + { + public void run() + { + delegate.islandPopulationUpdate(islandIndex, latestIslandPopulation.remove(islandIndex)); + } + }, delay, unit); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/EvolutionMonitor.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/EvolutionMonitor.java new file mode 100644 index 0000000..73e7bcb --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/EvolutionMonitor.java @@ -0,0 +1,244 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing.evolutionmonitor; + +import java.awt.BorderLayout; +import java.awt.Window; +import java.lang.reflect.InvocationTargetException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; +import org.uncommons.watchmaker.swing.ObjectSwingRenderer; +import org.uncommons.watchmaker.swing.SwingIslandEvolutionObserver; + +/** + * The Evolution Monitor is a component that can be attached to an + * {@link org.uncommons.watchmaker.framework.EvolutionEngine} to provide + * real-time information (in a Swing GUI) about the current state of the + * evolution. + * @param <T> The type of the evolved entities monitored by this component. + * @author Daniel Dyer + */ +public class EvolutionMonitor<T> implements IslandEvolutionObserver<T> +{ + private final List<IslandEvolutionObserver<? super T>> views = new LinkedList<IslandEvolutionObserver<? super T>>(); + + private JComponent monitorComponent; + private Window window = null; + + private final boolean islands; + + /** + * <p>Creates an EvolutionMonitor with a single panel that graphs the fitness scores + * of the population from generation to generation.</p> + * <p>If you are using {@link org.uncommons.watchmaker.framework.islands.IslandEvolution}, + * use the {@link #EvolutionMonitor(boolean)} constructor instead, to enable island support.</p> + */ + public EvolutionMonitor() + { + this(false); + } + + + /** + * Creates an EvolutionMonitor with a single panel that graphs the fitness scores + * of the population from generation to generation. + * @param islands Whether the monitor should be configured for displaying data from + * {@link org.uncommons.watchmaker.framework.islands.IslandEvolution}. Set this + * parameter to false when using a standard {@link org.uncommons.watchmaker.framework.EvolutionEngine} + * or if you don't want to display island-specific data for island evolution. + */ + public EvolutionMonitor(boolean islands) + { + this(new ObjectSwingRenderer(), islands); + } + + + /** + * Creates an EvolutionMonitor with a second panel that displays a graphical + * representation of the fittest candidate in the population. + * @param renderer Renders a candidate solution as a JComponent. + * @param islands Whether the monitor should be configured for displaying data from + * {@link org.uncommons.watchmaker.framework.islands.IslandEvolution}. Set this + * parameter to false when using a standard {@link org.uncommons.watchmaker.framework.EvolutionEngine} + */ + public EvolutionMonitor(final Renderer<? super T, JComponent> renderer, boolean islands) + { + this.islands = islands; + if (SwingUtilities.isEventDispatchThread()) + { + init(renderer); + } + else + { + try + { + SwingUtilities.invokeAndWait(new Runnable() + { + public void run() + { + init(renderer); + } + }); + } + catch (InterruptedException ex) + { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + catch (InvocationTargetException ex) + { + throw new IllegalStateException(ex); + } + } + } + + + private void init(Renderer<? super T, JComponent> renderer) + { + JTabbedPane tabs = new JTabbedPane(); + monitorComponent = new JPanel(new BorderLayout()); + monitorComponent.add(tabs, BorderLayout.CENTER); + + FittestCandidateView<T> candidateView = new FittestCandidateView<T>(renderer); + tabs.add("Fittest Individual", candidateView); + views.add(new SwingIslandEvolutionObserver<T>(candidateView, 300, TimeUnit.MILLISECONDS)); + + PopulationFitnessView fitnessView = new PopulationFitnessView(islands); + tabs.add(islands ? "Global Population" : "Population Fitness", fitnessView); + views.add(fitnessView); + + if (islands) + { + IslandsView islandsView = new IslandsView(); + tabs.add("Island Populations", islandsView); + views.add(new SwingIslandEvolutionObserver<Object>(islandsView, 300, TimeUnit.MILLISECONDS)); + } + + JVMView jvmView = new JVMView(); + tabs.add("JVM Memory", jvmView); + + StatusBar statusBar = new StatusBar(islands); + monitorComponent.add(statusBar, BorderLayout.SOUTH); + views.add(new SwingIslandEvolutionObserver<Object>(statusBar, 300, TimeUnit.MILLISECONDS)); + } + + + /** + * {@inheritDoc} + */ + public void populationUpdate(PopulationData<? extends T> populationData) + { + for (IslandEvolutionObserver<? super T> view : views) + { + view.populationUpdate(populationData); + } + } + + + /** + * {@inheritDoc} + */ + public void islandPopulationUpdate(int islandIndex, PopulationData<? extends T> populationData) + { + for (IslandEvolutionObserver<? super T> view : views) + { + view.islandPopulationUpdate(islandIndex, populationData); + } + } + + + public JComponent getGUIComponent() + { + return monitorComponent; + } + + + /** + * Displays the evolution monitor component in a new {@link JFrame}. There is no + * need to make sure this method is invoked from the Event Dispatch Thread, the + * method itself ensures that the window is created and displayed from the EDT. + * @param title The title for the new frame. + * @param exitOnClose Whether the JVM should exit when the frame is closed. Useful + * if this is the only application window. + */ + public void showInFrame(final String title, + final boolean exitOnClose) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + JFrame frame = new JFrame(title); + frame.setDefaultCloseOperation(exitOnClose ? JFrame.EXIT_ON_CLOSE : JFrame.DISPOSE_ON_CLOSE); + showWindow(frame); + } + }); + } + + + /** + * Displays the evolution monitor component in a new {@link JDialog}. There is no + * need to make sure this method is invoked from the Event Dispatch Thread, the + * method itself ensures that the window is created and displayed from the EDT. + * @param owner The owning frame for the new dialog. + * @param title The title for the new dialog. + * @param modal Whether the + */ + public void showInDialog(final JFrame owner, + final String title, + final boolean modal) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + JDialog dialog = new JDialog(owner, title, modal); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + showWindow(dialog); + } + }); + } + + + /** + * Helper method for showing the evolution monitor in a frame or dialog. + * @param newWindow The frame or dialog used to show the evolution monitor. + */ + private void showWindow(Window newWindow) + { + if (window != null) + { + window.remove(getGUIComponent()); + window.setVisible(false); + window.dispose(); + window = null; + } + newWindow.add(getGUIComponent(), BorderLayout.CENTER); + newWindow.pack(); + newWindow.setVisible(true); + this.window = newWindow; + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/FittestCandidateView.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/FittestCandidateView.java new file mode 100644 index 0000000..48e14eb --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/FittestCandidateView.java @@ -0,0 +1,101 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.swing.evolutionmonitor; + +import java.awt.BorderLayout; +import java.awt.Font; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * {@link EvolutionMonitor} view for displaying a graphical representation + * of the fittest candidate found so far. This allows us to monitor the + * progress of an evolutionary algorithm. + * @param <T> The type of the evolved entity displayed by this component. + * @author Daniel Dyer + */ +class FittestCandidateView<T> extends JPanel implements IslandEvolutionObserver<T> +{ + private static final Font BIG_FONT = new Font("Dialog", Font.BOLD, 16); + + private final Renderer<? super T, JComponent> renderer; + private final JLabel fitnessLabel = new JLabel("N/A", JLabel.CENTER); + private final JScrollPane scroller = new JScrollPane(); + + private T fittestCandidate = null; + + /** + * Creates a Swing view that uses the specified renderer to display + * evolved entities. + * @param renderer A renderer that convert evolved entities of the type + * recognised by this view into Swing components. + */ + FittestCandidateView(Renderer<? super T, JComponent> renderer) + { + super(new BorderLayout(0, 10)); + this.renderer = renderer; + + JPanel header = new JPanel(new BorderLayout()); + JLabel label = new JLabel("Fitness", JLabel.CENTER); + header.add(label, BorderLayout.NORTH); + fitnessLabel.setFont(BIG_FONT); + header.add(fitnessLabel, BorderLayout.CENTER); + add(header, BorderLayout.NORTH); + + scroller.setBackground(null); + scroller.getViewport().setBackground(null); + scroller.setBorder(null); + add(scroller, BorderLayout.CENTER); + + // Set names for easier identification in unit tests. + fitnessLabel.setName("FitnessLabel"); + } + + + public void populationUpdate(final PopulationData<? extends T> populationData) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + fitnessLabel.setText(String.valueOf(populationData.getBestCandidateFitness())); + // If the fittest candidate is already displayed (because it was the fittest + // candidate in the previous generation), don't incur the expense of rendering + // it again. Note that we still have to update the fitness score label above + // because the fitness may be different even if the candidate isn't (in the + // case where fitness is evaluated against other members of the population). + if (populationData.getBestCandidate() != fittestCandidate) + { + fittestCandidate = populationData.getBestCandidate(); + JComponent renderedCandidate = renderer.render(fittestCandidate); + scroller.setViewportView(renderedCandidate); + } + } + }); + } + + + public void islandPopulationUpdate(int islandIndex, final PopulationData<? extends T> populationData) + { + // Do nothing. + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/IslandsView.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/IslandsView.java new file mode 100644 index 0000000..fe784ca --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/IslandsView.java @@ -0,0 +1,122 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing.evolutionmonitor; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * An evolution monitor view that gives an insight into how the evolution is progressing on + * individual islands. + * @author Daniel Dyer + */ +class IslandsView extends JPanel implements IslandEvolutionObserver<Object> +{ + private static final String FITTEST_INDIVIDUAL_LABEL = "Fittest Individual"; + private static final String MEAN_FITNESS_LABEL = "Mean Fitness/Standard Deviation"; + + private final DefaultCategoryDataset bestDataSet = new DefaultCategoryDataset(); + private final DefaultStatisticalCategoryDataset meanDataSet = new DefaultStatisticalCategoryDataset(); + private final StatisticalLineAndShapeRenderer meanRenderer = new StatisticalLineAndShapeRenderer(); + + private final AtomicInteger islandCount = new AtomicInteger(0); + private final Object maxLock = new Object(); + private double max = 0; + + + + IslandsView() + { + super(new BorderLayout()); + add(createControls(), BorderLayout.SOUTH); + } + + + /** + * Creates the GUI controls for toggling graph display options. + * @return A component that can be added to the main panel. + */ + private JComponent createControls() + { + JPanel controls = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + final JCheckBox meanCheckBox = new JCheckBox("Show Mean and Standard Deviation", false); + meanCheckBox.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + } + }); + controls.add(meanCheckBox); + + return controls; + } + + + + public void islandPopulationUpdate(final int islandIndex, final PopulationData<? extends Object> populationData) + { + // Make sure the bars are added to the chart in order of island index, regardless of which island + // reports its results first. + if (islandIndex >= islandCount.get()) + { + try + { + SwingUtilities.invokeAndWait(new Runnable() + { + public void run() + { + // Don't need synchronisation here because SwingUtilities queues these updates + // and if a second update gets queued, the loop will be a no-op so it's not a problem. + for (Integer i = islandCount.get(); i <= islandIndex; i++) + { + bestDataSet.addValue(0, FITTEST_INDIVIDUAL_LABEL, i); + meanDataSet.add(0, 0, MEAN_FITNESS_LABEL, i); + islandCount.incrementAndGet(); + } + } + }); + } + catch (InterruptedException ex) + { + Thread.currentThread().interrupt(); + } + catch (InvocationTargetException ex) + { + throw new IllegalStateException(ex.getCause()); + } + } + } + + + public void populationUpdate(PopulationData<? extends Object> populationData) + { + synchronized (maxLock) + { + max = 0; + } + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/JVMView.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/JVMView.java new file mode 100644 index 0000000..7d80de4 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/JVMView.java @@ -0,0 +1,151 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing.evolutionmonitor; + +import java.awt.BasicStroke; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.Timer; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.DateAxis; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.ValueMarker; +import org.jfree.data.time.Second; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.ui.RectangleAnchor; +import org.jfree.ui.TextAnchor; + +/** + * Evolution monitor panel that displays information about the current + * state of the Java Virtual machine that is running the program. + * @author Daniel Dyer + */ +class JVMView extends JPanel +{ + private static final int MEGABYTE = 1048576; + + private final TimeSeries memoryUsageSeries = new TimeSeries("Memory Usage"); + private final TimeSeries heapSizeSeries = new TimeSeries("Heap Size"); + + private final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + + JVMView() + { + super(new BorderLayout()); + double maxMemory = (double) memoryBean.getHeapMemoryUsage().getMax() / MEGABYTE; + + ChartPanel heapPanel = new ChartPanel(createHeapChart(maxMemory), + false, // Properties + true, // Save + true, // Print + false, // Zoom + true); // Tooltips + heapPanel.setMouseZoomable(false); + add(heapPanel, BorderLayout.CENTER); + add(createControls(), BorderLayout.SOUTH); + + Timer timer = new Timer(5000, new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + addMemoryDataPoint(); + } + }); + + // Plot start values. + addMemoryDataPoint(); + + timer.start(); + } + + + private JFreeChart createHeapChart(double maxMemory) + { + TimeSeriesCollection dataSet = new TimeSeriesCollection(); + dataSet.addSeries(memoryUsageSeries); + dataSet.addSeries(heapSizeSeries); + JFreeChart chart = ChartFactory.createXYAreaChart("JVM Heap", + "Time", + "Megabytes", + dataSet, + PlotOrientation.VERTICAL, + true, // Legend. + false, // Tooltips. + false); + DateAxis timeAxis = new DateAxis("Time"); + timeAxis.setLowerMargin(0); + timeAxis.setUpperMargin(0); + chart.getXYPlot().setDomainAxis(timeAxis); + chart.getXYPlot().getRangeAxis().setLowerBound(0); + chart.getXYPlot().getRangeAxis().setUpperBound(maxMemory * 1.1); // Add 10% to leave room for marker. + + // Add a horizontal marker to indicate the heap growth limit. + ValueMarker marker = new ValueMarker(maxMemory, Color.BLACK, new BasicStroke(1)); + marker.setLabel("Maximum Permitted Heap Size (adjust with -Xmx)"); + marker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT); + marker.setLabelAnchor(RectangleAnchor.RIGHT); + chart.getXYPlot().addRangeMarker(marker); + + chart.getXYPlot().getRenderer().setSeriesPaint(0, Color.RED); + chart.getXYPlot().getRenderer().setSeriesPaint(1, new Color(0, 128, 0, 128)); + + return chart; + } + + + /** + * Creates the GUI controls for toggling graph display options. + * @return A component that can be added to the main panel. + */ + private JComponent createControls() + { + JPanel controls = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton gcButton = new JButton("Request GC"); + gcButton.setToolTipText("Perform garbage collection (the JVM may ignore this request)."); + gcButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent ev) + { + memoryBean.gc(); + } + }); + controls.add(gcButton); + return controls; + } + + + + private void addMemoryDataPoint() + { + MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); + double usedMegabytes = (double) heapUsage.getUsed() / MEGABYTE; + Second second = new Second(); + memoryUsageSeries.add(second, usedMegabytes); + heapSizeSeries.add(second, (double) heapUsage.getCommitted() / MEGABYTE); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/PopulationFitnessView.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/PopulationFitnessView.java new file mode 100644 index 0000000..3e4566f --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/PopulationFitnessView.java @@ -0,0 +1,228 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing.evolutionmonitor; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.SwingUtilities; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * {@link EvolutionMonitor} view for displaying a graph of population fitness data + * over the lifetime of the evolutionary algorithm. + * @author Daniel Dyer + */ +class PopulationFitnessView extends JPanel implements IslandEvolutionObserver<Object> +{ + private static final int SHOW_FIXED_GENERATIONS = 200; + + private final XYSeries bestSeries = new XYSeries("Fittest Individual"); + private final XYSeries meanSeries; + private final XYSeriesCollection dataSet = new XYSeriesCollection(); + private final ValueAxis domainAxis; + private final ValueAxis rangeAxis; + + private final JRadioButton allDataButton = new JRadioButton("All Data", false); + private final JCheckBox invertCheckBox = new JCheckBox("Invert Range Axis", false); + private final JFreeChart chart; + + private double maxY = 1; + private double minY = 0; + + + PopulationFitnessView(boolean islands) + { + super(new BorderLayout()); + meanSeries = new XYSeries(islands ? "Global Mean Fitness" : "Population Mean Fitness"); + dataSet.addSeries(bestSeries); + dataSet.addSeries(meanSeries); + chart = ChartFactory.createXYLineChart(islands ? "Global Population Fitness" : "Population Fitness", + islands ? "Epochs" : "Generations", + "Fitness", + dataSet, + PlotOrientation.VERTICAL, + true, // Legend. + false, // Tooltips. + false); + this.domainAxis = chart.getXYPlot().getDomainAxis(); + this.rangeAxis = chart.getXYPlot().getRangeAxis(); + domainAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + domainAxis.setLowerMargin(0); + domainAxis.setUpperMargin(0.05); + domainAxis.setRangeWithMargins(0, SHOW_FIXED_GENERATIONS); + rangeAxis.setRange(minY, maxY); + ChartPanel chartPanel = new ChartPanel(chart, + ChartPanel.DEFAULT_WIDTH, + ChartPanel.DEFAULT_HEIGHT, + ChartPanel.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartPanel.DEFAULT_MINIMUM_DRAW_HEIGHT, + ChartPanel.DEFAULT_MAXIMUM_DRAW_WIDTH, + ChartPanel.DEFAULT_MAXIMUM_DRAW_HEIGHT, + false, // Buffered + false, // Properties + true, // Save + true, // Print + false, // Zoom + false); // Tooltips + add(chartPanel, BorderLayout.CENTER); + add(createControls(islands), BorderLayout.SOUTH); + } + + + /** + * Creates the GUI controls for toggling graph display options. + * @return A component that can be added to the main panel. + */ + private JComponent createControls(boolean islands) + { + JPanel controls = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + allDataButton.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent ev) + { + updateDomainAxisRange(); + } + }); + String text = "Last " + SHOW_FIXED_GENERATIONS + (islands ? " Epochs" : " Generations"); + JRadioButton recentDataButton = new JRadioButton(text, true); + ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(allDataButton); + buttonGroup.add(recentDataButton); + + controls.add(allDataButton); + controls.add(recentDataButton); + + final JCheckBox meanCheckBox = new JCheckBox("Show Mean", true); + meanCheckBox.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + if (itemEvent.getStateChange() == ItemEvent.SELECTED) + { + dataSet.addSeries(meanSeries); + } + else + { + dataSet.removeSeries(meanSeries); + } + } + }); + controls.add(meanCheckBox); + + invertCheckBox.addItemListener(new ItemListener() + { + public void itemStateChanged(ItemEvent itemEvent) + { + rangeAxis.setInverted(invertCheckBox.isSelected()); + } + }); + controls.add(invertCheckBox); + + return controls; + } + + + /** + * If "all data" is selected, set the range of the domain axis to include all + * values. Otherwise set it to show the most recent 200 generations. + */ + private void updateDomainAxisRange() + { + int count = dataSet.getSeries(0).getItemCount(); + if (count < SHOW_FIXED_GENERATIONS) + { + domainAxis.setRangeWithMargins(0, SHOW_FIXED_GENERATIONS); + } + else if (allDataButton.isSelected()) + { + domainAxis.setRangeWithMargins(0, Math.max(SHOW_FIXED_GENERATIONS, count)); + } + else + { + domainAxis.setRangeWithMargins(count - SHOW_FIXED_GENERATIONS, count); + } + } + + + /** + * {@inheritDoc} + */ + public void populationUpdate(final PopulationData<?> populationData) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + chart.setNotify(false); // Avoid triggering a redraw for every change we make in this method. + if (populationData.getGenerationNumber() == 0) + { + if (!populationData.isNaturalFitness()) + { + invertCheckBox.setSelected(true); + } + // The graph might be showing data from a previous run, so clear it. + meanSeries.clear(); + bestSeries.clear(); + } + meanSeries.add(populationData.getGenerationNumber(), populationData.getMeanFitness()); + double best = populationData.getBestCandidateFitness(); + bestSeries.add(populationData.getGenerationNumber(), best); + + // We don't use JFreeChart's auto-range for the axes because it is inefficient + // (it degrades linearly with the number of items in the data set). Instead we track + // the minimum and maximum ourselves. + double high = Math.max(populationData.getMeanFitness(), populationData.getBestCandidateFitness()); + double low = Math.min(populationData.getMeanFitness(), populationData.getBestCandidateFitness()); + if (high > maxY) + { + maxY = high; + rangeAxis.setRange(minY, maxY); + } + if (low < minY) + { + minY = low; + rangeAxis.setRange(minY, maxY); + } + + updateDomainAxisRange(); + chart.setNotify(true); // Redraw all at once now. + } + }); + } + + + public void islandPopulationUpdate(int islandIndex, PopulationData<? extends Object> populationData) + { + // Do nothing. + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/StatusBar.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/StatusBar.java new file mode 100644 index 0000000..51c6651 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/StatusBar.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.swing.evolutionmonitor; + +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.islands.IslandEvolutionObserver; + +/** + * Status bar component for the evolution monitor. Can also be used separately to + * provide basic status information without having to use the full evolution monitor. + * @author Daniel Dyer + */ +public class StatusBar extends Box implements IslandEvolutionObserver<Object> +{ + private final JLabel generationsLabel = new JLabel("N/A", JLabel.RIGHT); + private final JLabel timeLabel = new JLabel("N/A", JLabel.RIGHT); + private final JLabel populationLabel = new JLabel("N/A", JLabel.RIGHT); + private final JLabel elitismLabel = new JLabel("N/A", JLabel.RIGHT); + + private final AtomicInteger islandPopulationSize = new AtomicInteger(-1); + private long elapsedTime; + private long epochTime; + + + /** + * Creates a status bar configured for non-island evolution. + */ + public StatusBar() + { + this(false); + } + + + /** + * @param islands Whether the status bar should be configured for updates from + * {@link org.uncommons.watchmaker.framework.islands.IslandEvolution}. Set this + * parameter to false when using a standard {@link org.uncommons.watchmaker.framework.EvolutionEngine} + */ + public StatusBar(boolean islands) + { + super(BoxLayout.X_AXIS); + add(new JLabel("Population: ")); + add(populationLabel); + add(createHorizontalStrut(15)); + add(new JLabel("Elitism: ")); + add(elitismLabel); + add(createHorizontalStrut(15)); + add(new JLabel(islands ? "Epochs: " : "Generations: ")); + add(generationsLabel); + add(createHorizontalStrut(15)); + add(new JLabel("Elapsed Time: ")); + add(timeLabel); + setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); + + // Set component names for easy look-up from tests. + populationLabel.setName("Population"); + elitismLabel.setName("Elitism"); + generationsLabel.setName("Generations"); + timeLabel.setName("Time"); + } + + + /** + * {@inheritDoc} + */ + public void populationUpdate(final PopulationData<?> populationData) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + if (populationData.getGenerationNumber() == 0) + { + int islandSize = islandPopulationSize.get(); + if (islandSize > 0) + { + int islandCount = populationData.getPopulationSize() / islandSize; + populationLabel.setText(islandCount + "x" + islandSize); + elitismLabel.setText(islandCount + "x" + populationData.getEliteCount()); + } + else + { + populationLabel.setText(String.valueOf(populationData.getPopulationSize())); + elitismLabel.setText(String.valueOf(populationData.getEliteCount())); + } + } + generationsLabel.setText(String.valueOf(populationData.getGenerationNumber() + 1)); + elapsedTime = populationData.getElapsedTime(); + epochTime = 0; + timeLabel.setText(formatTime(elapsedTime)); + } + }); + } + + + /** + * {@inheritDoc} + */ + public void islandPopulationUpdate(int islandIndex, + final PopulationData<? extends Object> populationData) + { + islandPopulationSize.compareAndSet(-1, populationData.getPopulationSize()); + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + // Only update the label if the time has advanced. Sometimes, due to threading + // variations, later updates have shorter elapsed times. + if (populationData.getElapsedTime() > epochTime) + { + epochTime = populationData.getElapsedTime(); + timeLabel.setText(formatTime(elapsedTime + epochTime)); + } + } + }); + } + + + private String formatTime(long time) + { + long seconds = time / 1000; + long minutes = seconds / 60; + seconds %= 60; + long hours = minutes / 60; + minutes %= 60; + StringBuilder buffer = new StringBuilder(); + if (hours < 10) + { + buffer.append('0'); + } + buffer.append(hours); + buffer.append(':'); + if (minutes < 10) + { + buffer.append('0'); + } + buffer.append(minutes); + buffer.append(':'); + if (seconds < 10) + { + buffer.append('0'); + } + buffer.append(seconds); + return buffer.toString(); + } +} diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/package-info.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/package-info.java new file mode 100644 index 0000000..b975985 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/evolutionmonitor/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. +//============================================================================= +/** + * This package provides an experimental evolution monitor Swing component. + * @author Daniel Dyer + */ +package org.uncommons.watchmaker.swing.evolutionmonitor; diff --git a/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/package-info.java b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/package-info.java new file mode 100644 index 0000000..d8dfc18 --- /dev/null +++ b/watchmaker/swing/src/java/main/org/uncommons/watchmaker/swing/package-info.java @@ -0,0 +1,23 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +/** + * This package provides a number of classes to simplify the development of + * Swing GUIs for evolutionary programs. As well as common controls for + * modifying evolution parameters, it includes a Swing implementation of the + * {@link org.uncommons.watchmaker.framework.interactive.Console} interface + * for interactive evolutionary algorithms. + */ +package org.uncommons.watchmaker.swing; diff --git a/watchmaker/swing/src/java/test/org/uncommons/swing/SwingBackgroundTaskTest.java b/watchmaker/swing/src/java/test/org/uncommons/swing/SwingBackgroundTaskTest.java new file mode 100644 index 0000000..3c45f5d --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/swing/SwingBackgroundTaskTest.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.swing; + +import javax.swing.SwingUtilities; +import org.testng.Reporter; +import org.testng.annotations.Test; + +/** + * Unit test for {@link SwingBackgroundTask}. Ensures code is + * executed on correct threads. + * @author Daniel Dyer + */ +public class SwingBackgroundTaskTest +{ + private boolean taskExecuted; + private boolean taskOnEDT; + private boolean postProcessingExecuted; + private boolean postProcessingOnEDT; + private boolean exceptionHandled; + + @Test + public void testExecutionThreads() throws InterruptedException + { + SwingBackgroundTask<Object> testTask = new SwingBackgroundTask<Object>() + { + @Override + protected Object performTask() + { + taskExecuted = true; + taskOnEDT = SwingUtilities.isEventDispatchThread(); + return null; + } + + @Override + protected void postProcessing(Object result) + { + super.postProcessing(result); + postProcessingExecuted = true; + postProcessingOnEDT = SwingUtilities.isEventDispatchThread(); + } + }; + testTask.execute(); + testTask.waitForCompletion(); + assert taskExecuted : "Task was not executed."; + assert postProcessingExecuted : "Post-processing was not executed."; + assert !taskOnEDT : "Task was executed on EDT."; + assert postProcessingOnEDT : "Post-processing was not executed on EDT."; + } + + + /** + * Exceptions in the {@link SwingBackgroundTask#performTask()} method should + * not be swallowed, they must be passed to the + * {@link SwingBackgroundTask#onError(Throwable)} method. + */ + @Test + public void testExceptionInTask() throws InterruptedException + { + SwingBackgroundTask<Object> testTask = new SwingBackgroundTask<Object>() + { + @Override + protected Object performTask() + { + throw new UnsupportedOperationException("Task failed."); + } + + + @Override + protected void onError(Throwable throwable) + { + // Make sure we've been passed the right exception. + if (throwable.getClass().equals(UnsupportedOperationException.class)) + { + exceptionHandled = true; + } + else + { + Reporter.log("Wrong exception class: " + throwable.getClass()); + } + } + }; + testTask.execute(); + testTask.waitForCompletion(); + assert exceptionHandled : "Exception was not handled."; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/AbortControlTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/AbortControlTest.java new file mode 100644 index 0000000..340ba97 --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/AbortControlTest.java @@ -0,0 +1,41 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.swing; + +import org.testng.annotations.Test; + +/** + * Unit test for evolution abort control. + * @author Daniel Dyer + */ +public class AbortControlTest +{ + /** + * Make sure that clicking the button causes the termination condition + * to be satisfied. + */ + @Test + public void testAbort() + { + AbortControl control = new AbortControl(); + assert !control.getTerminationCondition().shouldTerminate(null) : "Abort condition should be false."; + control.getControl().doClick(); // Should fire an event that changes the condition. + assert control.getTerminationCondition().shouldTerminate(null) : "Abort condition should be true."; + // Finally, ensure that reset works as expected. + control.reset(); + assert !control.getTerminationCondition().shouldTerminate(null) : "Abort condition should be false."; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/NumericParameterControlTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/NumericParameterControlTest.java new file mode 100644 index 0000000..e69b3ad --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/NumericParameterControlTest.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.swing; + +import org.testng.annotations.Test; +import org.uncommons.maths.number.NumberGenerator; + +/** + * Unit test for the numeric parameter GUI control. + * @author Daniel Dyer + */ +public class NumericParameterControlTest +{ + /** + * Make sure that the control works with integers. + */ + @Test + public void testIntegerValues() + { + NumericParameterControl<Integer> control = new NumericParameterControl<Integer>(50, 100, 1, 60); + NumberGenerator<Integer> generator = control.getNumberGenerator(); + + // Check the initial output of the generator. + int initialValue = generator.nextValue(); + assert initialValue == 60 : "Initial value should be 60, is " + initialValue; + + // Modify the position of the slider and check that the generator value changes. + control.getControl().setValue(87); + int adjustedValue = generator.nextValue(); + assert adjustedValue == 87 : "Adjusted value should be 87, is " + adjustedValue; + + // Reset the control and check that the output is reverted to the default. + control.reset(); + int resetValue = generator.nextValue(); + assert resetValue == 60 : "Reset value should be 60, is " + resetValue; + } + + + /** + * Make sure that the control works with doubles. + */ + @Test + public void testRealValues() + { + NumericParameterControl<Double> control = new NumericParameterControl<Double>(0d, 1d, 0.01d, 0.4d); + NumberGenerator<Double> generator = control.getNumberGenerator(); + + // Check the initial output of the generator. + double initialValue = generator.nextValue(); + assert initialValue == 0.4d : "Initial value should be 0.4, is " + initialValue; + + // Modify the position of the slider and check that the generator value changes. + control.getControl().setValue(0.73d); + double adjustedValue = generator.nextValue(); + assert adjustedValue == 0.73d : "Adjusted value should be 0.73, is " + adjustedValue; + + // Reset the control and check that the output is reverted to the default. + control.reset(); + double resetValue = generator.nextValue(); + assert resetValue == 0.4d : "Reset value should be 0.4, is " + resetValue; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/ObjectSwingRendererTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/ObjectSwingRendererTest.java new file mode 100644 index 0000000..a77bd3d --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/ObjectSwingRendererTest.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.swing; + +import java.math.BigDecimal; +import javax.swing.JComponent; +import javax.swing.text.JTextComponent; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.interactive.Renderer; + +/** + * Unit test for the {@link ObjectSwingRenderer} class. + * @author Daniel Dyer + */ +public class ObjectSwingRendererTest +{ + @Test + public void testRendering() + { + Renderer<Object, JComponent> renderer = new ObjectSwingRenderer(); + JTextComponent textComponent = (JTextComponent) renderer.render(BigDecimal.TEN); + assert textComponent.getText().equals("10") : "Wrong text rendered."; + assert !textComponent.isEditable() : "Text component should not be editable."; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/ProbabilityParameterControlTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/ProbabilityParameterControlTest.java new file mode 100644 index 0000000..1da22ea --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/ProbabilityParameterControlTest.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.swing; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import javax.swing.JSlider; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.uncommons.maths.random.Probability; + +/** + * Unit test for the {@link ProbabilityParameterControl} component. + * @author Daniel Dyer + */ +public class ProbabilityParameterControlTest +{ + private Robot robot; + + @BeforeMethod(groups = "display-required") + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod(groups = "display-required") + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test + public void testDefaultValue() + { + Probability defaultValue = new Probability(0.75d); + ProbabilityParameterControl control = new ProbabilityParameterControl(defaultValue); + assert control.getNumberGenerator().nextValue().equals(defaultValue) : "Wrong initial value."; + } + + + /** + * Initial value must not be less than the minimum. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDefaultValueTooLow() + { + new ProbabilityParameterControl(Probability.EVENS, + Probability.ONE, + 2, + new Probability(0.45d)); // Should throw an IllegalArgumentException. + } + + + /** + * Initial value must not be less than the minimum. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testDefaultValueTooHigh() + { + new ProbabilityParameterControl(Probability.ZERO, + Probability.EVENS, + 2, + new Probability(0.55d)); // Should throw an IllegalArgumentException. + } + + + /** + * Minimum must be less than maximum. + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void testMinimumHigherThanMaximum() + { + new ProbabilityParameterControl(new Probability(0.7d), + new Probability(0.6d), + 2, + new Probability(0.7d)); // Should throw an IllegalArgumentException. + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidDecimalPlaces() + { + new ProbabilityParameterControl(Probability.ZERO, + Probability.ONE, + 0, // Invalid, should trigger IllegalArgumentException. + Probability.EVENS); + } + + + @Test(dependsOnMethods = "testDefaultValue", + groups = "display-required") + public void testSlider() + { + Probability defaultValue = new Probability(0.75d); + ProbabilityParameterControl control = new ProbabilityParameterControl(defaultValue); + + JFrame frame = new JFrame(); + frame.add(control.getControl(), BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(300, 50); + frame.validate(); + frame.setVisible(true); + + JSlider slider = frameFixture.slider().component(); + assert slider.getValue() == 75 : "Wrong slider position: " + slider.getValue(); + String displayedValue = frameFixture.label().text(); + assert displayedValue.equals("0.75") : "Wrong value displayed: " + displayedValue; + + + frameFixture.slider().slideTo(80); // 80 ticks is a probability of 0.8. + robot.waitForIdle(); + double probability = control.getNumberGenerator().nextValue().doubleValue(); + assert probability == 0.8 : "Wrong probability: " + probability; + displayedValue = frameFixture.label().text(); + assert displayedValue.equals("0.80") : "Wrong value displayed: " + displayedValue; + } + + + @Test(dependsOnMethods = "testSlider", + groups = "display-required") + public void testReset() + { + Probability defaultValue = new Probability(0.75d); + ProbabilityParameterControl control = new ProbabilityParameterControl(defaultValue); + + JFrame frame = new JFrame(); + frame.add(control.getControl(), BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(300, 50); + frame.validate(); + frame.setVisible(true); + + JSlider slider = frameFixture.slider().component(); + frameFixture.slider().slideTo(80); // 80 ticks is a probability of 0.8. + + control.reset(); + assert control.getNumberGenerator().nextValue().equals(defaultValue) : "NumberGenerator reset failed."; + assert slider.getValue() == 75 : "JSlider reset failed."; + + String displayedValue = frameFixture.label().text(); + assert displayedValue.equals("0.75") : "Wrong value displayed: " + displayedValue; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/SelectionStrategyControlTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/SelectionStrategyControlTest.java new file mode 100644 index 0000000..bd83fe6 --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/SelectionStrategyControlTest.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.swing; + +import java.util.LinkedList; +import java.util.List; +import javax.swing.JComboBox; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.EvaluatedCandidate; +import org.uncommons.watchmaker.framework.SelectionStrategy; +import org.uncommons.watchmaker.framework.selection.RankSelection; +import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection; +import org.uncommons.watchmaker.framework.selection.TruncationSelection; + +/** + * Unit test for the {@link SelectionStrategyControl}. + * @author Daniel Dyer + */ +public class SelectionStrategyControlTest +{ + @Test + public void testInitialisation() + { + SelectionStrategy<Object> rank = new RankSelection(); + SelectionStrategy<Object> roulette = new RouletteWheelSelection(); + List<SelectionStrategy<? super Object>> strategies = new LinkedList<SelectionStrategy<? super Object>>(); + strategies.add(rank); + strategies.add(roulette); + SelectionStrategyControl<?> control = new SelectionStrategyControl<Object>(strategies); + JComboBox component = control.getControl(); + assert component.getItemCount() == 2 : "Combobox should contain 2 entries, is " + component.getItemCount(); + assert component.getItemAt(0) == rank : "First item should be rank selection."; + assert component.getItemAt(1) == roulette : "Second item should be roulette wheel selection."; + } + + + @Test + public void testChangeSelection() + { + SelectionStrategy<Object> quarter = new TruncationSelection(0.25); + SelectionStrategy<Object> half = new TruncationSelection(0.5); + List<SelectionStrategy<? super Object>> strategies = new LinkedList<SelectionStrategy<? super Object>>(); + strategies.add(quarter); + strategies.add(half); + SelectionStrategyControl<Object> control = new SelectionStrategyControl<Object>(strategies); + + List<EvaluatedCandidate<String>> population = new LinkedList<EvaluatedCandidate<String>>(); + population.add(new EvaluatedCandidate<String>("DDD", 4)); + population.add(new EvaluatedCandidate<String>("CCC", 3)); + population.add(new EvaluatedCandidate<String>("BBB", 2)); + population.add(new EvaluatedCandidate<String>("AAA", 1)); + + // Using the first selection strategy, only the fittest 25% of candidates should be selected from. + List<String> selection = control.getSelectionStrategy().select(population, true, 2, null); + assert selection.get(0).equals("DDD") : "Wrong candidate selected: " + selection.get(0); + assert selection.get(1).equals("DDD") : "Wrong candidate selected: " + selection.get(1); + + JComboBox component = control.getControl(); + component.setSelectedIndex(1); // Switch to 50% truncation. + + // Using the second selection strategy, only the fittest 50% of candidates should be selected from. + selection = control.getSelectionStrategy().select(population, true, 2, null); + assert selection.contains("CCC") : "Candidate CCC missing from selection."; + assert selection.contains("DDD") : "Candidate DDD missing from selection."; + } + + + @Test(dependsOnMethods = "testChangeSelection") + public void testReset() + { + SelectionStrategy<Object> quarter = new TruncationSelection(0.25); + SelectionStrategy<Object> half = new TruncationSelection(0.5); + List<SelectionStrategy<? super Object>> strategies = new LinkedList<SelectionStrategy<? super Object>>(); + strategies.add(quarter); + strategies.add(half); + SelectionStrategyControl<Object> control = new SelectionStrategyControl<Object>(strategies); + + control.getControl().setSelectedIndex(1); // Not the first strategy. + control.reset(); // Reset to the first strategy. + + List<EvaluatedCandidate<String>> population = new LinkedList<EvaluatedCandidate<String>>(); + population.add(new EvaluatedCandidate<String>("DDD", 4)); + population.add(new EvaluatedCandidate<String>("CCC", 3)); + population.add(new EvaluatedCandidate<String>("BBB", 2)); + population.add(new EvaluatedCandidate<String>("AAA", 1)); + + // Using the first selection strategy, only the fittest 25% of candidates should be selected from. + List<String> selection = control.getSelectionStrategy().select(population, true, 2, null); + assert selection.get(0).equals("DDD") : "Wrong candidate selected: " + selection.get(0); + assert selection.get(1).equals("DDD") : "Wrong candidate selected: " + selection.get(1); + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/SwingConsoleTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/SwingConsoleTest.java new file mode 100644 index 0000000..56f4f41 --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/SwingConsoleTest.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.swing; + +import java.awt.BorderLayout; +import java.util.Arrays; +import java.util.List; +import javax.swing.JFrame; +import javax.swing.JLabel; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.Test; + +/** + * Unit test for the {@link SwingConsole} class. + * @author Daniel Dyer + */ +public class SwingConsoleTest +{ + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testUserSelection() throws InterruptedException + { + Robot robot = BasicRobot.robotWithNewAwtHierarchy(); + final SwingConsole swingConsole = new SwingConsole(); + JFrame frame = new JFrame(); + frame.add(swingConsole, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(300, 100); + frame.validate(); + frame.setVisible(true); + + final List<JLabel> labels = Arrays.asList(new JLabel("Zero"), + new JLabel("One"), + new JLabel("Two")); + + final int[] selection = new int[1]; + new Thread(new Runnable() + { + public void run() + { + // This method blocks so we can't run it on the test thread. + selection[0] = swingConsole.select(labels); + } + }).start(); + Thread.sleep(250); // TO DO: Come up with a proper solution to this race condition. + frameFixture.button("Selection-1").click(); + + assert selection[0] == 1 + : "Second item (index 1) should have been selected, selection index was " + selection[0]; + + robot.cleanUp(); + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/EvolutionMonitorTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/EvolutionMonitorTest.java new file mode 100644 index 0000000..c3fb9e8 --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/EvolutionMonitorTest.java @@ -0,0 +1,107 @@ +//============================================================================= +// Copyright 2006-2010 Daniel W. Dyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//============================================================================= +package org.uncommons.watchmaker.swing.evolutionmonitor; + +import java.awt.Dialog; +import java.awt.Frame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.DialogMatcher; +import org.fest.swing.core.matcher.FrameMatcher; +import org.fest.swing.fixture.DialogFixture; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Basic unit test for the {@link EvolutionMonitor} component. + * @author Daniel Dyer + */ +public class EvolutionMonitorTest +{ + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testShowInFrame() + { + EvolutionMonitor<String> monitor = new EvolutionMonitor<String>(); + monitor.showInFrame("MonitorFrame", false); + robot.waitForIdle(); + // There ought to be a visible frame containing the evolution monitor. + Frame frame = robot.finder().find(FrameMatcher.withTitle("MonitorFrame").andShowing()); + assert monitor.getGUIComponent().isShowing() : "Evolution monitor should be showing."; + FrameFixture frameFixture = new FrameFixture(robot, frame); + frameFixture.close(); + robot.waitForIdle(); + assert !monitor.getGUIComponent().isShowing() : "Evolution monitor should not be showing."; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testShowInDialog() + { + EvolutionMonitor<String> monitor = new EvolutionMonitor<String>(); + monitor.showInDialog(null, "MonitorDialog", false); + robot.waitForIdle(); + // There ought to be a visible dialog containing the evolution monitor. + Dialog dialog = robot.finder().find(DialogMatcher.withTitle("MonitorDialog").andShowing()); + assert monitor.getGUIComponent().isShowing() : "Evolution monitor should be showing."; + DialogFixture dialogFixture = new DialogFixture(robot, dialog); + dialogFixture.close(); + robot.waitForIdle(); + assert !monitor.getGUIComponent().isShowing() : "Evolution monitor should not be showing."; + } + + + /** + * If the evolution monitor is already displayed in a window, a subsequent call to one of + * the show methods should result in that window being replaced. + */ + @Test(dependsOnMethods = {"testShowInFrame", "testShowInDialog"}, + groups = "display-required") // Will fail if run in a headless environment. + public void testShowInFrameThenShowInDialog() + { + EvolutionMonitor<String> monitor = new EvolutionMonitor<String>(true); + monitor.showInFrame("MonitorFrame", false); + robot.waitForIdle(); + // There ought to be a visible frame containing the evolution monitor. + Frame frame = robot.finder().find(FrameMatcher.withTitle("MonitorFrame").andShowing()); + + monitor.showInDialog(null, "MonitorDialog", false); + robot.waitForIdle(); + // There ought to be a visible dialog containing the evolution monitor. + robot.finder().find(DialogMatcher.withTitle("MonitorDialog").andShowing()); + assert monitor.getGUIComponent().isShowing() : "Evolution monitor should be showing."; + + assert !frame.isShowing() : "Frame should have been replaced by dialog."; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/FittestCandidateViewTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/FittestCandidateViewTest.java new file mode 100644 index 0000000..4411423 --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/FittestCandidateViewTest.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.swing.evolutionmonitor; + +import java.awt.BorderLayout; +import java.math.BigDecimal; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.text.JTextComponent; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; +import org.uncommons.watchmaker.framework.interactive.Renderer; +import org.uncommons.watchmaker.swing.ObjectSwingRenderer; + +/** + * Unit test for the {@link FittestCandidateView} evolution monitor panel. + * @author Daniel Dyer + */ +public class FittestCandidateViewTest +{ + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") + public void testUpdate() + { + Renderer<Object, JComponent> renderer = new ObjectSwingRenderer(); + FittestCandidateView<BigDecimal> view = new FittestCandidateView<BigDecimal>(renderer); + JFrame frame = new JFrame(); + frame.add(view, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(300, 300); + frame.validate(); + frame.setVisible(true); + + view.populationUpdate(new PopulationData<BigDecimal>(BigDecimal.TEN, 10, 5, 2, true, 5, 0, 1, 100)); + robot.waitForIdle(); + + // Check displayed fitness. + String fitnessText = frameFixture.label("FitnessLabel").text(); + assert fitnessText.equals("10.0") : "Wrong fitness score displayed: " + fitnessText; + + // Check rendered candidate. + frameFixture.textBox().requireNotEditable(); + String text = frameFixture.textBox().component().getText(); + assert text.equals("10") : "Candidate rendered incorrectly."; + } + + + /** + * If the view is updated with the same candidate it is already displaying, it should + * avoid the expense of re-rendering it. + */ + @Test(groups = "display-required", + dependsOnMethods = "testUpdate") + public void testUpdateSameCandidate() + { + Renderer<Object, JComponent> renderer = new ObjectSwingRenderer(); + FittestCandidateView<BigDecimal> view = new FittestCandidateView<BigDecimal>(renderer); + JFrame frame = new JFrame(); + frame.add(view, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(300, 300); + frame.validate(); + frame.setVisible(true); + + PopulationData<BigDecimal> data1 = new PopulationData<BigDecimal>(BigDecimal.TEN, 10, 5, 2, true, 5, 0, 1, 100); + // Render the first time. + view.populationUpdate(data1); + robot.waitForIdle(); + JTextComponent component1 = frameFixture.textBox().component(); + + // Render the same candidate for the second generation. + PopulationData<BigDecimal> data2 = new PopulationData<BigDecimal>(BigDecimal.TEN, 10, 5, 2, true, 5, 0, 2, 100); + view.populationUpdate(data2); + robot.waitForIdle(); + JTextComponent component2 = frameFixture.textBox().component(); + + assert component1 == component2 : "Rendered component should be the same."; + } +} diff --git a/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/StatusBarTest.java b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/StatusBarTest.java new file mode 100644 index 0000000..8dc416b --- /dev/null +++ b/watchmaker/swing/src/java/test/org/uncommons/watchmaker/swing/evolutionmonitor/StatusBarTest.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.swing.evolutionmonitor; + +import java.awt.BorderLayout; +import javax.swing.JFrame; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.FrameFixture; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.uncommons.watchmaker.framework.PopulationData; + +/** + * Unit test for the {@link StatusBar} class. + * @author Daniel Dyer + */ +public class StatusBarTest +{ + private Robot robot; + + @BeforeMethod + public void prepare() + { + robot = BasicRobot.robotWithNewAwtHierarchy(); + } + + + @AfterMethod + public void cleanUp() + { + robot.cleanUp(); + robot = null; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testFieldUpdates() + { + StatusBar statusBar = new StatusBar(); + JFrame frame = new JFrame(); + frame.add(statusBar, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 30); + frame.validate(); + frameFixture.show(); + + assert frameFixture.label("Population").text().equals("N/A") : "Wrong initial text for population label."; + assert frameFixture.label("Elitism").text().equals("N/A") : "Wrong initial text for elitism label."; + assert frameFixture.label("Generations").text().equals("N/A") : "Wrong initial text for generations label."; + assert frameFixture.label("Time").text().equals("N/A") : "Wrong initial text for elapsed time label."; + + statusBar.populationUpdate(new PopulationData<Object>(new Object(), 10, 8, 2, true, 10, 1, 0, 36610000)); + assert frameFixture.label("Population").text().equals("10") : "Wrong value for popluation label."; + assert frameFixture.label("Elitism").text().equals("1") : "Wrong value for elitism label."; + // Generation count is number + 1 (because generations start at zero). + assert frameFixture.label("Generations").text().equals("1") : "Wrong value for generations label."; + assert frameFixture.label("Time").text().equals("10:10:10") : "Wrong value for elapsed time label."; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testFieldUpdatesForIslandMode() + { + StatusBar statusBar = new StatusBar(true); + JFrame frame = new JFrame(); + frame.add(statusBar, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 30); + frame.validate(); + frameFixture.show(); + + assert frameFixture.label("Population").text().equals("N/A") : "Wrong initial text for population label."; + assert frameFixture.label("Elitism").text().equals("N/A") : "Wrong initial text for elitism label."; + assert frameFixture.label("Generations").text().equals("N/A") : "Wrong initial text for generations label."; + assert frameFixture.label("Time").text().equals("N/A") : "Wrong initial text for elapsed time label."; + + statusBar.islandPopulationUpdate(0, new PopulationData<Object>(new Object(), 10, 8, 2, true, 10, 1, 0, 36610000)); + statusBar.populationUpdate(new PopulationData<Object>(new Object(), 10, 8, 2, true, 50, 1, 0, 36610000)); + assert frameFixture.label("Population").text().equals("5x10") : "Wrong value for popluation label."; + assert frameFixture.label("Elitism").text().equals("5x1") : "Wrong value for elitism label."; + // Generation count is number + 1 (because generations start at zero). + assert frameFixture.label("Generations").text().equals("1") : "Wrong value for generations label."; + assert frameFixture.label("Time").text().equals("10:10:10") : "Wrong value for elapsed time label."; + } + + + @Test(groups = "display-required") // Will fail if run in a headless environment. + public void testTimeFormat() + { + StatusBar statusBar = new StatusBar(); + JFrame frame = new JFrame(); + frame.add(statusBar, BorderLayout.CENTER); + FrameFixture frameFixture = new FrameFixture(robot, frame); + frame.setSize(400, 30); + frame.validate(); + frameFixture.show(); + + // Previous test checks two-digit field values, this test checks that single-digit + // values and zeros are correctly padded. + statusBar.populationUpdate(new PopulationData<Object>(new Object(), 10, 8, 2, true, 10, 1, 0, 1000)); + assert frameFixture.label("Time").text().equals("00:00:01"); + } + +} diff --git a/watchmaker/swing/swing.iml b/watchmaker/swing/swing.iml new file mode 100644 index 0000000..09d2997 --- /dev/null +++ b/watchmaker/swing/swing.iml @@ -0,0 +1,31 @@ +<?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="module" module-name="framework" /> + <orderEntry type="library" name="Maven: org.uncommons.maths:uncommons-maths:1.2.2" level="project" /> + <orderEntry type="library" name="Maven: jfree:jcommon:1.0.12" level="project" /> + <orderEntry type="library" name="Maven: jfree:jfreechart:1.0.13" level="project" /> + <orderEntry type="library" name="Maven: com.google.collections:google-collections:1.0" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.testng:testng:6.2.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: junit:junit:3.8.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.beanshell:bsh:2.0b4" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: com.beust:jcommander:1.12" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.yaml:snakeyaml:1.6" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-swing:1.2.1" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-assert:1.2" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-util:1.1.3" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: org.easytesting:fest-reflect:1.2" level="project" /> + <orderEntry type="library" scope="TEST" name="Maven: net.jcip:jcip-annotations:1.0" level="project" /> + </component> +</module> + |