/*
* This file is part of JGAP.
*
* JGAP offers a dual license model containing the LGPL as well as the MPL.
*
* For licensing information please see the file license.txt included with JGAP
* or have a look at the top of class org.jgap.Chromosome which representatively
* includes the JGAP license policy applicable for any file delivered with JGAP.
*/
package org.jgap.impl;
import java.io.*;
import java.math.*;
import java.util.*;
import org.jgap.*;
import org.jgap.util.*;
import gnu.trove.*;
/**
* A basic implementation of NaturalSelector that models a roulette wheel.
* When a Chromosome is added, it gets a number of "slots" on the wheel equal
* to its fitness value. When the select method is invoked, the wheel is
* "spun" and the Chromosome occupying the spot on which it lands is selected.
* Then the wheel is spun again and again until the requested number of
* Chromosomes have been selected. Since Chromosomes with higher fitness
* values get more slots on the wheel, there's a higher statistical probability
* that they'll be chosen, but it's not guaranteed.
*
* @author Neil Rotstan
* @author Klaus Meffert
* @since 1.0
*/
public class WeightedRouletteSelector
extends NaturalSelectorExt implements ICloneable {
/** String containing the CVS revision. Read out via reflection!*/
private final static String CVS_REVISION = "$Revision: 1.44 $";
//delta for distinguishing whether a value is to be interpreted as zero
private static final double DELTA = 0.000001d;
private static final BigDecimal ZERO_BIG_DECIMAL = new BigDecimal(0.0d);
/**
* Represents the "roulette wheel." Each key in the Map is a Chromosome
* and each value is an instance of the SlotCounter inner class, which
* keeps track of how many slots on the wheel each Chromosome is occupying.
*/
private THashMap m_wheel = new THashMap();
/**
* Keeps track of the total number of slots that are in use on the
* roulette wheel. This is equal to the combined fitness values of
* all Chromosome instances that have been added to this wheel.
*/
private double m_totalNumberOfUsedSlots;
/**
* An internal pool in which discarded SlotCounter instances can be stored
* so that they can be reused over and over again, thus saving memory
* and the overhead of constructing new ones each time.
*/
private transient Pool m_counterPool;
private WeightedRouletteSelConfig m_config
= new WeightedRouletteSelConfig();
/**
* Default constructor.<p>
* Attention: The configuration used is the one set with the static method
* Genotype.setConfiguration.
*
* @throws InvalidConfigurationException
*
* @author Klaus Meffert
* @since 2.0
*/
public WeightedRouletteSelector()
throws InvalidConfigurationException {
this(Genotype.getStaticConfiguration());
}
/**
* @param a_config the configuration to use
*
* @throws InvalidConfigurationException
*
* @author Klaus Meffert
* @since 3.0
*/
public WeightedRouletteSelector(Configuration a_config)
throws InvalidConfigurationException {
super(a_config);
m_counterPool = new Pool();
m_config.m_doublettesAllowed = false;
}
/**
* Add a chromosome instance to this selector's working pool of chromosomes.
*
* @param a_chromosomeToAdd the chromosom to add to the pool
*
* @author Neil Rotstan
* @author Klaus Meffert
* @since 1.0
*/
protected synchronized void add(final IChromosome a_chromosomeToAdd) {
// The "roulette wheel" is represented by a Map. Each key is a
// Chromosome and each value is an instance of the SlotCounter inner
// class. The counter keeps track of the total number of slots that
// each chromosome is occupying on the wheel (which is equal to the
// combined total of their fitness values). If the Chromosome is
// already in the Map, then we just increment its number of slots
// by its fitness value. Otherwise we add it to the Map.
// -----------------------------------------------------------------
SlotCounter counter = (SlotCounter) m_wheel.get(a_chromosomeToAdd);
if (counter != null) {
// The Chromosome is already in the map.
// -------------------------------------
counter.increment();
}
else {
// We need to add this Chromosome and an associated SlotCounter
// to the map. First, we reset the Chromosome's
// isSelectedForNextGeneration flag to false. Later, if the
// Chromosome is actually selected to move on to the next
// generation population by the select() method, then it will
// be set to true.
// ------------------------------------------------------------
a_chromosomeToAdd.setIsSelectedForNextGeneration(false);
// We're going to need a SlotCounter. See if we can get one
// from the pool. If not, construct a new one.
// --------------------------------------------------------
counter = (SlotCounter) m_counterPool.acquirePooledObject();
if (counter == null) {
counter = new SlotCounter();
}
counter.reset(a_chromosomeToAdd.getFitnessValue());
m_wheel.put(a_chromosomeToAdd, counter);
}
}
/**
* Select a given number of Chromosomes from the pool that will move on
* to the next generation population. This selection should be guided by
* the fitness values, but fitness should be treated as a statistical
* probability of survival, not as the sole determining factor. In other
* words, Chromosomes with higher fitness values should be more likely to
* be selected than those with lower fitness values, but it should not be
* guaranteed.
*
* @param a_howManyToSelect the number of Chromosomes to select
* @param a_to_pop the population the Chromosomes will be added to
*
* @author Neil Rotstan
* @author Klaus Meffert
* @since 1.0
*/
public synchronized void selectChromosomes(int a_howManyToSelect,
Population a_to_pop) {
RandomGenerator generator = getConfiguration().getRandomGenerator();
scaleFitnessValues();
// Build three arrays from the key/value pairs in the wheel map: one
// that contains the fitness values for each chromosome, one that
// contains the total number of occupied slots on the wheel for each
// chromosome, and one that contains the chromosomes themselves. The
// array indices are used to associate the values of the three arrays
// together (eg, if a chromosome is at index 5, then its fitness value
// and counter values are also at index 5 of their respective arrays).
// -------------------------------------------------------------------
Set entries = m_wheel.entrySet();
int numberOfEntries = entries.size();
double[] fitnessValues = new double[numberOfEntries];
double[] counterValues = new double[numberOfEntries];
IChromosome[] chromosomes = new IChromosome[numberOfEntries];
m_totalNumberOfUsedSlots = 0.0d;
Iterator entryIterator = entries.iterator();
for (int i = 0; i < numberOfEntries; i++) {
Map.Entry chromosomeEntry = (Map.Entry) entryIterator.next();
IChromosome currentChromosome =
(IChromosome) chromosomeEntry.getKey();
SlotCounter currentCounter =
(SlotCounter) chromosomeEntry.getValue();
fitnessValues[i] = currentCounter.getFitnessValue();
counterValues[i] = fitnessValues[i] //currentCounter.getFitnessValue()
* currentCounter.getCounterValue();
chromosomes[i] = currentChromosome;
// We're also keeping track of the total number of slots,
// which is the sum of all the counter values.
// ------------------------------------------------------
m_totalNumberOfUsedSlots += counterValues[i];
}
// To select each chromosome, we just "spin" the wheel and grab
// whichever chromosome it lands on.
// ------------------------------------------------------------
IChromosome selectedChromosome;
for (int i = 0; i < a_howManyToSelect; i++) {
selectedChromosome = spinWheel(generator, fitnessValues, counterValues,
chromosomes);
selectedChromosome.setIsSelectedForNextGeneration(true);
if (a_to_pop.contains(selectedChromosome)) {
ICloneHandler cloner = getConfiguration().getJGAPFactory().
getCloneHandlerFor(selectedChromosome, null);
if (cloner != null) {
try {
IChromosome cloned = (IChromosome) cloner.perform(
selectedChromosome, null, null);
a_to_pop.addChromosome(cloned);
if (m_monitorActive) {
cloned.setUniqueIDTemplate(selectedChromosome.getUniqueID(), 1);
}
} catch (Exception ex) {
ex.printStackTrace();
a_to_pop.addChromosome(selectedChromosome);
}
}
else {
a_to_pop.addChromosome(selectedChromosome);
if (m_monitorActive) {
selectedChromosome.setUniqueIDTemplate(selectedChromosome.
getUniqueID(), 1);
}
}
}
else {
a_to_pop.addChromosome(selectedChromosome);
if (m_monitorActive) {
selectedChromosome.setUniqueIDTemplate("new", 1);
}
}
}
}
/**
* This method "spins" the wheel and returns the Chromosome that is
* "landed upon." Each time a chromosome is selected, one instance of it
* is removed from the wheel so that it cannot be selected again.
*
* @param a_generator the random number generator to be used during the
* spinning process
* @param a_fitnessValues an array of fitness values of the respective
* Chromosomes
* @param a_counterValues an array of total counter values of the
* respective Chromosomes
* @param a_chromosomes the respective Chromosome instances from which
* selection is to occur
* @return selected Chromosome from the roulette wheel
*
* @author Neil Rotstan
* @author Klaus Meffert
* @since 1.0
*/
private IChromosome spinWheel(final RandomGenerator a_generator,
final double[] a_fitnessValues,
double[] a_counterValues,
final IChromosome[] a_chromosomes) {
// Randomly choose a slot on the wheel.
// ------------------------------------
double selectedSlot =
a_generator.nextDouble() * m_totalNumberOfUsedSlots;
if (selectedSlot > m_totalNumberOfUsedSlots) {
selectedSlot = m_totalNumberOfUsedSlots;
}
// Loop through the wheel until we find our selected slot. Here's
// how this works: we have three arrays, one with the fitness values
// of the chromosomes, one with the total number of slots on the
// wheel that each chromosome occupies (its counter value), and
// one with the chromosomes themselves. The array indices associate
// each of the three together (eg, if a chromosome is at index 5,
// then its fitness value and counter value are also at index 5 of
// their respective arrays).
//
// We've already chosen a random slot number on the wheel from which
// we want to select the Chromosome. We loop through each of the
// array indices and, for each one, we add the number of occupied slots
// (the counter value) to an ongoing total until that total
// reaches or exceeds the chosen slot number. When that happenes,
// we've found the chromosome sitting in that slot and we return it.
// --------------------------------------------------------------------
double currentSlot = 0.0d;
FitnessEvaluator evaluator = getConfiguration().getFitnessEvaluator();
boolean isFitter2_1 = evaluator.isFitter(2, 1);
for (int i = 0; i < a_counterValues.length; i++) {
// Increment our ongoing total and see if we've landed on the
// selected slot.
// ----------------------------------------------------------
boolean found;
if (isFitter2_1) {
// Introduced DELTA to fix bug 1449651
found = selectedSlot - currentSlot <= DELTA;
}
else {
// Introduced DELTA to fix bug 1449651
found = Math.abs(currentSlot - selectedSlot) <= DELTA;
}
if (found) {
// Remove one instance of the chromosome from the wheel by
// decrementing the slot counter by the fitness value resp.
// resetting the counter if doublette chromosomes are not
// allowed.
// -------------------------------------------------------
if (!getDoubletteChromosomesAllowed()) {
m_totalNumberOfUsedSlots -= a_counterValues[i];
a_counterValues[i] = 0;
}
else {
a_counterValues[i] -= a_fitnessValues[i];
m_totalNumberOfUsedSlots -= a_fitnessValues[i];
}
// Introduced DELTA to fix bug 1449651
if (Math.abs(m_totalNumberOfUsedSlots) < DELTA) {
m_totalNumberOfUsedSlots = 0.0d;
}
// Now return our selected Chromosome.
// -----------------------------------
return a_chromosomes[i];
}
else {
currentSlot += a_counterValues[i];
}
}
// We have reached here because there were rounding errors when
// computing with doubles or because the last entry is the right one.
// ------------------------------------------------------------------
return a_chromosomes[a_counterValues.length - 1];
}
/**
* Empty out the working pool of Chromosomes.
*
* @author Neil Rotstan
* @since 1.0
*/
public synchronized void empty() {
// Put all of the old SlotCounters into the pool so that we can
// reuse them later instead of constructing new ones.
// ------------------------------------------------------------
m_counterPool.releaseAllObjects(m_wheel.values());
// Now clear the wheel and reset the internal state.
// -------------------------------------------------
m_wheel.clear();
m_totalNumberOfUsedSlots = 0;
}
private void scaleFitnessValues() {
// First, add up all the fitness values. While we're doing this,
// keep track of the largest fitness value we encounter.
// -------------------------------------------------------------
double largestFitnessValue = 0.0;
BigDecimal totalFitness = ZERO_BIG_DECIMAL;
Iterator counterIterator = m_wheel.values().iterator();
while (counterIterator.hasNext()) {
SlotCounter counter = (SlotCounter) counterIterator.next();
if (counter.getFitnessValue() > largestFitnessValue) {
largestFitnessValue = counter.getFitnessValue();
}
BigDecimal counterFitness = new BigDecimal(counter.getFitnessValue());
totalFitness = totalFitness.add(counterFitness.multiply(
new BigDecimal(counter.getCounterValue())));
}
// Now divide the total fitness by the largest fitness value to
// compute the scaling factor.
// ------------------------------------------------------------
if (largestFitnessValue > 0.000000d
&& totalFitness.floatValue() > 0.0000001d) {
double scalingFactor =
totalFitness.divide(new BigDecimal(largestFitnessValue),
BigDecimal.ROUND_HALF_UP).doubleValue();
// Divide each of the fitness values by the scaling factor to
// scale them down.
// ----------------------------------------------------------
counterIterator = m_wheel.values().iterator();
while (counterIterator.hasNext()) {
SlotCounter counter = (SlotCounter) counterIterator.next();
counter.scaleFitnessValue(scalingFactor);
}
}
}
/**
* @return always false as some Chromosome's could be returnd multiple times
*
* @author Klaus Meffert
* @since 2.0
*/
public boolean returnsUniqueChromosomes() {
return false;
}
/**
* Not supported by this selector! Please do not use it.
*
* @param a_doublettesAllowed do not use
*
* @author Klaus Meffert
* @since 2.0
*/
public void setDoubletteChromosomesAllowed(final boolean a_doublettesAllowed) {
throw new IllegalStateException("Weighted roulette selector does not"
+" support this parameter,"
+" please do not use it!");
}
/**
* @return TRUE: doublette chromosomes allowed to be added by the selector
*
* @author Klaus Meffert
* @since 2.0
*/
public boolean getDoubletteChromosomesAllowed() {
return true;
}
/**
* @return deep clone of this instance
*
* @author Klaus Meffert
* @since 3.2
*/
public Object clone() {
try {
WeightedRouletteSelector result = new WeightedRouletteSelector(
getConfiguration());
result.m_wheel = (THashMap) m_wheel.clone();
result.m_config = new WeightedRouletteSelConfig();
result.m_config.m_doublettesAllowed = m_config.m_doublettesAllowed;
return result;
} catch (InvalidConfigurationException iex) {
throw new CloneException(iex);
}
}
public boolean equals(Object o) {
WeightedRouletteSelector other = (WeightedRouletteSelector) o;
if (other == null) {
return false;
}
if (m_totalNumberOfUsedSlots != other.m_totalNumberOfUsedSlots) {
return false;
}
if (other.m_config == null) {
return false;
}
if (m_config.m_doublettesAllowed != other.m_config.m_doublettesAllowed) {
return false;
}
if (other.m_counterPool == null) {
return false;
}
if (!m_wheel.equals(other.m_wheel)) {
return false;
}
return true;
}
class WeightedRouletteSelConfig
implements Serializable {
/**
* Allows or disallows doublette chromosomes to be added to the selector
*/
public boolean m_doublettesAllowed;
}
}
/**
* Implements a counter that is used to keep track of the total number of
* slots that a single Chromosome is occupying in the roulette wheel. Since
* all equal copies of a chromosome have the same fitness value, the increment
* method always adds the fitness value of the chromosome. Following
* construction of this class, the reset() method must be invoked to provide
* the initial fitness value of the Chromosome for which this SlotCounter is
* to be associated. The reset() method may be reinvoked to begin counting
* slots for a new Chromosome.
*
* @author Neil Rotstan
* @since 1.0
*/
class SlotCounter {
/**
* The fitness value of the Chromosome for which we are keeping count of
* roulette wheel slots. Although this value is constant for a Chromosome,
* it's not declared final here so that the slots can be reset and later
* reused for other Chromosomes, thus saving some memory and the overhead
* of constructing them from scratch.
*/
private double m_fitnessValue;
/**
* The current number of Chromosomes represented by this counter.
*/
private int m_count;
/**
* Resets the internal state of this SlotCounter instance so that it can
* be used to count slots for a new Chromosome.
*
* @param a_initialFitness the fitness value of the Chromosome for which
* this instance is acting as a counter
*
* @author Neil Rotstan
* @since 1.0
*/
public void reset(final double a_initialFitness) {
m_fitnessValue = a_initialFitness;
m_count = 1;
}
/**
* Retrieves the fitness value of the chromosome for which this instance
* is acting as a counter.
*
* @return the fitness value that was passed in at reset time
*
* @author Neil Rotstan
* @since 1.0
*/
public double getFitnessValue() {
return m_fitnessValue;
}
/**
* Increments the value of this counter by the fitness value that was
* passed in at reset time.
*
* @author Neil Rotstan
* @since 1.0
*/
public void increment() {
m_count++;
}
/**
* Retrieves the current value of this counter: ie, the number of slots
* on the roulette wheel that are currently occupied by the Chromosome
* associated with this SlotCounter instance.
*
* @return the current value of this counter
*/
public int getCounterValue() {
return m_count;
}
/**
* Scales this SlotCounter's fitness value by the given scaling factor.
*
* @param a_scalingFactor the factor by which the fitness value is to be
* scaled
*
* @author Neil Rotstan
* @since 1.0
*/
public void scaleFitnessValue(final double a_scalingFactor) {
m_fitnessValue /= a_scalingFactor;
}
}