The AHA Model
Revision: 12809
Reference implementation 04 (HEDG02_04)
|
The AHA Model is implemented in the modern Fortran F2003 and partially F2008 standards. Because not all widespread compilers support the full set of these language standards, the model code uses only those that are supported by the following minimum compiler versions:
Modern Fortran fully supports object oriented programming with polymorphic objects and rich set of array functions which are also usable in the object oriented code.
This document provides a very brief overview of the main features of modern Fortran that are widely used in the AHA Model code.
Stated simply, the object-oriented programming paradigm is based on the notion of object. Here object is an entity that integrates data and procedures that are implemented to manipulate these data. In the simplest case, data can be considered as the "properties"" or "attributes" that describe the object. Procedures that are linked with the object, on the other hand, provide other derived attributes of the object or describe what the object can "do".
Different objects can be arranged in various ways (e.g. form more complex objects like arrays). For instance a population of agents (another object) can be simply formed by arranging individual agents (other objects) into an array. Various agents can also interact with each other.
For example, a single "agent" object is an entity having such attributes as sex, spatial position, body mass, body length etc. It can also have such boolean attributes as "is alive" (true or false). For any such object, one can calculate instantaneous risk of predation and other transient derived properties. Also, the agent can interact with objects of various other kinds. For example, an agent can change its spatial position (its position attribute is changed), approach a food item and "eat" it (basically, absorb the mass attribute of the item, the item is destroyed thereafter). Agent can also do many other things, e.g. "die". The functions that are linked to the object are usually called methods.
When an instance of the object is created, it is initialised in a function (e.g. init
) that is often called the constructor. Another procedure is sometimes implemented to destroy and deallocate the object, it is the destructor.
Object-oriented code in modern Fortran is based on what is called type-bound procedures.
Briefly, a derived type is declared using the type
keyword; it can contain several intrinsic and other derived types. Thus, a data structure is implemented.
type, public :: SPATIAL_POINT real(SRP) :: x, y, depth character(len=LABEL_LENGTH) :: label .... end type SPATIAL_POINT
Any instances of the object can be declared using type
keyword.
type(SPATIAL_POINT) :: some_point, another_point
A procedure can then be declared that operates specifically on this type. The first parameter this
refers to the object that the procedure operates on.
The base object this
is declared as class
in the procedure, which allows to accept any extension of the this
object as the first parameter. This is called "polymorphic objects."
Note that the other parameters (non this
) can be declared as class
or as type
. In the former case, the procedure could accept any extensions (the procedure is then polymorphic) of the object, while in the latter, only this specific type
(non-polymorphic procedure).
Components of the object are separated from its name with the percent sign %
, e.g. the x
coordinate is this%x.
function spatial_distance_3d (this, other) result (distance_euclidean) class(SPATIAL_POINT), intent(in) :: this real(SRP) :: distance_euclidean class(SPATIAL_POINT), intent(in) :: other distance_euclidean = dist( [this%x, this%y, this%depth], & [other%x, other%y, other%depth] ) end function spatial_distance_3d
The procedure is then included into the derived type declaration.
The name of the procedure that is implemented (e.g. spatial_distance_3d
in the example above) is not called directly in calculations and can be declared private
. Instead, a public interface name is declared in the derived type that defines how the procedure should be called, in the example below it is distance
.
Note that the interface name can coincide for several different objects, however the actual procedure name (spatial_distance_3d
) must be unique within the module that defines the derived type and its procedures.
type, public :: SPATIAL_POINT real(SRP) :: x, y, depth character(len=LABEL_LENGTH) :: label .... contains procedure, public :: distance => spatial_distance_3d .... end type SPATIAL_POINT
Now, the procedure is called for the specific instance of the object (it comes to the procedure as the this
first "self" parameter) using the public interface name (distance
) rather than the "actual" procedure name (spatial_distance_3d
).
type(SPATIAL_POINT) :: point_a, point_b ... distance_between_points = point_a%distance( point_b )
An extension object can be declared using extends
keyword, that will use all the properties and type-bound procedures of the base object and add its own additional ones. This allows creating complex inheritance hierarchies across objects.
type, public, extends(SPATIAL_POINT) :: SPATIAL_MOVING ! The following component adds an array of history of the object ! movements: type(SPATIAL_POINT), dimension(HISTORY_SIZE_SPATIAL) :: history ... contains .... procedure, public :: go_up => spatial_moving_go_up procedure, public :: go_down => spatial_moving_go_down .... end type SPATIAL_MOVING
Thus, the structure of the module that defines an inheritance hierarchy of objects and their type-bound functions is like this:
module SPATIAL_OBJECTS ! Declarations of objects: type, public :: SPATIAL_POINT real(SRP) :: x, y, depth character(len=LABEL_LENGTH) :: label .... contains procedure, public :: distance => spatial_distance_3d .... end type SPATIAL_POINT .... type, public, extends(SPATIAL_POINT) :: SPATIAL_MOVING ! The following component adds an array of history of the object ! movements: type(SPATIAL_POINT), dimension(HISTORY_SIZE_SPATIAL) :: history ... contains .... procedure, public :: go_up => spatial_moving_go_up procedure, public :: go_down => spatial_moving_go_down .... end type SPATIAL_MOVING ..... ! other declarations ..... contains ! Here go all the procedures declared in this module function spatial_distance_3d (this, other) result (distance_euclidean) class(SPATIAL_POINT), intent(in) :: this real(SRP) :: distance_euclidean class(SPATIAL_POINT), intent(in) :: other distance_euclidean = dist( [this%x, this%y, this%depth], & [other%x, other%y, other%depth] ) end function spatial_distance_3d .... ! Any other procedures ...... end module SPATIAL_OBJECTS
Relationships between different kinds of objects can be represented graphically in a class diagram. See Object-oriented programming and modelling section of the HEDTOOLS manual for more information.
Modern Fortran includes a powerful concept of elemental procedures. Such procedures (subroutines of functions) are declared using simple scalar parameters. However, they can also accept parameters that are arbitrary arrays.
There are strict limits on the procedures that can be elemental
. Basically, there should be no side effects in the procedure code (the code should only affect the defined parameters that are in the parameter list), if the code calls any procedures or functions, these should be declared as pure
(free of any side effects), each parameter must have explicit intent
declared. Notably, calling random numbers (random_number
) or any input/output are not allowed.
Here is an example of a simple elemental function:
elemental function add (a,b) result (sum) real :: a, b, sum sum = a + b end function add
It can be used with scalar arguments as normal:
real :: x, y, z z = add(x, y)
However, it can also be used with arrays and combinations of arrays and scalars:
real :: a real, dimension(100,100) :: x, y, z1, z2 z1 = add(a, x) ! scalar added to array z2 = add(x, y) ! two arrays are added element by element
In such a case, the function is executed in element-wise manner. For this reason, the arrays should have conforming dimensionality and sizes.
Elemental procedures also work with the object oriented code. This allows implementation complex algorithm in a very concise manner, avoiding code duplication across different-sized arrays.
For example, the simple function below allows to get the spatial position of a spatial object or any extension.
elemental function spatial_get_current_pos_3d_o(this) result(coordinates) class(SPATIAL_POINT), intent(in) :: this type(SPATIAL_POINT) :: coordinates coordinates%x = this%x coordinates%y = this%y coordinates%depth = this%depth end function spatial_get_current_pos_3d_o
However, it can also be used with an array of such spatial objects and returns an array of their positions. For example, it can return an array of spatial positions for a whole population of agents, or for its slice.
call LOG_DBG("Coordinates: " // TOSTR(parents%individual(1:10)%location()))
Using elemental procedures in the object oriented Fortran code allows to substitute such loop-based code:
do i = 1, size(neighbours) dist_here(i) = this%distance(neighbours(i)) end do
by a shorter, simpler (and usually better optimised compiled code) whole-array based code:
dist_here = this%distance(neighbours)
Most arithmetic functions in modern Fortran accept scalar as well as array arguments (usually elemental). There are also many other useful intrinsic array functions that allow writing very concise and easily understandable code. An additional advantage is that these procedures are usually highly optimised by the compiler and can be executed in parallel multi-threading mode if automatic parallelisation is enabled.
! Maximum and minimum value of an array: a = maxval(array) b = minval(array) ! ... and their locations: i = maxloc(array) j = minloc(array) ! array sum: total = sum(array) ! count elements with a mask: i = count( x > y ) ! masked array assignment: where ( x == missing ) x = y ! indexed parallel array assignment: forall ( i=1:popsize ) x(i) = 1.0/i ! ... and many more
Using such whole-array based functions allows implementation of very concise and comprehensible code that is also highly optimised for multithreading execution.
! Number of agents alive call csv_record_append(file_record, count(parents%individual%is_alive())) .... ! Average body mass call csv_record_append(file_record, average(parents%individual%body_mass))
Modern Fortran allows to define arbitrary operators and redefine intrinsic operators.
User defined operators are declared using interface operator
and must refer to a specific function that implements the operator.
interface operator (.above.) procedure spatial_check_located_below end interface operator (.above.) .... .... function spatial_check_located_below(this, check_object) result (are_below) class(SPATIAL_POINT), intent(in) :: this class(SPATIAL_POINT), intent(in) :: check_object logical :: are_below if ( check_object%dpos() > this%dpos() ) then are_below = .TRUE. else are_below = .FALSE. end if end function spatial_check_located_below
Once defined, such operators can be used with object oriented code like the intrinsic operators:
do i=1, max if ( this%perceive_consp%conspecifics_seen(i) .above. the_agent ) then number_above = number_above + 1 end if end do
or using whole-array operators even simpler:
number_above=count(this%perceive_consp%conspecifics_seen .above. the_agent)
User-defined operators can refer to elemental functions, however can be used only with scalar arguments.
The associate
construct allows to declare a shortcut that is then used in place of a long and complex expression or variable. This includes indexing variables in do
loops and parts of the object hierarchy.
Here is an example:
do ind = 1, size(this%individual) ...... associate ( AGENT => this%individual(ind) ) call csv_record_append(file_record, AGENT%person_number ) call csv_record_append(file_record, AGENT%genome_label ) call csv_record_append(file_record, conv_l2r(AGENT%alive) ) call csv_record_append(file_record, conv_l2r(AGENT%sex_is_male)) call csv_record_append(file_record, AGENT%body_length ) call csv_record_append(file_record, AGENT%body_length_birth ) call csv_record_append(file_record, AGENT%control_unselected ) call csv_record_append(file_record, AGENT%body_mass ) .... end associate ..... end do
Modern Fortran supports pointers, in the same way as many other programming languages like C or C++. However, the use of pointers is more limited in Fortran, which is also not bad because inaccurate use of pointers can cause severe problems with the memory (e.g. when a pointer refers to a nonexistent object).
Basically, pointer is an alias to some other object, e.g. a variable or array. The use of pointers can make code more flexible and dynamic.
The pointer is declared as a specific type (intrinsic or derived) with the pointer
attribute. The object that is used as the target for the pointer should be declared with the same type and the target
attribute.
type(POPULATION), public, target :: generation_one type(POPULATION), public, pointer :: parents
After this, one can simply make the object parents
an alias of generation_one
:
parents => generation_one
Now the object parents
points to the location in the computer memory where the object generation_one
resides. It can consequently be used instead of the "target." For example, all the type-bound functions that are defined for generation_one
can be called for parents
.
call parents%sort_by_fitness()