QHG Tutorial 04 - Parthenogenesis

In this chapter we describe a class whose agent can, in addition to dying and moving, perform asexual reproduction..

For this we need to add two new actions: Fertility and Verhulst.

Fertilty marks agents as fertile depending on gender and age, while Verhulst calculates birth probabilities based on local capacities and other parameters and registers a birth if conditions allow it (see Verhulst for details)

Population code:

Action code:

The Header File

This class must include the header files for the two new actions, and it also defines a derived agent structure which has an additional file for the agent’s age. This member is changed by GetOld, and read by OldAgeDeath.

#ifndef __TUT_PARTHENOPOP_H__
#define __TUT_PARTHENOPOP_H__

#include "SPopulation.h"

#include "GetOld.h"
#include "OldAgeDeath.h"
#include "RandomMove.h"

#include "Fertility.h"
#include "Verhulst.h"

#include "tut_ParthenoPop.h"

The agents need to member fields for reproduction:

struct tut_ParthenoAgent : Agent {

    float m_fAge;

    float m_fLastBirth;
    int m_iMateIndex;
};

m_fLastBirth records the step at which this agent had its last birth. MateIndex is required, because the Verhulst action can also handle sexual reproduction. For our purposes we’ll simply set an agents mate index to its own index.

The class gets two additional pointers for the new actions:

class tut_ParthenoPop : public  SPopulation<tut_ParthenoAgent> {
public:
    tut_ParthenoPop(SCellGrid *pCG, PopFinder *pPopFinder, int iLayerSize, IDGen **apIDG, uint32_t *aulState, uint *aiSeeds);
    virtual ~tut_ParthenoPop();

    int addPopSpecificAgentData(int iAgentIndex, char **ppData);
    void addPopSpecificAgentDataTypeQDF(hid_t *hAgentDataType);

int makePopSpecificOffspring(int iAgent, int iMother, int iFather);

protected:
    GetOld<tut_ParthenoAgent>       *m_pGO;
    OldAgeDeath<tut_ParthenoAgent>  *m_pOAD;
    RandomMove<tut_ParthenoAgent>   *m_pRM;

    Verhulst<tut_ParthenoAgent>     *m_pVerhulst;
    Fertility<tut_ParthenoAgent>    *m_pFert;

};

The input/output auxiliary functions addPopSpecificAgentData() and addPopSpecificAgentDataTypeQDF() will have to be modified to take into account the new agent member fields. Then there is also a new method makePopSpecificOffspring() which allows the population to perform specific operations after an agent is born (e.g. initialize member fields to particular values).

Code: tut_ParthenoPop.h

The Implementation

In the implementation we have to include hdf5.h because we need to modify some HDF5 structures:

#include <omp.h>
#include <hdf5.h>

Furthermore, we have to add includes of the cpp files for the new actions and include the header file of this class:

#include "SPopulation.cpp"
#include "LayerBuf.cpp"
#include "Prioritizer.cpp"
#include "Action.cpp"

#include "GetOld.cpp"
#include "OldAgeDeath.cpp"
#include "RandomMove.cpp"

#include "Verhulst.cpp"
#include "Fertility.cpp"

#include "tut_ParthenoPop.h"

The constructor now must create instances of all actions and add them to the priority list.

//----------------------------------------------------------------------------
// constructor
//
tut_ParthenoPop::tut_ParthenoPop(SCellGrid *pCG, PopFinder *pPopFinder, int iLayerSize, IDGen **apIDG, uint32_t *aulState, uint *aiSeeds)
    : SPopulation<tut_ParthenoAgent>(pCG, pPopFinder, iLayerSize, apIDG, aulState, aiSeeds) {


    m_pGO  = new GetOld<tut_ParthenoAgent>(this, m_pCG, "");
    m_pOAD = new OldAgeDeath<tut_ParthenoAgent>(this, m_pCG, "", m_apWELL);
    m_pRM  = new RandomMove<tut_ParthenoAgent>(this, m_pCG, "", m_apWELL);

    m_pVerhulst = new Verhulst<tut_ParthenoAgent>(this, m_pCG, "", m_apWELL);
    m_pFert     = new Fertility<tut_ParthenoAgent>(this, m_pCG, "");


    // adding all actions to prioritizer

    m_prio.addAction(m_pGO);
    m_prio.addAction(m_pOAD);
    m_prio.addAction(m_pRM);

    m_prio.addAction(m_pVerhulst);
    m_prio.addAction(m_pFert);

}

The destructor must delete all instances created in the constructor:

//----------------------------------------------------------------------------
// destructor
//
tut_ParthenoPop::~tut_ParthenoPop() {

    if (m_pGO != NULL) {
        delete m_pGO;
    }
    if (m_pOAD != NULL) {
        delete m_pOAD;
    }
    if (m_pRM != NULL) {
        delete m_pRM;
    }
     if (m_pVerhulst != NULL) {
        delete m_pVerhulst;
    }
    if (m_pFert != NULL) {
        delete m_pFert;
    }

}

The method addPopSpecificAgentData() must read the age and the last birth step from the input string. The mate index is volatile - it is set and used during a step, and is ignored otherwise. Therefore, we don’t bother to save it or read it:

//----------------------------------------------------------------------------
// addPopSpecificAgentData
// read additional data from pop file (the mat index is volatile, so we don't try to read it)
//
int tut_ParthenoPop::addPopSpecificAgentData(int iAgentIndex, char **ppData) {

    int iResult = 0;

    if (iResult == 0) {
        iResult = addAgentDataSingle(ppData, &m_aAgents[iAgentIndex].m_fAge);
    }

    if (iResult == 0) {
        iResult = addAgentDataSingle(ppData, &m_aAgents[iAgentIndex].m_fLastBirth);
    }

    return iResult;
}

The method addPopSpecificAgentDataTypeQDF() extends the agent’s HDF data type with the age field and the lastbirth field:

//----------------------------------------------------------------------------
// addPopSpecificAgentDataTypeQDF
// extend the agent data type to include additional data in the output
//
void tut_ParthenoPop::addPopSpecificAgentDataTypeQDF(hid_t *hAgentDataType) {

    tut_ParthenoAgent sa;
    H5Tinsert(*hAgentDataType, "Age",  qoffsetof(sa, m_fAge), H5T_NATIVE_FLOAT);
    H5Tinsert(*hAgentDataType, "LastBirth",  qoffsetof(sa, m_fLastBirth), H5T_NATIVE_FLOAT);

    // the mate index is volatile, so we don't add it to the type
}

The method makePopSpecificOffspring() initializes the agents’ new fields:

//----------------------------------------------------------------------------
// makePopSpecificOffspring
//
int tut_ParthenoPop::makePopSpecificOffspring(int iAgent, int iMother, int iFather) {
    int iResult = 0;

    m_aAgents[iAgent].m_fAge       = 0.0;
    m_aAgents[iAgent].m_fLastBirth = -1.0;
    m_aAgents[iAgent].m_iMateIndex = iAgent;
    m_aAgents[iAgent].m_iGender    = 0;

    return iResult;
}

The agent has age 0 at birth, hasn’t given birth so far, it will be its own mate (see above), and the gender is set to fremale.

Code: tut_ParthenoPop.cpp

XML and DAT

The xml file for the population attributes now contains entries for the new actions:

  <class name="tut_ParthenoPop" species_name="sapiens" species_id="104">

  <module name="OldAgeDeath">
    <param  name="OAD_max_age" value="60.0"/>
    <param  name="OAD_uncertainty" value="0.1"/>
  </module>
  <module name="RandomMove">
    <param  name="RandomMove_prob" value="0.01"/>
  </module>

  <module name="Fertility">
    <param  name="Fertility_interbirth" value="2.0"/>
    <param  name="Fertility_max_age" value="50.0"/>
    <param  name="Fertility_min_age" value="15.0"/>
  </module>

  <module name="Verhulst">
    <param  name="Verhulst_b0" value="0.8" />
    <param  name="Verhulst_d0" value="0.001" />
    <param  name="Verhulst_theta" value="0.1" />
    <param  name="Verhulst_K" value="20" />
  </module>

  <priorities>
    <prio  name="GetOld" value="2"/>
    <prio  name="OldAgeDeath" value="3"/>
    <prio  name="RandomMove" value="6"/>
    <prio  name="Fertility" value="4"/>
    <prio  name="Verhulst" value="5"/>
  </priorities>
</class>

For details about the parameters look at the code description for OldAgeDeath, RandomMove, Fertility, and Verhulst.

The priority levels are set such that in a step the agents die first, the survivors can the mate an move.

The dat file tut_Partheno.dat defines postions and attributes for 20 agents, the last attribute pertainng to the new member m_fAge.

#Longitude;Latitude;LifeState;AgentID;BirthTime;Gender;Age;lastbirth
8;47;1; 1;-10.0;0;10.0;-1.0
8;47;1; 2;-10.0;0;10.0;-1.0
8;47;1; 3;-10.0;0;10.0;-1.0
...
8;47;1;20;-10.0;0;10.0;-1.0

In this case all agents are placed at the same location, and all agents are female (Gender: 0=female, 1=male)

Feel free to modify the values in the xml file and see how this changes the outcome.

Grid

For this example we use a subdivided icosahedron

Compiling and Running

Again, we assume that you have created the tutorial directories with the script build_tut_dirs.py.

First lets create alternative population and action directories with the script build_alt_modpop.py:

${QHG4_DIR}/useful_stuff/build_alt_modpop.py tut_04 tut_ParthenoPop

This builds alternative population directories tut_04_pop and tut_04_mod with the files needed to compile tut_ParthenoPop.

To compile, change to the QHG4 root directory and type:

SHORT=tut_04 make clean QHGMain

This is the simplest way to compile QHG (for more information see Compiling QHG)

Now cd to the tutorial subdirectory “tutorial_04’ and start QHGMain

${QHG4_DIR}/app/QHGMain --read-config=tutorial_04.cfg > tutorial_04.out

This tells QHGMain to read the parameters from the file tutorial_04.cfg and write all output to the file tutorial_04.out (this output contains more information than the logfile)

The config file tutorial_04.cfg was created when the tutorial directories were created with build_tut_dirs.py.

--data-dirs=${QHG4_DIR}/tutorial_data/grids/
--events='write|grid+geo+pop:sapiens@20000'
--grid=ico32s.qdf
--log-file=tutorial_04.log
--num-iters=3000
--output-dir=output/tutorial_04
--output-prefix=tut
--pops='tut_Partheno.xml:tut_Partheno.dat'
--shuffle=92244
--start-time=-3000
data-dirs

A list of directories to search for data (not relevant for the OldAgeDie populations).

events

A list of events (see Events). Here a single event is defined: write output at step 20000 (or at the end of the simulation run).

grid

Path to the grid on which to run the simulation.

log-file

Log file for ‘high level’ messages.

num-iters

Number of iterations for the simulation.

output-dir

Output directory for qdf files.

output-prefix

A prefix prepended to the names of all output files

pops

The files for population attributes (tut_Partheno.xml) and agent attributes (tut_Partheno.dat). These files were created by build_tut_dirs.py.

shuffle

A kind of seed for the random number generators. Same shuffle values result in identical simulations.

start-time

The ‘real’ time corresponding to step 0.

The Output File

The output files, in this case tutorial_04.out, always have the same structure. First there are messages concerning the start of the program and intialisation. If the simulation does not run, there’s a chance that you find th readoon why here.

The second part is a list of every step describing the numbers of births, deaths and moves, as well as the time the step took to execute.

At the end there is a short summary of the state at the end of the program: number of iterations, total time of execution, nuber of surviving agents. This is follwed by the contents of the log file.

If you look at the output for the steps, you see that there now are births, deaths and moves:

After step 2995 (0.213805 s): total   215396 agents
  sapiens   22432 births r
  sapiens   21673 deaths r
  sapiens      22 births p
  sapiens    1637 moves
After step 2996 (0.212654 s): total   216155 agents
  sapiens   21734 births r
  sapiens   22148 deaths r
  sapiens      61 births p
  sapiens    1755 moves
After step 2997 (0.212161 s): total   215741 agents
  sapiens   21695 births r
  sapiens   22155 deaths r
  sapiens     453 deaths p
  sapiens    1599 moves
In this listing
births r denotes the nmuber of births who made use of the dead agent recycling, i.e. there was no need to move a passive location to the avtive chain;
births p denotes the number “normal” births, i.e. where a location was moved from the passive chain to the active chain;
deaths r denotes the number of deaths whose locations were recycled;
deaths p denotes the number of ordinary deaths, were an active location is moved to the passive chain;

For more information see ref:”Agent Management<AgentManagement_ref>.

At the beginning of the last part you find the summary:

-----
Finished Simulation
Number of threads: 8
Number of iterations: 3000
Used time: 605.461267 (00:10:05)
Number of agents after last step
  sapiens:    215343
    total:    215343

So our population has grown from 20 to 215’850 agents!

At the very end of the output file:

*** sapiens: time spent in performBirths(ulong iNumBirths, int *piBirthData): 0.296328
*** total number of births  47893869
*** total number of deaths  47677939
*** total number of moves   3598840

The execution of s single step with more than 200’000 agents took 0.2 seconds This is quite fast, but we can do better: recompile QHG again, but this time we use optimization. This is done by defining the environment variable OPT for the compilation:

cd ${QHG4_DIR}
OPT=1 SHORT=tut_04 make clean QHGMain

Change back to the tutoriall directory aAnd run it again:

${QHG4_DIR}/app/QHGMain --read-config=tutorial_04.cfg > tutorial_04o.out

Now look at the output file:

After step 2995 (0.043201 s): total   215396 agents
  sapiens   22432 births r
  sapiens   21673 deaths r
  sapiens      22 births p
  sapiens    1637 moves
After step 2996 (0.042833 s): total   216155 agents
  sapiens   21734 births r
  sapiens   22148 deaths r
  sapiens      61 births p
  sapiens    1755 moves
After step 2997 (0.042090 s): total   215741 agents
  sapiens   21695 births r
  sapiens   22155 deaths r
  sapiens     453 deaths p
  sapiens    1599 moves

and

-----
Finished Simulation
Number of threads: 8
Number of iterations: 3000
Used time: 97.332564 (00:01:37)
Number of agents after last step
  sapiens:    215343
    total:    215343

The optimization made the simulation run 5 times faster: one step with more than 200’000 agents takes only 0.04 seconds!