simulatingrisk 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simulatingrisk/__init__.py +1 -0
- simulatingrisk/about_app.md +2 -0
- simulatingrisk/app.py +50 -0
- simulatingrisk/batch_run.py +145 -0
- simulatingrisk/charts/histogram.js +51 -0
- simulatingrisk/charts/histogram.py +72 -0
- simulatingrisk/hawkdove/README.md +106 -0
- simulatingrisk/hawkdove/app.py +90 -0
- simulatingrisk/hawkdove/model.py +391 -0
- simulatingrisk/hawkdove/run.py +31 -0
- simulatingrisk/hawkdove/server.py +189 -0
- simulatingrisk/hawkdovemulti/README.md +92 -0
- simulatingrisk/hawkdovemulti/analysis_utils.py +83 -0
- simulatingrisk/hawkdovemulti/app.py +242 -0
- simulatingrisk/hawkdovemulti/batch_run.py +328 -0
- simulatingrisk/hawkdovemulti/model.py +462 -0
- simulatingrisk/hawkdovemulti/run_simulation.ipynb +55 -0
- simulatingrisk/hawkdovemulti/simrisk_batch.slurm +44 -0
- simulatingrisk/risky_bet/README.md +45 -0
- simulatingrisk/risky_bet/app.py +17 -0
- simulatingrisk/risky_bet/model.py +237 -0
- simulatingrisk/risky_bet/run.py +46 -0
- simulatingrisk/risky_bet/server.py +102 -0
- simulatingrisk/risky_food/README.md +32 -0
- simulatingrisk/risky_food/__init__.py +0 -0
- simulatingrisk/risky_food/app.py +19 -0
- simulatingrisk/risky_food/model.py +250 -0
- simulatingrisk/risky_food/run.py +20 -0
- simulatingrisk/risky_food/server.py +78 -0
- simulatingrisk/stag_hunt/README.md +15 -0
- simulatingrisk/stag_hunt/__init__.py +0 -0
- simulatingrisk/stag_hunt/model.py +123 -0
- simulatingrisk/stag_hunt/run.py +45 -0
- simulatingrisk/utils.py +31 -0
- simulatingrisk-1.0.0.dist-info/METADATA +113 -0
- simulatingrisk-1.0.0.dist-info/RECORD +40 -0
- simulatingrisk-1.0.0.dist-info/WHEEL +5 -0
- simulatingrisk-1.0.0.dist-info/entry_points.txt +2 -0
- simulatingrisk-1.0.0.dist-info/licenses/LICENSE +201 -0
- simulatingrisk-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
utility methods for analyzing data collected generated by this model
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import altair as alt
|
|
6
|
+
import polars as pl
|
|
7
|
+
|
|
8
|
+
from simulatingrisk.hawkdovemulti.model import RiskState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def groupby_population_risk_category(df):
|
|
12
|
+
"""takes a polars dataframe populated with model data generated
|
|
13
|
+
by hawk/dove multi model, groups by population risk category and
|
|
14
|
+
adds group labels."""
|
|
15
|
+
# currently written for polars dataframe
|
|
16
|
+
|
|
17
|
+
# group on risk category to get totals for the number of runs that
|
|
18
|
+
# ended up in each different type
|
|
19
|
+
poprisk_grouped = df.group_by("population_risk_category").count()
|
|
20
|
+
poprisk_grouped = poprisk_grouped.rename(
|
|
21
|
+
{"population_risk_category": "risk_category"}
|
|
22
|
+
)
|
|
23
|
+
poprisk_grouped = poprisk_grouped.sort("risk_category")
|
|
24
|
+
|
|
25
|
+
# add column with readable group labels for the numeric categories
|
|
26
|
+
poprisk_grouped = poprisk_grouped.with_columns(
|
|
27
|
+
pl.Series(
|
|
28
|
+
name="type",
|
|
29
|
+
values=poprisk_grouped["risk_category"].map_elements(
|
|
30
|
+
RiskState.category, return_type=pl.datatypes.String
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return poprisk_grouped
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def graph_population_risk_category(poprisk_grouped):
|
|
38
|
+
"""given a dataframe grouped by :meth:`groupby_population_risk_category`,
|
|
39
|
+
generate an altair chart graphing the number of runs in each type,
|
|
40
|
+
grouped and labeled by the larger categories."""
|
|
41
|
+
return (
|
|
42
|
+
alt.Chart(poprisk_grouped)
|
|
43
|
+
.mark_bar(width=15)
|
|
44
|
+
.encode(
|
|
45
|
+
x=alt.X(
|
|
46
|
+
"risk_category",
|
|
47
|
+
title="risk category",
|
|
48
|
+
axis=alt.Axis(tickCount=13), # 13 categories
|
|
49
|
+
scale=alt.Scale(domain=[1, 13]),
|
|
50
|
+
),
|
|
51
|
+
y=alt.Y("count", title="Number of runs"),
|
|
52
|
+
color=alt.Color("type", title="type"),
|
|
53
|
+
)
|
|
54
|
+
.properties(title="Distribution of runs by final population risk category")
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def grouped_risk_totals(df):
|
|
59
|
+
"""Given a Polars dataframe populated with model data generated
|
|
60
|
+
by hawk/dove multi model, calculate total number of agents by
|
|
61
|
+
groups of risk level categories."""
|
|
62
|
+
|
|
63
|
+
# NOTE: based on risk level groupings used in
|
|
64
|
+
# model method for calculating population risk category
|
|
65
|
+
|
|
66
|
+
return df.with_columns(
|
|
67
|
+
# risk inclined: 0, 1, 2
|
|
68
|
+
pl.col("total_r0")
|
|
69
|
+
.add(pl.col("total_r1"))
|
|
70
|
+
.add(pl.col("total_r2"))
|
|
71
|
+
.alias("risk_inclined"),
|
|
72
|
+
# risk moderate: 3, 4, 5, 6
|
|
73
|
+
pl.col("total_r3")
|
|
74
|
+
.add(pl.col("total_r4"))
|
|
75
|
+
.add(pl.col("total_r5"))
|
|
76
|
+
.add(pl.col("total_r6"))
|
|
77
|
+
.alias("risk_moderate"),
|
|
78
|
+
# risk avoidant: 7, 8, 9
|
|
79
|
+
pl.col("total_r7")
|
|
80
|
+
.add(pl.col("total_r8"))
|
|
81
|
+
.add(pl.col("total_r9"))
|
|
82
|
+
.alias("risk_avoidant"),
|
|
83
|
+
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# solara/jupyterviz app
|
|
2
|
+
import altair as alt
|
|
3
|
+
from mesa.experimental import JupyterViz
|
|
4
|
+
import solara
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel
|
|
8
|
+
from simulatingrisk.hawkdove.server import (
|
|
9
|
+
agent_portrayal,
|
|
10
|
+
common_jupyterviz_params,
|
|
11
|
+
draw_hawkdove_agent_space,
|
|
12
|
+
neighborhood_sizes,
|
|
13
|
+
)
|
|
14
|
+
from simulatingrisk.hawkdove.model import divergent_colors_10
|
|
15
|
+
|
|
16
|
+
# start with common hawk/dove params, then add params for variable risk
|
|
17
|
+
jupyterviz_params_var = common_jupyterviz_params.copy()
|
|
18
|
+
jupyterviz_params_var.update(
|
|
19
|
+
{
|
|
20
|
+
"risk_adjustment": {
|
|
21
|
+
"type": "Select",
|
|
22
|
+
"value": "adopt",
|
|
23
|
+
"values": ["none", "adopt", "average"],
|
|
24
|
+
"description": "If and how agents update their risk level",
|
|
25
|
+
},
|
|
26
|
+
"risk_distribution": {
|
|
27
|
+
"type": "Select",
|
|
28
|
+
"value": "uniform",
|
|
29
|
+
"values": HawkDoveMultipleRiskModel.risk_distribution_options,
|
|
30
|
+
"description": "Distribution for initial risk attitudes",
|
|
31
|
+
},
|
|
32
|
+
"adjust_every": {
|
|
33
|
+
"label": "Adjustment frequency (# rounds)",
|
|
34
|
+
"type": "SliderInt",
|
|
35
|
+
"min": 1,
|
|
36
|
+
"max": 30,
|
|
37
|
+
"step": 1,
|
|
38
|
+
"value": 10,
|
|
39
|
+
"description": "How many rounds between risk adjustment",
|
|
40
|
+
},
|
|
41
|
+
"adjust_neighborhood": {
|
|
42
|
+
"type": "Select",
|
|
43
|
+
"value": 8,
|
|
44
|
+
"values": neighborhood_sizes,
|
|
45
|
+
"label": "Adjustment neighborhood size",
|
|
46
|
+
},
|
|
47
|
+
"adjust_payoff": {
|
|
48
|
+
"type": "Select",
|
|
49
|
+
"label": "Adjustment comparison period",
|
|
50
|
+
"value": "recent",
|
|
51
|
+
"values": HawkDoveMultipleRiskModel.supported_adjust_payoffs,
|
|
52
|
+
"description": "Compare recent payoff (since last adjustment "
|
|
53
|
+
+ "round) or total (cumulative from start) when adjusting risk attitudes",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# use same divergent color scale across charts
|
|
59
|
+
color_scale_opts = {"domain": list(range(10)), "range": divergent_colors_10}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def plot_agents_by_risk(model):
|
|
63
|
+
"""plot total number of agents for each risk attitude"""
|
|
64
|
+
agent_df = model.datacollector.get_agent_vars_dataframe().reset_index().dropna()
|
|
65
|
+
if agent_df.empty:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
last_step = agent_df.Step.max()
|
|
69
|
+
# plot current status / last round
|
|
70
|
+
last_round = agent_df[agent_df.Step == last_step]
|
|
71
|
+
# count number of agents for each status
|
|
72
|
+
grouped = last_round.groupby("risk_level", as_index=False).agg(
|
|
73
|
+
total=("AgentID", "count")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# bar chart to show number of agents for each risk attitude
|
|
77
|
+
# configure domain to always display all statuses;
|
|
78
|
+
# limit changes depending on if diagonals are included
|
|
79
|
+
# (NOTE: bug in mesa 2.12, checkbox param does not propagate)
|
|
80
|
+
bar_chart = (
|
|
81
|
+
alt.Chart(grouped)
|
|
82
|
+
.mark_bar(width=15)
|
|
83
|
+
.encode(
|
|
84
|
+
x=alt.X(
|
|
85
|
+
"risk_level",
|
|
86
|
+
title="risk attitude",
|
|
87
|
+
axis=alt.Axis(tickCount=model.max_risk_level + 1),
|
|
88
|
+
scale=alt.Scale(domain=[model.min_risk_level, model.max_risk_level]),
|
|
89
|
+
),
|
|
90
|
+
y=alt.Y("total", title="Number of agents"),
|
|
91
|
+
# NOTE: could apply divergent color scheme here, but it's actually
|
|
92
|
+
# distracting from the main point of this chart, which is quantitative
|
|
93
|
+
# color=alt.Color("risk_level:N").scale(**color_scale_opts),
|
|
94
|
+
)
|
|
95
|
+
.properties(title="Number of agents with each risk attitude")
|
|
96
|
+
)
|
|
97
|
+
return solara.FigureAltair(bar_chart)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def plot_risklevel_changes(model):
|
|
101
|
+
"""plot the number of agents who updated their risk attitude on
|
|
102
|
+
the last adjustment round"""
|
|
103
|
+
model_df = model.datacollector.get_model_vars_dataframe().reset_index()
|
|
104
|
+
if model_df.empty:
|
|
105
|
+
return
|
|
106
|
+
# subset dataframe to only the adjustment rounds
|
|
107
|
+
model_df = model_df[:: model.adjust_round_n]
|
|
108
|
+
if model_df.empty:
|
|
109
|
+
return
|
|
110
|
+
# limit to fields we need
|
|
111
|
+
model_df = model_df[["index", "num_agents_risk_changed", "sum_risk_level_changes"]]
|
|
112
|
+
# rename columns before they become variable labels
|
|
113
|
+
model_df.rename(
|
|
114
|
+
columns={
|
|
115
|
+
"num_agents_risk_changed": "agents",
|
|
116
|
+
"sum_risk_level_changes": "risk attitude totals",
|
|
117
|
+
},
|
|
118
|
+
inplace=True,
|
|
119
|
+
)
|
|
120
|
+
# "melt" to flatten so we can graph as two variables in altair
|
|
121
|
+
melted_df = (
|
|
122
|
+
model_df.melt(id_vars=["index"])
|
|
123
|
+
.dropna()
|
|
124
|
+
.rename(columns={"variable": "category"})
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
line_chart = (
|
|
128
|
+
alt.Chart(melted_df)
|
|
129
|
+
.mark_line()
|
|
130
|
+
.encode(
|
|
131
|
+
y=alt.Y(
|
|
132
|
+
"value",
|
|
133
|
+
title="# changes",
|
|
134
|
+
scale=alt.Scale(domain=[0, model.num_agents]),
|
|
135
|
+
),
|
|
136
|
+
x=alt.X("index"),
|
|
137
|
+
color="category",
|
|
138
|
+
)
|
|
139
|
+
.properties(title="Risk attitude adjustments")
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return solara.FigureAltair(line_chart)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def plot_hawks_by_risk(model):
|
|
146
|
+
"""plot rolling mean of percent of agents in each risk level
|
|
147
|
+
who chose hawk over last several rounds"""
|
|
148
|
+
|
|
149
|
+
# in the first round, mesa returns a dataframe full of NAs; ignore that
|
|
150
|
+
agent_df = (
|
|
151
|
+
model.datacollector.get_agent_vars_dataframe()
|
|
152
|
+
.reset_index()
|
|
153
|
+
.dropna(subset=["AgentID"])
|
|
154
|
+
)
|
|
155
|
+
if agent_df.empty:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
last_step = agent_df.Step.max()
|
|
159
|
+
# limit to last N rounds (how many ?)
|
|
160
|
+
last_n_rounds = agent_df[agent_df.Step.gt(last_step - 60)].copy()
|
|
161
|
+
last_n_rounds["hawk"] = last_n_rounds.choice.apply(
|
|
162
|
+
lambda x: 1 if x == "hawk" else 0
|
|
163
|
+
)
|
|
164
|
+
# for each step and risk level, get number of agents and number of hawks
|
|
165
|
+
grouped = (
|
|
166
|
+
last_n_rounds.groupby(["Step", "risk_level"], as_index=False)
|
|
167
|
+
.agg(hawk=("hawk", "sum"), agents=("AgentID", "count"))
|
|
168
|
+
.sort_values("Step")
|
|
169
|
+
)
|
|
170
|
+
# calculate percent hawk within each group
|
|
171
|
+
grouped["percent_hawk"] = grouped.apply(lambda row: row.hawk / row.agents, axis=1)
|
|
172
|
+
# now calculate rolling percent within each risk attitude
|
|
173
|
+
# thanks to https://stackoverflow.com/a/53339204
|
|
174
|
+
grouped["rolling_pct_hawk"] = grouped.groupby("risk_level")[
|
|
175
|
+
"percent_hawk"
|
|
176
|
+
].transform(lambda x: x.rolling(15, 1).mean())
|
|
177
|
+
|
|
178
|
+
# starting domain 0-50 so it doesn't jump / expand as much
|
|
179
|
+
max_step = max(last_step or 0, 50)
|
|
180
|
+
min_step = max(max_step - 50, 0)
|
|
181
|
+
|
|
182
|
+
chart = (
|
|
183
|
+
alt.Chart(grouped[grouped.Step.gt(min_step - 1)])
|
|
184
|
+
.mark_line()
|
|
185
|
+
.encode(
|
|
186
|
+
x=alt.X("Step", scale=alt.Scale(domain=[min_step, max_step])),
|
|
187
|
+
y=alt.Y(
|
|
188
|
+
"rolling_pct_hawk",
|
|
189
|
+
title="rolling % hawk",
|
|
190
|
+
scale=alt.Scale(domain=[0, 1]),
|
|
191
|
+
),
|
|
192
|
+
color=alt.Color("risk_level:N", title="risk attitude").scale(
|
|
193
|
+
**color_scale_opts
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
.properties(title="Rolling average percent hawk by risk level")
|
|
197
|
+
)
|
|
198
|
+
return solara.FigureAltair(chart)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def plot_wealth_by_risklevel(model):
|
|
202
|
+
"""plot wealth distribution for each risk level"""
|
|
203
|
+
agent_df = model.datacollector.get_agent_vars_dataframe().reset_index().dropna()
|
|
204
|
+
if agent_df.empty:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
last_step = agent_df.Step.max()
|
|
208
|
+
# plot current status / last round
|
|
209
|
+
last_round = agent_df[agent_df.Step == last_step]
|
|
210
|
+
|
|
211
|
+
wealth_chart = (
|
|
212
|
+
alt.Chart(last_round)
|
|
213
|
+
.mark_boxplot(extent="min-max")
|
|
214
|
+
.encode(
|
|
215
|
+
alt.X(
|
|
216
|
+
"risk_level",
|
|
217
|
+
scale=alt.Scale(domain=[model.min_risk_level, model.max_risk_level]),
|
|
218
|
+
title="risk attitude",
|
|
219
|
+
),
|
|
220
|
+
alt.Y("points", title="wealth").scale(zero=False),
|
|
221
|
+
)
|
|
222
|
+
.properties(title="Cumulative wealth by risk attitude")
|
|
223
|
+
)
|
|
224
|
+
return solara.FigureAltair(wealth_chart)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
page = JupyterViz(
|
|
228
|
+
HawkDoveMultipleRiskModel,
|
|
229
|
+
jupyterviz_params_var,
|
|
230
|
+
measures=[
|
|
231
|
+
plot_agents_by_risk,
|
|
232
|
+
plot_hawks_by_risk,
|
|
233
|
+
plot_wealth_by_risklevel,
|
|
234
|
+
plot_risklevel_changes,
|
|
235
|
+
# plot_hawks,
|
|
236
|
+
],
|
|
237
|
+
name="Hawk/Dove game with multiple risk attitudes",
|
|
238
|
+
agent_portrayal=agent_portrayal,
|
|
239
|
+
space_drawer=draw_hawkdove_agent_space,
|
|
240
|
+
)
|
|
241
|
+
# required to render the visualization with Jupyter/Solara
|
|
242
|
+
page
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import csv
|
|
5
|
+
import multiprocessing
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from mesa.batchrunner import _collect_data, _make_model_kwargs
|
|
10
|
+
from tqdm.auto import tqdm
|
|
11
|
+
|
|
12
|
+
from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel
|
|
13
|
+
|
|
14
|
+
neighborhood_sizes = list(HawkDoveMultipleRiskModel.neighborhood_sizes)
|
|
15
|
+
|
|
16
|
+
# NOTE: it's better to be explicit about even parameters
|
|
17
|
+
# instead of relying on model defaults, because
|
|
18
|
+
# parameters specified here are included in data exports
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# combination of parameters we want to run
|
|
22
|
+
params = {
|
|
23
|
+
"default": {
|
|
24
|
+
"grid_size": [5, 10, 25], # , 50], # 100],
|
|
25
|
+
"risk_adjustment": ["adopt", "average"],
|
|
26
|
+
"play_neighborhood": neighborhood_sizes,
|
|
27
|
+
"observed_neighborhood": neighborhood_sizes,
|
|
28
|
+
"adjust_neighborhood": neighborhood_sizes,
|
|
29
|
+
"hawk_odds": [0.5, 0.25, 0.75],
|
|
30
|
+
"adjust_every": [2, 10, 20],
|
|
31
|
+
"risk_distribution": HawkDoveMultipleRiskModel.risk_distribution_options,
|
|
32
|
+
"adjust_payoff": HawkDoveMultipleRiskModel.supported_adjust_payoffs,
|
|
33
|
+
# random?
|
|
34
|
+
},
|
|
35
|
+
# specific scenarios to allow paired statistical tests
|
|
36
|
+
"risk_adjust": {
|
|
37
|
+
# any risk adjustment
|
|
38
|
+
"risk_adjustment": ["adopt", "average"],
|
|
39
|
+
"risk_distribution": "uniform",
|
|
40
|
+
# use model defaults; grid size must be specified
|
|
41
|
+
"grid_size": [5, 10, 25],
|
|
42
|
+
},
|
|
43
|
+
"payoff": {
|
|
44
|
+
"adjust_payoff": HawkDoveMultipleRiskModel.supported_adjust_payoffs,
|
|
45
|
+
"risk_distribution": "uniform",
|
|
46
|
+
# use model defaults; grid size must be specified
|
|
47
|
+
"grid_size": 25,
|
|
48
|
+
},
|
|
49
|
+
"distribution": {
|
|
50
|
+
"risk_distribution": HawkDoveMultipleRiskModel.risk_distribution_options,
|
|
51
|
+
# adopt tends to converge faster; LB also says it's more interesting & simpler
|
|
52
|
+
"risk_adjustment": "adopt",
|
|
53
|
+
# use model defaults; grid size must be specified
|
|
54
|
+
"grid_size": 10,
|
|
55
|
+
},
|
|
56
|
+
"no_adjustment": {
|
|
57
|
+
# no risk adjustment
|
|
58
|
+
"risk_adjustment": None,
|
|
59
|
+
"risk_distribution": HawkDoveMultipleRiskModel.risk_distribution_options,
|
|
60
|
+
"play_neighborhood": neighborhood_sizes,
|
|
61
|
+
"observed_neighborhood": neighborhood_sizes,
|
|
62
|
+
# adjust payoff doesn't matter since we're not adjusting
|
|
63
|
+
"grid_size": [5, 10, 25],
|
|
64
|
+
# maybe also hawk odds
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# method for multiproc running model with a set of params
|
|
70
|
+
def run_hawkdovemulti_model(args):
|
|
71
|
+
run_id, iteration, params, max_steps, data_collection_period = args
|
|
72
|
+
# simplified model runner adapted from mesa batch run code
|
|
73
|
+
|
|
74
|
+
model = HawkDoveMultipleRiskModel(**params)
|
|
75
|
+
while model.running and model.schedule.steps <= max_steps:
|
|
76
|
+
try:
|
|
77
|
+
model.step()
|
|
78
|
+
# by default, signals propagate to all processes
|
|
79
|
+
# take advantage of that to exit and save results
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
# if we get a ctrl-c / keyboard interrupt, stop looping
|
|
82
|
+
# and finish data collection to report on whatever was completed
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
# by default, collect data for the last step
|
|
86
|
+
# (scheduler is 1-based index but data collection is 0-based)
|
|
87
|
+
if data_collection_period == "end":
|
|
88
|
+
collect_steps = [model.schedule.steps - 1]
|
|
89
|
+
elif data_collection_period == "adjustment_round":
|
|
90
|
+
# when requested, collect data at every adjustment round
|
|
91
|
+
every_n = params.get("adjust_every", 10)
|
|
92
|
+
collect_steps = range(0, max_steps, every_n)
|
|
93
|
+
elif data_collection_period == "every_round":
|
|
94
|
+
collect_steps = range(0, max_steps)
|
|
95
|
+
|
|
96
|
+
# make a dict of run id and params for combination with model data
|
|
97
|
+
run_data = {"RunId": run_id, "iteration": iteration, "Step": "-"}
|
|
98
|
+
run_data.update(params)
|
|
99
|
+
all_model_data = []
|
|
100
|
+
all_agent_data = []
|
|
101
|
+
|
|
102
|
+
# collect data at the specified data collection points
|
|
103
|
+
for step in collect_steps:
|
|
104
|
+
try:
|
|
105
|
+
model_data, agent_data = _collect_data(model, step)
|
|
106
|
+
# preserve order: run, iteration, step, params first
|
|
107
|
+
# then data collection from model
|
|
108
|
+
model_run_data = run_data.copy()
|
|
109
|
+
model_run_data["Step"] = step
|
|
110
|
+
model_run_data.update(model_data)
|
|
111
|
+
all_model_data.append(model_run_data)
|
|
112
|
+
|
|
113
|
+
# add step to every agent data entry
|
|
114
|
+
agent_data = [
|
|
115
|
+
{
|
|
116
|
+
"Step": step,
|
|
117
|
+
**agent_data,
|
|
118
|
+
}
|
|
119
|
+
for agent_data in agent_data
|
|
120
|
+
]
|
|
121
|
+
all_agent_data.extend(agent_data)
|
|
122
|
+
except IndexError:
|
|
123
|
+
# if we requested a step that isn't available, collect last round
|
|
124
|
+
# (should capture converged status)
|
|
125
|
+
model_data, agent_data = _collect_data(model, -1)
|
|
126
|
+
model_run_data = run_data.copy()
|
|
127
|
+
model_run_data["Step"] = step
|
|
128
|
+
model_run_data.update(model_data)
|
|
129
|
+
all_model_data.append(model_run_data)
|
|
130
|
+
# add step to every agent data entry
|
|
131
|
+
agent_data = [
|
|
132
|
+
{
|
|
133
|
+
"Step": step,
|
|
134
|
+
**agent_data,
|
|
135
|
+
}
|
|
136
|
+
for agent_data in agent_data
|
|
137
|
+
]
|
|
138
|
+
all_agent_data.extend(agent_data)
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
# populate run id and iteration for every row of agent data
|
|
142
|
+
all_agent_data = [
|
|
143
|
+
{
|
|
144
|
+
"RunId": run_id,
|
|
145
|
+
"iteration": iteration,
|
|
146
|
+
**agent_data,
|
|
147
|
+
}
|
|
148
|
+
for agent_data in all_agent_data
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
return all_model_data, all_agent_data
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def batch_run(
|
|
155
|
+
params,
|
|
156
|
+
iterations,
|
|
157
|
+
number_processes,
|
|
158
|
+
max_steps,
|
|
159
|
+
progressbar,
|
|
160
|
+
collect_agent_data,
|
|
161
|
+
file_prefix,
|
|
162
|
+
max_runs,
|
|
163
|
+
param_choice,
|
|
164
|
+
data_collection_period,
|
|
165
|
+
):
|
|
166
|
+
run_params = params.get(param_choice)
|
|
167
|
+
|
|
168
|
+
param_combinations = _make_model_kwargs(run_params)
|
|
169
|
+
total_param_combinations = len(param_combinations)
|
|
170
|
+
total_runs = total_param_combinations * iterations
|
|
171
|
+
print(
|
|
172
|
+
f"{total_param_combinations} parameter combinations, "
|
|
173
|
+
+ f"{iterations} iteration{'s' if iterations != 1 else ''}, "
|
|
174
|
+
+ f"{total_runs} total runs"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# create a list of all the parameters to run, with run id and iteration
|
|
178
|
+
runs_list = []
|
|
179
|
+
run_id = 0
|
|
180
|
+
for params in param_combinations:
|
|
181
|
+
for iteration in range(iterations):
|
|
182
|
+
runs_list.append(
|
|
183
|
+
(run_id, iteration, params, max_steps, data_collection_period)
|
|
184
|
+
)
|
|
185
|
+
run_id += 1
|
|
186
|
+
|
|
187
|
+
# if maximum runs is specified, truncate the list of run arguments
|
|
188
|
+
if max_runs:
|
|
189
|
+
runs_list = runs_list[:max_runs]
|
|
190
|
+
|
|
191
|
+
# collect data in a directory for this model
|
|
192
|
+
data_dir = os.path.join("data", "hawkdovemulti")
|
|
193
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
194
|
+
datestr = datetime.today().isoformat().replace(".", "_").replace(":", "")
|
|
195
|
+
model_output_filename = os.path.join(data_dir, f"{file_prefix}{datestr}_model.csv")
|
|
196
|
+
if collect_agent_data:
|
|
197
|
+
agent_output_filename = os.path.join(
|
|
198
|
+
data_dir, f"{file_prefix}{datestr}_agent.csv"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
message = f"Saving data collection results to:\n {model_output_filename}"
|
|
202
|
+
if collect_agent_data:
|
|
203
|
+
message += f"\n {agent_output_filename}"
|
|
204
|
+
print(message)
|
|
205
|
+
|
|
206
|
+
# open output files so data can be written as it is generated
|
|
207
|
+
with open(model_output_filename, "w", newline="") as model_output_file:
|
|
208
|
+
if collect_agent_data:
|
|
209
|
+
agent_output_file = open(agent_output_filename, "w", newline="")
|
|
210
|
+
|
|
211
|
+
model_dict_writer = None
|
|
212
|
+
agent_dict_writer = None
|
|
213
|
+
|
|
214
|
+
# adapted from mesa batch run code
|
|
215
|
+
with tqdm(total=total_runs, disable=not progressbar) as pbar:
|
|
216
|
+
with multiprocessing.Pool(number_processes) as pool:
|
|
217
|
+
for model_data, agent_data in pool.imap_unordered(
|
|
218
|
+
run_hawkdovemulti_model, runs_list
|
|
219
|
+
):
|
|
220
|
+
# initialize dictwriter and start csv after the first batch
|
|
221
|
+
if model_dict_writer is None:
|
|
222
|
+
# get field names from first entry
|
|
223
|
+
model_dict_writer = csv.DictWriter(
|
|
224
|
+
model_output_file, model_data[0].keys()
|
|
225
|
+
)
|
|
226
|
+
model_dict_writer.writeheader()
|
|
227
|
+
|
|
228
|
+
model_dict_writer.writerows(model_data)
|
|
229
|
+
|
|
230
|
+
if collect_agent_data:
|
|
231
|
+
if agent_dict_writer is None:
|
|
232
|
+
# get field names from first entry
|
|
233
|
+
agent_dict_writer = csv.DictWriter(
|
|
234
|
+
agent_output_file, agent_data[0].keys()
|
|
235
|
+
)
|
|
236
|
+
agent_dict_writer.writeheader()
|
|
237
|
+
|
|
238
|
+
agent_dict_writer.writerows(agent_data)
|
|
239
|
+
|
|
240
|
+
pbar.update()
|
|
241
|
+
|
|
242
|
+
if collect_agent_data:
|
|
243
|
+
agent_output_file.close()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def main():
|
|
247
|
+
parser = argparse.ArgumentParser(
|
|
248
|
+
prog="hawk/dove batch_run",
|
|
249
|
+
description="Batch run for hawk/dove multiple risk attitude simulation.",
|
|
250
|
+
epilog="""Data files will be created in data/hawkdovemulti/
|
|
251
|
+
relative to current path.""",
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"-i",
|
|
255
|
+
"--iterations",
|
|
256
|
+
type=int,
|
|
257
|
+
help="Number of iterations to run for each set of parameters "
|
|
258
|
+
+ "(default: %(default)s)",
|
|
259
|
+
default=100,
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
"-m",
|
|
263
|
+
"--max-steps",
|
|
264
|
+
help="Maximum steps to run simulations if they have not already "
|
|
265
|
+
+ "converged (default: %(default)s)",
|
|
266
|
+
default=1000, # new convergence logic seems to converge around 400
|
|
267
|
+
type=int,
|
|
268
|
+
)
|
|
269
|
+
parser.add_argument(
|
|
270
|
+
"-p",
|
|
271
|
+
"--processes",
|
|
272
|
+
type=int,
|
|
273
|
+
help="Number of processes to use (default: all available CPUs)",
|
|
274
|
+
default=None,
|
|
275
|
+
)
|
|
276
|
+
parser.add_argument(
|
|
277
|
+
"--progress",
|
|
278
|
+
help="Display progress bar",
|
|
279
|
+
action=argparse.BooleanOptionalAction,
|
|
280
|
+
default=True,
|
|
281
|
+
)
|
|
282
|
+
parser.add_argument(
|
|
283
|
+
"--agent-data",
|
|
284
|
+
help="Store agent data",
|
|
285
|
+
action=argparse.BooleanOptionalAction,
|
|
286
|
+
default=False,
|
|
287
|
+
)
|
|
288
|
+
parser.add_argument(
|
|
289
|
+
"--file-prefix",
|
|
290
|
+
help="Prefix for data filenames (no prefix by default)",
|
|
291
|
+
default="",
|
|
292
|
+
)
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--max-runs",
|
|
295
|
+
help="Stop after the specified number of runs "
|
|
296
|
+
+ "(for development/troubleshooting)",
|
|
297
|
+
type=int,
|
|
298
|
+
default=None,
|
|
299
|
+
)
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--params",
|
|
302
|
+
help="Run a specific set of parameters",
|
|
303
|
+
choices=params.keys(),
|
|
304
|
+
default="default",
|
|
305
|
+
)
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"--collect-data",
|
|
308
|
+
help="When and how often to collect model and agent data",
|
|
309
|
+
choices=["end", "adjustment_round", "every_round"],
|
|
310
|
+
default="end",
|
|
311
|
+
)
|
|
312
|
+
args = parser.parse_args()
|
|
313
|
+
batch_run(
|
|
314
|
+
params,
|
|
315
|
+
args.iterations,
|
|
316
|
+
args.processes,
|
|
317
|
+
args.max_steps,
|
|
318
|
+
args.progress,
|
|
319
|
+
args.agent_data,
|
|
320
|
+
args.file_prefix,
|
|
321
|
+
args.max_runs,
|
|
322
|
+
args.params,
|
|
323
|
+
args.collect_data,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
main()
|