Model Definition
This page describes the functions and data structures employed to define the model and to instantiate an uninitialized simulation. In Vahana the simulation state and the transition functions that changes the state are decoupled, eliminating the need to define the transition functions prior to creating the simulation instance.
The usual workflow is as follows:
- Define the Agent and Edge types
- Create a
ModelTypes
instance - Register the Agent and Edge types and optionally the Parameters and Globals
- Call
create_model
to create an model (state space) object - Call
create_simulation
to create an uninitialized simulation of this model
The simplest example for such a workflow is:
struct Agent end
struct EdgeState end
const sim = ModelTypes() |>
register_agenttype!(Agent) |>
register_edgetype!(EdgeState) |>
create_model("Minimal Example") |>
create_simulation()
After the simulation is created, the next step is its initialization, which is described on the next page.
Construct the Model
Before we can construct a model, we need to register all agent and edge types.
Vahana.ModelTypes
— TypeModelTypes
This struct stores all (internal) information about the agent and edge types of the model and is therefore the starting point of the model definition.
Call the ModelType constructor without parameters, and then use the created object as the first parameter in register_agenttype!
and register_edgetype!
, whereby the |> operator can be used to concatenate these registrations.
See also create_model
,
Vahana.register_agenttype!
— Functionregister_agenttype!(types::ModelTypes, ::Type{T}, [hints...])
Register an additional agent type to types
.
An agent type is a struct that define the state space for agents of type T
. These structs must be "bits types", meaning the type is immutable and contains only primitive types and other bits types.
By default, it is assumed that an agent can die (be removed from the simulation) by returning nothing
in the transition function (see apply!
. In the case that agents are never removed, the hint :Immortal can be given to improve the performance of the simulation.
If agents of this type never access the state of other agents of the same type in a transition function (e.g., via agentstate or neighborstates calls), then the :Independent hint can be given. This avoids unnecessary copies of the agents.
A pipeable version of register_agenttype!
is also available, enabling the construction of a Model via a sequence of register_*
calls. This pipeline starts with ModelTypes()
as the source and terminates in the create_model
function as the destination.
See also add_agent!
and add_agents!
Vahana.register_edgetype!
— Functionregister_edgetype!(types::ModelTypes, ::Type{T}, [hints...; kwargs...])
Register an additional edge type to types
.
An edge type T is a structure that defines the state (field) of Edge{T}
. These structs must be "bit types", that is, the type is immutable and contains only primitive types and other bit types. Often these types T are stateless, in which case they are used as a tag to distinguish between the existing types of edges of the model.
The internal data structures used to store the graph in memory can be modified by the hints parameters:
:IgnoreFrom
: The ID of the source agent is not stored. This implies that the state of the agents on the source of the edge is not accessible via e.g. theneighborstates
function.:Stateless
: Store only the ID of the source agent.:SingleType
: All target agents have the same type, needs also keywordtarget
(see below).:SingleEdge
: Each agent can be the target for max. one edge.:IgnoreSourceState
: The ID of the source agent is not used to access the state of the agent with this ID.:NumEdgesOnly
: Combines:IgnoreFrom
and:Stateless
:HasEdgeOnly
: Combines:IgnoreFrom
,:Stateless
and:SingleEdge
If :SingleType
is set, the keyword argument target
must be added. The value of this argument must be the type of the target node. If the target
keyword exists, but the :SingleType
hint is not explicitly specified, it will be set implicitly
If it is known how many agents of this type exist, this can also be specified via the optional size
keyword. This can improve performance, but can also increase the memory usage.
A pipeable version of register_edgetype!
is also available, enabling the construction of a Model via a sequence of register_*
calls. This pipeline starts with ModelTypes()
as the source and terminates in the create_model
function as the destination.
See also Edge Hints, add_edge!
and add_edges!
We can then use the ModelTypes
instance to construct the model. Which means that optimized methods that can be used to access or modify the simulation state during the transition function are generated. See Performance Tuning for details.
The ModelTypes
instance can subsequently be utilized to construct the model. This involves that optimized methods are generated, enabling access or modification of the simulation state during the execution of the transition function. For comprehensive details regarding this process, please refer to the Performance Tuning documentation.
As create_model
adds new methods, it increments the "world age counter". This means, that you can not construct the model in the same function as you initialize it. But you can call create_simulation
in the same function so
const model = create_model(modeltypes, "Minimal Example")
function create_and_init()
sim = create_simulation(model, nothing, nothing)
add_agent!(sim, Agent())
end
is valid code.
Vahana.create_model
— Functioncreate_model(types::ModelTypes, name::String)
Creates a struct that is a subtype of the Simulation
type and methods corresponding to the type information of types
to the Julia session. The new structure is named name
, and all methods are specific to this structure using Julia's multiple dispatch concept, so it is possible to have different models in the same Julia session (as long as name
is different).
Returns a Model
that can be used in create_simulation
to create a concrete simulation.
Vahana.Model
— TypeModel
A Model instance is created by create_model
and can then be used to create one or multiple simulations via create_simulation
.
Register Parameters and Globals
Parameters are constant values used throughout the simulation, while globals are variables that can change during the simulation run. You can register them using register_param!
and register_global!
, or by defining custom types and pass instances of this types to create_simulation
.
Vahana.register_param!
— Functionregister_param!(types::ModelTypes, name::Symbol, default_value::T)
There are two ways to set up parameters for a simulation. Either the instance of a struct defined by the modeler must be passed via the param
argument to create_simulation
, or the individual parameters are passed using register_param!
before create_model
is called. The parameters can also be set after create_simulation
and before finish_init!
is called.
A pipeable version of register_param!
is also available, enabling the construction of a Model via a sequence of register_*
calls. This pipeline starts with ModelTypes()
as the source and terminates in the create_model
function as the destination.
Vahana.register_global!
— Functionregister_global!(types::ModelTypes, name::Symbol, default_value::T)
There are two ways to set up globals for a simulation. Either the instance of a struct defined by the modeler must be passed via the globals
argument to create_simulation
, or the individual parameters are passed using register_global!
before create_model
is called.
A pipeable version of register_global!
is also available, enabling the construction of a Model via a sequence of register_*
calls. This pipeline starts with ModelTypes()
as the source and terminates in the create_model
function as the destination.
Create a Simulation
Finally, create an uninitialized simulation based on your model using
Vahana.create_simulation
— Functioncreate_simulation(model::Model, [params = nothing, globals = nothing; name = model.name, filename = name, overwrite_file = true, logging = false, debug = false])
Creates and return a new simulation object, which stores the complete state of a simulation.
model
is an Model
instance created by create_model
.
params
must be a struct (or nothing
) that contains all parameters of a simulation. Parameter values are constant in a simulation run and can be retrieved via the param
function.
globals
must be a mutable struct (or nothing
). The values of these fields are accessible for all agents via the get_global
function. The values can be changed by calling set_global!
or push_global!
.
The optional keyword argument name
is used as meta-information about the simulation and has no effect on the dynamics, since name
is not accessible in the transition functions. If name
is not given, the name of the model is used instead.
The optional filename
keyword is a string that will be used as the name of the hdf5 file when a simulation or a part of it is written via e.g. write_snapshot
.
The file is created when a write...
function is called for the first time, and it is created in an h5
subfolder. By default an existing file with the same filename
will be overwritten, but this behavior can be disabled with the overwrite_file
keyword, in which case the files will be numbered. If filename
is not provided, the name
argument is used instead.
The keywords logging
is a boolean flag that enables an automatical log file. The log file contains information such as the time spent in different functions. When also debug
is set to true, the log file contains more details and the stream will be flushed after each write.
As with the hdf5 files, overwrite_file the
keyword determines whether the log files are overwritten or numbered. The numbering is set independently from the numbering of the hdf5 files.
The simulation starts in an uninitialized state. After adding the agents and edges for the initial state, it is necessary to call finish_init!
before applying a transition function for the first time.
See also create_model
, param
, get_global
, set_global!
, push_global!
and finish_init!
This creates a blank simulation ready for initialization.
Remember, in Vahana, a "model" specifies the possible state space, while a simulation is a concrete realization within this space. Transition functions, defined later, determine how the simulation evolves over time.
By following these steps, you set up the structure of your simulation. The next phase involves initializing the simulation with agents and edges.