In the previous tutorial, we saw how to tackle an opinion dynamics problem using agents as a class in an Object Oriented Programming (OOP) approach. In many topics of interest in (socio-)physics and chemistry, we deal with a large number of particles, be it electrons, atoms, agents, stars, … These are contained in a superstructure (electrons⇒atom, atoms⇒molecule/solid, agents⇒population, stars⇒galaxy,…) which is generally represented in the code as an array. As we noted in the previous tutorial, there were several variables which were global to the agents, but we implemented them as properties of the agents anyhow. As a result, a significant amount of additional memory needed to be allocated for storing in essence the same data. This was done to prevent the need of having to provide this information at every function call.
Returning to our problem of interest, we now consider two object classes: The TAgent-class and the TPopulation-class. This leads to several possible ways this problem can be implemented.
- Array of TAgents: As was done in the previous tutorial, we only make a class of the agents, and put them in an array.
- TPopulation of TAgents: In this case we construct a class called TPopulation of which one property is the set of TAgents. The TPopulation-class also contains some of the global variables as properties, and operations on this set are methods of the TPopulation-class.
- TPopulation-class without TAgent-class: In this last case, the agents are dissolved, and their properties are stored in array-properties of the TPopulation-class. The methods of the TAgent-class now become methods of the TPopulation-class. And the global variables become additional properties of the TPopulation-class.
Although the true OOP-programmer may only consider the second option the way to go, we will consider the third option in this tutorial.
Lets have a look at our old TAgent-class properties and how they can be transformed into a TPopulation-class properties:
|Opinion (agent specific)||array of opinions, with the array-index indicating to which agent it belongs.|
|Tolerance to other opinions (agent specific)||array of tolerances, with the array-index indicating to which agent it belongs.|
|Convergence speed (global, one copy per agent)||scalar property of the population (one copy per population)|
|Tolerance formula parameters (global, in case of the Sobkowicz-paper, one copy per agent)||scalar properties of the population (one copy per population)|
In addition, all TAgent-methods can just be transferred, but will get an additional parameter: the agent-index. The result is the following TPopulation-class:
- type, public :: TPopulationClass
- integer :: nagents !< number of agents
- real, allocatable :: oi(:) !< opinion
- real, allocatable :: di(:) !< tolerance
- real :: mu !< global convergence speed [0-->0.5]
- real :: dmin !< min tolerance
- real :: dmax !< min tolerance
- integer :: L !< exponent for tolerance update
- procedure,pass(this), public :: initialize
- procedure,pass(this), private :: resetTalkListenLists
- procedure,pass(this), private :: randomizeTalkListenLists
- procedure,pass(this), private :: RemoveSelfInteraction
- procedure,pass(this), public :: getOpinion
- procedure,pass(this), public :: setOpinion
- procedure,pass(this), public :: updateOpinion
- procedure,pass(this), public :: free => FREE_SOBPOPULATION
- end type TPopulationClass
The get and set procedures now become:
- function getOpinion (this,AgentIndex) Result(Opinion)
- class(TPopulationClass),intent(in) :: this
- integer, intent(in) :: AgentIndex
- real :: opinion
- if ((AgentIndex>0).and.(AgentIndex<=this%nagents)) opinion = this%oi(AgentIndex)
- end function getOpinion
- subroutine setOpinion(this,AgentIndex,Opinion)
- class(TPopulationClass),intent(inout) :: this
- integer, intent(in) :: AgentIndex
- real, intent(in) :: Opinion
- if ((AgentIndex>0).and.(AgentIndex<=this%nagents)) this%oi(AgentIndex)=Opinion
- end subroutine setOpinion
where in both cases the if-else clause makes sure no out-of-bounds operations occur (never trust your user to know what he/she is doing).
Almost the entire initialization block of the Tutorial1-subroutine is pulled into the initialization procedure of our TPopulation-class. Additionally, there is no updateTolarance method, since it is directly incorporated into the updateOpinion subroutine. I chose this option because, to my opinion, a method updating a single agent tolerance conflicts with the idea of using opinions and tolerances as arrays.
Finally, there are also three new methods: resetTalkListenLists, randomizeTalkListenLists, and RemoveSelfInteraction. Which were three global helper subroutines in our previous TAgent-class only tutorial. There these subroutines generated and updated lists of interactions between the TAgent objects. Since our current class affects the entire population as a whole, it is sensible to introduce these subroutines as methods of the TPopulation-class.
The resulting Tutorial2 subroutine becomes quite a bit smaller, even though the general layout of the program has not changed compared to the TAgent-class Tutorial1 subroutine (note, however, that the inner loop over the agents is now included in the update opinion procedure of the TPopulation-class):
- subroutine RunTutorial2()
- type(TPopulationClass) :: population
- integer :: nr, nrstep, nrrun, mesh, nragents, simsteps, runs
- integer:: L
- real :: mu, dmin, dmax
- real, allocatable :: opiniondynamics(:,:)
- real, allocatable :: opinionarray(:)
- write(*,'(A)') "----Starting Tutorial 2 subprogram.----"
- mesh=100 ! # gridpoints/bins in the opinion space?
- call population%initialize(nragents,mu,dmin,dmax,L)
- do nr=1,nragents
- end do
- do nrrun=1,runs ! 200 repeats to average over
- do nrstep=1,simsteps ! 500 simulation steps
- call population%updateOpinion()
- do nr=1,nragents
- opiniondynamics(int(population%getOpinion(nr)*mesh)+1,nrstep)= &
- & opiniondynamics(int(population%getOpinion(nr)*mesh)+1,nrstep)+1
- end do
- end do ! end nrstep (1 timestep)
- end do ! end 1 run of ensemble
- write(*,*) "Writing results"
- call writeOpiniondynamics(opiniondynamics,mesh,nragents,simsteps,runs)
- call population%free()
- write(*,'(A)') "-------End Tutorial 2 subprogram.------"
- end subroutine RunTutorial2
The TPopulation object is only called upon in 5 instances: To initialize and clean up the population, to update the opinion, and twice to get the agent opinions to make a histogram. Also, only one support subroutine is left, i.e. the one that writes our resulting histogram.
Now let me draw your attention to the part of the code where the histogram is created:
do nr=1,nragents opiniondynamics(int(population%getOpinion(nr)*mesh)+1,nrstep)= & & opiniondynamics(int(population%getOpinion(nr)*mesh)+1,nrstep)+1 end do
Although our agent opinions are stored in a single array property of the TPopulation-class, we make a method call for every single agent of our set. This was quite natural in our first implementation with TAgent-objects, and quite natural for cases where you only need 1 specific agent’s opinion. However, in this specific case we want all these opinions, without exception, so it would be more natural to make getOpinion return the entire array. Because, we don’t want to loose our function returning a single opinion, and because we want both opinion-getters to be accessible using the same name (call it laziness) we will use generic type-bound procedures. In our TPopulation-class this looks as follows:
- type, public :: TPopulationClass
- procedure,pass(this), private :: getOpinion_single
- procedure,pass(this), private :: getOpinion_array
- generic, public :: getOpinion => getOpinion_single, getOpinion_array
- end type TpopulationClass
We define two versions of the private procedures getOpinion_single/array (one returning just a single opinion, and one returning the entire array), and we create a generic procedure getOpinion which is type-bound to getOpinion_single and getOpinion_array. By making our generic method public, we provide indirect access to the two procedures, while these themselves remain inaccessible (i.e. the user has less to worry about). Note, however, that the parameter lists for the two procedures needs to differ, otherwise it is unclear for the compiler which one should be used.
As a result, we can rewrite the above do-loop (using a temporary variable to store the entire opinion array):
opinionarray=int(population%getOpinion()*mesh)+1 do nr=1,nragents opiniondynamics(opinionarray(nr),nrstep)= & & opiniondynamics(opinionarray(nr),nrstep)+1 end do
This way, the number of function-calls is reduced by a factor as large as the number of agents, which should have an impact on the performance.
Our two implementations contain the same amount of initial programmer optimization: none. There are no “special tricks” being used other than what comes natural to the variable types used, which makes them well suited for comparison when it comes to performance.
What is the advantage of using a TPopulation class that contains arrays of agent properties over a TPopulation class containing an array of TAgent-class objects? As noted at the start, these examples are also applicable for other physical/chemical problems, and typical for all these problems is the drive to investigate systems containing ever more agents/atoms/electrons/stars/… As such speed is an important issue. If you can speed up a program by a factor of 2, this means that in the same amount of time you may be able to handle a system twice the size. One of the great strengths of Fortran are it’s intrinsic array operations (as we used extensively during our rewrite of the histogram-loop above). However, also in other languages, the use of arrays to store comparable properties is useful, because it means that these data are stored close to one-another in memory, leading to fewer cache-misses.
In addition, we can also reduce the run-time overhead of calling small functions a large number of times (e.g. getOpinion asking for a single opinion). Comparing our implementations using the TAgents class and the TPopulation class shows the latter to be about 25-50% faster, showing that the choice of our objects has a significant (initial) influence on performance.