villager

DOI R build status Codecov test coverage License: MIT

villager is a framework for creating and running agent based models in R. Its purpose is to provide an extensible framework where modeling can be done in native R.

Features

Installing

villager can be installed from CRAN by running the following,

install.packages("villager")

Takeaways

When reading though the Readme and vignettes, it’s important to take note of a few concepts

Using villager

villager is about modeling populations with (optional) associated resources. It supports a community level aggregation of agents, referred to as villages or an individual village. Agents, which are referred to as gender-neutral agents, are members of community level aggregations.

villager compliant models must conform to the function template below. The agent_mgr and resource_mgr are responsible for interacting with the individual agents and resources.

test_model <- function(current_state, previous_state, model_data, agent_mgr, resource_mgr) {
  ...
  ...
}

Creating & Managing Agents

Agents are created by instantiating the agent class. There are a number of agent properties that can be passed to the constructor.

test_model <- function(current_state, previous_state, model_data, agent_mgr, resource_mgr) {
  mother <- agent$new(first_name="Kirsten", last_name="Taylor", age=9125)
  father <- agent$new(first_name="Joshua", last_name="Thompson", age=7300)
  daughter <- agent$new(first_name="Mariylyyn", last_name="Thompson", age=10220)
}

To add agents to the simulation, use the provided agent_mgr object to call add_agent. Because the classes are R6, the object can be modified after being added to the manager and the changes will be persisted without needing to re-add the villager. For example, setting a daughter’s mother and her father below. Note that the standard way is to modify the properties beforehand, although not strictly necessary.

test_model <- function(current_state, previous_state, model_data, agent_mgr, resource_mgr) {
  agent_mgr <- agent_manager$new()
  agent_mgr$add_agent(mother, father, daughter)
  daughter$mother_id <- mother$identifier
  daughter$father_id <- father$identifier
}

The agent manager can also be used to pair agents, representative of a relationship or social bond.

agent_mgr$agent_mgr$connect_agents(mother, father)

Creating & Managing Resources

Resources are similar to agents in that they’re both R6 classes, are instantiated similarly, and are also managed by an object passed into the model. An example of creating resources and adding them to the simulation is given below.

test_model <- function(current_state, previous_state, model_data, agent_mgr, resource_mgr) {
  corn_resource <- resource$new(name="corn", quantity = 10)
  fish_resource <- resource$new(name="fish", quantity = 15)
  corn_resource$quantity=5
  
  resource_mgr <- resource_manager$new()
  resource_mgr$add_resource(corn_resource, fish_resource)
  fish_resource$quantity=5
}

State

Objects of type village, agent, and resourcehave particular states at a particular time. As the simulation progresses, the state of these change based on model logic. At the end of each time step, the state of each object is saved, giving a complete record of the system’s evolution. The essence of any agent based model is changing the state at each time step. villager provides a mechanism for defining the initial state and for changing the state throughout the simulation.

Managing the Initial State

Creating the initial state is done by creating a function that resembles model functions from above. The manager classes are used to populate the village with an initial population of agents and resources.

initial_condition <- function(current_state, model_data, agent_mgr, resource_mgr) {
  # Create the initial villagers
  mother <- agent$new(first_name="Kirsten", last_name="Taylor", age=9125)
  father <- agent$new(first_name="Joshua", last_name="Thompson", age=7300)
  daughter <- agent$new(first_name="Mariylyyn", last_name="Thompson", age=10220)
  daughter$mother_id <- mother$identifier
  daughter$father_id <- father$identifier
  
  # Add the agents to the manager
  agent_mgr$connect_agents(mother, father)
  agent_mgr$add_agent(mother, father, daughter)
  
  # Create the resources
  corn_resource <- resource$new(name="corn", quantity = 10)
  fish_resource <- resource$new(name="fish", quantity = 15)
  
  # Add the resources to the manager
  resource_mgr$add_resource(corn_resource, fish_resource)
}

Creating Villages and Running Models

Models are tied to particular village instances. This binding is done when villages are created, shown below. Models can have names and must always be paired with an initial condition function and a model function.

small_village <- village$new("Test Model 1", initial_condition, test_model)

The simulator class is responsible for running simulations. It encapsulates all of the villages and controls the duration of the simulation. The simulator below runs for 100 time steps: roughly 13 years. The simulator can be paired with any number of villages, in the case of the simulator below, there’s only a single village.

simulator <- simulation$new(100, list(small_village))
simulator$run_model()

Example: A small village with a single family

We can combine the examples above into a full simulation that…

library(villager)
initial_condition <- function(current_state, model_data, agent_mgr, resource_mgr) {
  # Create the initial villagers
  mother <- agent$new(first_name="Kirsten", last_name="Taylor", age=9125)
  father <- agent$new(first_name="Joshua", last_name="Thompson", age=7300)
  daughter <- agent$new(first_name="Mariylyyn", last_name="Thompson", age=10220)
  daughter$mother_id <- mother$identifier
  daughter$father_id <- father$identifier
  
  # Add the agents to the manager
  agent_mgr$connect_agents(mother, father)
  agent_mgr$add_agent(mother, father, daughter)
  
  # Create the resources
  corn_resource <- resource$new(name="corn", quantity = 10)
  fish_resource <- resource$new(name="fish", quantity = 15)
  
  # Add the resources to the manager
  resource_mgr$add_resource(corn_resource, fish_resource)
}

test_model <- function(current_state, previous_state, model_data, agent_mgr, resource_mgr) {
print(paste("Step:", current_state$step))
  for (agent in agent_mgr$get_living_agents()) {
    agent$age <- agent$age+1
    if (agent$age >= 4383) {
      agent$profession <- "Farmer"
    }
  }
}

small_village <- village$new("Test Model", initial_condition, test_model)
simulator <- simulation$new(4745, list(small_village))
simulator$run_model()

Example: Creating new villagers

To demonstrate programatically creating villagers, consider the model below that has the following logic.

library(villager)
    current_day <- current_state$step
    print(current_day)
    if((current_day%%2) == 0) {
      # Then it's an even day
      # Create two new agents whose first names are random numbers
      for (i in 1:2) {
        name <- runif(1, 0.0, 100)
        new_agent <- agent$new(first_name <- name, last_name <- "Smith")
        agent_mgr$add_agent(new_agent)
      }
    } else {
      # It's an odd day
      living_agents <- agent_mgr$get_living_agents()
      # Kill the first one
      living_agents[[1]]$alive <- FALSE
    }
  }
  coastal_village <- village$new("Test village", initial_condition, model)
  simulator <- simulation$new(4, villages = list(coastal_village))
  simulator$run_model()
  mgr <- simulator$villages[[1]]$agent_mgr

Advanced Usage

In the examples above, the default properties of agents and resources were used. It’s possible that these won’t cover all the needs for more diverse models. There are vignettes on extending the agent and resource classes to handle these situations.

Contributing

Code contributions are welcome as pull requests to the develop branch. Bugs, comments, and questions can be submitted as Github Issues.