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.
Files changed (40) hide show
  1. simulatingrisk/__init__.py +1 -0
  2. simulatingrisk/about_app.md +2 -0
  3. simulatingrisk/app.py +50 -0
  4. simulatingrisk/batch_run.py +145 -0
  5. simulatingrisk/charts/histogram.js +51 -0
  6. simulatingrisk/charts/histogram.py +72 -0
  7. simulatingrisk/hawkdove/README.md +106 -0
  8. simulatingrisk/hawkdove/app.py +90 -0
  9. simulatingrisk/hawkdove/model.py +391 -0
  10. simulatingrisk/hawkdove/run.py +31 -0
  11. simulatingrisk/hawkdove/server.py +189 -0
  12. simulatingrisk/hawkdovemulti/README.md +92 -0
  13. simulatingrisk/hawkdovemulti/analysis_utils.py +83 -0
  14. simulatingrisk/hawkdovemulti/app.py +242 -0
  15. simulatingrisk/hawkdovemulti/batch_run.py +328 -0
  16. simulatingrisk/hawkdovemulti/model.py +462 -0
  17. simulatingrisk/hawkdovemulti/run_simulation.ipynb +55 -0
  18. simulatingrisk/hawkdovemulti/simrisk_batch.slurm +44 -0
  19. simulatingrisk/risky_bet/README.md +45 -0
  20. simulatingrisk/risky_bet/app.py +17 -0
  21. simulatingrisk/risky_bet/model.py +237 -0
  22. simulatingrisk/risky_bet/run.py +46 -0
  23. simulatingrisk/risky_bet/server.py +102 -0
  24. simulatingrisk/risky_food/README.md +32 -0
  25. simulatingrisk/risky_food/__init__.py +0 -0
  26. simulatingrisk/risky_food/app.py +19 -0
  27. simulatingrisk/risky_food/model.py +250 -0
  28. simulatingrisk/risky_food/run.py +20 -0
  29. simulatingrisk/risky_food/server.py +78 -0
  30. simulatingrisk/stag_hunt/README.md +15 -0
  31. simulatingrisk/stag_hunt/__init__.py +0 -0
  32. simulatingrisk/stag_hunt/model.py +123 -0
  33. simulatingrisk/stag_hunt/run.py +45 -0
  34. simulatingrisk/utils.py +31 -0
  35. simulatingrisk-1.0.0.dist-info/METADATA +113 -0
  36. simulatingrisk-1.0.0.dist-info/RECORD +40 -0
  37. simulatingrisk-1.0.0.dist-info/WHEEL +5 -0
  38. simulatingrisk-1.0.0.dist-info/entry_points.txt +2 -0
  39. simulatingrisk-1.0.0.dist-info/licenses/LICENSE +201 -0
  40. 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()