Construct version 5.4.4
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: 04/04/24


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 InteractionMessage reference. The Model constructor only requires a reference to Construct to populate it's various members. All the names of Construct's library 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). NodesetManager::get_nodeset can be used to query ns_manager for a Nodeset and the nodes it contains. Nodesets are shared amongst all models. Nodes are immutable objects once created in a Nodeset. Each node has a name, index, and a set of attributes represented in a dynet::ParameterMap. Nodesets can be created using NodesetManager::create_nodeset and nodes can be added to the nodeset using Nodeset::add_node. Once the simulation process has begun after all Construct components have been loaded, it is assumed that a nodeset will not change in size.

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. These functions default to querying for a 2 dimensional graph, but can query for a 3d graph by supplying a slice dimension. 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 instead. 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

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. Information in these data structure are stored using InteractionItem::item_keys as std::set or std::map keys. Common types of items can be created and parsed using various member function in order to maintain consistency in item parsing among models. Models use specific keys in an item's attributes to determine whether it is safe to parse an item using its various member functions. These keys can be checked using InteractionItem::contains. For instance a knowledge item has the InteractionItem::item_keys::knowledge added to the set of attributes and its presence allows models to assume the presence of an element in InteractionItem::indexes at key InteractionItem::item_keys::knowledge.

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 by models in the Model::think function. Models can then pass each InteractionItem in these messages to each other Model::intercept using Construct::intercept. In this function each item can be manipulated and if the function returns true, the model should delete the item. Model::update is used to modify messages in Construct's message queue in situations where Model::intercept can't. Finally all messages added to Construct' private message queue are dispersed one by one in the Model::communicate function. Model::cleanup is called after all messages have been distributed. 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.

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 operation is typically performed by InteractionMessageParsers stored in Construct::message_parsers.

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!