Demo models‎ > ‎Demo07‎ > ‎

3. The PersonModel class

3.1 Objects

The Model extends the AbstractSimulationManager class. This requires implementing the buildObjects() and the buildSchedule() methods. The buildObjects() method contains the instructions to create all the agents and the objects that represent the virtual environment for model execution (Box 2). (The @Override annotation is used by the Java interpreter to ensure that the programmer is aware that the method declared is overriding the same method declared in the superclass). In Demo07, this involves loading the parameters for the simulation and the initial population, made of persons and households. Three other methods complete the simulation setup: initializeNonDatabaseAttributes() initializes attributes that do not appear in the input database, such as the education level; addPersonsToHouseholds() registers household members, and cleanInitialPopulation() checks the internal consistency of the initial population and removes errors, making sure that all marriage partnerships are bilateral, that all partners belong to the same household, and that no empty households exist. (This method is absent in the LIAM2 implementation, which does not get rid of all the errors in the initial database).
 
 

@Override

public void buildObjects() {

 

         Parameters.loadParameters();         

         persons = (List<Person>) DatabaseUtils.loadTable(Person.class);

         households = (List<Household>) DatabaseUtils.loadTable(Household.class);

        

         initializeNonDatabaseAttributes();

         addPersonsToHouseholds();

         cleanInitialPopulation();

}

Box 2. The PersonsModel.buildObjects() method.
 
 
 
As we have seen, the general rule is that parameters should not be hard-coded in the simulation. The only exception is with control parameters that can be changed from the GUI before the simulation starts or while the simulation is running in order to experiment with the model behavior in interactive mode. Control parameters are properties of a simulation, they are annotated with @GUIparameter (replacing the deprecated @ModelParameter annotation) and are automatically loaded into the JAS-mine GUI. In demo07 there are just three such parameters, as described in Box 3.
 
 

@GUIparameter(description="Simulation begins at year [valid range 2002-2060]")

private Integer startYear = 2002;

 

@GUIparameter(description="Simulation ends at year [valid range 2003-2061]")

private Integer endYear = 2061;

 

@GUIparameter(description="Retirement age for women")

private Integer wemra = 61;

Box 3. PersonsModel: control parameters.

3.2 Schedule 


The buildSchedule() method contains the plan of events for the simulation. Events are planned based on a discrete event simulation paradigm. This means that events can be scheduled dynamically at specific points in time. The frequency of repetition of an event can be specified in the case of periodic events. An event can be created and managed by the simulation engine (a system event e.g. terminating the simulation), it can be sent to all the components of a collection or list of agents or it can be sent to a specific object/instance. Events can be grouped together if they share the same schedule.
 
In demo07, all events are scheduled right from the beginning of the simulation (there is no dynamic scheduling), and occur on a yearly basis. They are grouped in an EventGroup called modelSchedule, which is scheduled at every simulation period starting at 0 with the schedule(Event event, long atTime, int withLoop) method: 
 
getEngine().getEventList().schedule(modelSchedule, 0, 1); 
 
The events of demo07 are typically directed to a collection of objects – persons or households – and are inserted into an EventGroup with the instruction 
 
modelSchedule.addCollectionEvent(Object object, [some action the object must perform]); 
 
The actions to be performed can be specified in two ways. The simplest is to use Java reflection and simply specify the object’s method name to be invoked. For instance, asking all persons to perform the aging() method would require the instruction:
 
modelSchedule.addCollectionEvent(persons, Person.class, “ageing”); 
 
Java reflection, however, generally has a reputation for being quite slow. A better approach is to use the EventListener interface. When an object implements this interface, it must define an onEvent() method that will receive specific enumerations to be interpreted. We will describe how the Person class implement the onEvent() method in Section 4.3. For now, we simply note that by using the EventListener interface, the scheduling of the ageing() method becomes: 
 
modelSchedule.addCollectionEvent(persons, Person.Processes.Ageing); 
 
By default, the broadcasting of an event to a collection of objects is performed in safe mode (read only), and does not allow the concurrent modification of the collection itself. This is not a problem with the ageing() process, as ageing per se does not entail any modification in the list of persons, that is, it does not add or remove anyone. This is not true with other processes, like birth() or death(). In order to allow the collection to be changed while iterated by the simulation engine, this feature has to be switched off, as in 
 
modelSchedule.addCollectionEvent(persons, Person.Processes.Death, false); 
 
The last argument specifies that the collection is subject to changes while being iterated, and the JAS-mine engine treats it accordingly.
 
The order of the events in the simulation follows the original LIAM2 implementation and is specified as in Box 4: there is a first set of demographic events (ageing, death, birth, marriage, exit from parental home, divorce, household composition) and then a set of events that define the work status (whether in education, retired, other non-employed, or employed).
 
 
 
        

        @Override

        public void buildSchedule() {

              

               EventGroup modelSchedule = new EventGroup();        

               // 1: Ageing

               modelSchedule.addCollectionEvent(persons, Person.Processes.Ageing);

              

               // 2: Death

               modelSchedule.addCollectionEvent(persons, Person.Processes.Death, false);

       

               // 3: Birth

               modelSchedule.addCollectionEvent(persons, Person.Processes.Birth, false);

       

               // 4: Marriage

               modelSchedule.addCollectionEvent(persons, Person.Processes.ToCouple);

               modelSchedule.addEvent(this, Processes.MarriageMatching);

              

               // 5: Exit from parental home

               modelSchedule.addCollectionEvent(persons, Person.Processes.GetALife);

              

               // 6: Divorce

               modelSchedule.addEvent(this, Processes.DivorceAlignment);

               modelSchedule.addCollectionEvent(persons, Person.Processes.Divorce);

              

               // 7: Household composition

               // (for reporting only: household composition is updated whenever needed throughout

                                                                               // the simulation)

               modelSchedule.addCollectionEvent(households,

                       Household.Processes.HouseholdComposition);

              

               // 8: Education

               modelSchedule.addCollectionEvent(persons, Person.Processes.InEducation);

              

               // 9: Work

               modelSchedule.addEvent(this, Processes.InWorkAlignment);

       

               getEngine().getEventList().schedule(modelSchedule, 0, 1);                                        

               getEngine().getEventList().schedule(

                  new SingleTargetEvent(this, Processes.Stop), endYear - startYear);

  }

Box 4. The PersonsModel.buildSchedule() method.
 
 
Marriage is performed in two steps. First, a subset of suitable males and females are selected for matching by invoking the method Person.toCouple() (see Section 4.3); then, matching takes place. Matching uses a 'centralised' algorithm and is therefore performed by the Model itself (see Section 3.4 for more details). Consequently, this event is a single target event, rather than a collection event, and is inserted into our EventGroup modelSchedule with the instruction 
 
modelSchedule.addEvent(this, Processes.MarriageMatching); 
 
Similarly, the divorce and work events are subject to alignment and are managed directly by the Model, with the methods divorceAlignment() and inWorkAlignment(), though divorce also requires some actions taken by the individuals themselves – in the divorce() method in the Person class – after they have been selected to divorce. householdComposition() is the only method which is directed to the collection of households. It simply updates the number of adults and children in each household for reporting purposes. A final single target event is scheduled for the last year of the simulation with the method schedule(Event event, long atTime): its target is the Model itself and brings the simulation to a halt: 
 
getEngine().getEventList().schedule(new SingleTargetEvent(this, Processes.Stop), endYear - startYear);

3.3 The EventListener interface

 
Since the Model performs actions during the simulation, as with the Person and Household classes, it implements the EventListener interface. This requires first to enumerate all the actions that the Model is supposed to perform (this is done by defining the specific enum Processes), and then to specify the method onEvent() – see Box 5.
 
 
 

public enum Processes {

               MarriageMatching,

               DivorceAlignment,

               InWorkAlignment,

               Stop;

        }

       

        @Override

        public void onEvent(Enum<?> type) {

               switch ((Processes) type) {  

               case DivorceAlignment:

                       divorceAlignment();

                       break;

               case InWorkAlignment:

                       inWorkAlignment();

                       break;

               case MarriageMatching:

                       marriageMatching();

                       break;

               case Stop:

                       log.info("Model completed");

                       getEngine().pause();

                       break;

               }

        }

 
Box 5. Implementation of the EventListener interface in PersonsModel.
 
We now dig into the matching and alignment methods performed by the Model.
 

3.4 The matching algorithm

 

Prior to matching, a sample of the population to marry at this time is determined randomly using the Person.toCouple() method (see Section 4.3). Subsequently, matching involves first ordering all the females; then, for each female starting from the top of the ranking, all males are ordered and the most suitable male is matched. This continues until there are either no more females or males to match. Females are ordered according to their age difference (in absolute value) with respect to the average age in the pool of females to be matched, |age-mean(age)|; the female whose age is closest to the average is ranked first. To compute this ranking, the average age of the subset of females selected for matching is required. There are a number of ways to perform this computation, which is preliminary to the application of the matching algorithm. The one that is implemented in demo07 makes use of Java closures (Box 6). (Technically, a closure is a function that refers to free variables in their lexical context. A free variable is an identifier (the identity of the person which is included in the evaluation set, in our example) that has a definition outside the closure: it is not defined by the closure, but it is used by the closure. In other words, these free variables inside the closure have the same meaning they would have had outside the closure.)

 

        final AverageClosure averageAge = new AverageClosure() {                          

               @Override

               public void execute(Object input) {

                       add( ((Person) input).getAge() );                                  

               }

        };

              

        Aggregate.applyToFilter(getPersons(), new FemaleToCoupleFilter(), averageAge);

Box 6. Computing the average age for the eligible females in PersonsModel.marriageMatching().

The JAS-mine collection package defines an AverageClosure as a closure that receives values from objects as an input and returns the mean of these values as an output. Here, it is used to compute the average age of a given set of persons. The set is defined by applying the FemaleToCouple filter to the list of all persons, with the instruction 
 
Aggregate.applyToFilter(getPersons(), new FemaleToCoupleFilter(), averageAge);
 
The averageAge closure now contains the average age of all filtered females. In turn, the FemaleToCouple filter simply selects the female persons who have the toCouple flag switched on (Box 7).
 
 
 

public class FemaleToCoupleFilter implements Predicate {

 

        @Override

        public boolean evaluate(Object object) {

               Person agent = (Person) object;

               return (agent.getGender().equals(Gender.Female) && agent.getToCouple());

        }

 

}

Box 7. The FemaleToCouple filter.


Having the filters specified as separate classes, grouped in the separate package data.filters, might look cumbersome at first (and there are other ways to do this, see the online documentation) but allows to keep the core code clean while using the standard Apache Predicate approach to filtering – remember that the JAS-mine approach supports the use of existing software solutions whenever possible, and envisages to keep the specificities of the JAS-mine libraries to a minimum in order to minimise the 'black box' feeling of many simulation platforms. 

 
Matching is then performed, following the LIAM2 implementation, by making use of a simple one-way matching procedure (the agents in one collection – females in our example – choose, while the agents in the other collection – males – remain passive) implemented in the SimpleMatching class: 
 
matching(collection1, filter1, comparator1, collection2, filter2, matchingScoreClosure,     matchingClosure);
 
and it is invoked as 
 
SimpleMatching.getInstance().matching(...); 
 
The matching method requires 7 arguments:
  1. collection1: the first collection (e.g. all individuals in the population);
  2. filter1: a filter to be applied to the first collection (e.g. all females with the toCouple flag on);
  3. comparator1: a comparator to sort the filtered collection, which determines the order that the agents in the filtered collection will be matched.
  4. collection2: the second collection, which can be the same as collection1 (e.g. all individuals in the population) or a different one; the two collections do not need to have the same size;
  5. filter2: a filter to be applied to the second collection (e.g. all males with the toCouple flag on);
  6. matchingScoreClosure: a piece of code that assigns, for every element of the filtered collection1, a double value to each element of the filtered collection2, as a measure of the quality of the match between every pair;
  7. matchingClosure: a piece of code that determines what to do upon matching.
 
As in the computation of the average age, the use of closures – which are relatively new to the Java language – allows a great simplification of the code. While it is not required that the user knows about closures, it is interesting to understand why they are so useful. In the example, suppose that the females in the population are sorted according to some criterion, for example beauty: the prettiest woman is the first to choose a partner, the second prettiest is the second to choose etc. The matchingScoreClosure sorts all possible mates according to some other criterion, for example wealth. Hence, the prettiest woman gets the richest man, the second prettiest gets the second richest, etc. In such a case, a comparator would suffice to order the males in the population, as the ranking is the same irrespective of the female who is evaluating them. But suppose now that the attractiveness of a man depends on the age differential between himself and the potential partner: in such a case, the ranking is specific to each woman in the population. A simple comparator would still do the job, but the comparator should be able to access the identity of the woman who is making the evaluation as an argument, which requires a lot of not-so-straightforward coding. Closures allow to bypass this technical requirement because they can pass a functionality as an argument to another method; in other words, they treat functionality as method argument, or code as data.
 
Closures in the matching() method are easier used than explained: the 7 arguments are listed in Box 8. 
   
 

        SimpleMatching.getInstance().matching(

               // collection1: the whole population

               persons,

 

               // filter1:   

               new FemaleToCoupleFilter(),

 

               // comparator1: a comparator that

         // assigns priority to the individual that has a lower difficulty in matching

         // (this is determined by an individual’s age in relation to the average)       

               new Comparator<Person>() {   

 

                       @Override

                       public int compare(Person female1, Person female2) {

                              return (int) Math.signum(

                                     Math.abs(female1.getAge() - averageAge.getAverage()) -

                                      Math.abs(female2.getAge() - averageAge.getAverage()));

                       }                     

               },

              

               // collection2: same as collection1  

               persons,

              

               // filter2:   

               new MaleToCoupleFilter(),

              

               // MatchingScoreClosure: a closure that, given a specific female,

               // computes for every male in the population a matching score      

               new MatchingScoreClosure<Person>() { 

                      

                       @Override

                       public Double getValue(Person female, Person male) {

                              return female.getMarriageScore(male);

 

                       }

               },

              

                // matchingClosure: a closure that creates a link between a specific

               // female and a specific male, and sets up a new household.

               new MatchingClosure<Person>() {      

                      

                       @Override

                       public void match(Person female, Person male) {                                   

                              female.marry(male);                         

                              male.marry(female);

 

                       }

               }

        );      

Box 8. The matching algorithm in PersonsModel.marriageMatching().
 

3.5 Alignment

 
Alignment involves comparing the provisional outcomes of the simulation with some external aggregate targets, and then modifying the simulation outcomes in order to match the external totals. We show how this is implemented in demo07 by looking at the divorceAlignment() method; the inWork() alignment method works similarly. When it comes to divorce, as in marriageMatching(), the focus is on females: males are passive recipients of their partners’ choices. Different targets are specified for different age groups and simulated years; as we have seen in Sections 1 and 2, these are read from the file p_divorce.xls and stored in the MultiKeyCoefficientMap pDivorce in the Parameters class. The divorceAlignment() method works cell by cell, that is, it aligns each age group of the population to its year-specific target: this means that the alignment algorithm is applied once for every age group (as defined in the p_divorce.xls parameter file). The structure of the method is therefore as follows:
 
For each age group: do alignment:
  • Read target from pDivorce.
  • Select the relevant subgroup of married females.
  • Compute, for each of the selected females, a probability to divorce that depends on the age group to which they belong.
  • Select the couples that divorce by applying the SBD algorithm: each female is ranked according to the signed difference between their divorce probability and a random number uniformly distributed between 0 and 1; then, the number of couples equal to the target are selected to divorce by starting with the top ranked female and going down the ranks until the target number is reached. (The ranking involves a stochastic component (the random number that is subtracted from the divorce probability score) in order to give individuals with a low predicted probability some chance to experience the event. As we have already noted, the SBD algorithm is quite distortive and its use is deprecated in JAS-mine; it is employed here only for consistency with the LIAM2 implementation.)

The MultiKeyCoefficientMap pDivorce, which contains the targets, has a three dimensional key: the lower and upper bounds for the age group, and the year of the simulation. The age group-specific and year-specific targets are read with the instruction reported in Box 9.
 
 
 

    MultiKeyCoefficientMap pDivorceMap = Parameters.getpDivorce();

 

    for (MapIterator iterator = pDivorceMap.mapIterator(); iterator.hasNext();) {

 

               iterator.next();

               MultiKey mk = (MultiKey) iterator.getKey();

               int ageFrom = (Integer) mk.getKey(0);

               int ageTo = (Integer) mk.getKey(1);

               double divorceTarget = ( (Number) pDivorceMap.getValue(

                       ageFrom,

                       ageTo,

                       getStartYear() + SimulationEngine.getInstance().getTime())).doubleValue();

               […]

    }

Box 9. PersonsModel.divorceAlignment(): reading the targets.

The alignment methods require 4 arguments:
  1. collection: a collection of individuals whose outcome or probability of an event has to be aligned (e.g. all the population);
  2. filter: a filter to be applied to the collection (e.g. all females selected to divorce);
  3. alignmentProbabilityClosure or alignmentOutcomeClosure: a piece of code that i) computes for each element of the filtered collection a probability for the event (in the case that the alignment method is aligning probabilities, as in the SBD algorithm) or an outcome (in the case that the alignment method is aligning outcomes), and ii) applies to each element of the filtered collection the specific instructions coming from the alignment method used;
  4. targetShare or targetNumber: the share or number of elements in the filtered collection that are expected to experience the transition; the SBD algorithm uses targetShare.

Box 10 shows how the alignment method is implemented in demo07.

    new SBDAlignment<Person>().align(

 

               // collection:

               persons,

 

               // filter:

               new FemaleToDivorce(ageFrom, ageTo),

 

               // alignmentProbabilityClosure:

               new AlignmentProbabilityClosure<Person>() {

 

                       // i) compute the probability of divorce

                       @Override

                       public double getProbability(Person agent) {

                              return agent.computeDivorceProb();

                       }

                       // ii) determine what to do with the aligned probabilities

                       @Override
                      public void align(Person agent, double alignedProbability) {

 

                              boolean divorce = RegressionUtils.event(
                                     alignedProbability,
                                     SimulationEngine.getRnd()

                              );


                             agent.setToDivorce(divorce);

 

                       }

               },

              

               // targetShare:

               divorceTarget

   };

Box 10. PersonsModel.divorceAlignment(): applying the SBD alignment algorithm.


Previous: 2. Parameters             Next: 4. The Person class