QHG Tutorial 06 - Environment Interaction I

In this chapter we extend our example by making the agents aware of the environment, the altitude in particular.

To do this we use a SingleEvaluator, and replace the RandomMove object by a WeightedMove object. To each cell the SingleEvaluator assigns preference values (usually by means of a PolyLine object) to the cell itself and its neighbours. For each cell the preference values are cumulated which form the basis for a weighted random number drawn by Weighted move.

../images/SingleEvalFull.png

The environment values (e.g. altitude) of a cell are copied to an array, to which a poly line function is applied. The results are cumulated and are used as a lookup table to create a weighted random number. In this example a uniform random number R between 0 and the highest number in the cumulated array is drawn, and the index of the smallest cumulated value greater than R is chosen. Here the random number 0.81 is less than the value at index 4, 0.95, so the chosen index is 4. Those indexes are selected with a probability proportional to the preference values of he corresponding cells

Population code:

Action code:

The Header File

The include files reflect the addition of the SingleEvaluator and the replacing of RandomMove with WeightedMove, but the agent still has the same fields:

The class now has a pointer to WeightedMove instead of RandomMove, and an additional pointer to a SingleEvaluator:

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

    int makePopSpecificOffspring(int iAgent, int iMother, int iFather);
    int addPopSpecificAgentData(int iAgentIndex, char **ppData);
    void addPopSpecificAgentDataTypeQDF(hid_t *hAgentDataType);
    virtual int updateEvent(int EventID, char *pData, float fT);
    virtual void flushEvents(float fT);

 protected:
    GetOld<tut_EnvironAltAgent>          *m_pGO;
    OldAgeDeath<tut_EnvironAltAgent>     *m_pOAD;
    Fertility<tut_EnvironAltAgent>       *m_pFert;
    Verhulst<tut_EnvironAltAgent>        *m_pVerhulst;
    RandomPair<tut_EnvironAltAgent>      *m_pPair;

    WeightedMove<tut_EnvironAltAgent>    *m_pWM;
    SingleEvaluator<tut_EnvironAltAgent> *m_pSEAlt;

    double *m_adEnvWeights;
    double *m_adCapacities;

    bool m_bUpdateNeeded;

};

#endif

The member variables reflect the new situation of required actions. Apart from makePopSpecificOffspring, addPopSpecificAgentData and addPopSpecificAgentDataTypeQDF, there are two new member functions updateEvent and flushEvents.

Code: tut_EnvironAltPop.h

The Implementation

The include files, too, reflect the changes in the actions:

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

#include "GetOld.cpp"
#include "OldAgeDeath.cpp"
#include "Fertility.cpp"
#include "Verhulst.cpp"
#include "RandomPair.cpp"

#include "WeightedMove.cpp"
#include "SingleEvaluator.cpp"

#include "tut_EnvironAltPop.h"

Apart from creating instances of all the actions and adding them to the priority list, the constructor also allocates an array m_adEnvWeights of floats needed for communication between SingleEvaluator and WeightedMove, and passes it to their constructors.

During the simulation, SingleEvaluator examines the neighborhood of each cell, calculates weights for each cell and writes them to m_adEnvWeights. These weights are used by WeightedMove to decide which neighboring cell to move to (or not to move at all).

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

    m_pGO       = new GetOld<tut_EnvironAltAgent>(this, m_pCG, "");
    m_pOAD      = new OldAgeDeath<tut_EnvironAltAgent>(this, m_pCG, "", m_apWELL);
    m_pFert     = new Fertility<tut_EnvironAltAgent>(this, m_pCG, "");
    m_pVerhulst = new Verhulst<tut_EnvironAltAgent>(this, m_pCG, "", m_apWELL);
    m_pPair     = new RandomPair<tut_EnvironAltAgent>(this, m_pCG, "", m_apWELL);

    // create and initialize the array for coommunication
    m_adEnvWeights = new double[m_pCG->m_iNumCells * (m_pCG->m_iConnectivity + 1)];
    memset(m_adEnvWeights, 0, m_pCG->m_iNumCells * (m_pCG->m_iConnectivity + 1)*sizeof(double));

    // pass the array to the SingleEvaluator and the WeightedMove
    m_pSEAlt = new SingleEvaluator<tut_EnvironAltAgent>(this, m_pCG, "Alt", m_adEnvWeights, (double*)m_pCG->m_pGeography->m_adAltitude, "AltCapPref", true, EVENT_ID_GEO);
    m_pWM = new WeightedMove<tut_EnvironAltAgent>(this, m_pCG, "", m_apWELL, m_adEnvWeights);

    // adding all actions to prioritizer
    m_prio.addAction(m_pGO);
    m_prio.addAction(m_pOAD);
    m_prio.addAction(m_pFert);
    m_prio.addAction(m_pVerhulst);
    m_prio.addAction(m_pPair);
    m_prio.addAction(m_pSEAlt);
    m_prio.addAction(m_pWM);
}

The destructor deletes all action objects created by the constructor, as well as the communication array.

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

    if (m_pGO != NULL) {
        delete m_pGO;
    }
    if (m_pOAD != NULL) {
        delete m_pOAD;
    }
    if (m_pFert != NULL) {
        delete m_pFert;
    }
    if (m_pVerhulst != NULL) {
        delete m_pVerhulst;
    }
    if (m_pPair != NULL) {
        delete m_pPair;
    }
    if (m_pSEAlt != NULL) {
        delete m_pSEAlt;
    }
    if (m_pWM != NULL) {
        delete m_pWM;
    }

    if (m_adEnvWeights != NULL) {
        delete[] m_adEnvWeioghts;
    }
}

The methods addPopSpecificAgentData() and addPopSpecificAgentDataTypeQDF() are practically identical to the ones in the previous chapter, so there’s no need to look at them in more detail.

The method makePopSpecificOffspring() is also identical to the one in the previous examples.

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


    m_aAgents[iAgent].m_fAge = 0.0;
    m_aAgents[iAgent].m_fLastBirth = 0.0;
    m_aAgents[iAgent].m_iMateIndex = -3;
    // SPopulation assigns random genders to a baby, ehich is ok here

    return iResult;
}

The mate index is set to a negative number to denote that this agent is not paired (yet).

The method updateEvents() is called when an event is fired.

//----------------------------------------------------------------------------
// updateEvent
//  react to events
//    EVENT_ID_GEO      : kill agent if under ice or water
//
int tut_EnvironAltPop::updateEvent(int iEventID, char *pData, float fT) {
    if (iEventID == EVENT_ID_GEO) {
        // drown
        int iFirstAgent = getFirstAgentIndex();
        if (iFirstAgent != LBController::NIL) {
            int iLastAgent = getLastAgentIndex();

#pragma omp parallel for
            for (int iAgent = iFirstAgent; iAgent <= iLastAgent; iAgent++) {
                if (m_aAgents[iAgent].m_iLifeState > LIFE_STATE_DEAD) {
                    int iCellIndex = m_aAgents[iAgent].m_iCellIndex;
                    if ((this->m_pCG->m_pGeography->m_adAltitude[iCellIndex] < 0) ||
                        (this->m_pCG->m_pGeography->m_abIce[iCellIndex] > 0)) {
                        registerDeath(iCellIndex, iAgent);
                    }
                }
            }
        }
        // make sure they are removed before step starts
        if (m_bRecycleDeadSpace) {
            recycleDeadSpaceNew();
        } else {
            performDeaths();
        }
        // update counts
        updateTotal();
        updateNumAgentsPerCell();
        // clear lists to avoid double deletion
        initListIdx();
    }

    notifyObservers(iEventID, pData);

    return 0;
}

Here we only react if a change happened to the geography (usually when a new QDF file is loaded. In this case we check all agents in a parallelized loop adding those under water (negative altitude) and thos under ice to the death list Subsequently, the deaths are actually performed, and several counts ar updated.

Code: tut_EnvironAltPop.cpp

XML and DAT

The xml file for the population contains a new <module> for SingleEvaluator, and a WeightedMove module instead of a RandomMove:

 <class name="tut_EnvironAltPop" 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="WeightedMove">
     <param  name="WeightedMove_prob" value="0.07"/>
   </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>

   <module name="SingleEvaluator" id="Alt" >
       <param  name="AltCapPref" value="-0.1 0 0.1 0.01 1500 1.0 2000 1 3000 -9999" />
   </module>

   <priorities>
     <prio  name="GetOld" value="1"/>
     <prio  name="OldAgeDeath" value="2"/>
     <prio  name="WeightedMove" value="3"/>
     <prio  name="SingleEvaluator[Alt]" value="4"/>
     <prio  name="Fertility" value="5"/>
     <prio  name="RandomPair" value="6"/>
     <prio  name="Verhulst" value="7"/>
   </priorities>

</class>

The value of the “AltCapPref” parameter describes a polyline: they are actually coordinate pairs describing the poly line. These agents “hate” the seas (negative altitudes) and hogh mountains, but mostly prefer altitudes between 1500 and 2000 meters.

The DAT file is basically equal to the one in the previous example, with the exception that we moved the starting point to South Africa.

#Longitude;Latitude;LifeState;AgentID;BirthTime;Gender;Age;LastBirth
28.0<46;-26.044;1; 1;-10.0;0;10.0;-1.0
28.046;-26.044;1; 2;-10.0;1;10.0;-1.0
28.046;-26.044;1; 3;-10.0;0;10.0;-1.0
28.046;-26.044;1; 4;-10.0;1;10.0;-1.0
...
28.046;-26.044;1;19;-10.0;0;10.0;-1.0
28.046;-26.044;1;20;-10.0;1;10.0;-1.0

Grid

For this example we use a subdivided icosahedron with earths’s geography: there are oceans and continents.

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_06 tut_EnvironAltPop

This builds alternative population directories tut_06_pop and tut_06_mod with the files needed to compile tut_EnvironAltPop.

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

OPT=1 SHORT=tut_06 make clean QHGMain

Here we set the environemnt variable OPT to get optimized compilation (see Compiling QHG)

Now cd to the tutorial subdirectory tutorial_06 and start QHGMain

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

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

The config file tutorial_06.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@1000'
--grid=eq64Alt.qdf
--log-file=tutorial_06.log
--num-iters=10000
--output-dir=output/tutorial_06
--output-prefix=tut
--pops='tut_EnvironAlt.xml:tut_EnvironAlt.dat'
--shuffle=92244
--start-time=-10000
data-dirs

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

events

A list of events (see Events). Here we write output files containing grid, geography and agent data every 1000 steps.

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 (10000 steps)

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_SexualPop.xml) and agent attributes (tut_SexualPop.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.

Visualization

The configuration script (see above) provides a --events parameter which causes an output of grid, geography, and the agents every 1000 steps. These QDF files can be visualized in two ways:

QDF2PNFNew

The tool QDF2PNGNew projects data from the icosahedral grid to a plain rectilinear image. The images below were produced with calls like this:

${QHG4_DIR}/tools_io/QDF2PNGNew -g ~/tutorials/tutorial_06/output/tutorial_06/tut_pop-sapiens_SG_003000.qdf \
             -q ~/tutorials/tutorial_06/output/tutorial_06/tut_pop-sapiens_SG_003000.qdf \
             -s 720x360 -o tut06_003000.png \
             -a  'alt|geo:-6000:0:6000,pop_sapiens|fadeout:0:10:#FFFF00FF' \
             -r -25 -c over -t "step 3000"

(replace “~/tutorials” with the name you gave for the tutorials directory)

For details on how to call QDF2PNGNew, refer to its documentation.

../images/tut06_Multi3.png

Sequence of snapshots at intervals of 1000 steps. The yellow dots represent the agents. In the first image the initial populaition sits at the center of the red circle.

VisIt

With the visualization software VisIt we can create images showing the data on the sphere:

../images/envalt.png

Sequence of snapshots at intervals of 1000 steps. The yellow dots represent the agents.