The Basic Structure of your Model¶
In progress
This document is a work in progress if you see any errors, or exclusions or have any problems, please get in touch with us.
ABSESpy is designed to be flexible and modular. It has grown into a package of considerable size and this notebook aims to help you understand the basic structure of a model. Indeed, one of ABSESpy premises is that individual modules should be easily integrated. We call this the loose coupling of multiple modules.
Main components¶
For building your own model, three main components are necessary to be included:
MainModel
: stores several parameters and global variables.BaseHuman
: This module focuses on the human aspect of SES, offering insights and simulations related to human behavior, decisions, and interactions.BaseNature
: As the name suggests, this module is all about the natural environments, allowing for simulations and analyses of different ecological components.
from abses import MainModel, BaseNature, BaseHuman
# without including any parameters, you can init a MainModel
test1 = MainModel()
By default, create a MainModel
instance without parameters is possible. However, in a practical case, it requires three optional parameters:
parameters
: All parameters that should be used in the simulation of model.nature_class
: The base module of the nature subsystem.human_class
: The base module of the human subsystem.
When create an instance of MainModel
, you also create two instances: BaseNature
and BaseHuman
and store them as nature
and human
properties of model.
from omegaconf import dictconfig
# We can pass some parameters.
parameters = {"time": {"start": "2000", "freq": "Y"}}
# instance `MainModel`
model = MainModel(parameters=parameters)
# All the primary components have a same property `state`.
print(model.state)
print(model.human.state)
print(model.nature.state)
init init init
The property state
is accessible in model
, human
, and nature
for showing the current state of simulation. In the above code, all of the states are "init", which means just being instanced without any other operation. More information about modeling flow can be accessible in tutorial of modeling flow.
When state = 'init'
, it means a default function .initialize()
of BaseHuman
and BaseNature
have been called. When designing a custom model, it's a great way to setup some prerequisites, like loading data...
# Including more parameters.
parameters_2 = parameters | {
"human": {"test_human": 1},
"nature": {"test_nature": "I'm a parameter!"},
}
# subclass of `BaseNature`
class Nature(BaseNature):
def initialize(self):
print("Init natural module.")
# subclass of `BaseHuman`
class Human(BaseHuman):
def initialize(self):
print("Init human module.")
# creating an instance
model = MainModel(
parameters=parameters_2, nature_class=Nature, human_class=Human
)
Init natural module. Init human module.
In the above codes, by subclassing from BaseHuman
and BaseNature
, we initialize the model by inputing a custom Human
module and Nature
module. As inputs of initializing a MainModel
instance, their custom function .initialize()
were called once, seperately. It means, obviously, the custom classes replaced default BaseNature
and BaseHuman
. We can also find this fact through accessing the corresponding parameters of them:
# withdraw the parameter from the human module.
model.human.params.test_human
1
# withdraw the parameter from the nature module.
model.nature.params.test_nature
"I'm a parameter!"
For better parameters managment, we have another tutorial of control parameters to demonstrate in detail. So far, what you have to know is that the custom Nature
and Human
modules can access the parameters input into the model because they mock a structure of parameters like this:
time:
start: '2000' # start simulation since 2000.
end: '2005' # The simulation won't be end until 2005.
freq: 'M' # Each counting tick means 1 month in the real world.
human:
test_human: 1
nature:
test_nature: "I'm a parameter!"
Each class of the model components has a specific name, i.e., here they are human
and nature
. The corresponding modules would search the parameters structure of model, then withdraw a specific parameter when necessary. In the next sections, you will find this rule will still be applied for sub-modules and actors (agents).
Create extensible sub-modules¶
Since a social-ecological system is a typical complex system, our users may find their model will growth super big when simulating a real-world case. Therefore, it would be super useful to split the whole model into several logical functional chunks. Different from the two base modules BaseNature
and BaseHuman
, submodules should be a subclass of HumanModule
or PatchModule
.
from abses.human import HumanModule
class Society(HumanModule):
"""How agent behaves in a society."""
class Economy(HumanModule):
"""Some logics in a economic sub-system."""
# create the submodule by `create_module` function.
society = model.human.create_module(Society)
economy = model.human.create_module(Economy, name="economy")
# added two submodules to the human module.
model.human.modules
<abses._bases.modules._ModuleFactory at 0x31eee3d10>
economy.name
'economy'
Since we assigned the name of sub-module Economy
, of cause its name is "economy". At the same time, if we don't assign the name of submodule, it's name will be lowercase __class__.__name__
. Therefore, module name of Society
should be 'society'. The module names are very important for searching the corresponding parameters.
society.name
'society'
Submodules don't have property of 'state'. In stead, they can be turned off or turned on. By default, a new module will be opened, we can turn it off in some cases.
print(society.opening)
# turn it off
society.opening = False
print(society.opening)
True False
Global variable and its observers¶
There are two properties in a model:
agents
: A container of all agents in the model.glob_vars
: A list of all global parameters, which can be accesiible by all components and actors (agents).
agents
is a unique signleton instance of AgentsContainer
for a specific model where all agents are stored. You can operate the agents here by refering this agents operation tutorial. Now, let us try to create some agents for showing how the global variables work.
model.glob_vars
[]
model.agents
<ModelAgents: >
from abses import Actor
actors = model.agents.new(Actor, 3)
# Now the container has three actors.
model.agents
<ModelAgents: (3)Actor>
model.test_glob = "I'm a global variable!"
model.add_glob_vars("test_glob")
# let us choose an agent randomly.
agent = model.agents.get().random.choice()
agent.test_glob
"I'm a global variable!"
Since they are variable, they may be changed sometimes and you need notify any change to the observers.
# change a variable and notify all the observers.
model.test_glob = "variable is ... variable, change it if you want."
# notify any change to the observers.
model.notify()
agent.test_glob
'variable is ... variable, change it if you want.'
And since it's called a global variable, it's not just the actors can access them, but also the components (modules and sub-modules).
# Module can access the global variable
model.nature.test_glob
'variable is ... variable, change it if you want.'