CellSim#
CellSim is a SFML app in which some basic shapes will evolve behaviors to hunt or scavenger food(other cells).
This example uses an Observer Population
meaning that the Population
is not the owner of the data, you use the observer Population when you give it a T* as the template parameter.
Cell#
The Cell class needs to fulfill meta::is_populable_v<Cell>
to be able to use it in Population
reproduce
and distance
are static methods.
The Cell
also needs to have a constructor
that takes a Genome
and
a EvoVector<std::uint8_t>
to be able to have children from a pair of Cells.
Cell(JsonBox::Object o) noexcept{
// omitted
}
JsonBox::Value toJson() noexcept{
// ommited
}
void Cell::setID(std::size_t ID) noexcept{
evoVector.setID(ID);
genome.setID(ID);
}
std::size_t Cell::getID() const noexcept{
return evoVector.getID();
}
void Cell::setSpeciesID(std::size_t spcID) noexcept{
evoVector.setSpeciesID(spcID);
genome.setSpeciesID(spcID);
}
void Cell::setFitness(double fit) noexcept{
evoVector.setFitness(fit);
genome.setFitness(fit);
}
void Cell::addFitness(double amount) noexcept{
evoVector.addFitness(amount);
genome.addFitness(amount);
}
double Cell::getFitness() const noexcept{
return evoVector.getFitness();
}
std::size_t Cell::getSpeciesID() const noexcept{
return evoVector.getSpeciesID();;
}
// more code
Cell Cell::reproduce(const Cell& cell1, const Cell& cell2) noexcept{
return Cell(Genome::reproduce(cell1.genome, cell2.genome), EvoVector<std::uint8_t>::reproduce(cell1.evoVector, cell2.evoVector));
}
double Cell::distance(const Cell& cell1, const Cell& cell2,
[[maybe_unused]] double c1,
[[maybe_unused]] double c2,
[[maybe_unused]] double c3) noexcept{
double gDistance = Genome::distance(cell1.genome, cell2.genome) * c1;
double esDistance = cell1.getBodyType() != cell2.getBodyType() ? 5.0:0.0;
for(auto i=1u;i<cell1.evoVector.size();++i){
esDistance += static_cast<double>(cell1.evoVector[i] != cell2.evoVector[i]) * c2;
}
return (gDistance + esDistance) / 2.0;
}
Population Management#
The population Management consist on creating and replacing those that died with new cells from the best of the population
in which we use an specialized Tournament
to select the best and set the losers from the dead cells.
CreateCells#
When first creating the Population<Cell*>
we will need to create the initial population
which is handled in the if condition, the else condition will handle the replacement
of dead cells with new cells.
The method will return a Cell*
which then Population
will use addMember
to
add it to the Population
and assign or create its own Species
.
Cell* CellSim::createCells() noexcept{
// handle initial creation of population and regrowPopulation
if(cells.size() < opts.maxCellNum){
auto& c = cells.emplace_back(makeCellGenome());
c.setID(genID());
c.setPosition(randomGen().random(0.f, bounds.width),
randomGen().random(0.f, bounds.height));
lastAlive = cells.size() - 1;
return &c;
}else{
if(lastAlive < (opts.maxCellNum - 1)){
++lastAlive;
}
Cell* c = &cells[lastAlive];
auto pos = c->getPosition();
*c = makeCellGenome();
c->setPosition(pos);
c->setID(genID());
return c;
}
}
Replace#
This method is responsable for taking the toReplace
and toAdd
vectors from Population::reproduce
and
process them.
the if(toReplace.empty())
is to handle Population::regrowPopulationFromElites
in case we want to call it
instead of Population::regrowPopulation
.
The else if
is the most likely path to be executed as Tournament
will always return an empty selection
if there are not dead cells.
We call Population::removeMember
to remove the dead cell from Population<Cell*>
and his Species
if is the last member alive, then we will call Population::addMember
for the new Cell
to be added or assigned a new Species
.
void CellSim::replace(std::vector<Cell*>& toReplace, std::vector<Cell>& toAdd) noexcept{
if(toReplace.empty()){
for(auto& add:toAdd){
if(std::distance(std::begin(cells) + lastAlive, std::end(cells)) > 0){
Cell* c = &cells[++lastAlive];
auto pos = c->getPosition();
pop->removeMember(*c);
*c = std::move(add);
c->setPosition(pos);
c->setID(genID());
pop->addMember(c, opts.coefficients[0], opts.coefficients[1], opts.coefficients[2]);
}else{
auto& c = cells.emplace_back(std::move(add));
c.setID(genID());
c.setPosition(randomGen().random(0.f, bounds.width),
randomGen().random(0.f, bounds.height));
pop->addMember(&c, opts.coefficients[0], opts.coefficients[1], opts.coefficients[2]);
if(lastAlive < (opts.maxCellNum - 1)){
++lastAlive;
}
}
}
}else if(toReplace.size() == toAdd.size()){
for(auto i=0u;i<toAdd.size();++i){
auto pos = toReplace[i]->getPosition();
pop->removeMember(*toReplace[i]);
toAdd[i].setID(genID());
toAdd[i].setPosition(pos);
*toReplace[i] = std::move(toAdd[i]);
pop->addMember(toReplace[i], opts.coefficients[0], opts.coefficients[1], opts.coefficients[2]);
if(lastAlive < (opts.maxCellNum - 1)){
++lastAlive;
}
}
}
}
removeCellsFromSpecies#
This method will be responsable to kill the cells who species is over maxAge
set in Population
after we call
Population::increaseAgeAndRemoveOldSpecies
, we call this function to increase the age of
the Species
and if their avg Fitness is stagnant(it doesn’t get better) their age is increased x2 and is sooner removed from the
Population
.
void CellSim::removeCellsFromSpecies(std::vector<std::size_t>&& ids) noexcept{
for(auto& id:ids){
for(auto i=0u;i<(lastAlive + 1);++i){
if(cells[i].getSpeciesID() == id){
std::swap(cells[i], cells[lastAlive]);
if(lastAlive > 0){
--lastAlive;
}
}
}
}
}
nextGeneration#
This method will advance to the next generation, we use an specialized Tournament
(we show it on the next section).
When we call Population::reproduce
it will return a pair of two vectors, one std::vector<Cell*>
and a std::vector<Cell>
the first one are the dead cells and the second the children from the best cells.
void CellSim::nextGeneration() noexcept{
avgs->calcAvgs(*pop);
// we use an specialization of Tournament<Cell*> to only select those cells that are not alive. ("Tournament.hpp")
auto sa = SelectionAlgorithms::Tournament<Cell*>{opts.maxCellNum, opts.rounds};
auto res = pop->reproduce(sa, opts.interspecies);
removeCellsFromSpecies(pop->increaseAgeAndRemoveOldSpecies());
replace(res.first, res.second);
// remove dead cells to let the pop regrow in case of extinction
for(auto i=(lastAlive+1);i<cells.size();++i){
pop->removeMember(cells[i]);
}
pop->regrowPopulation([this](){
return this->createCells();
}, opts.coefficients[0], opts.coefficients[1], opts.coefficients[2]);
++gen;
genInfo.setString("Generation: " + std::to_string(gen) + " - AVG Fitness: " +
std::to_string(pop->computeAvgFitness()) + " - Species: " +
std::to_string(pop->getSpeciesSize()));
}
Tournament specialization#
As said before the Tournament
is specialized to Cell*
This code is the one for interspecies reproduction
template<>
std::vector<Selected<Cell*>> Tournament<Cell*>::operator()(std::vector<std::remove_pointer_t<Cell*>*>& members, [[maybe_unused]] std::size_t numberToSelect) noexcept{
std::vector<Selected<Cell*>> selected;
std::vector<pointer> livingCells;
livingCells.reserve(members.size());
std::queue<pointer> deadCells;
for(auto& cell:members){
if(cell->isAlive()){
livingCells.emplace_back(cell);
}else if(!cell->canBeEaten()){
deadCells.emplace(cell);
}
}
if((members.size() == livingCells.size()) || livingCells.empty()){
return selected;
}
selected.reserve(deadCells.size());
while(!deadCells.empty()){
auto father = fight(livingCells);
auto mother = fight(livingCells);
if(father.first == mother.first){
mother = fight(livingCells);
}
auto cell = deadCells.front();
deadCells.pop();
selected.emplace_back(father.first, mother.first, cell);
}
return selected;
}
This code is the one for Species
reproduction.
template<>
std::vector<Selected<Cell*>> Tournament<Cell*>::operator()(std::map<std::size_t, std::unique_ptr<Species<Cell*>>>& species, std::size_t numberToSelect) noexcept{
std::vector<Selected<Cell*>> selected;
selected.reserve(numberToSelect);
for(auto& [id, sp]: species){
auto& members = sp->getMembers();
std::vector<pointer> livingCells;
livingCells.reserve(members.size());
std::queue<pointer> deadCells;
for(auto& cell:members){
if(cell->isAlive()){
livingCells.emplace_back(cell);
}else if(!cell->canBeEaten()){
deadCells.emplace(cell);
}
}
if(livingCells.empty()){
continue;
}
while(!deadCells.empty()){
auto father = fight(livingCells);
auto mother = fight(livingCells);
if(father.first == mother.first){
mother = fight(livingCells);
}
auto cell = deadCells.front();
deadCells.pop();
selected.emplace_back(father.first, mother.first, cell);
}
}
return selected;
}
the full code is here