Skip to content

Commit

Permalink
More options for hawk/dove neighborhoods (#46)
Browse files Browse the repository at this point in the history
* Refine hawk/dove neighborhoods (4,8,24; play/choose neighborhoods)

* Implement an adjustment neighborhood for variable hawk/dove model

* Clean up default args for hawk/dove extending classes

* Test logic for setting choose neighborhood size

* Rename parameters for clarity/consistency

* pin to mesa 2.1.2 for now

* Rename hawk/dove variable risk to multiple risk for clarity

* Adjust hawk/dove choose logic for different neighborhood sizes

* Update hawk/dove game to always use risk levels 0-8

* Update hawk/dove charts for risk levels always 0-8
  • Loading branch information
rlskoeser authored Nov 28, 2023
1 parent 137c35e commit 0a6601a
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 293 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
"mesa>=2.1.2",
"mesa==2.1.2",
# "mesa @ git+https://github.com/projectmesa/mesa.git@main",
"matplotlib",
"altair>5.0.1"
Expand Down
10 changes: 6 additions & 4 deletions simulatingrisk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import solara

from simulatingrisk.hawkdove.app import page as hawkdove_page
from simulatingrisk.hawkdovevar.app import page as hawkdove_var_page
from simulatingrisk.hawkdovemulti.app import page as hawkdove_multi_page
from simulatingrisk.risky_bet.app import page as riskybet_page
from simulatingrisk.risky_food.app import page as riskyfood_page

Expand All @@ -21,8 +21,8 @@ def hawkdove():


@solara.component
def hawkdove_var():
return hawkdove_var_page
def hawkdove_multi():
return hawkdove_multi_page


@solara.component
Expand All @@ -41,7 +41,9 @@ def riskyfood():
path="hawkdove-single", component=hawkdove, label="Hawk/Dove (single r)"
),
solara.Route(
path="hawkdove-variable", component=hawkdove_var, label="Hawk/Dove (variable r)"
path="hawkdove-multiple",
component=hawkdove_multi,
label="Hawk/Dove (multiple r)",
),
solara.Route(path="riskybet", component=riskybet, label="Risky Bet"),
solara.Route(path="riskyfood", component=riskyfood, label="Risky Food"),
Expand Down
12 changes: 6 additions & 6 deletions simulatingrisk/batch_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from mesa import batch_run

from simulatingrisk.hawkdove.model import HawkDoveSingleRiskModel
from simulatingrisk.hawkdovevar.model import HawkDoveVariableRiskModel
from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel
from simulatingrisk.risky_bet.model import RiskyBetModel
from simulatingrisk.risky_food.model import RiskyFoodModel

Expand Down Expand Up @@ -76,14 +76,14 @@ def hawkdove_singlerisk_batch_run(args):
save_results("hawkdove_single", results)


def hawkdove_variablerisk_batch_run(args):
def hawkdove_multiplerisk_batch_run(args):
params = {
"grid_size": 10,
"risk_adjustment": "adopt", # run adopt only for now
}
iterations = 100
results = batch_run(
HawkDoveVariableRiskModel,
HawkDoveMultipleRiskModel,
parameters=params,
iterations=iterations,
number_processes=1,
Expand All @@ -92,7 +92,7 @@ def hawkdove_variablerisk_batch_run(args):
max_steps=250, # converges fairly quickly, don't run 1000 times
)
# include the mode in the output filename
save_results("hawkdove_variable", results)
save_results("hawkdove_multiple", results)


def save_results(simulation, results):
Expand Down Expand Up @@ -132,8 +132,8 @@ def save_results(simulation, results):
# help="Mode for initializing agent risk attitudes",
# )
hawkdove_parser.set_defaults(func=hawkdove_singlerisk_batch_run)
hawkdovevar_parser = subparsers.add_parser("hawkdove-var")
hawkdovevar_parser.set_defaults(func=hawkdove_variablerisk_batch_run)
hawkdove_multi_parser = subparsers.add_parser("hawkdove-multi")
hawkdove_multi_parser.set_defaults(func=hawkdove_multiplerisk_batch_run)

args = parser.parse_args()
# run appropriate function based on the selected subcommand
Expand Down
7 changes: 3 additions & 4 deletions simulatingrisk/hawkdove/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ Players arranged on a lattice [options for both 4 neighbors (AYBD) and 8 neighbo
- If I play DOVE and neighbor plays DOVE: 2.1
- If I play DOVE and neighbor plays HAWK: 1
- If I play HAWK and neighbor plays HAWK: 0

Each player on a lattice (grid in Mesa):
- Has parameter $r$ [from 0 to 8, or 0 to 4 for four neighbors]
- Has parameter $r$ [from 0 to 8]
- Let `d` be the number of neighbors who played DOVE during the previous round. If $d > r$, then play HAWK. Otherwise play DOVE. (Agents who are risk-avoidant only play HAWK if there are a lot of doves around them. More risk-avoidance requires a higher number of doves to get an agent to play HAWK.)
- The proportion of neighbors who play DOVE corresponds to your probability of encountering a DOVE when playing a randomly-selected neighbor. The intended interpretation is that you maximize REU for this probability of your opponent playing DOVE. Thus, $r$ corresponds to the probability above which playing HAWK maximizes REU.
- Choice for the first round could be randomly determined, or add parameters to see how initial conditions matter?
- [OR VARY FIRST ROUND: what proportion starts as HAWK
- Who is a HAWK and who is a DOVE is randomly determined; proportion set at the beginning of each simulation. E.g. 30% are HAWKS; if we have 100 players, then each player has a 30% chance of being HAWK;
- Who is a HAWK and who is a DOVE is randomly determined; proportion set at the beginning of each simulation. E.g. 30% are HAWKS; if we have 100 players, then each player has a 30% chance of being HAWK;
- Call this initial parameter HAWK-ODDS; default is 50/50


Expand Down Expand Up @@ -91,4 +91,3 @@ Another way to visualize the risk attitudes and choices in this game is this tab
<tr><th>7</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>H</td></tr>
<tr><th>8</th><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td><td>D</td></tr>
</table>

109 changes: 82 additions & 27 deletions simulatingrisk/hawkdove/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,47 @@ def initial_choice(self, hawk_odds=None):
def choice_label(self):
return "hawk" if self.choice == Play.HAWK else "dove"

def get_neighbors(self, size):
"""get all neighbors for a supported neighborhood size"""
check_neighborhood_size(size)
# 4 and 8 neighborhood use default radius 1
# 8 and 24 both use moore neighborhood (includes diagonals)
opts = {"moore": True}
if size == 4:
# use von neumann neighborhood instead of moore (no diagonal)
opts["moore"] = False

# for 24 size neighborhood, use radius 2
if size == 24:
opts["radius"] = 2

return self.model.grid.get_neighbors(self.pos, include_center=False, **opts)

@property
def neighbors(self):
# use configured neighborhood (with or without diagonals) on the model;
# don't include the current agent
return self.model.grid.get_neighbors(
self.pos, moore=self.model.include_diagonals, include_center=False
)
def play_neighbors(self):
"""neighbors to play against, based on model play neighborhood size"""
return self.get_neighbors(self.model.play_neighborhood)

@property
def observed_neighbors(self):
"""neighbors to look at when deciding what to play;
based on model observed neighborhood size"""
return self.get_neighbors(self.model.observed_neighborhood)

@property
def num_dove_neighbors(self):
"""count how many neighbors played DOVE on the last round"""
return len([n for n in self.neighbors if n.last_choice == Play.DOVE])
"""count how many neighbors played DOVE on the last round
(uses `observed_neighborhood` size from model)"""
return len([n for n in self.observed_neighbors if n.last_choice == Play.DOVE])

@property
def proportional_num_dove_neighbors(self):
"""adjust the number of dove neighbors based on ratio between
play neighborhood and observed neighborhood, to scale observations
to the range of agent risk level."""
ratio = self.model.max_risk_level / self.model.observed_neighborhood
# always round to an integer
return round(ratio * self.num_dove_neighbors)

def choose(self):
"decide what to play this round"
Expand All @@ -90,15 +119,15 @@ def choose(self):
# (any risk is acceptable).
# agent with r = max should always take the safe option
# (no risk is acceptable)
if self.num_dove_neighbors > self.risk_level:
if self.proportional_num_dove_neighbors > self.risk_level:
self.choice = Play.HAWK
else:
self.choice = Play.DOVE

def play(self):
# play against each neighbor and calculate cumulative payoff
payoff = 0
for n in self.neighbors:
for n in self.play_neighbors:
payoff += self.payoff(n)
# update total points based on payoff this round
self.points += payoff
Expand Down Expand Up @@ -136,8 +165,10 @@ class HawkDoveModel(mesa.Model):
Model for hawk/dove game with risk attitudes.
:param grid_size: number for square grid size (creates n*n agents)
:param include_diagonals: whether agents should include diagonals
or not when considering neighbors (default: True)
:param play_neighborhood: size of neighborhood each agent plays
against; 4, 8, or 24 (default: 8)
:param observed_neighborhood: size of neighborhood each agent looks
at when choosing what to play; 4, 8, or 24 (default: 8)
:param hawk_odds: odds for playing hawk on the first round (default: 0.5)
:param risk_adjustment: strategy agents should use for adjusting risk;
None (default), adopt, or average
Expand All @@ -153,19 +184,29 @@ class HawkDoveModel(mesa.Model):
min_window = 15
#: class to use when initializing agents
agent_class = HawkDoveAgent
#: supported neighborhood sizes
neighborhood_sizes = {4, 8, 24}
#: minimu risk level
min_risk_level = 0 # TODO: allow -1 ?
#: maximum risk level allowed
max_risk_level = 8

def __init__(
self,
grid_size,
include_diagonals=True,
play_neighborhood=8,
observed_neighborhood=8,
hawk_odds=0.5,
):
super().__init__()
# assume a fully-populated square grid
self.num_agents = grid_size * grid_size
# mesa get_neighbors supports moore neighborhood (include diagonals)
# and von neumann (exclude diagonals)
self.include_diagonals = include_diagonals
for nsize in [play_neighborhood, observed_neighborhood]:
check_neighborhood_size(nsize)

self.play_neighborhood = play_neighborhood
self.observed_neighborhood = observed_neighborhood

# distribution of first choice (50/50 by default)
self.hawk_odds = hawk_odds

Expand Down Expand Up @@ -262,6 +303,15 @@ def converged(self):
)


def check_neighborhood_size(size):
# neighborhood size check, shared by model and agent
if size not in HawkDoveModel.neighborhood_sizes:
raise ValueError(
f"{size} is not a supported neighborhood size; "
+ f"must be one of {HawkDoveModel.neighborhood_sizes}"
)


class HawkDoveSingleRiskAgent(HawkDoveAgent):
"""
An agent with a risk attitude playing Hawk or Dove; must be initialized
Expand All @@ -273,23 +323,28 @@ def set_risk_level(self):


class HawkDoveSingleRiskModel(HawkDoveModel):
"""hawk/dove simulation where all agents have the same risk atttitude"""
"""hawk/dove simulation where all agents have the same risk atttitude.
Adds a required `agent_risk_level` parameter; supports all
parameters in :class:`HawkDoveModel`.
"""

#: class to use when initializing agents
agent_class = HawkDoveSingleRiskAgent

risk_attitudes = "single"

def __init__(
self,
grid_size,
agent_risk_level,
include_diagonals=True,
hawk_odds=0.5,
):
def __init__(self, grid_size, agent_risk_level, *args, **kwargs):
if (
agent_risk_level > self.max_risk_level
or agent_risk_level < self.min_risk_level
):
raise ValueError(
f"Agent risk level {agent_risk_level} is out of range; must be between "
+ f"{self.min_risk_level} - {self.max_risk_level}"
)

# store agent risk level
self.agent_risk_level = agent_risk_level

# pass through options and initialize base class
super().__init__(
grid_size, include_diagonals=include_diagonals, hawk_odds=hawk_odds
)
super().__init__(grid_size, *args, **kwargs)
30 changes: 20 additions & 10 deletions simulatingrisk/hawkdove/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import solara
import pandas as pd

from simulatingrisk.hawkdove.model import Play, divergent_colors_9, divergent_colors_5
from simulatingrisk.hawkdove.model import (
Play,
divergent_colors_9,
HawkDoveModel,
)


def agent_portrayal(agent):
Expand All @@ -25,11 +29,8 @@ def agent_portrayal(agent):
# "color": "tab:gray",
}

# color based on risk level
if agent.model.include_diagonals:
colors = divergent_colors_9
else:
colors = divergent_colors_5
# color based on risk level; risk levels are always 0-8
colors = divergent_colors_9

portrayal["Color"] = colors[agent.risk_level]
# copy to lowercase color for solara
Expand Down Expand Up @@ -57,6 +58,8 @@ def agent_portrayal(agent):
"grid_size": grid_size,
}

neighborhood_sizes = sorted(list(HawkDoveModel.neighborhood_sizes))

# parameters common to both hawk/dove variants
common_jupyterviz_params = {
"grid_size": {
Expand All @@ -67,10 +70,17 @@ def agent_portrayal(agent):
"max": 100,
"step": 1,
},
"include_diagonals": {
"type": "Checkbox",
"value": True,
"label": "Include diagonal neighbors",
"play_neighborhood": {
"type": "Select",
"value": 8,
"values": neighborhood_sizes,
"label": "Play neighborhood size",
},
"observed_neighborhood": {
"type": "Select",
"value": 8,
"values": neighborhood_sizes,
"label": "Observed neighborhood (determines choice of play)",
},
"hawk_odds": {
"type": "SliderFloat",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import solara


from simulatingrisk.hawkdovevar.model import HawkDoveVariableRiskModel
from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel
from simulatingrisk.hawkdove.server import (
agent_portrayal,
common_jupyterviz_params,
draw_hawkdove_agent_space,
neighborhood_sizes,
)
from simulatingrisk.hawkdove.app import plot_hawks

Expand All @@ -31,6 +32,12 @@
"value": 10,
"description": "How many rounds between risk adjustment",
},
"adjust_neighborhood": {
"type": "Select",
"value": 8,
"values": neighborhood_sizes,
"label": "Adjustment neighborhood size",
},
}
)

Expand Down Expand Up @@ -60,9 +67,8 @@ def plot_agents_by_risk(model):
x=alt.X(
"risk_level",
title="risk attitude",
# don't display any 0.5 ticks when max is 4
axis=alt.Axis(tickCount=model.num_neighbors + 1),
scale=alt.Scale(domain=[0, model.num_neighbors]),
axis=alt.Axis(tickCount=model.max_risk_level + 1),
scale=alt.Scale(domain=[model.min_risk_level, model.max_risk_level]),
),
y=alt.Y("total", title="Number of agents"),
)
Expand Down Expand Up @@ -124,10 +130,10 @@ def plot_hawks_by_risk(model):


page = JupyterViz(
HawkDoveVariableRiskModel,
HawkDoveMultipleRiskModel,
jupyterviz_params_var,
measures=[plot_hawks, plot_agents_by_risk, plot_hawks_by_risk],
name="Hawk/Dove game with variable risk attitudes",
name="Hawk/Dove game with multiple risk attitudes",
agent_portrayal=agent_portrayal,
space_drawer=draw_hawkdove_agent_space,
)
Expand Down
Loading

0 comments on commit 0a6601a

Please sign in to comment.