Real-world SES modeling enhancements¶
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.
Real-world time configurations¶
In Socio-Ecological Systems (SES) modeling, a real-world event's natural or human-induced duration can range from seconds to centuries. Recognizing the importance of time in modeling real-world problems, ABSESpy introduces a superior time control mechanism that stands out from traditional agent-based modeling frameworks.
'Tick' mode¶
The same as traditional agent-based modeling framework and, by default, ABSESpy
records each simulating step as an increment of counting ticker.
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
from abses import MainModel
# create a tick mode model.
model = MainModel()
model.time.ticking_mode
'tick'
TimeDriver
is the major class where most time-related functions are implementationed. For detailed usage, please check out the API documentation.
Under this simplest and most-popular mode, let's try to go 5 steps. Notice that tick
of time went from 0
to 5
.
model.time
model.time.go(5)
# Counter
model.time
<TimeDriver: tick[0]>
<TimeDriver: tick[5]>
Another important property of TimeDriver
, the start_dt
stores when the TimeDriver
was run firstly under the 'tick' mode.
model.time.start_dt
DateTime(2024, 5, 11, 17, 9, 38, 536958)
'duration' mode¶
However, by introducing a feature what we named as a Duration Mode, ABSESpy
makes much easier to simulate the actual progression of time. Bellow is a simple implementation of yearly-step model:
parameters = {
"time": {
"years": 1,
}
}
model = MainModel(parameters=parameters)
# Another ticking model.
model.time.ticking_mode
'duration'
By introducing a param of years = 1
under the 'time' session, our model ticking-mode changed to 'duration'. Which means whenever the model goes a step, the simulation is like one year of real-world time.
model.time
# go six years... ...
model.time.go(6)
model.time
<TimeDriver: 2024-05-11 17:09:38>
<TimeDriver: 2030-05-11 17:09:38>
As you could see, the real-time counted from a start of the current time, we can change the behaviors by inputing different parameters of 'time' session. See this parameters management tutorial to learn how to make full use of this feature. Bellow is the possible parameters table:
TimeDriver
accepts below parameters:
Parameter Name | Expected Data Type | Default Value | Description |
---|---|---|---|
start | str, None | None | If None: use the current time, else: should be a string which can be parsed by pendulum.parse() . |
end | str, int, None | None | If it's a string that can be parsed into datetime the model should end until achieving this time; if int: the model should end in that tick; if None no auto-end. |
irregular | bool | False | If False: not dive into an irregular mode (tick-mode); if True, the model will solve as an irregular mode. |
years | int | 0 | Time duration in years for the duration mode. |
months | int | 0 | Time duration in months for the duration mode. |
weeks | int | 0 | Time duration in weeks for the duration mode. |
days | int | 0 | Time duration in days for the duration mode. |
hours | int | 0 | Time duration in hours for the duration mode. |
minutes | int | 0 | Time duration in minutes for the duration mode. |
seconds | int | 0 | Time duration in seconds for the duration mode. |
'Irregular' mode¶
This is a highly customisable mode, but accordingly, it is not commonly used and requires more code to be written by the user, so it is only briefly described here. To enable this mode, make sure that the record
parameter of the time module is turned on, and that there can't be any parameters that trigger the duration
mode (e.g. years
, months
, days
, hours
, minutes
and seconds
)
parameters = {
"time": {"irregular": True, "start": "2020-01-01", "end": "2022-01-01"}
}
model = MainModel(parameters=parameters)
model.time.go(years=1)
model.time.go(ticks=0, months=5)
model.time.go(ticks=3, days=100)
model.time
model.time.end_dt
model.time.should_end
<TimeDriver: irregular[4] 2022-03-28 00:00:00>
DateTime(2022, 1, 1, 0, 0, 0)
True
Auto-update Dynamic Variables¶
Of the most important reasons to use real-world data and time is dynamically loading and updating time-series datasets.
For testing this feature, let's create a time-series data by pandas.
import pandas as pd
dt_index = pd.date_range("2000-01-01", "2020-01-01", freq="Y")
data_1 = pd.Series(data=range(len(dt_index)), index=dt_index.year)
data_1
/var/folders/s9/w7bh_d6x1h915wcvpbp117tm0000gn/T/ipykernel_31727/3255829277.py:3: FutureWarning: 'Y' is deprecated and will be removed in a future version, please use 'YE' instead. dt_index = pd.date_range("2000-01-01", "2020-01-01", freq="Y")
2000 0 2001 1 2002 2 2003 3 2004 4 2005 5 2006 6 2007 7 2008 8 2009 9 2010 10 2011 11 2012 12 2013 13 2014 14 2015 15 2016 16 2017 17 2018 18 2019 19 dtype: int64
For selecting the data from a corresponding year dynamically, we need to define a _DynamicVariable
. In the belowing testing model
parameters = {
"time": {
"start": "2000-12-31",
"years": 5, # Notice this, each step corresponds to 5 real-world years.
}
}
# setup a testing model.
model = MainModel(parameters=parameters)
# define a function to solve the data_1.
def withdraw_data(data, time):
"""Function for dynamic data withdraw"""
return data.loc[time.year]
# define the dyanamic data, storing `withdrawing function` and the `data source`.
model.human.add_dynamic_variable(
name="data_1", data=data_1, function=withdraw_data
)
Since we store a time-series data withdrawing rule, we can access the data dyanamically in the future whenever the time goes by.
model.human.dynamic_var("data_1")
0
Next selection should be 5 (because the real-world time goes 5 years per step).
model.time.go()
model.human.dynamic_var("data_1")
5
Then, 10... and so on
# should be 10
model.time.go()
model.human.dynamic_var("data_1")
# should be 15
model.time.go()
model.human.dynamic_var("data_1")
10
15
Dynamic data may be beneficial because modeling the real-world SES problem often requires various datasets as inputs. You won't want to re-calculate the data in each step... So! Just define them as dynamic variables when initializing or setting up a module by uploading a withdraw data function
and a data source
. It should also be applied to spatial datasets! Like selecting a raster data through some withdrawing function like xarray.DataArray.sel(time=...)
.
Conditional Time-based Triggering¶
Triggering some function based on a specific condition is another advanced application and highlight advantage of using real-world time. In ABSESpy
we provide a decorator named time_condition
to do so. The below use case is intuitive: our custom Actor
class TestActor
has a function but we only want to use it on the day of the beginning of a year. Therefore, we define a condition dictionary {'month': 1, 'day': 1}
. Therefore, in the 10 times run, the function is called only once.
from abses.time import time_condition
from abses import Actor
class TestActor(Actor):
@time_condition(condition={"month": 1, "day": 1}, when_run=True)
def happy_new_year(self):
print("Today is 1th, January, Happy new year!")
parameters = {"time": {"start": "1996-12-24", "days": 1}}
model = MainModel(parameters=parameters)
agent = model.agents.new(TestActor, 1, singleton=True)
for _ in range(10):
print(f"Time now is {model.time}")
model.time.go()
agent.happy_new_year()
Time now is <TimeDriver: 1996-12-24 00:00:00> Time now is <TimeDriver: 1996-12-25 00:00:00> Time now is <TimeDriver: 1996-12-26 00:00:00> Time now is <TimeDriver: 1996-12-27 00:00:00> Time now is <TimeDriver: 1996-12-28 00:00:00> Time now is <TimeDriver: 1996-12-29 00:00:00> Time now is <TimeDriver: 1996-12-30 00:00:00> Time now is <TimeDriver: 1996-12-31 00:00:00> Today is 1th, January, Happy new year! Time now is <TimeDriver: 1997-01-01 00:00:00> Time now is <TimeDriver: 1997-01-02 00:00:00>
It should be called again in the next year beginning (i.e., 1998-01-01
) if we run this model longer... It means, the function will be called when the condition is fully satisfied. However, we can setup an opposite case by setting parameter when_run = False
:
class TestActor_2(Actor):
@time_condition(condition={"month": 1, "day": 6}, when_run=False)
def working(self):
print("I have to work today 😭!")
agent_2 = model.agents.new(TestActor_2, 1, singleton=True)
for _ in range(5):
print(f"Time now is {model.time}")
model.time.go()
agent_2.working()
Time now is <TimeDriver: 1997-01-03 00:00:00> I have to work today 😭! Time now is <TimeDriver: 1997-01-04 00:00:00> I have to work today 😭! Time now is <TimeDriver: 1997-01-05 00:00:00> Time now is <TimeDriver: 1997-01-06 00:00:00> I have to work today 😭! Time now is <TimeDriver: 1997-01-07 00:00:00> I have to work today 😭!
In the above case, the agent_2
didn't have to work on 6th, January (as we set in the condition dictionary) 😄!
This ensures that certain actions or events only occur at the right moments in your simulation, mirroring real-world occurrences with higher fidelity.