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,391 @@
1
+ from enum import Enum
2
+ from collections import deque
3
+ import math
4
+ import statistics
5
+
6
+ import mesa
7
+
8
+ from simulatingrisk.utils import coinflip
9
+
10
+ Play = Enum("Play", ["HAWK", "DOVE"])
11
+ play_choices = [Play.HAWK, Play.DOVE]
12
+
13
+
14
+ # divergent color scheme, ten colors
15
+ # from https://colorbrewer2.org/?type=diverging&scheme=RdYlGn&n=10
16
+ divergent_colors_10 = [
17
+ "#a50026",
18
+ "#d73027",
19
+ "#f46d43",
20
+ "#fdae61",
21
+ "#fee08b",
22
+ "#d9ef8b",
23
+ "#a6d96a",
24
+ "#66bd63",
25
+ "#1a9850",
26
+ "#006837",
27
+ ]
28
+
29
+ # divergent color scheme, five colors
30
+ # from https://colorbrewer2.org/?type=diverging&scheme=RdYlGn&n=5
31
+ divergent_colors_5 = ["#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641"]
32
+
33
+
34
+ class HawkDoveAgent(mesa.Agent):
35
+ """
36
+ An agent with a risk attitude playing Hawk or Dove
37
+ """
38
+
39
+ def __init__(self, unique_id, model, hawk_odds=None):
40
+ super().__init__(unique_id, model)
41
+
42
+ self.points = 0
43
+ self.choice = self.initial_choice(hawk_odds)
44
+ self.last_choice = None
45
+
46
+ # risk level must be set by base class, since initial
47
+ # conditions are specific to single / variable risk games
48
+ self.set_risk_level()
49
+
50
+ def set_risk_level(self):
51
+ raise NotImplementedError
52
+
53
+ def __repr__(self):
54
+ return (
55
+ f"<{self.__class__.__name__} id={self.unique_id} "
56
+ + f"r={self.risk_level} points={self.points}>"
57
+ )
58
+
59
+ def initial_choice(self, hawk_odds=None):
60
+ # first round : choose what to play randomly or based on initial hawk odds
61
+ opts = {}
62
+ if hawk_odds is not None:
63
+ opts["weight"] = hawk_odds
64
+ return coinflip(play_choices, **opts)
65
+
66
+ @property
67
+ def choice_label(self):
68
+ return "hawk" if self.choice == Play.HAWK else "dove"
69
+
70
+ def get_neighbors(self, size):
71
+ """get all neighbors for a supported neighborhood size"""
72
+ check_neighborhood_size(size)
73
+ # 4 and 8 neighborhood use default radius 1
74
+ # 8 and 24 both use moore neighborhood (includes diagonals)
75
+ opts = {"moore": True}
76
+ if size == 4:
77
+ # use von neumann neighborhood instead of moore (no diagonal)
78
+ opts["moore"] = False
79
+
80
+ # for 24 size neighborhood, use radius 2
81
+ if size == 24:
82
+ opts["radius"] = 2
83
+
84
+ return self.model.grid.get_neighbors(self.pos, include_center=False, **opts)
85
+
86
+ @property
87
+ def play_neighbors(self):
88
+ """neighbors to play against, based on model play neighborhood size"""
89
+ return self.get_neighbors(self.model.play_neighborhood)
90
+
91
+ @property
92
+ def observed_neighbors(self):
93
+ """neighbors to look at when deciding what to play;
94
+ based on model observed neighborhood size"""
95
+ return self.get_neighbors(self.model.observed_neighborhood)
96
+
97
+ @property
98
+ def num_dove_neighbors(self):
99
+ """count how many neighbors played DOVE on the last round
100
+ (uses `observed_neighborhood` size from model)"""
101
+ return len([n for n in self.observed_neighbors if n.last_choice == Play.DOVE])
102
+
103
+ @property
104
+ def proportional_num_dove_neighbors(self):
105
+ """adjust the number of dove neighbors to scale observations
106
+ to a standard range of risk levels."""
107
+ # for convenience and simplicity, we scale the neighborhood so
108
+ # that agent risk levels can always be defined as 0-9 no matter
109
+ # the neighborhood size. (Can also be thought of as always
110
+ # scaling to a neighborhood size of 8.).
111
+ # Agent risk levels are 0-9 but scaling is 1-8, since 0 and 9
112
+ # always play hawk and dove respectively.
113
+ ratio = 8 / self.model.observed_neighborhood
114
+ # always round to an integer
115
+ return round(ratio * self.num_dove_neighbors)
116
+
117
+ def choose(self):
118
+ "decide what to play this round"
119
+ # first choice is random since we don't have any information
120
+ # about neighbors' choices
121
+ if self.model.schedule.steps == 0:
122
+ return
123
+
124
+ # after the first round, choose based on what neighbors did last time
125
+
126
+ # choose based on the number of neighbors who played
127
+ # dove last round and agent risk level
128
+
129
+ # agent with r = 0 should always take the risky choice
130
+ # (any risk is acceptable).
131
+ # agent with r = max should always take the safe option
132
+ # (no risk is acceptable)
133
+ if self.proportional_num_dove_neighbors >= self.risk_level:
134
+ choice = Play.HAWK
135
+ else:
136
+ choice = Play.DOVE
137
+
138
+ # based on model configuration, should agent play randomly instead?
139
+ if self.model.random_play_odds and coinflip(
140
+ [True, False], weight=self.model.random_play_odds
141
+ ):
142
+ # if a random play is selected, flip a coin between hawk and dove
143
+ choice = coinflip([Play.HAWK, Play.DOVE])
144
+
145
+ self.choice = choice
146
+
147
+ def play(self):
148
+ # play against each neighbor and calculate cumulative payoff
149
+ payoff = 0
150
+ for n in self.play_neighbors:
151
+ payoff += self.payoff(n)
152
+ # update total points based on payoff this round
153
+ self.points += payoff
154
+
155
+ # store this round's choice as previous choice
156
+ self.last_choice = self.choice
157
+
158
+ def payoff(self, other):
159
+ """
160
+ If I play HAWK and neighbor plays DOVE: 3
161
+ If I play DOVE and neighbor plays DOVE: 2
162
+ If I play DOVE and neighbor plays HAWK: 1
163
+ If I play HAWK and neighbor plays HAWK: 0
164
+ """
165
+ if self.choice == Play.HAWK:
166
+ if other.choice == Play.DOVE:
167
+ return 3
168
+ if other.choice == Play.HAWK:
169
+ return 0
170
+ elif self.choice == Play.DOVE:
171
+ if other.choice == Play.DOVE:
172
+ return 2
173
+ if other.choice == Play.HAWK:
174
+ return 1
175
+
176
+ @property
177
+ def points_rank(self):
178
+ if self.points:
179
+ return math.floor(self.points / self.model.max_agent_points * 10)
180
+ return 0
181
+
182
+
183
+ class HawkDoveModel(mesa.Model):
184
+ """
185
+ Model for hawk/dove game with risk attitudes.
186
+
187
+ :param grid_size: number for square grid size (creates n*n agents)
188
+ :param play_neighborhood: size of neighborhood each agent plays
189
+ against; 4, 8, or 24 (default: 8)
190
+ :param observed_neighborhood: size of neighborhood each agent looks
191
+ at when choosing what to play; 4, 8, or 24 (default: 8)
192
+ :param hawk_odds: odds for playing hawk on the first round (default: 0.5)
193
+ """
194
+
195
+ #: whether the simulation is running
196
+ running = True # required for batch run
197
+ #: readable status (running/converged)
198
+ status = "running"
199
+
200
+ #: size of deque/fifo for recent values
201
+ rolling_window = 30
202
+ #: minimum size before calculating rolling average
203
+ min_window = 15
204
+ #: class to use when initializing agents
205
+ agent_class = HawkDoveAgent
206
+ #: supported neighborhood sizes
207
+ neighborhood_sizes = {4, 8, 24}
208
+ #: minimum risk level
209
+ min_risk_level = 0
210
+ #: maximum risk level allowed
211
+ max_risk_level = 9
212
+
213
+ def __init__(
214
+ self,
215
+ grid_size,
216
+ play_neighborhood=8,
217
+ observed_neighborhood=8,
218
+ hawk_odds=0.5,
219
+ random_play_odds=0.00,
220
+ ):
221
+ super().__init__()
222
+ # check parameters for combinations that aren't allowed together
223
+ if grid_size < 5:
224
+ if play_neighborhood > 8:
225
+ raise ValueError(
226
+ "Play neighborhood %d is too large for grid size %d",
227
+ play_neighborhood,
228
+ grid_size,
229
+ )
230
+ if observed_neighborhood > 8:
231
+ raise ValueError(
232
+ "Observed neighborhood %d is too large for grid size %d",
233
+ observed_neighborhood,
234
+ grid_size,
235
+ )
236
+
237
+ # assume a fully-populated square grid
238
+ self.num_agents = grid_size * grid_size
239
+ for nsize in [play_neighborhood, observed_neighborhood]:
240
+ check_neighborhood_size(nsize)
241
+
242
+ self.play_neighborhood = play_neighborhood
243
+ self.observed_neighborhood = observed_neighborhood
244
+
245
+ # distribution of first choice (50/50 by default)
246
+ self.hawk_odds = hawk_odds
247
+ # how often should agents make a random play
248
+ self.random_play_odds = random_play_odds
249
+
250
+ # create fifos to track recent behavior to detect convergence
251
+ self.recent_percent_hawk = deque([], maxlen=self.rolling_window)
252
+ self.recent_rolling_percent_hawk = deque([], maxlen=self.rolling_window)
253
+
254
+ # initialize a single grid (each square inhabited by a single agent);
255
+ # configure the grid to wrap around so everyone has neighbors
256
+ self.grid = mesa.space.SingleGrid(grid_size, grid_size, True)
257
+ self.schedule = mesa.time.StagedActivation(self, ["choose", "play"])
258
+
259
+ # initialize all agents
260
+ agent_opts = self.new_agent_options()
261
+ for i in range(self.num_agents):
262
+ # add to scheduler and place randomly in an empty spot
263
+ agent = self.agent_class(i, self, **agent_opts)
264
+ self.schedule.add(agent)
265
+ self.grid.move_to_empty(agent)
266
+
267
+ self.datacollector = mesa.DataCollector(**self.get_data_collector_options())
268
+
269
+ def get_data_collector_options(self):
270
+ # method to return options for data collection,
271
+ # so subclasses can modify
272
+ return {
273
+ "model_reporters": {
274
+ "max_agent_points": "max_agent_points",
275
+ "percent_hawk": "percent_hawk",
276
+ "rolling_percent_hawk": "rolling_percent_hawk",
277
+ "status": "status",
278
+ # explicitly track total agents, instead of inferring from grid size
279
+ "total_agents": "num_agents",
280
+ },
281
+ "agent_reporters": {
282
+ "risk_level": "risk_level",
283
+ "choice": "choice_label",
284
+ "points": "points",
285
+ },
286
+ }
287
+
288
+ def new_agent_options(self):
289
+ # generate and return a dictionary with common options
290
+ # for initializing all agents
291
+ return {"hawk_odds": self.hawk_odds}
292
+
293
+ def step(self):
294
+ """
295
+ A model step. Used for collecting data and advancing the schedule
296
+ """
297
+ self.schedule.step()
298
+ # check if simulation has converged and should stop running
299
+ if self.converged:
300
+ self.status = "converged"
301
+ self.running = False
302
+
303
+ # collect data after status is updated, so data collected
304
+ # for last round will reflect converged status
305
+ self.datacollector.collect(self)
306
+
307
+ @property
308
+ def max_agent_points(self):
309
+ # what is the current largest point total of any agent?
310
+ return max([a.points for a in self.schedule.agents])
311
+
312
+ @property
313
+ def percent_hawk(self):
314
+ # what percent of agents chose hawk?
315
+ hawks = [a for a in self.schedule.agents if a.choice == Play.HAWK]
316
+ phawk = len(hawks) / self.num_agents
317
+ # add to recent values
318
+ self.recent_percent_hawk.append(phawk)
319
+ return phawk
320
+
321
+ @property
322
+ def rolling_percent_hawk(self):
323
+ # make sure we have enough values to check
324
+ if len(self.recent_percent_hawk) > self.min_window:
325
+ rolling_phawk = statistics.mean(self.recent_percent_hawk)
326
+ # add to recent values
327
+ self.recent_rolling_percent_hawk.append(rolling_phawk)
328
+ return rolling_phawk
329
+
330
+ @property
331
+ def converged(self):
332
+ # check if the simulation is stable and should stop running
333
+ # calculating based on rolling percent hawk; when this is stable
334
+ # within our rolling window, return true
335
+ # - currently checking for single value;
336
+ # could allow for a small amount variation if necessary
337
+
338
+ # in variable risk with risk adjustment, numbers are not strictly equal
339
+ # but do get close and fairly stable; round to two digits before comparing
340
+ rounded_set = set([round(x, 2) for x in self.recent_rolling_percent_hawk])
341
+ return (
342
+ len(self.recent_rolling_percent_hawk) > self.min_window
343
+ and len(rounded_set) == 1
344
+ )
345
+
346
+
347
+ def check_neighborhood_size(size):
348
+ # neighborhood size check, shared by model and agent
349
+ if size not in HawkDoveModel.neighborhood_sizes:
350
+ raise ValueError(
351
+ f"{size} is not a supported neighborhood size; "
352
+ + f"must be one of {HawkDoveModel.neighborhood_sizes}"
353
+ )
354
+
355
+
356
+ class HawkDoveSingleRiskAgent(HawkDoveAgent):
357
+ """
358
+ An agent with a risk attitude playing Hawk or Dove; must be initialized
359
+ with a risk level
360
+ """
361
+
362
+ def set_risk_level(self):
363
+ self.risk_level = self.model.agent_risk_level
364
+
365
+
366
+ class HawkDoveSingleRiskModel(HawkDoveModel):
367
+ """hawk/dove simulation where all agents have the same risk atttitude.
368
+ Adds a required `agent_risk_level` parameter; supports all
369
+ parameters in :class:`HawkDoveModel`.
370
+ """
371
+
372
+ #: class to use when initializing agents
373
+ agent_class = HawkDoveSingleRiskAgent
374
+
375
+ risk_attitudes = "single"
376
+
377
+ def __init__(self, grid_size, agent_risk_level, *args, **kwargs):
378
+ if (
379
+ agent_risk_level > self.max_risk_level
380
+ or agent_risk_level < self.min_risk_level
381
+ ):
382
+ raise ValueError(
383
+ f"Agent risk level {agent_risk_level} is out of range; must be between "
384
+ + f"{self.min_risk_level} - {self.max_risk_level}"
385
+ )
386
+
387
+ # store agent risk level
388
+ self.agent_risk_level = agent_risk_level
389
+
390
+ # pass through options and initialize base class
391
+ super().__init__(grid_size, *args, **kwargs)
@@ -0,0 +1,31 @@
1
+ import mesa
2
+
3
+ from simulatingrisk.hawkdove.model import HawkDoveModel
4
+ from simulatingrisk.hawkdove.server import (
5
+ agent_portrayal,
6
+ grid_size,
7
+ model_params,
8
+ # risk_bins,
9
+ )
10
+ from simulatingrisk.charts.histogram import HistogramModule
11
+
12
+
13
+ risk_histogram = HistogramModule(list(range(9)), 175, 500, "risk levels", "risk_level")
14
+ points_histogram = HistogramModule(
15
+ list(range(10)), 45, 200, "cumulative payoff percentile", "points_rank"
16
+ )
17
+
18
+ grid = mesa.visualization.CanvasGrid(agent_portrayal, grid_size, grid_size, 500, 500)
19
+
20
+
21
+ server = mesa.visualization.ModularServer(
22
+ HawkDoveModel,
23
+ # [grid, histogram, world_chart, risk_chart],
24
+ [grid, risk_histogram, points_histogram],
25
+ "Hawk/Dove risk attitude Simulation",
26
+ model_params=model_params,
27
+ )
28
+ server.port = 8521 # The default
29
+ server.launch()
30
+
31
+ server.launch()
@@ -0,0 +1,189 @@
1
+ """
2
+ Configure visualization elements and instantiate a server
3
+ """
4
+
5
+ import altair as alt
6
+ import solara
7
+ import pandas as pd
8
+
9
+ from simulatingrisk.hawkdove.model import (
10
+ Play,
11
+ divergent_colors_10,
12
+ HawkDoveModel,
13
+ )
14
+
15
+
16
+ def agent_portrayal(agent):
17
+ # initial display
18
+ portrayal = {
19
+ # styles for mesa runserver
20
+ "Shape": "circle",
21
+ # "Color": "gray",
22
+ # "Filled": "true",
23
+ "Layer": 0,
24
+ "r": 0.2,
25
+ "risk_level": agent.risk_level,
26
+ "choice": str(agent.choice),
27
+ # styles for solara / jupyterviz
28
+ "size": 25,
29
+ # "color": "tab:gray",
30
+ }
31
+ # specific to multiple risk attitude variant
32
+ if hasattr(agent, "risk_level_changed"):
33
+ portrayal["risk_level_changed"] = agent.risk_level_changed
34
+
35
+ # color based on risk level; risk levels are always 0-9
36
+ colors = divergent_colors_10
37
+
38
+ portrayal["Color"] = colors[agent.risk_level]
39
+ # copy to lowercase color for solara
40
+ portrayal["color"] = portrayal["Color"]
41
+
42
+ # filled for hawks, hollow for doves
43
+ # (shapes would be better...)
44
+ portrayal["Filled"] = agent.choice == Play.HAWK
45
+ portrayal["choice"] = "hawk" if agent.choice == Play.HAWK else "dove"
46
+
47
+ # size based on points within current distribution after first round
48
+ if agent.points > 0:
49
+ # # set radius based on relative points, but don't go smaller than 1 radius
50
+ # # or too large to fit in the grid
51
+ portrayal["r"] = (agent.points_rank / 15) + 0.2
52
+ # # size for solara / jupyterviz
53
+ portrayal["size"] = (agent.points_rank / 15) * 50
54
+
55
+ return portrayal
56
+
57
+
58
+ grid_size = 10
59
+
60
+ model_params = {
61
+ "grid_size": grid_size,
62
+ }
63
+
64
+ neighborhood_sizes = sorted(list(HawkDoveModel.neighborhood_sizes))
65
+
66
+ # parameters common to both hawk/dove variants
67
+ common_jupyterviz_params = {
68
+ "grid_size": {
69
+ "type": "SliderInt",
70
+ "value": grid_size,
71
+ "label": "Grid Size",
72
+ "min": 10,
73
+ "max": 100,
74
+ "step": 1,
75
+ },
76
+ "play_neighborhood": {
77
+ "type": "Select",
78
+ "value": 8,
79
+ "values": neighborhood_sizes,
80
+ "label": "Play neighborhood size",
81
+ },
82
+ "observed_neighborhood": {
83
+ "type": "Select",
84
+ "value": 8,
85
+ "values": neighborhood_sizes,
86
+ "label": "Observed neighborhood (determines choice of play)",
87
+ },
88
+ "hawk_odds": {
89
+ "type": "SliderFloat",
90
+ "value": 0.5,
91
+ "label": "Hawk Odds (first choice)",
92
+ "min": 0.0,
93
+ "max": 1.0,
94
+ "step": 0.1,
95
+ },
96
+ "random_play_odds": {
97
+ "type": "SliderFloat",
98
+ "value": 0.01,
99
+ "label": "Random play odds",
100
+ "min": 0.0,
101
+ "max": 1.0,
102
+ "step": 0.01,
103
+ },
104
+ }
105
+
106
+ # in single-risk variant, risk level is set for all agents at init time
107
+ jupyterviz_params = common_jupyterviz_params.copy()
108
+ jupyterviz_params["agent_risk_level"] = {
109
+ "type": "SliderInt",
110
+ "label": "Agent risk attitude",
111
+ "min": 0,
112
+ "max": 8,
113
+ "step": 1,
114
+ "value": 2,
115
+ }
116
+
117
+
118
+ def draw_hawkdove_agent_space(model, agent_portrayal):
119
+ # custom agent space chart, modeled on default
120
+ # make_space method in mesa jupyterviz code,
121
+ # but using altair so we can contrl shapes as well as color and size
122
+ all_agent_data = []
123
+ for i in range(model.grid.width):
124
+ for j in range(model.grid.height):
125
+ agent_data = {}
126
+ content = model.grid._grid[i][j]
127
+ if not content:
128
+ continue
129
+ if not hasattr(content, "__iter__"):
130
+ # Is a single grid
131
+ content = [content]
132
+ for agent in content:
133
+ # use all data from agent portrayal, and add x,y coordinates
134
+ agent_data = agent_portrayal(agent)
135
+ agent_data["x"] = i
136
+ agent_data["y"] = j
137
+ all_agent_data.append(agent_data)
138
+
139
+ df = pd.DataFrame(all_agent_data)
140
+ # print(all_agent_data)
141
+
142
+ # use grid x,y coordinates to plot, but supress axis labels
143
+
144
+ # currently passing in actual colors, not a variable to use for color
145
+ # use domain/range to use color for display
146
+ hawkdove_domain = ("hawk", "dove")
147
+ shape_range = ("triangle-up", "circle")
148
+
149
+ # when risk attitude is variable,
150
+ # use divergent color scheme to indicate risk level
151
+ if model.risk_attitudes == "variable":
152
+ colors = list(set(a["color"] for a in all_agent_data))
153
+ chart_color = alt.Color("color").legend(None).scale(domain=colors, range=colors)
154
+ elif model.risk_attitudes == "single":
155
+ chart_color = (
156
+ alt.Color("choice")
157
+ # .legend(None)
158
+ .scale(domain=hawkdove_domain, range=["orange", "blue"])
159
+ )
160
+
161
+ # optionally display information from multi-risk attitude variant
162
+ if "risk_level_changed" in df.columns:
163
+ outer_color = alt.Color(
164
+ "risk_level_changed", title="adjusted risk attitude"
165
+ ).scale(
166
+ domain=[False, True],
167
+ range=["transparent", "black"],
168
+ )
169
+ else:
170
+ outer_color = chart_color
171
+
172
+ agent_chart = (
173
+ alt.Chart(df)
174
+ .mark_point() # filled=True)
175
+ .encode(
176
+ x=alt.X("x", axis=None), # no x-axis label
177
+ y=alt.Y("y", axis=None), # no y-axis label
178
+ size=alt.Size("size", title="points rank"), # relabel size for legend
179
+ # when fill and color differ, color acts as an outline
180
+ fill=chart_color,
181
+ color=outer_color,
182
+ shape=alt.Shape( # use shape to indicate choice
183
+ "choice", scale=alt.Scale(domain=hawkdove_domain, range=shape_range)
184
+ ),
185
+ )
186
+ .configure_view(strokeOpacity=0) # hide grid/chart lines
187
+ )
188
+
189
+ return solara.FigureAltair(agent_chart)
@@ -0,0 +1,92 @@
1
+ # Hawk-Dove with multiple risk attitudes
2
+
3
+ This is a variation of the [Hawk/Dove game with risk attitudes](../hawkdove/).
4
+ This version adds multiple risk attitudes, with options for updating
5
+ risk attitudes periodically based on comparing success of neighboring agents.
6
+
7
+ The basic mechanics of the game are the same. This model adds options
8
+ for agent risk adjustment (none, adopt, average) and period of risk
9
+ adjustment (by default, every ten rounds). The payoff used to compare
10
+ agents when adjusting risk attitudes can either be recent (since the
11
+ last adjustment round) or total points for the whole game. The
12
+ adjustment neighborhood, or which neighboring agents are considered
13
+ when adjusting risk attitudes, can be configured to 4, 8, or 24.
14
+
15
+ Initial risk attitudes are set by the model. Risk distribution can
16
+ be configured to use a normal distribution, uniform (random), bimodal,
17
+ skewed left, or skewed right.
18
+
19
+ Like the base hawk/dove risk attitude game, there is also a
20
+ configuration to add some chance of agents playing hawk/dove randomly
21
+ instead of choosing based on the rules of the game.
22
+
23
+ ## Convergence
24
+
25
+ The model is configured to stop automatically when it has stabilized.
26
+
27
+ Model and agent data collection reports on whether agents updated their
28
+ risk level in the last adjustment round, and model data collection
29
+ includes a status of "running" or "converged".
30
+
31
+ ### With Adjustment
32
+
33
+ When adjustment is enabled (adopt / average), convergence is checked
34
+ after the simulation has run for a minimum of 50 rounds. The simulation
35
+ is considered stable when individual agents are no longer adjusting risk attitudes,
36
+ or when the number of agents in each risk attitude are relatively stable
37
+ (e.g., agents are swapping risk attitudes but the overall total for each
38
+ category is stable).
39
+
40
+ Convergence is reached when an adjustment round occurs and _either_:
41
+
42
+ - _zero_ agents adjust their risk attitude
43
+ - the total changes of agents per risk attitudes is less than 7% of the population size
44
+
45
+ ### Without Adjustment
46
+
47
+ If adjustment is not enabled, convergence logic falls back to the
48
+ implementation of the hawk/dove single-risk attitude simulation. In this case,
49
+ convergence is based on a stable rolling % average of agents playing hawk.
50
+
51
+
52
+ ## Batch running
53
+
54
+ This module includes a custom batch run script to run the simulation and
55
+ collect data across a large combination of parameters and generate data
56
+ files with collected model and agent data.
57
+
58
+ To run the script locally from the root project directory:
59
+ ```sh
60
+ simulatingrisk/hawkdovemulti/batch_run.py
61
+ ```
62
+ Use `-h` or `--help` to see options.
63
+
64
+ If this project has been installed with pip or similar, the script is
65
+ available as `simrisk-hawkdovemulti-batchrun`.
66
+
67
+ To run the batch run script on an HPC cluster:
68
+
69
+ - Create a conda environment and install dependencies and this project.
70
+ (Major mesa dependencies available with conda are installed first as
71
+ conda packages)
72
+
73
+ ```sh
74
+ module load anaconda3/2023.9
75
+ conda create --name simrisk pandas networkx matplotlib numpy tqdm click
76
+ conda activate simrisk
77
+ pip install git+https://github.com/Princeton-CDH/simulating-risk.git@hawkdove-batchrun
78
+ ```
79
+ For convenience, an example [slurm batch script](simrisk_batch.slurm) is
80
+ included for running the batch run script (some portions are
81
+ specific to Princeton's Research Computing HPC environment.)
82
+
83
+ - Customize the slurm batch script as desired, copy it to the cluster, and submit
84
+ the job: `sbatch simrisk_batch.slurm`
85
+
86
+ By default, the batch run script will use all available processors, and will
87
+ create model and agent data files under a `data/hawkdovemulti/` directory
88
+ relative to the working directory where the script is called.
89
+
90
+
91
+
92
+