diff --git a/pyproject.toml b/pyproject.toml
index c140f63..ab01470 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"
diff --git a/simulatingrisk/app.py b/simulatingrisk/app.py
index 9eda061..e18a36b 100644
--- a/simulatingrisk/app.py
+++ b/simulatingrisk/app.py
@@ -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
@@ -21,8 +21,8 @@ def hawkdove():
@solara.component
-def hawkdove_var():
- return hawkdove_var_page
+def hawkdove_multi():
+ return hawkdove_multi_page
@solara.component
@@ -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"),
diff --git a/simulatingrisk/batch_run.py b/simulatingrisk/batch_run.py
index fce64c7..8f76c0b 100755
--- a/simulatingrisk/batch_run.py
+++ b/simulatingrisk/batch_run.py
@@ -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
@@ -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,
@@ -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):
@@ -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
diff --git a/simulatingrisk/hawkdove/README.md b/simulatingrisk/hawkdove/README.md
index c1f3a7c..7f9ef05 100644
--- a/simulatingrisk/hawkdove/README.md
+++ b/simulatingrisk/hawkdove/README.md
@@ -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
@@ -91,4 +91,3 @@ Another way to visualize the risk attitudes and choices in this game is this tab
7 | D | D | D | D | D | D | D | D | H |
8 | D | D | D | D | D | D | D | D | D |
-
diff --git a/simulatingrisk/hawkdove/model.py b/simulatingrisk/hawkdove/model.py
index c6a05ee..e6bc2c0 100644
--- a/simulatingrisk/hawkdove/model.py
+++ b/simulatingrisk/hawkdove/model.py
@@ -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"
@@ -90,7 +119,7 @@ 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
@@ -98,7 +127,7 @@ def choose(self):
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
@@ -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
@@ -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
@@ -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
@@ -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)
diff --git a/simulatingrisk/hawkdove/server.py b/simulatingrisk/hawkdove/server.py
index 0e89587..b27b0cd 100644
--- a/simulatingrisk/hawkdove/server.py
+++ b/simulatingrisk/hawkdove/server.py
@@ -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):
@@ -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
@@ -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": {
@@ -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",
diff --git a/simulatingrisk/hawkdovevar/app.py b/simulatingrisk/hawkdovemulti/app.py
similarity index 88%
rename from simulatingrisk/hawkdovevar/app.py
rename to simulatingrisk/hawkdovemulti/app.py
index 0252618..2d68ab2 100644
--- a/simulatingrisk/hawkdovevar/app.py
+++ b/simulatingrisk/hawkdovemulti/app.py
@@ -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
@@ -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",
+ },
}
)
@@ -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"),
)
@@ -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,
)
diff --git a/simulatingrisk/hawkdovevar/model.py b/simulatingrisk/hawkdovemulti/model.py
similarity index 84%
rename from simulatingrisk/hawkdovevar/model.py
rename to simulatingrisk/hawkdovemulti/model.py
index 63b69be..c5d614e 100644
--- a/simulatingrisk/hawkdovevar/model.py
+++ b/simulatingrisk/hawkdovemulti/model.py
@@ -1,11 +1,11 @@
import statistics
from collections import Counter
-from enum import Enum
+from enum import IntEnum
from simulatingrisk.hawkdove.model import HawkDoveModel, HawkDoveAgent
-class HawkDoveVariableRiskAgent(HawkDoveAgent):
+class HawkDoveMultipleRiskAgent(HawkDoveAgent):
"""
An agent with random risk attitude playing Hawk or Dove. Optionally
adjusts risks based on most successful neighbor, depending on model
@@ -16,8 +16,11 @@ def set_risk_level(self):
# risk level is based partially on neighborhood size,
# which is configurable at the model level
- # generate a random risk level between zero and number of neighbors
- self.risk_level = self.random.randint(0, self.model.num_neighbors)
+ # generate a random risk level between zero and 8
+ # (using same range for all neighborhood sizes)
+ self.risk_level = self.random.randint(
+ self.model.min_risk_level, self.model.max_risk_level
+ )
def play(self):
super().play()
@@ -25,12 +28,18 @@ def play(self):
if self.model.adjustment_round:
self.adjust_risk()
+ @property
+ def adjust_neighbors(self):
+ """neighbors to look at when adjusting risk attitude; uses
+ model adjust_neighborhood size"""
+ return self.get_neighbors(self.model.adjust_neighborhood)
+
@property
def most_successful_neighbor(self):
"""identify and return the neighbor with the most points"""
# sort neighbors by points, highest points first
# adapted from risky bet wealthiest neighbor
- return sorted(self.neighbors, key=lambda x: x.points, reverse=True)[0]
+ return sorted(self.adjust_neighbors, key=lambda x: x.points, reverse=True)[0]
def adjust_risk(self):
# look at neighbors
@@ -53,7 +62,7 @@ def adjust_risk(self):
)
-class RiskState(Enum):
+class RiskState(IntEnum):
"""Categorization of population risk states"""
# majority risk inclined
@@ -91,36 +100,36 @@ def category(cls, val):
return "no majority"
-class HawkDoveVariableRiskModel(HawkDoveModel):
+class HawkDoveMultipleRiskModel(HawkDoveModel):
"""
- Model for hawk/dove game with variable risk attitudes.
+ Model for hawk/dove game with variable risk attitudes. Supports
+ all parameters in :class:`~simulatingrisk.hawkdove.model.HawkDoveModel`
+ and adds several parmeters to control if and how agents adjust
+ their risk attitudes (strategy, frequency, and neighborhood size).
- :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 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
:param adjust_every: when risk adjustment is enabled, adjust every
N rounds (default: 10)
+ :param adjust_neighborhood: size of neighborhood to look at when
+ adjusting risk attitudes; 4, 8, or 24 (default: play_neighborhood)
"""
risk_attitudes = "variable"
- agent_class = HawkDoveVariableRiskAgent
+ agent_class = HawkDoveMultipleRiskAgent
supported_risk_adjustments = (None, "adopt", "average")
def __init__(
self,
grid_size,
- include_diagonals=True,
- hawk_odds=0.5,
risk_adjustment=None,
adjust_every=10,
+ adjust_neighborhood=None,
+ *args,
+ **kwargs,
):
- super().__init__(
- grid_size, include_diagonals=include_diagonals, hawk_odds=hawk_odds
- )
+ super().__init__(grid_size, *args, **kwargs)
# convert string input from solara app parameters to None
if risk_adjustment == "none":
risk_adjustment = None
@@ -135,12 +144,9 @@ def __init__(
)
self.risk_adjustment = risk_adjustment
self.adjust_round_n = adjust_every
-
- @property
- def num_neighbors(self) -> int:
- # number of neighbors for each agent - depends on whether
- # diagonals are included or not
- return 8 if self.include_diagonals else 4
+ # if adjust neighborhood is not specified, then use the same size
+ # as play neighborhood
+ self.adjust_neighborhood = adjust_neighborhood or self.play_neighborhood
@property
def adjustment_round(self) -> bool:
diff --git a/tests/test_hawkdove.py b/tests/test_hawkdove.py
index 17298bd..f0b38f2 100644
--- a/tests/test_hawkdove.py
+++ b/tests/test_hawkdove.py
@@ -13,21 +13,23 @@
def test_agent_neighbors():
- # initialize model with a small grid, include diagonals
- model = HawkDoveSingleRiskModel(3, include_diagonals=True, agent_risk_level=4)
+ # initialize model with a small grid, neighborhood of 8
+ model = HawkDoveSingleRiskModel(3, play_neighborhood=8, agent_risk_level=4)
# every agent should have 8 neighbors when diagonals are included
- assert all([len(agent.neighbors) == 8 for agent in model.schedule.agents])
+ assert all([len(agent.play_neighbors) == 8 for agent in model.schedule.agents])
- # every agent should have 4 neighbors when diagonals are not included
- model = HawkDoveSingleRiskModel(3, include_diagonals=False, agent_risk_level=2)
- assert all([len(agent.neighbors) == 4 for agent in model.schedule.agents])
+ # neighborhood of 4
+ model = HawkDoveSingleRiskModel(3, play_neighborhood=4, agent_risk_level=2)
+ assert all([len(agent.play_neighbors) == 4 for agent in model.schedule.agents])
+
+ # neighborhood of 24 (grid needs to be at least 5x5)
+ model = HawkDoveSingleRiskModel(5, play_neighborhood=24, agent_risk_level=5)
+ assert all([len(agent.play_neighbors) == 24 for agent in model.schedule.agents])
def test_agent_initial_choice():
grid_size = 100
- model = HawkDoveSingleRiskModel(
- grid_size, include_diagonals=False, agent_risk_level=5
- )
+ model = HawkDoveSingleRiskModel(grid_size, agent_risk_level=5)
# for now, initial choice is random (hawk-odds param still todo)
initial_choices = [a.choice for a in model.schedule.agents]
choice_count = Counter(initial_choices)
@@ -40,9 +42,7 @@ def test_agent_initial_choice():
def test_agent_initial_choice_hawkodds():
grid_size = 100
# specify hawk-odds other than 05
- model = HawkDoveSingleRiskModel(
- grid_size, include_diagonals=False, hawk_odds=0.3, agent_risk_level=2
- )
+ model = HawkDoveSingleRiskModel(grid_size, hawk_odds=0.3, agent_risk_level=2)
initial_choices = [a.choice for a in model.schedule.agents]
choice_count = Counter(initial_choices)
# expect about 30% hawks
@@ -74,21 +74,39 @@ def test_agent_repr():
def test_model_single_risk_level():
risk_level = 3
- model = HawkDoveSingleRiskModel(
- 5, include_diagonals=True, agent_risk_level=risk_level
- )
+ model = HawkDoveSingleRiskModel(5, agent_risk_level=risk_level)
for agent in model.schedule.agents:
assert agent.risk_level == risk_level
# handle zero properly (should not be treated the same as None)
risk_level = 0
- model = HawkDoveSingleRiskModel(
- 5, include_diagonals=True, agent_risk_level=risk_level
- )
+ model = HawkDoveSingleRiskModel(5, agent_risk_level=risk_level)
for agent in model.schedule.agents:
assert agent.risk_level == risk_level
+def test_bad_neighborhood_size():
+ with pytest.raises(ValueError):
+ HawkDoveSingleRiskModel(3, play_neighborhood=3, agent_risk_level=6)
+ with pytest.raises(ValueError):
+ agent = HawkDoveSingleRiskAgent(1, Mock(agent_risk_level=2))
+ agent.get_neighbors(5)
+
+
+def test_observed_neighborhood_size():
+ # observed neighborhood size is also configurable
+ # common options, irrelevant for this test
+ opts = {"agent_risk_level": 1, "play_neighborhood": 4}
+ model = HawkDoveSingleRiskModel(3, observed_neighborhood=4, **opts)
+ assert model.observed_neighborhood == 4
+ model = HawkDoveSingleRiskModel(3, observed_neighborhood=8, **opts)
+ assert model.observed_neighborhood == 8
+ model = HawkDoveSingleRiskModel(3, observed_neighborhood=24, **opts)
+ assert model.observed_neighborhood == 24
+ with pytest.raises(ValueError):
+ HawkDoveSingleRiskModel(3, observed_neighborhood=23, **opts)
+
+
def test_num_dove_neighbors():
# initialize an agent with a mock model
agent = HawkDoveSingleRiskAgent(1, Mock(agent_risk_level=2))
@@ -99,7 +117,7 @@ def test_num_dove_neighbors():
Mock(last_choice=Play.DOVE),
]
- with patch.object(HawkDoveSingleRiskAgent, "neighbors", mock_neighbors):
+ with patch.object(HawkDoveSingleRiskAgent, "observed_neighbors", mock_neighbors):
assert agent.num_dove_neighbors == 1
@@ -113,7 +131,7 @@ def test_agent_choose():
agent.model.schedule.steps = 1
# given a specified number of dove neighbors and risk level
- with patch.object(HawkDoveAgent, "num_dove_neighbors", 3):
+ with patch.object(HawkDoveAgent, "proportional_num_dove_neighbors", 3):
# an agent with `r=0` will always take the risky choice
# (any risk is acceptable).
agent.risk_level = 0
@@ -137,6 +155,39 @@ def test_agent_choose():
assert agent.choice == Play.DOVE
+def test_proportional_num_dove_neighbors():
+ model = HawkDoveSingleRiskModel(4, agent_risk_level=3)
+ agent = HawkDoveSingleRiskAgent(1, model)
+
+ ## equal play/observed; scales to 8 (risk level range)
+ model.observed_neighborhood = 4
+ with patch.object(HawkDoveAgent, "num_dove_neighbors", 3):
+ assert agent.proportional_num_dove_neighbors == 6
+
+ model.observed_neighborhood = 8
+ with patch.object(HawkDoveAgent, "num_dove_neighbors", 5):
+ assert agent.proportional_num_dove_neighbors == 5
+
+ # observe more than 8
+ model.observed_neighborhood = 24
+ with patch.object(HawkDoveAgent, "num_dove_neighbors", 20):
+ assert agent.proportional_num_dove_neighbors == 7
+
+
+def test_agent_choose_when_observe_play_differ():
+ # confirm that adjusted value is used to determine play
+
+ model = HawkDoveSingleRiskModel(
+ 4, agent_risk_level=3, observed_neighborhood=24, play_neighborhood=8
+ )
+ agent = HawkDoveSingleRiskAgent(3, model)
+ with patch.object(HawkDoveAgent, "num_dove_neighbors", 5):
+ agent.choose() == Play.DOVE
+
+ with patch.object(HawkDoveAgent, "num_dove_neighbors", 6):
+ agent.choose() == Play.HAWK
+
+
def test_agent_play():
agent = HawkDoveSingleRiskAgent(1, Mock(agent_risk_level=3))
# on the first round, last choice should be unset
@@ -148,7 +199,7 @@ def test_agent_play():
agent.choice = Play.HAWK
neighbor_hawk = Mock(choice=Play.HAWK)
neighbor_dove = Mock(choice=Play.DOVE)
- with patch.object(HawkDoveAgent, "neighbors", [neighbor_hawk, neighbor_dove]):
+ with patch.object(HawkDoveAgent, "play_neighbors", [neighbor_hawk, neighbor_dove]):
agent.play()
# should get 3 points against dove and 0 against the hawk
assert agent.points == 3 + 0
diff --git a/tests/test_hawkdovevar.py b/tests/test_hawkdovevar.py
deleted file mode 100644
index 7796d69..0000000
--- a/tests/test_hawkdovevar.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import statistics
-from unittest.mock import patch, Mock
-
-import pytest
-
-from simulatingrisk.hawkdovevar.model import (
- HawkDoveVariableRiskModel,
- HawkDoveVariableRiskAgent,
- RiskState,
-)
-
-
-def test_init():
- model = HawkDoveVariableRiskModel(5)
- # defaults
- assert model.risk_adjustment is None
- assert model.hawk_odds == 0.5
- assert model.include_diagonals is True
- # unused but should be set to default
- assert model.adjust_round_n == 10
-
- # init with risk adjustment
- model = HawkDoveVariableRiskModel(
- 5,
- include_diagonals=False,
- hawk_odds=0.2,
- risk_adjustment="adopt",
- adjust_every=5,
- )
-
- assert model.risk_adjustment == "adopt"
- assert model.adjust_round_n == 5
- assert model.hawk_odds == 0.2
- assert model.include_diagonals is False
-
- # handle string none for solara app parameters
- model = HawkDoveVariableRiskModel(5, risk_adjustment="none")
- assert model.risk_adjustment is None
-
- # complain about invalid adjustment type
- with pytest.raises(ValueError, match="Unsupported risk adjustment 'bogus'"):
- HawkDoveVariableRiskModel(3, risk_adjustment="bogus")
-
-
-def test_num_neighbors():
- with_diagonals = HawkDoveVariableRiskModel(3)
- assert with_diagonals.num_neighbors == 8
-
- no_diagonals = HawkDoveVariableRiskModel(3, include_diagonals=False)
- assert no_diagonals.num_neighbors == 4
-
-
-def test_init_variable_risk_level():
- model = HawkDoveVariableRiskModel(
- 5,
- include_diagonals=True,
- )
- # when risk level is variable/random, agents should have different risk levels
- risk_levels = set([agent.risk_level for agent in model.schedule.agents])
- assert len(risk_levels) > 1
-
-
-adjustment_testdata = [
- # init parameters, expected adjustment round
- ({"risk_adjustment": None}, None),
- ({"risk_adjustment": "adopt"}, 10),
- ({"risk_adjustment": "average"}, 10),
- ({"risk_adjustment": "average", "adjust_every": 3}, 3),
-]
-
-
-@pytest.mark.parametrize("params,expect_adjust_step", adjustment_testdata)
-def test_adjustment_round(params, expect_adjust_step):
- model = HawkDoveVariableRiskModel(3, **params)
-
- run_for = (expect_adjust_step or 10) + 1
-
- # step through the model enough rounds to encounter one adjustment rounds
- # if adjustment is enabled; start at 1 (step count starts at 1)
- for i in range(1, run_for):
- model.step()
- if i == expect_adjust_step:
- assert model.adjustment_round
- else:
- assert not model.adjustment_round
-
-
-def test_population_risk_category():
- model = HawkDoveVariableRiskModel(3)
- model.schedule = Mock()
-
- # majority risk inclined
- model.schedule.agents = [Mock(risk_level=0), Mock(risk_level=1), Mock(risk_level=2)]
- assert model.population_risk_category == RiskState.c1
- # three risk-inclined agents and one risk moderate
- model.schedule.agents.append(Mock(risk_level=4))
- assert model.population_risk_category == RiskState.c2
-
- # majority risk moderate
- model.schedule.agents = [Mock(risk_level=4), Mock(risk_level=3), Mock(risk_level=5)]
- assert model.population_risk_category == RiskState.c7
-
- # majority risk avoidant
- model.schedule.agents = [Mock(risk_level=6), Mock(risk_level=7), Mock(risk_level=8)]
- assert model.population_risk_category == RiskState.c12
-
-
-def test_riskstate_label():
- # enum value or integer value
- assert RiskState.category(RiskState.c1) == "majority risk inclined"
- assert RiskState.category(2) == "majority risk inclined"
- assert RiskState.category(RiskState.c5) == "majority risk moderate"
- assert RiskState.category(6) == "majority risk moderate"
- assert RiskState.category(RiskState.c11) == "majority risk avoidant"
- assert RiskState.category(RiskState.c13) == "no majority"
- assert RiskState.category(13) == "no majority"
-
-
-def test_most_successful_neighbor():
- # initialize an agent with a mock model
- agent = HawkDoveVariableRiskAgent(1, Mock(), 1000)
- mock_neighbors = [
- Mock(points=2),
- Mock(points=4),
- Mock(points=23),
- Mock(points=31),
- ]
-
- with patch.object(HawkDoveVariableRiskAgent, "neighbors", mock_neighbors):
- assert agent.most_successful_neighbor.points == 31
-
-
-def test_agent_play_adjust():
- mock_model = Mock(risk_adjustment="adopt")
- agent = HawkDoveVariableRiskAgent(1, mock_model)
- # simulate no neighbors to skip payoff calculation
- with patch.object(
- HawkDoveVariableRiskAgent, "neighbors", new=[]
- ) as mock_adjust_risk:
- with patch.object(HawkDoveVariableRiskAgent, "adjust_risk") as mock_adjust_risk:
- # when it is not an adjustment round, should not call adjust risk
- mock_model.adjustment_round = False
- agent.play()
- assert mock_adjust_risk.call_count == 0
-
- # should call adjust risk when the model indicates
- mock_model.adjustment_round = True
- agent.play()
- assert mock_adjust_risk.call_count == 1
-
-
-def test_adjust_risk_adopt():
- # initialize an agent with a mock model
- agent = HawkDoveVariableRiskAgent(1, Mock(risk_adjustment="adopt"))
- # set a known risk level
- agent.risk_level = 2
- # adjust wealth as if the model had run
- agent.points = 20
- # set a mock neighbor with more points than current agent
- neighbor = Mock(points=1500, risk_level=3)
- with patch.object(HawkDoveVariableRiskAgent, "most_successful_neighbor", neighbor):
- agent.adjust_risk()
- # default behavior is to adopt successful risk level
- assert agent.risk_level == neighbor.risk_level
-
- # now simulate a wealthiest neighbor with fewer points than current agent
- neighbor.points = 12
- neighbor.risk_level = 3
- prev_risk_level = agent.risk_level
- agent.adjust_risk()
- # risk level should not be changed
- assert agent.risk_level == prev_risk_level
-
-
-def test_adjust_risk_average():
- # same as previous test, but with average risk adjustment strategy
- agent = HawkDoveVariableRiskAgent(1, Mock(risk_adjustment="average"))
- # set a known risk level
- agent.risk_level = 2
- # adjust points as if the model had run
- agent.points = 300
- # set a neighbor with more points than current agent
- neighbor = Mock(points=350, risk_level=3)
- with patch.object(HawkDoveVariableRiskAgent, "most_successful_neighbor", neighbor):
- prev_risk_level = agent.risk_level
- agent.adjust_risk()
- # new risk level should be average of previous and most successful
- assert agent.risk_level == round(
- statistics.mean([neighbor.risk_level, prev_risk_level])
- )