Skip to content

Development Tutorial Birth Death Cycle

Matthew Andres Moreno edited this page May 13, 2023 · 1 revision

This document describes the interaction between organisms and the population.

 

The Death of an Organism

When an organism is killed off, its location in the population (that is, its population cell) needs to be emptied, the scheduler needs to be notified, and various statistics need to be updated to reflect the recently deceased. All of this is handled with the method below. In a default Avida setup, this method will only be called when a cell needs to be cleared out for a new occupant, but you've already seen this method used when you implemented the kill-related events in a previous homework. We'll see it also called from code displayed in the next section of this document.

void cPopulation::KillOrganism(cPopulationCell& in_cell)
{
  // do we actually have something to kill?
  if (in_cell.IsOccupied() == false) return;

  // Statistics...
  cOrganism* organism = in_cell.GetOrganism();
  cGenotype* genotype = organism->GetGenotype();
  m_world->GetStats().RecordDeath();

  num_organisms--;
  genotype->RemoveOrganism();

  // And clear it!
  in_cell.RemoveOrganism();
  if (!organism->GetIsRunning()) delete organism;
  else organism->GetPhenotype().SetToDelete();

  // Alert the scheduler that this cell has a 0 merit.
  schedule->Adjust(in_cell.GetID(), cMerit(0));

  // Update the archive (note: genotype adjustment may be deferred)
  m_world->GetClassificationManager().AdjustGenotype(*genotype);
}

This method takes as an argument the cell that needs to be emptied. It starts off by making sure that the cell in question actually has an organism in it to be killed. If not, it stops right there. If so, it records some statistics about that organism's life, and updates its counter of living organisms to reflect that there is one fewer.

Once the statistics are finished, the cell itself is cleared with the cPopulationCell::RemoveOrganism() method (in which the pointer to the organism it once contained is set to NULL). At this point if the organism is not the currently running CPU it will be deleted. Otherwise, a flag is set that marks the organism for deletion after the current instruction has finished executing. Finally, the scheduler (which is the object that doles out CPU cycles to the individual organisms) is updated to reflect that this cell is now empty.

 

Activating an Organism in a Specific Cell

If an organism is going to be placed into a specific cell of the population, the method ActivateOrganism can be called on the population, telling it the location in memory of the organism to be placed and the cell to place it in. This method will call the KillOrganism() method to make sure the cell is unoccupied. This method is called from the Inject() method as well as ActivateOffspring(), described below. Here is the ActivateOrganism method:

void cPopulation::ActivateOrganism(cAvidaContext& ctx, cOrganism* in_organism, cPopulationCell& target_cell)
{
  in_organism->SetOrgInterface(new cPopulationInterface(m_world));

  // If the organism does not have a genotype, give it one!  No parent
  // information is provided so we must set parents to NULL.
  if (in_organism->GetGenotype() == NULL) {
    cGenotype* new_genotype = m_world->GetClassificationManager().GetGenotype(in_organism->GetGenome(), NULL, NULL);
    in_organism->SetGenotype(new_genotype);
  }
  cGenotype* in_genotype = in_organism->GetGenotype();

  // Save the old genotype from this cell...
  cGenotype* old_genotype = NULL;
  if (target_cell.IsOccupied()) {
    old_genotype = target_cell.GetOrganism()->GetGenotype();
    
    // Sometimes a new organism will kill off the last member of its genotype
    // in the population.  Normally this would remove the genotype, so we
    // want to defer adjusting that genotype until the new one is placed.
    old_genotype->IncDeferAdjust();
  }

  // Update the contents of the target cell.
  KillOrganism(target_cell);
  target_cell.InsertOrganism(*in_organism);

  // Update the archive...
  in_genotype->AddOrganism();

  if (old_genotype != NULL) {
    old_genotype->DecDeferAdjust();
    m_world->GetClassificationManager().AdjustGenotype(*old_genotype);
  }
  m_world->GetClassificationManager().AdjustGenotype(*in_genotype);

  // Initialize the time-slice for this new organism.
  schedule->Adjust(target_cell.GetID(), in_organism->GetPhenotype().GetMerit());

  // Special handling for certain birth methods.
  if (m_world->GetConfig().BIRTH_METHOD.Get() == POSITION_CHILD_FULL_SOUP_ELDEST) {
    reaper_queue.Push(&target_cell);
  }

  num_organisms++;

  // Statistics...
  m_world->GetStats().RecordBirth(target_cell.GetID(), in_genotype->GetID(),
                                  in_organism->GetPhenotype().ParentTrue());
}

The first thing we do build a new interface object and attach it to the organism object. Next we check to see if the organism has already been assigned its genotype. If an organism was born from a parent in the population, it will have been assigned a genotype by the time this method is called. If it does not have a genotype, however, the classification manager object will be called to look up any genotypes that match this genome. The classification manager will either return an exact match, or else create a new genotype, add it to the archive, and return its pointer. In either case, we now have a genotype for this organism.

Before we erase the organism currently in this cell, we want to keep track of what genotype it was part of for use in updating the archive later. We then kill the organism in the cell (as described above) and insert the new one. The cPopulationCell::InsertOrganism() method will setup the organism based on the environmental conditions of this cell (mutation rate, tasks rewarded, etc), and store the organism for future use.

We then adjust the genotype to let it know a new organism of its type has been created, and tell the classification manager that it should also adjust the genotypes to reflect their new abundances (one genotype has grown by one, the other has shrunk, so the genotype ordering may change). Other maintenance we need to do at this point includes adjusting the scheduler to let it know the merit of this new organism, and the reaper_queue if we keep track of the birth order of organisms so that we can always kill off the oldest in the population.

Finally, we adjust some more statistics by incrementing the number of organisms in the population and let the statistics object know that a new organism was born, with all of its information. Remember, if this cell was already occupied, KillOrganism() would have decremented it, so this will properly reflect the number of organisms alive in the population at any moment.

 

Placing an Offspring in the Population

When an organism gives birth, we must collect some relevant statistics, which can best be accomplished in the population object. Then we must place the offspring into its own cell in the population. This is all done with the cPopulation::ActivateOffspring method. This method takes as arguments the parent organism and child genome that we're working with. It is called by the divide command via the population interface. As this method is quite long, refer to it in the source code in cPopulation.cc while reading the remainder of this section.

The first step in activating an offspring involves performing some book keeping on the parent's phenotype via the DivideReset() method. After this, the child genome is submitted to the birth chamber. The birth chamber is responsible for handling the details of reproduction, such as genome recombination in sexual populations. The SubmitOffspring() method will add organism objects to the child_array for each offspring produced.

The next section of code is in charge of finding where in the population each child organism should be placed. The cell of the parent is looked up, and then the PositionChild() method is called to determine the child's cell.

If the parent is not about to be killed off (due to being replaced by one of the children), we actually want to do a bit more work -- we need to adjust it in the schedule in case its merit has changed over this gestation cycle, and (if we are on a grid) we want to turn the child so that it is facing its parent.

Finally, we collect a bunch of statistics for the parent's genotype object, and we run ActivateOffspring for each offspring produced using the cell we have chosen in order to place the child into the population.

 

Injecting an Organism into the Population

Injecting a genome into the population that does not have a parent (such as with the Inject action) is somewhat easier to deal with. Basically, all that this method needs to do is build the organism with the proper genome, determine its genotype, setup its phenotype, put it into a test CPU to get its initial merit, and activate it! You should be able to go line-by-line through the code to see how exactly this happens for yourself. See cPopulation::InjectGenome, and more importantly cPopulation::InjectGenotype in cPopulation.cc.

Clone this wiki locally