package gdsc.smlm.ga;
import java.util.ArrayList;
import java.util.List;
import gdsc.core.logging.TrackProgress;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2015 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
/**
* Contains a population of individuals that may crossover and mutate to evolve.
* <p>
* For simplicity the individuals have one chromosome sequence.
*/
/*
* An extension would be to have an Individual class that has many Chromosomes, each is allowed to crossover with its
* matching pair and then segregation occurs to new individuals.
*/
public class Population<T extends Comparable<T>>
{
private List<? extends Chromosome<T>> individuals;
private int populationSize = 500;
private int failureLimit = 3;
private int iteration = 0;
// This introduces a dependency on another gdsc.smlm package
private TrackProgress tracker = null;
/**
* Create a population of individuals
*
* @param individuals
* The population of individuals
* @throws InvalidPopulationSize
* if the population is less than 2
*/
public Population(List<? extends Chromosome<T>> individuals)
{
if (individuals == null)
throw new InvalidPopulationSize(0, 1);
checkSize(individuals.size());
this.individuals = individuals;
}
private void checkSize(int size)
{
if (size < 1)
throw new InvalidPopulationSize(0, 1);
}
/**
* @return the individuals
*/
public List<? extends Chromosome<T>> getIndividuals()
{
return individuals;
}
/**
* Evolve the population of individuals until convergence of the most fit individual in the population.
* <p>
* The population will grow until the desired population size by recombination of individual pairs chosen from the
* population by the selection strategy. Child sequences will be subject to mutation. The fitness of all the
* individuals in the new population is evaluated and convergence checked for the fittest individual. If the initial
* population is small (<2 or <Chromosome<T>.length()) then mutation will be used to expand it before recombination.
* <p>
* The process of grow, evaluate, select is repeated until convergence.
* <p>
* Note: the subset of individuals selected for the next generation by the selection strategy will be unchanged
* (i.e. no mutation). This allows the fittest individuals to remain unchanged.
*
* @param mutator
* @param recombiner
* @param checker
* @throws InvalidPopulationSize
* if the population is less than 2 (this can occur after selection)
* @return The best individual
*/
public Chromosome<T> evolve(Mutator<T> mutator, Recombiner<T> recombiner, FitnessFunction<T> fitnessFunction,
SelectionStrategy<T> selectionStrategy, ConvergenceChecker<T> checker)
{
// Reset the fitness
for (Chromosome<T> c : individuals)
c.setFitness(null);
// Find the best individual
grow(selectionStrategy, mutator, recombiner);
Chromosome<T> current = evaluateFitness(fitnessFunction);
Chromosome<T> previous;
boolean converged = false;
while (!converged)
{
previous = current;
// Select the best individuals and expand the population
if (!select(selectionStrategy))
{
current = null;
break;
}
grow(selectionStrategy, mutator, recombiner);
// Evaluate the fitness and check convergence
current = evaluateFitness(fitnessFunction);
converged = checker.converged(previous, current);
}
if (tracker != null)
tracker.status("Converged [%d]", iteration);
return current;
}
private void grow(SelectionStrategy<T> selectionStrategy, Mutator<T> mutator, Recombiner<T> recombiner)
{
iteration++;
start("Grow");
if (individuals.size() >= populationSize)
return;
ArrayList<Chromosome<T>> newIndividuals = new ArrayList<Chromosome<T>>(populationSize - individuals.size());
// Check for a minimum population size & mutate the individuals to achieve it.
// This allows a seed population of 1 to evolve.
final int minSize = Math.max(2, individuals.get(0).length());
int target = minSize - individuals.size();
if (target > 0)
{
if (tracker != null)
tracker.progress(individuals.size(), populationSize);
int next = 0;
int fails = 0;
while (newIndividuals.size() < target && fails < failureLimit)
{
Chromosome<T> c = mutator.mutate(individuals.get(next++ % individuals.size()));
if (c != null && !isDuplicate(newIndividuals, c))
{
newIndividuals.add(c);
fails = 0;
if (tracker != null)
tracker.progress(newIndividuals.size() + individuals.size(), populationSize);
}
else
{
fails++;
}
}
// Combine the lists
newIndividuals.addAll(individuals);
individuals = newIndividuals;
if (individuals.size() < 2)
{
end();
return; // Failed to mutate anything to achieve a breeding population
}
}
// Now breed the population
selectionStrategy.initialiseBreeding(individuals);
target = populationSize - individuals.size();
int previousSize = -1;
int fails = 0;
while (newIndividuals.size() < target && fails < failureLimit)
{
previousSize = newIndividuals.size();
// Select two individuals for recombination
ChromosomePair<T> pair = selectionStrategy.next();
Chromosome<T>[] children = recombiner.cross(pair.c1, pair.c2);
if (children != null && children.length != 0)
{
// New children have been generated so mutate them
for (int i = 0; i < children.length && newIndividuals.size() < target; i++)
{
Chromosome<T> c = mutator.mutate(children[i]);
if (c == null)
continue;
// Ignore duplicates
if (isDuplicate(newIndividuals, c))
continue;
newIndividuals.add(c);
}
}
if (previousSize == newIndividuals.size())
fails++;
else
{
fails = 0;
if (tracker != null)
tracker.progress(newIndividuals.size() + individuals.size(), populationSize);
}
}
selectionStrategy.finishBreeding();
// Combine the lists
newIndividuals.addAll(individuals);
individuals = newIndividuals;
end();
}
/**
* Check for duplicates in the current and new populations
*
* @param newIndividuals
* The new population
* @param c
* The chromosome
* @return true if a duplicate
*/
private boolean isDuplicate(ArrayList<? extends Chromosome<T>> newIndividuals, Chromosome<T> c)
{
final double[] s = c.sequence();
for (Chromosome<T> i : this.individuals)
if (match(i, s))
return true;
for (Chromosome<T> i : newIndividuals)
if (match(i, s))
return true;
return false;
}
/**
* Check if a chromosome matches the sequence
*
* @param c
* The chromosome
* @param s
* The sequence
* @return True if a match
*/
private boolean match(Chromosome<T> c, double[] s)
{
final double[] s2 = c.sequence();
for (int i = 0; i < s.length; i++)
if (s[i] != s2[i])
return false;
return true;
}
/**
* Calculate the fitness of the population
*
* @param fitnessFunction
* @return The fittest individual
*/
private Chromosome<T> evaluateFitness(FitnessFunction<T> fitnessFunction)
{
start("Score");
Chromosome<T> best = null;
T max = null;
// Subset only those with no fitness score (the others must be unchanged)
ArrayList<Chromosome<T>> subset = new ArrayList<Chromosome<T>>(individuals.size());
long count = 0;
for (Chromosome<T> c : individuals)
{
final T f = c.getFitness();
if (f == null)
{
subset.add(c);
}
else
{
if (tracker != null)
tracker.progress(++count, individuals.size());
if (f.compareTo(max) < 0)
{
max = f;
best = c;
}
}
}
fitnessFunction.initialise(subset);
for (Chromosome<T> c : subset)
{
final T f = fitnessFunction.fitness(c);
c.setFitness(f);
if (f != null && f.compareTo(max) < 0)
{
max = f;
best = c;
}
if (tracker != null)
tracker.progress(++count, individuals.size());
}
fitnessFunction.shutdown();
end();
return best;
}
/**
* Select a subset of the population
*
* @param selection
* The selection strategy
* @return True if a valid population was selected (size>=1)
*/
private boolean select(SelectionStrategy<T> selection)
{
start("Select");
individuals = selection.select(individuals);
end();
return !individuals.isEmpty();
}
/**
* Get the population size limit to achieve when growing the population
*
* @return the populationSize
*/
public int getPopulationSize()
{
return populationSize;
}
/**
* Set the population size limit to achieve when growing the population
*
* @param populationSize
* the population size to set
*/
public void setPopulationSize(int populationSize)
{
checkSize(populationSize);
this.populationSize = populationSize;
}
/**
* Get the number of failed recombinations/mutations to allow before the stopping attempts to grow the population
*
* @return the failure limit
*/
public int getFailureLimit()
{
return failureLimit;
}
/**
* Set the number of failed recombinations/mutations to allow before the stopping attempts to grow the population
*
* @param failureLimit
* the failure limit
*/
public void setFailureLimit(int failureLimit)
{
this.failureLimit = failureLimit;
}
/**
* @return the tracker
*/
public TrackProgress getTracker()
{
return tracker;
}
/**
* Set a tracker to allow the progress to be followed
*
* @param tracker
* the tracker to set
*/
public void setTracker(TrackProgress tracker)
{
this.tracker = tracker;
}
/**
* Get the iteration. The iteration is increased each time the population grows as part of the [grow, evaluate,
* select] cycle.
*
* @return the iteration
*/
public int getIteration()
{
return iteration;
}
private void start(String stage)
{
if (tracker != null)
{
tracker.status(stage + " [%d]", iteration);
tracker.progress(0);
}
}
private void end()
{
if (tracker != null)
tracker.progress(1);
}
}