-
Notifications
You must be signed in to change notification settings - Fork 3
Using an Environment
Sometimes, it's impossible to evaluate a chromosome without knowing information about it's surroundings, such as the rest of the population. (This, by the way, in the case in nature - where the fitness an individual depends on its environment and the way it interacts with the other individuals).
For cases like this, you can use an "environment". Let's use an example to see how it's done.
For this tutorial, we won't be solving a problem. Instead, we'll see how we can use the search engine to simulate a habitat.
Our habitat will have two types of chromosomes in it. Oc2Producers - who consume oxygen (O), and produce carbon dioxide (CO2), and OProducers - who consume carbon dioxide (CO2), and produce oxygen (O). To make our habitat more robust, all chromosomes will create more then they consume (that is, if a OProducer consumes 1 CO2, it will produce 2 Os).
The chromosome's fitness will depend on the O and CO2 in the environment. Thus, Oc2Producers will get a better evaluation if there is a lot of O in the environment, while OProducers will get a better evaluation if there is a lot of CO2 in the environment.
Let's start with an enum that will define the types of chromosomes:
enum ChromosomeType
{
OProducer,
Oc2Producer
}
Our habitat will contain 2 type of chromosomes: OProducer, Oc2Producer. The chromosome will get its type in its constructor.
class MyChromosome : IChromosome
{
private readonly Random random = new Random();
public ChromosomeType Type { get; private set; }
public MyChromosome(ChromosomeType type)
{
Type = type;
}
public double Evaluate()
{
// We don't need to implement this. Evaluations will be handled by our custom ChromosomeEvaluator.
throw new NotImplementedException();
}
/// <summary>
/// A mutation will change the type of a chromosome
/// </summary>
public void Mutate()
{
Type = Type == ChromosomeType.OProducer ? ChromosomeType.Oc2Producer : ChromosomeType.OProducer;
}
public override string ToString() => Type.ToString();
}
Our crossover manager will operate as following:
- If both chromosomes are OProducers: return a chromosome of type OProducer.
- If both chromosomes are Oc2Producer : return a chromosome of type Oc2Producer.
- In any other case: return an OProducerswith a probability of 0.5, and an Oc2Producer with a probability of 0.5.
class CrossoverManager : ICrossoverManager
{
private readonly Random random = new Random();
public IChromosome Crossover(IChromosome chromosome1, IChromosome chromosome2)
{
var type1 = ((MyChromosome) chromosome1).Type;
var type2 = ((MyChromosome) chromosome2).Type;
if (type1 == ChromosomeType.OProducer && type2 == ChromosomeType.OProducer)
return new MyChromosome(ChromosomeType.OProducer);
if (type1 == ChromosomeType.Oc2Producer && type2 == ChromosomeType.Oc2Producer)
return new MyChromosome(ChromosomeType.Oc2Producer);
return new MyChromosome(random.NextDouble() < 0.5 ? ChromosomeType.OProducer : ChromosomeType.Oc2Producer);
}
}
Our PopulationGenerator will generate chromosomes of type OProducers with a probability of 0.5, and one of type Oc2Producers with a probability of 0.5
class PopulationGenerator : IPopulationGenerator
{
private readonly Random random = new Random();
public IEnumerable<IChromosome> GeneratePopulation(int size)
{
var chromosomes = new IChromosome[size];
for (int i = 0; i < size; i++)
chromosomes[i] = new MyChromosome(random.NextDouble() < 0.5
? ChromosomeType.OProducer
: ChromosomeType.Oc2Producer);
return chromosomes;
}
}
Our environment will hold the amounts of O and OC2 available. To calculate this, it will go through the chromosomes in the population, and for every OProducer add 2 Os and reduce one OC2. For every Oc2Producer it will add 2 OC2s and reduce one O.
class MyEnvironment : IEnvironment
{
public void UpdateEnvierment(IChromosome[] chromosomes, int generation)
{
O = 0;
OC2 = 0;
foreach (var chromosome in chromosomes)
{
var myChromosome = (MyChromosome) chromosome;
if (myChromosome.Type == ChromosomeType.OProducer)
{
O += 2;
OC2 -= 1;
}
else
{
OC2 += 2;
O -= 1;
}
}
O = Math.Max(0, O);
OC2 = Math.Max(0, OC2);
}
public int O { get; private set; }
public int OC2 { get; private set; }
public override string ToString() => $"O: {O}; OC2 {OC2}";
Let's create the ChromosomeEvaluator. The ChromosomeEvaluator will evaluate the chromosomes based on the O and OC2 in the environment.
class ChromosomeEvaluator : IChromosomeEvaluator
{
private MyEnvironment environment;
public void SetEnvierment(IEnvironment environment)
{
this.environment = (MyEnvironment)environment;
}
public double Evaluate(IChromosome chromosome)
{
var type = ((MyChromosome) chromosome).Type;
double baseEvaluation = type == ChromosomeType.OProducer ? environment.OC2 : environment.O;
return (baseEvaluation + 1) / EnvironmentForm.POPULATION_SIZE;
}
Finally, lets put everything together and run the search:
var engine = new GeneticSearchEngineBuilder(POPULATION_SIZE, GENERATIONS, new CrossoverManager(),
new PopulationGenerator()).SetCustomChromosomeEvaluator(new ChromosomeEvaluator())
.SetEnvironment(new MyEnvironment()).SetMutationProbability(0.01).Build();
while (<SomeStopCondition>){
var result = engine.Next();
// Do something with the result here.
}
You can find the complete code (together with a poorly designed GUI), here.