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