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,462 @@
|
|
|
1
|
+
import statistics
|
|
2
|
+
from collections import Counter, deque
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
from simulatingrisk.hawkdove.model import HawkDoveAgent, HawkDoveModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HawkDoveMultipleRiskAgent(HawkDoveAgent):
|
|
10
|
+
"""
|
|
11
|
+
An agent with random risk attitude playing Hawk or Dove. Optionally
|
|
12
|
+
adjusts risks based on most successful neighbor, depending on model
|
|
13
|
+
configuration.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
#: points since last adjustment round; starts at 0
|
|
17
|
+
recent_points = 0
|
|
18
|
+
|
|
19
|
+
#: whether or not risk level changed on the last adjustment round
|
|
20
|
+
risk_level_changed = False
|
|
21
|
+
|
|
22
|
+
def set_risk_level(self):
|
|
23
|
+
# get risk attitude from model based on configured distribution
|
|
24
|
+
self.risk_level = self.model.get_risk_attitude()
|
|
25
|
+
|
|
26
|
+
def play(self):
|
|
27
|
+
# save total points before playing so we only need to calculate
|
|
28
|
+
# current round payoff once
|
|
29
|
+
prev_points = self.points
|
|
30
|
+
super().play()
|
|
31
|
+
# when enabled by the model, periodically adjust risk level
|
|
32
|
+
|
|
33
|
+
# add payoff from current round to recent points
|
|
34
|
+
self.recent_points += self.points - prev_points
|
|
35
|
+
|
|
36
|
+
if self.model.adjustment_round:
|
|
37
|
+
self.adjust_risk()
|
|
38
|
+
# reset to zero to track points until next adjustment round
|
|
39
|
+
self.recent_points = 0
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def adjust_neighbors(self):
|
|
43
|
+
"""neighbors to look at when adjusting risk attitude; uses
|
|
44
|
+
model adjust_neighborhood size"""
|
|
45
|
+
return self.get_neighbors(self.model.adjust_neighborhood)
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def compare_payoff_field(self):
|
|
49
|
+
"""determine which payoff to compare depending on model option:
|
|
50
|
+
(cumulative/total or points since last adjustment round)"""
|
|
51
|
+
return "recent_points" if self.model.adjust_payoff == "recent" else "points"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def compare_payoff(self):
|
|
55
|
+
"""payoff value to use for adjustment comparison
|
|
56
|
+
(depends on model configuration)"""
|
|
57
|
+
return getattr(self, self.compare_payoff_field)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def most_successful_neighbor(self):
|
|
61
|
+
"""identify and return the neighbor with the most points"""
|
|
62
|
+
# sort neighbors by points, highest points first
|
|
63
|
+
# adapted from risky bet wealthiest neighbor
|
|
64
|
+
|
|
65
|
+
return sorted(
|
|
66
|
+
self.adjust_neighbors,
|
|
67
|
+
key=lambda x: getattr(x, self.compare_payoff_field),
|
|
68
|
+
reverse=True,
|
|
69
|
+
)[0]
|
|
70
|
+
|
|
71
|
+
def adjust_risk(self):
|
|
72
|
+
# look at neighbors
|
|
73
|
+
# if anyone has more points
|
|
74
|
+
# either adopt their risk attitude or average theirs with yours
|
|
75
|
+
|
|
76
|
+
best = self.most_successful_neighbor
|
|
77
|
+
|
|
78
|
+
# if most successful neighbor has more points and a different
|
|
79
|
+
# risk attitude, adjust
|
|
80
|
+
if (
|
|
81
|
+
best.compare_payoff > self.compare_payoff
|
|
82
|
+
and best.risk_level != self.risk_level
|
|
83
|
+
):
|
|
84
|
+
# adjust risk based on model configuration
|
|
85
|
+
if self.model.risk_adjustment == "adopt":
|
|
86
|
+
# adopt neighbor's risk level
|
|
87
|
+
self.risk_level = best.risk_level
|
|
88
|
+
elif self.model.risk_adjustment == "average":
|
|
89
|
+
# average theirs with mine, then round to a whole number
|
|
90
|
+
# since this model uses discrete risk levels
|
|
91
|
+
self.risk_level = round(
|
|
92
|
+
statistics.mean([self.risk_level, best.risk_level])
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# track that risk attitude has been updated
|
|
96
|
+
self.risk_level_changed = True
|
|
97
|
+
else:
|
|
98
|
+
# track that risk attitude was not changed
|
|
99
|
+
self.risk_level_changed = False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RiskState(IntEnum):
|
|
103
|
+
"""Categorization of population risk states"""
|
|
104
|
+
|
|
105
|
+
# majority risk inclined
|
|
106
|
+
c1 = 1
|
|
107
|
+
c2 = 2
|
|
108
|
+
c3 = 3
|
|
109
|
+
c4 = 4
|
|
110
|
+
|
|
111
|
+
# majority risk moderate
|
|
112
|
+
c5 = 5
|
|
113
|
+
c6 = 6
|
|
114
|
+
c7 = 7
|
|
115
|
+
c8 = 8
|
|
116
|
+
|
|
117
|
+
# majority risk avoidant
|
|
118
|
+
c9 = 9
|
|
119
|
+
c10 = 10
|
|
120
|
+
c11 = 11
|
|
121
|
+
c12 = 12
|
|
122
|
+
|
|
123
|
+
# no clear majority
|
|
124
|
+
c13 = 13
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def category(cls, val):
|
|
128
|
+
# handle both integer and risk state enum value
|
|
129
|
+
if isinstance(val, RiskState):
|
|
130
|
+
val = val.value
|
|
131
|
+
if val in {1, 2, 3, 4}:
|
|
132
|
+
return "majority risk inclined"
|
|
133
|
+
if val in {5, 6, 7, 8}:
|
|
134
|
+
return "majority risk moderate"
|
|
135
|
+
if val in {9, 10, 11, 12}:
|
|
136
|
+
return "majority risk avoidant"
|
|
137
|
+
return "no majority"
|
|
138
|
+
|
|
139
|
+
def __str__(self):
|
|
140
|
+
# override string method to return just the numeric value,
|
|
141
|
+
# for better serialization of collected data
|
|
142
|
+
return str(self.value)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class HawkDoveMultipleRiskModel(HawkDoveModel):
|
|
146
|
+
"""
|
|
147
|
+
Model for hawk/dove game with variable risk attitudes. Supports
|
|
148
|
+
all parameters in :class:`~simulatingrisk.hawkdove.model.HawkDoveModel`
|
|
149
|
+
and adds several parmeters to control if and how agents adjust
|
|
150
|
+
their risk attitudes (strategy, frequency, and neighborhood size).
|
|
151
|
+
|
|
152
|
+
:param risk_adjustment: strategy agents should use for adjusting risk;
|
|
153
|
+
None (default), adopt, or average
|
|
154
|
+
:param adjust_every: when risk adjustment is enabled, adjust every
|
|
155
|
+
N rounds (default: 10)
|
|
156
|
+
:param adjust_neighborhood: size of neighborhood to look at when
|
|
157
|
+
adjusting risk attitudes; 4, 8, or 24 (default: play_neighborhood)
|
|
158
|
+
:param adjust_payoff: when comparing neighbors points for risk adjustment,
|
|
159
|
+
consider cumulative payoff (`total`) or payoff since the
|
|
160
|
+
last adjustment round (`recent`) (default: recent)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
risk_attitudes = "variable"
|
|
164
|
+
agent_class = HawkDoveMultipleRiskAgent
|
|
165
|
+
|
|
166
|
+
supported_risk_adjustments = (None, "adopt", "average")
|
|
167
|
+
supported_adjust_payoffs = ("recent", "total")
|
|
168
|
+
risk_distribution_options = (
|
|
169
|
+
"uniform",
|
|
170
|
+
"normal",
|
|
171
|
+
"skewed left",
|
|
172
|
+
"skewed right",
|
|
173
|
+
"bimodal",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
grid_size,
|
|
179
|
+
risk_adjustment="adopt",
|
|
180
|
+
risk_distribution="uniform",
|
|
181
|
+
adjust_every=10,
|
|
182
|
+
adjust_neighborhood=None,
|
|
183
|
+
adjust_payoff="recent",
|
|
184
|
+
*args,
|
|
185
|
+
**kwargs,
|
|
186
|
+
):
|
|
187
|
+
# convert string input from solara app parameters to None
|
|
188
|
+
if risk_adjustment == "none":
|
|
189
|
+
risk_adjustment = None
|
|
190
|
+
|
|
191
|
+
# check parameters
|
|
192
|
+
if risk_distribution not in self.risk_distribution_options:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"Unsupported risk distribution '{risk_distribution}'; "
|
|
195
|
+
+ f"must be one of {', '.join(self.risk_distribution_options)}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# make sure risk adjustment is valid
|
|
199
|
+
if risk_adjustment not in self.supported_risk_adjustments:
|
|
200
|
+
risk_adjust_opts = ", ".join(
|
|
201
|
+
[opt or "none" for opt in self.supported_risk_adjustments]
|
|
202
|
+
)
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"Unsupported risk adjustment '{risk_adjustment}'; "
|
|
205
|
+
+ f"must be one of {risk_adjust_opts}"
|
|
206
|
+
)
|
|
207
|
+
if adjust_payoff not in self.supported_adjust_payoffs:
|
|
208
|
+
adjust_payoffs_opts = ", ".join(self.supported_adjust_payoffs)
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"Unsupported adjust payoff option '{adjust_payoff}'; "
|
|
211
|
+
+ f"must be one of {adjust_payoffs_opts}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# initialize a risk attitude generator based on configured distrbution
|
|
215
|
+
# must be set before calling super for agent init
|
|
216
|
+
self.risk_distribution = risk_distribution
|
|
217
|
+
self.risk_attitude_generator = self.get_risk_attitude_generator()
|
|
218
|
+
|
|
219
|
+
super().__init__(grid_size, *args, **kwargs)
|
|
220
|
+
|
|
221
|
+
self.risk_adjustment = risk_adjustment
|
|
222
|
+
self.adjust_round_n = adjust_every
|
|
223
|
+
# if adjust neighborhood is not specified, then use the same size
|
|
224
|
+
# as play neighborhood
|
|
225
|
+
self.adjust_neighborhood = adjust_neighborhood or self.play_neighborhood
|
|
226
|
+
# store whether to compare cumulative payoff or since last adjustment round
|
|
227
|
+
self.adjust_payoff = adjust_payoff
|
|
228
|
+
|
|
229
|
+
self.recent_total_per_risk_level = deque([], maxlen=2)
|
|
230
|
+
|
|
231
|
+
def _risk_level_in_bounds(self, value):
|
|
232
|
+
# check if a generated risk level is within bounds
|
|
233
|
+
return self.min_risk_level <= value <= self.max_risk_level
|
|
234
|
+
|
|
235
|
+
def get_risk_attitude_generator(self):
|
|
236
|
+
"""return a generator that will return risk attitudes for individual
|
|
237
|
+
agents based on the configured distribution."""
|
|
238
|
+
if self.risk_distribution == "uniform":
|
|
239
|
+
# uniform/random: generate random integer within risk level range
|
|
240
|
+
while True:
|
|
241
|
+
yield self.random.randint(self.min_risk_level, self.max_risk_level)
|
|
242
|
+
if self.risk_distribution == "normal":
|
|
243
|
+
# return values from a normal distribution centered around 4.5
|
|
244
|
+
while True:
|
|
245
|
+
yield round(self.random.gauss(4.5, 1.5))
|
|
246
|
+
elif self.risk_distribution == "skewed left":
|
|
247
|
+
# return values from a triangler distribution centered around 0
|
|
248
|
+
while True:
|
|
249
|
+
yield round(
|
|
250
|
+
self.random.triangular(self.min_risk_level, self.max_risk_level, 0)
|
|
251
|
+
)
|
|
252
|
+
elif self.risk_distribution == "skewed right":
|
|
253
|
+
# return values from a triangular distribution centered around 9
|
|
254
|
+
while True:
|
|
255
|
+
yield round(
|
|
256
|
+
self.random.triangular(self.min_risk_level, self.max_risk_level, 9)
|
|
257
|
+
)
|
|
258
|
+
elif self.risk_distribution == "bimodal":
|
|
259
|
+
# to generate a bimodal distribution, alternately generate
|
|
260
|
+
# values from two different normal distributions centered
|
|
261
|
+
# around the beginning and end of our risk attitude range
|
|
262
|
+
while True:
|
|
263
|
+
yield round(self.random.gauss(0, 1.5))
|
|
264
|
+
yield round(self.random.gauss(9, 1.5))
|
|
265
|
+
# NOTE: on smaller grids, using 0/9 makes it extremely
|
|
266
|
+
# unlikely to get mid-range risk values (4/5)
|
|
267
|
+
|
|
268
|
+
def get_risk_attitude(self):
|
|
269
|
+
"""return the next value from risk attitude generator, based on
|
|
270
|
+
configured distribution."""
|
|
271
|
+
val = next(self.risk_attitude_generator)
|
|
272
|
+
|
|
273
|
+
# for bimodal distribution, clamp values to range
|
|
274
|
+
if self.risk_distribution == "bimodal":
|
|
275
|
+
return max(self.min_risk_level, min(self.max_risk_level, val))
|
|
276
|
+
|
|
277
|
+
# for all other distributions:
|
|
278
|
+
# occasionally generators will return values that are out of range.
|
|
279
|
+
# rather than capping to the min/max and messing up the distribution,
|
|
280
|
+
# just get the next value
|
|
281
|
+
while not self._risk_level_in_bounds(val):
|
|
282
|
+
val = next(self.risk_attitude_generator)
|
|
283
|
+
return val
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def adjustment_round(self) -> bool:
|
|
287
|
+
"""is the current round an adjustment round?"""
|
|
288
|
+
# check if the current step is an adjustment round
|
|
289
|
+
# when risk adjustment is enabled, agents should adjust their risk
|
|
290
|
+
# strategy every N rounds;
|
|
291
|
+
return (
|
|
292
|
+
self.risk_adjustment
|
|
293
|
+
and self.schedule.steps > 0
|
|
294
|
+
and self.schedule.steps % self.adjust_round_n == 0
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def get_data_collector_options(self):
|
|
298
|
+
# in addition to common hawk/dove data points,
|
|
299
|
+
# we want to include population risk category
|
|
300
|
+
opts = super().get_data_collector_options()
|
|
301
|
+
model_reporters = {
|
|
302
|
+
"population_risk_category": "population_risk_category",
|
|
303
|
+
"num_agents_risk_changed": "num_agents_risk_changed",
|
|
304
|
+
"sum_risk_level_changes": "sum_risk_level_changes",
|
|
305
|
+
}
|
|
306
|
+
for risk_level in range(self.min_risk_level, self.max_risk_level + 1):
|
|
307
|
+
field = f"total_r{risk_level}"
|
|
308
|
+
model_reporters[field] = field
|
|
309
|
+
|
|
310
|
+
opts["model_reporters"].update(model_reporters)
|
|
311
|
+
opts["agent_reporters"].update({"risk_level_changed": "risk_level_changed"})
|
|
312
|
+
return opts
|
|
313
|
+
|
|
314
|
+
def step(self):
|
|
315
|
+
# delete cached property before the next round begins,
|
|
316
|
+
# so we recalcate values for current round before collecting data
|
|
317
|
+
try:
|
|
318
|
+
# store risk level total for previous round
|
|
319
|
+
if hasattr(self, "total_per_risk_level"):
|
|
320
|
+
if (
|
|
321
|
+
not self.recent_total_per_risk_level
|
|
322
|
+
or self.total_per_risk_level != self.recent_total_per_risk_level[-1]
|
|
323
|
+
):
|
|
324
|
+
# add to recent values if changed or new
|
|
325
|
+
self.recent_total_per_risk_level.append(self.total_per_risk_level)
|
|
326
|
+
# else:
|
|
327
|
+
# self.recent_total_per_risk_level.append(self.total_per_risk_level)
|
|
328
|
+
del self.total_per_risk_level
|
|
329
|
+
del self.sum_risk_level_changes
|
|
330
|
+
except AttributeError:
|
|
331
|
+
# property hasn't been set yet on the first round, ok to ignore
|
|
332
|
+
pass
|
|
333
|
+
super().step()
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def num_agents_risk_changed(self):
|
|
337
|
+
return len([a for a in self.schedule.agents if a.risk_level_changed])
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def converged(self):
|
|
341
|
+
# check if the simulation is stable and should stop running
|
|
342
|
+
# based on the number of agents changing their risk level
|
|
343
|
+
|
|
344
|
+
# checking whether agents risk level changed only works
|
|
345
|
+
# when adjustment is enabled; if it is not, fallback
|
|
346
|
+
# do base model logic, which is based on rolling avg % hawk
|
|
347
|
+
if not self.risk_adjustment:
|
|
348
|
+
return super().converged
|
|
349
|
+
|
|
350
|
+
# this simulation typically takes around 1000 rounds to converge,
|
|
351
|
+
# so don't even bother checking until at least 50 rounds
|
|
352
|
+
return self.schedule.steps > max(self.adjust_round_n, 50) and (
|
|
353
|
+
self.num_agents_risk_changed == 0
|
|
354
|
+
# NOTE: could adjust the threshold here
|
|
355
|
+
or self.sum_risk_level_changes <= len(self.schedule.agents) * 0.07
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
@cached_property
|
|
359
|
+
def total_per_risk_level(self):
|
|
360
|
+
# tally the number of agents for each risk level
|
|
361
|
+
return Counter([a.risk_level for a in self.schedule.agents])
|
|
362
|
+
|
|
363
|
+
@cached_property
|
|
364
|
+
def sum_risk_level_changes(self):
|
|
365
|
+
# calculate the total in absolute changes across all risk levels
|
|
366
|
+
# since most recent adjustment round
|
|
367
|
+
|
|
368
|
+
# requires at two sets of totals to compare
|
|
369
|
+
if len(self.recent_total_per_risk_level) != 2:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
a = self.recent_total_per_risk_level[0]
|
|
373
|
+
b = self.recent_total_per_risk_level[1]
|
|
374
|
+
changes = {}
|
|
375
|
+
# for each risk level, calculate the absolute difference
|
|
376
|
+
for rlevel, total in a.items():
|
|
377
|
+
changes[rlevel] = abs(total - b[rlevel])
|
|
378
|
+
|
|
379
|
+
return sum([val for val in changes.values()])
|
|
380
|
+
|
|
381
|
+
def __getattr__(self, attr):
|
|
382
|
+
# support dynamic properties for data collection on total by risk level
|
|
383
|
+
if attr.startswith("total_r"):
|
|
384
|
+
try:
|
|
385
|
+
r = int(attr.replace("total_r", ""))
|
|
386
|
+
# only handle risk levels that are in bounds
|
|
387
|
+
if r > self.max_risk_level or r < self.min_risk_level:
|
|
388
|
+
raise AttributeError
|
|
389
|
+
return self.total_per_risk_level[r]
|
|
390
|
+
except ValueError:
|
|
391
|
+
# ignore and throw attribute error
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
raise AttributeError
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def population_risk_category(self):
|
|
398
|
+
# calculate a category of risk distribution for the population
|
|
399
|
+
# based on the proportion of agents in different risk categories
|
|
400
|
+
# (categorization scheme defined by LB)
|
|
401
|
+
|
|
402
|
+
# count the number of agents in three groups:
|
|
403
|
+
risk_counts = self.total_per_risk_level
|
|
404
|
+
# TODO: define these on the class for reuse in analysis?
|
|
405
|
+
total = {
|
|
406
|
+
"risk_inclined": risk_counts[0] + risk_counts[1] + risk_counts[2],
|
|
407
|
+
"risk_moderate": risk_counts[3]
|
|
408
|
+
+ risk_counts[4]
|
|
409
|
+
+ risk_counts[5]
|
|
410
|
+
+ risk_counts[6],
|
|
411
|
+
"risk_avoidant": risk_counts[7] + risk_counts[8] + risk_counts[9],
|
|
412
|
+
}
|
|
413
|
+
# for each group, calculate percent of agents in that category
|
|
414
|
+
total_agents = len(self.schedule.agents)
|
|
415
|
+
percent = {key: val / total_agents for key, val in total.items()}
|
|
416
|
+
|
|
417
|
+
# majority risk inclined (> 50%)
|
|
418
|
+
if percent["risk_inclined"] > 0.5:
|
|
419
|
+
# If < 10% are RM & < 10% are RA: let c = 1
|
|
420
|
+
if percent["risk_moderate"] < 0.1 and percent["risk_avoidant"] < 0.1:
|
|
421
|
+
return RiskState.c1
|
|
422
|
+
# If > 10% are RM & < 10% are RA: let c = 2
|
|
423
|
+
if percent["risk_moderate"] > 0.1 and percent["risk_avoidant"] < 0.1:
|
|
424
|
+
return RiskState.c2
|
|
425
|
+
# If > 10% are RM & > 10% are RA: let c = 3
|
|
426
|
+
if percent["risk_moderate"] > 0.1 and percent["risk_avoidant"] > 0.1:
|
|
427
|
+
return RiskState.c3
|
|
428
|
+
# If < 10% are RM & > 10% are RA: let c = 4
|
|
429
|
+
if percent["risk_moderate"] < 0.1 and percent["risk_avoidant"] > 0.1:
|
|
430
|
+
return RiskState.c4
|
|
431
|
+
|
|
432
|
+
# majority risk moderate
|
|
433
|
+
if percent["risk_moderate"] > 0.5:
|
|
434
|
+
# If < 10% are RI & < 10% are RA: let c = 7
|
|
435
|
+
if percent["risk_inclined"] < 0.1 and percent["risk_avoidant"] < 0.1:
|
|
436
|
+
return RiskState.c7
|
|
437
|
+
# If > 10% are RI & < 10% are RA: let c = 5
|
|
438
|
+
if percent["risk_inclined"] > 0.1 and percent["risk_avoidant"] < 0.1:
|
|
439
|
+
return RiskState.c5
|
|
440
|
+
# If > 10% are RI & > 10% are RA: let c = 6
|
|
441
|
+
if percent["risk_inclined"] > 0.1 and percent["risk_avoidant"] > 0.1:
|
|
442
|
+
return RiskState.c6
|
|
443
|
+
# If < 10% are RI & > 10% are RA: let c = 8
|
|
444
|
+
if percent["risk_inclined"] < 0.1 and percent["risk_avoidant"] > 0.1:
|
|
445
|
+
return RiskState.c8
|
|
446
|
+
|
|
447
|
+
# majority risk avoidant
|
|
448
|
+
if percent["risk_avoidant"] > 0.5:
|
|
449
|
+
# If < 10% are RM & < 10% are RI: let c = 12
|
|
450
|
+
if percent["risk_moderate"] < 0.1 and percent["risk_inclined"] < 0.1:
|
|
451
|
+
return RiskState.c12
|
|
452
|
+
# If > 10% are RM & < 10% are RI: let c = 11
|
|
453
|
+
if percent["risk_moderate"] > 0.1 and percent["risk_inclined"] < 0.1:
|
|
454
|
+
return RiskState.c11
|
|
455
|
+
# If > 10% are RM & > 10% are RI: let c = 10
|
|
456
|
+
if percent["risk_moderate"] > 0.1 and percent["risk_inclined"] > 0.1:
|
|
457
|
+
return RiskState.c10
|
|
458
|
+
# If < 10% are RM & > 10% are RI: let c = 9
|
|
459
|
+
if percent["risk_moderate"] < 0.1 and percent["risk_inclined"] > 0.1:
|
|
460
|
+
return RiskState.c9
|
|
461
|
+
|
|
462
|
+
return RiskState.c13
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "code",
|
|
5
|
+
"execution_count": 2,
|
|
6
|
+
"id": "0b11fbff-159d-43d1-8511-c57e3a0e483a",
|
|
7
|
+
"metadata": {},
|
|
8
|
+
"outputs": [
|
|
9
|
+
{
|
|
10
|
+
"data": {
|
|
11
|
+
"application/vnd.jupyter.widget-view+json": {
|
|
12
|
+
"model_id": "86863e550e08409fb011ef20abb9884e",
|
|
13
|
+
"version_major": 2,
|
|
14
|
+
"version_minor": 0
|
|
15
|
+
},
|
|
16
|
+
"text/html": [
|
|
17
|
+
"Cannot show widget. You probably want to rerun the code cell above (<i>Click in the code cell, and press Shift+Enter <kbd>⇧</kbd>+<kbd>↩</kbd></i>)."
|
|
18
|
+
],
|
|
19
|
+
"text/plain": [
|
|
20
|
+
"Cannot show ipywidgets in text"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"metadata": {},
|
|
24
|
+
"output_type": "display_data"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"source": [
|
|
28
|
+
"from simulatingrisk.hawkdovemulti.app import page\n",
|
|
29
|
+
"\n",
|
|
30
|
+
"page"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"metadata": {
|
|
35
|
+
"kernelspec": {
|
|
36
|
+
"display_name": "Python 3 (ipykernel)",
|
|
37
|
+
"language": "python",
|
|
38
|
+
"name": "python3"
|
|
39
|
+
},
|
|
40
|
+
"language_info": {
|
|
41
|
+
"codemirror_mode": {
|
|
42
|
+
"name": "ipython",
|
|
43
|
+
"version": 3
|
|
44
|
+
},
|
|
45
|
+
"file_extension": ".py",
|
|
46
|
+
"mimetype": "text/x-python",
|
|
47
|
+
"name": "python",
|
|
48
|
+
"nbconvert_exporter": "python",
|
|
49
|
+
"pygments_lexer": "ipython3",
|
|
50
|
+
"version": "3.12.7"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"nbformat": 4,
|
|
54
|
+
"nbformat_minor": 5
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#SBATCH --job-name=simrisk # job short name
|
|
3
|
+
#SBATCH --nodes=1 # node count
|
|
4
|
+
#SBATCH --ntasks=1 # total number of tasks across all nodes
|
|
5
|
+
#SBATCH --cpus-per-task=20 # cpu-cores per task
|
|
6
|
+
#SBATCH --mem-per-cpu=525M # memory per cpu-core
|
|
7
|
+
#SBATCH --time=02:00:00 # total run time limit (HH:MM:SS)
|
|
8
|
+
#SBATCH --mail-type=begin # send email when job begins
|
|
9
|
+
#SBATCH --mail-type=end # send email when job ends
|
|
10
|
+
#SBATCH --mail-type=fail # send email if job fails
|
|
11
|
+
#SBATCH --mail-user=EMAIL
|
|
12
|
+
|
|
13
|
+
# Template for batch running hawkdovemulti simulation with slurm.
|
|
14
|
+
# Assumes a conda environment named simrisk is set up with required dependencies.
|
|
15
|
+
#
|
|
16
|
+
# Update before using:
|
|
17
|
+
# - EMAIL for slurm notification
|
|
18
|
+
# - customize path for working directory (set username if using Princeton HPC)
|
|
19
|
+
# (and make sure the directory exists)
|
|
20
|
+
# - add an SBATCH array directive if desired
|
|
21
|
+
# - customize the batch run command as appropriate
|
|
22
|
+
# - configure the time appropriately for the batch run
|
|
23
|
+
|
|
24
|
+
module purge
|
|
25
|
+
module load anaconda3/2023.9
|
|
26
|
+
conda activate simrisk
|
|
27
|
+
|
|
28
|
+
# change working directory for data output
|
|
29
|
+
cd /scratch/network/<USER>/simrisk
|
|
30
|
+
|
|
31
|
+
# test run: one iteration, max of 200 steps, no progress bar
|
|
32
|
+
# (completed in ~18 minutes on 20 CPUs)
|
|
33
|
+
#simrisk-hawkdovemulti-batchrun --iterations 1 --max-step 200 --no-progress
|
|
34
|
+
|
|
35
|
+
# longer run: 10 iterations, max of 200 steps, no progress bar
|
|
36
|
+
#simrisk-hawkdovemulti-batchrun --iterations 10 --max-step 200 --no-progress
|
|
37
|
+
|
|
38
|
+
# To generate data for a larger total number of iterations,
|
|
39
|
+
# run the script as a job array.
|
|
40
|
+
# e.g. for 100 iterations, run with --iterations 10 and 10 tasks with #SBATCH --array=0-9
|
|
41
|
+
# and add a file prefix option to generate separate files that can be grouped
|
|
42
|
+
simrisk-hawkdovemulti-batchrun --iterations 10 --max-step 125 --no-progress --file-prefix "job${SLURM_ARRAY_JOB_ID}_task${SLURM_ARRAY_TASK_ID}_"
|
|
43
|
+
|
|
44
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Risky Bet Simulation
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Game: agents start with random fixed risk attitudes (similar to the
|
|
6
|
+
risky food simulation), and decide whether or not to make a risky bet.
|
|
7
|
+
Every ten rounds, agents adjust their risk attitude based on the
|
|
8
|
+
relative wealth of their neighbors.
|
|
9
|
+
|
|
10
|
+
SETUP:
|
|
11
|
+
- Fixed agents on an NxN grid with random risk attitudes
|
|
12
|
+
- Every agent starts with $1000 initial wealth
|
|
13
|
+
|
|
14
|
+
EACH ROUND:
|
|
15
|
+
- The model selects a probability `p` of the RISKY bet paying off
|
|
16
|
+
- Each agent has a risk attitude `r`, and will take the RISKY bet if `p` > `r`
|
|
17
|
+
- The model flips a coin with bias `p` to determine whether the RISKY bet paid off.
|
|
18
|
+
- For agents who choose the SAFE option (no bet), money is unchanged; for agents who took the RISKY bet, money is either multiplied by 1.5 (if the bet paid off) or 0.5 (if it didn't).
|
|
19
|
+
END ROUND
|
|
20
|
+
|
|
21
|
+
EVERY 10 ROUNDS, adjust risk attitudes:
|
|
22
|
+
- Each agent looks at their neighbors (4).
|
|
23
|
+
- If anyone has more money, either adopt their risk attitude or average between current risk attitude and theirs (configurable via a model intialization parameter).
|
|
24
|
+
- Reset wealth back to the initial value ($1000).
|
|
25
|
+
|
|
26
|
+
Collect data to track how the distribution of risk attitudes changes over time.
|
|
27
|
+
Visualize a grid using a divergent color spectrum with for risk levels; use
|
|
28
|
+
eleven bins, with bins for 0 - 0.05 and 0.95 - 1.0, since risk = 0, 0.5, and 1
|
|
29
|
+
are all special cases we want clearly captured.
|
|
30
|
+
|
|
31
|
+
## Running the simulation
|
|
32
|
+
|
|
33
|
+
- Install python dependencies as described in the main project readme (requires mesa)
|
|
34
|
+
- Use the main `simulating-risk` project directory as your working directory
|
|
35
|
+
- To run with `mesa runserver` from the command line:
|
|
36
|
+
- Configure python to include the current directory in import path;
|
|
37
|
+
for C-based shells, run `setenv PYTHONPATH .` ; for bash, run `export $PYTHONPATH=.`
|
|
38
|
+
- To run interactively with mesa runserver: `mesa runserver simulatingrisk/risky_bet/`
|
|
39
|
+
- To run with `solara` from the commandline:
|
|
40
|
+
- `solara run --host localhost simulatingrisk/risky_bet/app.py`
|
|
41
|
+
- To run in a Jupyter notebook or Colab, import the `JupyterViz` page object:
|
|
42
|
+
```python
|
|
43
|
+
from simulatingrisk.risky_bet.app import page
|
|
44
|
+
page
|
|
45
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# solara/jupyterviz app
|
|
2
|
+
from mesa.experimental import JupyterViz
|
|
3
|
+
|
|
4
|
+
from simulatingrisk.risky_bet.model import RiskyBetModel
|
|
5
|
+
from simulatingrisk.risky_bet.server import agent_portrayal, jupyterviz_params
|
|
6
|
+
from simulatingrisk.charts.histogram import plot_risk_histogram
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
page = JupyterViz(
|
|
10
|
+
RiskyBetModel,
|
|
11
|
+
jupyterviz_params,
|
|
12
|
+
measures=[plot_risk_histogram],
|
|
13
|
+
name="Risky Bet",
|
|
14
|
+
agent_portrayal=agent_portrayal,
|
|
15
|
+
)
|
|
16
|
+
# required to render the visualization with Jupyter/Solara
|
|
17
|
+
page
|