Skip to content

Commit

Permalink
Hawk/dove batch run code for data analysis (#60)
Browse files Browse the repository at this point in the history
* Suppress convergence print statement for batch runs

* Customize risk category string output for batch run data collection

* Custom batch runner for hawk/dove multi risk sim

* Configure hawk/dove multi batch run to be installed as a script

* Step argument needs to be an integer also

* Revise multiprocessing based on slurm cpu efficiency report

* Correct basic hawk/dove readme for scope

* Use configured neighborhood sizes; correct typo in help string

* Add file prefix option for generated data files

* Turn random play odds completely off by default

* Add readme for hawk/dove multi risk game; document batch running

* Add sample batch run slurm script

* Update simulatingrisk/hawkdove/model.py

Co-authored-by: Laure Thompson <[email protected]>

* Update simulatingrisk/hawkdovemulti/README.md

Co-authored-by: Laure Thompson <[email protected]>

* Update simulatingrisk/hawkdovemulti/batch_run.py

Co-authored-by: Laure Thompson <[email protected]>

* Update simulatingrisk/hawkdovemulti/simrisk_batch.slurm

Co-authored-by: Laure Thompson <[email protected]>

* Update simulatingrisk/hawkdovemulti/batch_run.py

Co-authored-by: Laure Thompson <[email protected]>

* Cleanup items flagged by @laurejt in code review

* Set file prefix default in argparse for consistency

* Document step offset between scheduler and data collection

* Better documentation for sample slurm batch script

---------

Co-authored-by: Laure Thompson <[email protected]>
  • Loading branch information
rlskoeser and laurejt authored Jan 23, 2024
1 parent 907c290 commit c3affde
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 10 deletions.
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ classifiers = [
dependencies = [
"mesa==2.1.5",
# "mesa @ git+https://github.com/projectmesa/mesa.git@main",
"matplotlib",
"matplotlib", # required by mesa
"altair>5.0.1"
]
dynamic = ["version", "readme"]

[project.scripts]
simrisk-hawkdovemulti-batchrun = "simulatingrisk.hawkdovemulti.batch_run:main"

[tool.setuptools.dynamic]
version = {attr = "simulatingrisk.__version__"}
readme = {file = ["README.md"]}
Expand Down
7 changes: 4 additions & 3 deletions simulatingrisk/hawkdove/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hawk-Dove with risk attitudes

Hawk/Dove game with variable risk attitudes
Hawk/Dove game with risk attitudes

## Game description

Expand All @@ -14,6 +14,7 @@ This is a variant of the Hawk/Dove Game: https://en.wikipedia.org/wiki/Chicken_(
BACKGROUND: An unpublished paper by Simon Blessenohl shows that the equilibrium in this game is different for EU maximizers than for REU maximizers (all with the same risk-attitude), and that REU maximizers do better as a population (basically, play DOVE more often)

We want to know: what happens when different people have _different_ risk-attitudes.
(See also variant simulation [Hawk/Dove game with multiple risk attitudes](../hawkdovemulti/). )

GAME: Hawk-Dove with risk-attitudes

Expand Down Expand Up @@ -49,7 +50,7 @@ This game has a discrete set of options instead of probability, so instead of de
<tr><th>r</th></th><th>0</th><th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th></tr>
<tr>
<th>Plays H when:</th>
<td>never</td>
<td>never</td>
<td>$\geq1$ D</td>
<td>$\geq2$ D</td>
<td>$\geq3$ D</td>
Expand All @@ -62,7 +63,7 @@ This game has a discrete set of options instead of probability, so instead of de
</tr>
<tr><td></td>
<td colspan="4">risk seeking</td>
<td>EU maximizer<br>(risk neutral)</td>
<td>EU maximizer<br>(risk neutral)</td>
<td>EU maximizer<br>(risk neutral)</td>
<td colspan="4">risk avoidant</td>
</tr>
Expand Down
14 changes: 8 additions & 6 deletions simulatingrisk/hawkdove/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ class HawkDoveModel(mesa.Model):
agent_class = HawkDoveAgent
#: supported neighborhood sizes
neighborhood_sizes = {4, 8, 24}
#: minimu risk level
#: minimum risk level
min_risk_level = 0
#: maximum risk level allowed
max_risk_level = 9
Expand All @@ -212,7 +212,7 @@ def __init__(
play_neighborhood=8,
observed_neighborhood=8,
hawk_odds=0.5,
random_play_odds=0.01,
random_play_odds=0.00,
):
super().__init__()
# assume a fully-populated square grid
Expand Down Expand Up @@ -276,10 +276,12 @@ def step(self):
self.datacollector.collect(self)
if self.converged:
self.running = False
print(
f"Stopping after {self.schedule.steps} rounds. "
+ f"Final rolling average % hawk: {round(self.rolling_percent_hawk, 2)}"
)
# FIXME: this output is annoying in batch runs
# print(
# f"Stopping after {self.schedule.steps} rounds. "
# + "Final rolling average % hawk: "
# + f"{self.rolling_percent_hawk: .2f}"
# )

@property
def max_agent_points(self):
Expand Down
63 changes: 63 additions & 0 deletions simulatingrisk/hawkdovemulti/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Hawk-Dove with multiple risk attitudes

This is a variation of the [Hawk/Dove game with risk attitudes](../hawkdove/).
This version adds multiple risk attitudes, with options for updating
risk attitudes periodically based on comparing success of neighboring agents.

The basic mechanics of the game are the same. This model adds options
for agent risk adjustment (none, adopt, average) and period of risk
adjustment (by default, every ten rounds). The payoff used to compare
agents when adjusting risk attitudes can either be recent (since the
last adjustment round) or total points for the whole game. The
adjustment neighborhood, or which neighboring agents are considered
when adjusting risk attitudes, can be configured to 4, 8, or 24.

Initial risk attitudes are set by the model. Risk distribution can
be configured to use a normal distribution, uniform (random), bimodal,
skewed left, or skewed right.

Like the base hawk/dove risk attitude game, there is also a
configuration to add some chance of agents playing hawk/dove randomly
instead of choosing based on the rules of the game.

## Batch running

This module includes a custom batch run script to run the simulation and
collect data across a large combination of parameters and generate data
files with collected model and agent data.

To run the script locally from the root project directory:
```sh
simulatingrisk/hawkdovemulti/batch_run.py
```
Use `-h` or `--help` to see options.

If this project has been installed with pip or similar, the script is
available as `simrisk-hawkdovemulti-batchrun`.

To run the batch run script on an HPC cluster:

- Create a conda environment and install dependencies and this project.
(Major mesa dependencies available with conda are installed first as
conda packages)

```sh
module load anaconda3/2023.9
conda create --name simrisk pandas networkx matplotlib numpy tqdm click
conda activate simrisk
pip install git+https://github.com/Princeton-CDH/simulating-risk.git@hawkdove-batchrun
```
For convenience, an example [slurm batch script](simrisk_batch.slurm) is
included for running the batch run script (some portions are
specific to Princeton's Research Computing HPC environment.)

- Customize the slurm batch script as desired, copy it to the cluster, and submit
the job: `sbatch simrisk_batch.slurm`

By default, the batch run script will use all available processors, and will
create model and agent data files under a `data/hawkdovemulti/` directory
relative to the working directory where the script is called.




186 changes: 186 additions & 0 deletions simulatingrisk/hawkdovemulti/batch_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env python

import argparse
import csv
from datetime import datetime
import multiprocessing
import os

from tqdm.auto import tqdm

from mesa.batchrunner import _make_model_kwargs, _collect_data

from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel


neighborhood_sizes = list(HawkDoveMultipleRiskModel.neighborhood_sizes)

# combination of parameters we want to run
params = {
"grid_size": [10, 25, 50], # 100],
"risk_adjustment": ["adopt", "average"],
"play_neighborhood": neighborhood_sizes,
"observed_neighborhood": neighborhood_sizes,
"adjust_neighborhood": neighborhood_sizes,
"hawk_odds": [0.5, 0.25, 0.75],
"adjust_every": [2, 10, 20],
"risk_distribution": HawkDoveMultipleRiskModel.risk_distribution_options,
"adjust_payoff": HawkDoveMultipleRiskModel.supported_adjust_payoffs,
# random?
}


# method for multiproc running model with a set of params
def run_hawkdovemulti_model(args):
run_id, iteration, params, max_steps = args
# simplified model runner adapted from mesa batch run code

model = HawkDoveMultipleRiskModel(**params)
while model.running and model.schedule.steps <= max_steps:
model.step()

# collect data for the last step
# (scheduler is 1-based index but data collection is 0-based)
step = model.schedule.steps - 1

model_data, all_agents_data = _collect_data(model, step)

# combine run id, step, and params, with collected model data
run_data = {"RunId": run_id, "iteration": iteration, "Step": step}
run_data.update(params)
run_data.update(model_data)

agent_data = [
{
"RunId": run_id,
"iteration": iteration,
"Step": step,
**agent_data,
}
for agent_data in all_agents_data
]

return run_data, agent_data


def batch_run(
params, iterations, number_processes, max_steps, progressbar, file_prefix
):
param_combinations = _make_model_kwargs(params)
total_param_combinations = len(param_combinations)
total_runs = total_param_combinations * iterations
print(
f"{total_param_combinations} parameter combinations, "
+ f"{iterations} iteration{'s' if iterations != 1 else ''}, "
+ f"{total_runs} total runs"
)

# create a list of all the parameters to run, with run id and iteration
runs_list = []
run_id = 0
for params in param_combinations:
for iteration in range(iterations):
runs_list.append((run_id, iteration, params, max_steps))
run_id += 1

# collect data in a directory for this model
data_dir = os.path.join("data", "hawkdovemulti")
os.makedirs(data_dir, exist_ok=True)
datestr = datetime.today().isoformat().replace(".", "_").replace(":", "")
model_output_filename = os.path.join(data_dir, f"{file_prefix}{datestr}_model.csv")
agent_output_filename = os.path.join(data_dir, f"{file_prefix}{datestr}_agent.csv")
print(
f"Saving data collection results to:\n {model_output_filename}"
+ f"\n {agent_output_filename}"
)
# open output files so data can be written as it is generated
with open(model_output_filename, "w", newline="") as model_output_file, open(
agent_output_filename, "w", newline=""
) as agent_output_file:
model_dict_writer = None
agent_dict_writer = None

# adapted from mesa batch run code
with tqdm(total=total_runs, disable=not progressbar) as pbar:
with multiprocessing.Pool(number_processes) as pool:
for model_data, agent_data in pool.imap_unordered(
run_hawkdovemulti_model, runs_list
):
# initialize dictwriter and start csv after the first batch
if model_dict_writer is None:
# get field names from first entry
model_dict_writer = csv.DictWriter(
model_output_file, model_data.keys()
)
model_dict_writer.writeheader()

model_dict_writer.writerow(model_data)

if agent_dict_writer is None:
# get field names from first entry
agent_dict_writer = csv.DictWriter(
agent_output_file, agent_data[0].keys()
)
agent_dict_writer.writeheader()

agent_dict_writer.writerows(agent_data)

pbar.update()


def main():
parser = argparse.ArgumentParser(
prog="hawk/dove batch_run",
description="Batch run for hawk/dove multiple risk attitude simulation.",
epilog="""Data files will be created in data/hawkdovemulti/
relative to current path.""",
)
parser.add_argument(
"-i",
"--iterations",
type=int,
help="Number of iterations to run for each set of parameters "
+ "(default: %(default)s)",
default=100,
)
parser.add_argument(
"-m",
"--max-steps",
help="Maximum steps to run simulations if they have not already "
+ "converged (default: %(default)s)",
default=125, # typically converges quickly, around step 60 without randomness
type=int,
)
parser.add_argument(
"-p",
"--processes",
type=int,
help="Number of processes to use (default: all available CPUs)",
default=None,
)
parser.add_argument(
"--progress",
help="Display progress bar (default: %(default)s)",
action=argparse.BooleanOptionalAction,
default=True,
)
parser.add_argument(
"--file-prefix",
help="Prefix for data filenames (no prefix by default)",
default="",
)
# may want to add an option to configure output dir

args = parser.parse_args()
batch_run(
params,
args.iterations,
args.processes,
args.max_steps,
args.progress,
args.file_prefix,
)


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions simulatingrisk/hawkdovemulti/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ def category(cls, val):
return "majority risk avoidant"
return "no majority"

def __str__(self):
# override string method to return just the numeric value,
# for better serialization of collected data
return str(self.value)


class HawkDoveMultipleRiskModel(HawkDoveModel):
"""
Expand Down
44 changes: 44 additions & 0 deletions simulatingrisk/hawkdovemulti/simrisk_batch.slurm
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
#SBATCH --job-name=simrisk # job short name
#SBATCH --nodes=1 # node count
#SBATCH --ntasks=1 # total number of tasks across all nodes
#SBATCH --cpus-per-task=20 # cpu-cores per task
#SBATCH --mem-per-cpu=525M # memory per cpu-core
#SBATCH --time=02:00:00 # total run time limit (HH:MM:SS)
#SBATCH --mail-type=begin # send email when job begins
#SBATCH --mail-type=end # send email when job ends
#SBATCH --mail-type=fail # send email if job fails
#SBATCH --mail-user=EMAIL

# Template for batch running hawkdovemulti simulation with slurm.
# Assumes a conda environment named simrisk is set up with required dependencies.
#
# Update before using:
# - EMAIL for slurm notification
# - customize path for working directory (set username if using Princeton HPC)
# (and make sure the directory exists)
# - add an SBATCH array directive if desired
# - customize the batch run command as appropriate
# - configure the time appropriately for the batch run

module purge
module load anaconda3/2023.9
conda activate simrisk

# change working directory for data output
cd /scratch/network/<USER>/simrisk

# test run: one iteration, max of 200 steps, no progress bar
# (completed in ~18 minutes on 20 CPUs)
#simrisk-hawkdovemulti-batchrun --iterations 1 --max-step 200 --no-progress

# longer run: 10 iterations, max of 200 steps, no progress bar
#simrisk-hawkdovemulti-batchrun --iterations 10 --max-step 200 --no-progress

# To generate data for a larger total number of iterations,
# run the script as a job array.
# e.g. for 100 iterations, run with --iterations 10 and 10 tasks with #SBATCH --array=0-9
# and add a file prefix option to generate separate files that can be grouped
simrisk-hawkdovemulti-batchrun --iterations 10 --max-step 125 --no-progress --file-prefix "job${SLURM_ARRAY_JOB_ID}_task${SLURM_ARRAY_TASK_ID}_"


Loading

0 comments on commit c3affde

Please sign in to comment.