Construct version 5.4.2
An agent based modeling framework

Liscensed by the Center for Computational Analysis of Social and Organizational Systems (CASOS). For academic use only. For commerical use contact CASOS.

Current Primary Author: Stephen Dipple

Supporting Authors: Michael Kowalchuck and Neal Altman

Last Updated: 11/02/23


Welcome to the Construct Model Guide. Construct is an agent based modeling software framework. Many models have been created using this framework. This guide's aim is to expain all the tools that Construct offers to create your own models.

Some of the reasons to use Construct's framework are simplified input, ouput, data storage, and cross model interaction. Construct comes prebuilt with the ability to parse DynetML and CSV files as well as the ability to output them to those formats. The Graph data structure will allow for both easy iteration through elements and memory efficient data storage. Finally the Construct class facilitates communication between models. This communication can come in the form of direct interaction via the ModelManager, indirect interaction through the Graphs as they are shared between all models, or passive interaction via the InteractionMessageQueue.

In the following sections, I'll be going over these concepts which will allow a user to create their own models. This guide assumes that the user already has good knowledge of how to format input xml files for Construct. This guide will also assume the user has a basic knowledge of c++ programming, including standard library data structures, iterators, and pointers.

Creating a Model

An example model has been created with the neccessary components to allow users to out of the gate begin working. The Template.h and Template.cpp files contain the model "Template Model". All models inheriet from the Model class. Not only is this useful for storing all the active models, but the models inheriet the Construct, NodesetManager, GraphManager, and Random pointers. The constructor of a model is required to have a specific form, but all other member functions are optional and default back to the Model implementation of those functions. When setting verbose runtime equal to true in the input xml file, a message will appear when a model's function has not been implemented. The functions include initialize, think, update, communicate, and clean_up. These functions take no arguments and return no values except communicate which takes a InteractionMessageQueue iterator. The construct takes the Construct pointer, the list of parameters, and the model's name. Of these the Construct pointer and model's name is passed to the Model constructor. All the names of Construct's libarary of models is contained in the model_names namespace.

A block in the constructor for the Template model has been provided that isn't compiled, but gives plenty of examples of using Construct in the model. From here can do some basic operations like "std::cout << "Hello World" << std::endl;" and execute it by include the "Template Model" in the input XML's models list. After moving on from the toy model, you can create your own model by doing the following steps:

  1. Copy and rename the header and implementation file
  2. Change the header guards for the header file
  3. Rename (do not refactor) the class name in both the header and implementation file
  4. include the newly created header in Supp_Library.h
  5. add your new class to the dynet::create_model function with the appropriate else if statement for your model's constructor.

Nodes and Graphs

As Construct is a framework for network agent based models, it is important to learn to use nodes and graphs. With your newly created model, you have access to the NodesetManager (ns_manager) and the GraphManager (graph_manager). Nodes are immutable objects once created by the NodesetManager. Models are not able to modify them. You can get a Nodeset by using NodesetManager::get_nodeset. You can then either get a Node by using its index or by its name from a Nodeset. Each node has a name, index, and a set of attributes represented in a dynet::ParameterMap. A Nodeset can be created using NodesetManager::create_nodeset, but it must become an immuatble Nodeset via NodesetManager::turn_to_const before it can be used in a network. Nodesets are shared amongst all models.

Graphs are also shared amongst models. You can access a graph using one of two functions; GraphManager::load_required and GraphManager::load_optional. Both functions require the name of the graph as well as dimension information in the form of nodesets. Any graphs loaded must match the submitted nodesets. The functions have a default argument for the slice dimension. Using the default value implies the loaded graph should be a two dimensional Graph. Suppling a slice nodeset automatically implies the Graph represents a three dimension structure. If the dimensions do not match the loaded Graph, a dynet::construct_exception is thrown. For GraphManager::load_required, if a Graph does not already exist with the submitted name, a dynet::construct_exception is also thrown. GraphManager::load_optional will return a nullptr, if the requrested Graph doesn't already exist. An overload of GraphManager::load_optional, which takes dimension representation and a default value, will create a Graph, add it to the GraphManager, and return it if the requested Graph is not already loaded.

Graphs are specialized based on the dimension densities. If both dimensions are dense, an array is used. If both are sparse, a binary tree is instead used. The use of Graph's functions do not change in terms of result based on the dimension representation, but memory and cpu performance will. Graph has a host of iterators to minimize cpu usage when dimensions are sparse and are recommended to be used whenever possible.

Because a binary tree is used for the sparse representation, not all network elements will be stored in memory as an entry. Due to this, if a Graph element is accessed by any means and it does not have an entry stored in memory, the Graph's default value will be returned. This default value can be any value valid for that data type so it is best not to assume a default value when implementing operations on a Graph. The Graph::at functions come with an overload that automatically detects if the new value of an element is being set to the default value (see Graph::at(unsigned int, unsigned int, const T&)). If the passed value does not equal to the default value, it will use the complement Graph::at function to set the element. Otherwise, the entry in memory is removed.

Exchanging Messages

InteractionMessages can be added to Construct's private message queue which are dispersed to models for parsing in the Model::communicate function. This message collection and dispersion offers a significantly different interaction between models compared to the sharing of data structures such as Graphs. Messages are created independently in the Model::think function. These messages are then amended, deleted, or cause other messages to be created in the Model::update function. Finally all messages added to Construct' private message queue are dispersed one by one in the Model::communicate function. These are all rules that Construct's library of models follow, but is not a strict requirement of any supplimentary models created by interested developers.

Messages contain a sender and receiver index which correspond the indexes of agent nodes. Messages also contain a CommunicationMedium. This medium is used in determining how the message is parsed. The actual content of the message is contained in the vector of InteractionItems.

Each InteractionItem in a message has a set of attributes, indexes, and values which are made public to developers. However to shortcut the creation and parsing of common items, various member function are typically used to keep formatting consistent. The functions assigned a unique attribute to ensure an item is only parsed in a specific way. For instance a knowledge item has the "knowledge" item key, InteractionItem::item_keys::knowledge added to the set of attributes. Then the items requires the associated indexes and values for that type of item. In the case of a knowledge item, a knowledge index is required. This information can found in the various member functions of InteractionItem.

Final Thoughts

Much effort has been made to allow for as much customization as possible for future models while maintaining predictable behaviour for existing models and minizing the amount of upfront information that must be absorbed to use this software. Some more advanced techniques would be to have a model inheriet from an already existing model that has many of the same behaviour. This is done often with the Standard Interaction Model. Models can use the pointer provided by the ModelManager to directly interact with other models such as the Location model. The order of the model member function calling can be manipulated. In addition, it is important to ensure that things like knowledge items are only parsed once. This saves on computation time and memory space and is handled by an independent model KnowledgeParsing. This model is created and added to the model list by the Standard Interaction Model, Social_Media_no_followers Model, and Location Model.

This work is constantly evovling and as such errors in the revision and implementation process do occur. If a typo, an error in the code, or an unknown exception is found, please send the relevant information on to the ORA Google Group. For an unknown exception, please include the input xml file and all terminal output. Suggestions and critiques are also welcome to improve clarity and sussinctness of the software.

Thank you and enjoy!