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,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
|
+
|