Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented the Moran process on graphs #799

Merged
merged 7 commits into from
Jan 5, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 34 additions & 32 deletions axelrod/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,37 @@ class Graph(object):
[[node1, node2, weights], ...]
Weights can be omitted for an undirected graph.

For efficiency, neighbors are cached in dictionaries.
For efficiency, neighbors are cached in dictionaries. Undirected graphs
are implemented as directed graphs in which every edge (s, t) has the
opposite edge (t, s).
"""

def __init__(self, edges=None, directed=False):
self.directed = directed
self.original_edges = edges
self.out_mapping = defaultdict(lambda: defaultdict(float))
self.in_mapping = defaultdict(lambda: defaultdict(float))
self.directed = directed
self._edges = []
if edges:
self.add_edges(edges)

def add_vertex(self, label):
self._vertices.add(label)

def add_edge(self, source, target, weight=1.):
self.out_mapping[source][target] = weight
self.in_mapping[target][source] = weight
if not self.directed:
def add_edge(self, source, target, weight=None):
if (source, target) not in self._edges:
self._edges.append((source, target))
self.out_mapping[source][target] = weight
self.in_mapping[target][source] = weight
if not self.directed and (source != target) and \
(target, source) not in self._edges:
self._edges.append((target, source))
self.out_mapping[target][source] = weight
self.in_mapping[source][target] = weight

def add_edges(self, edges):
try:
for source, target, weight in edges:
self.add_edge(source, target, weight)
except ValueError:
for source, target in edges:
self.add_edge(source, target, 1.0)
for edge in edges:
self.add_edge(*edge)

def edges(self):
return self._edges

def vertices(self):
"""Returns the set of vertices of the graph."""
Expand All @@ -62,20 +66,13 @@ def in_dict(self, target):
"""Returns a dictionary of the incoming edges of source with weights."""
return self.in_mapping[target]

def normalize_weights(self):
"""Normalizes the weights coming out of each vertex to be probability
distributions."""
new_edges = []
for source in self.out_mapping.keys():
total = float(sum(out_mapping[source].values()))
for target, weight in self.out_mapping.items():
self.out_mapping[target] = weight / total
self._edges = new_edges
def in_vertices(self, source):
"""Returns a list of the outgoing vertices."""
return list(self.in_mapping[source].keys())

def __getitem__(self, k):
"""Returns the dictionary of outgoing edges. You can access the weight
of an edge with g[source][target]."""
return self.out_mapping[k]
def __repr__(self):
s = "<Graph: {}>".format(repr(self.original_edges))
return s


## Example Graphs
Expand Down Expand Up @@ -104,9 +101,11 @@ def cycle(length, directed=False):
return graph


def complete_graph(length, directed=False):
def complete_graph(length, loops=True):
"""
Produces a complete graph of size `length`.
Produces a complete graph of size `length`, with loops.
https://en.wikipedia.org/wiki/Complete_graph

Parameters
----------
length: int
Expand All @@ -117,10 +116,13 @@ def complete_graph(length, directed=False):
-------
a Graph object
"""
graph = Graph(directed=directed)
offset = 1
if loops:
offset = 0
graph = Graph(directed=False)
edges = []
for i in range(length):
for j in range(length):
for j in range(i + offset, length):
edges.append((i, j))
graph.add_edges(edges)
return graph
18 changes: 15 additions & 3 deletions axelrod/moran.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def fitness_proportionate_selection(scores):

class MoranProcess(object):
def __init__(self, players, turns=100, noise=0, deterministic_cache=None,
mutation_rate=0., mode='bd'):
mutation_rate=0., mode='bd', match_class=Match):
"""
An agent based Moran process class. In each round, each player plays a
Match with each other player. Players are assigned a fitness score by
Expand Down Expand Up @@ -64,7 +64,10 @@ def __init__(self, players, turns=100, noise=0, deterministic_cache=None,
probability `mutation_rate`
mode: string, bd
Birth-Death (bd) or Death-Birth (db)
match_class: subclass of Match
The match type to use for scoring
"""
self.match_class = match_class
self.turns = turns
self.noise = noise
self.initial_players = players # save initial population
Expand Down Expand Up @@ -208,7 +211,7 @@ def score_all(self):
for i, j in self._matchup_indices():
player1 = self.players[i]
player2 = self.players[j]
match = Match(
match = self.match_class(
(player1, player2), turns=self.turns, noise=self.noise,
deterministic_cache=self.deterministic_cache)
match.play()
Expand Down Expand Up @@ -252,7 +255,7 @@ def __len__(self):
class MoranProcessGraph(MoranProcess):
def __init__(self, players, interaction_graph, reproduction_graph=None,
turns=100, noise=0, deterministic_cache=None,
mutation_rate=0., mode='bd'):
mutation_rate=0., mode='bd', match_class=Match):
"""
An agent based Moran process class. In each round, each player plays a
Match with each neighboring player according to the interaction graph.
Expand All @@ -271,6 +274,13 @@ def __init__(self, players, interaction_graph, reproduction_graph=None,
population. This is not the only method yet emulates the common method
in the literature.

Note: the weighted graph case is not yet implemented, nor is birth-bias,
death-bias, or Link Dynamics updating; however the most common use cases
are implemented.

See [Shakarian2013]_ for more detail on the process and different
updating modes.

Parameters
----------
players, iterable of axelrod.Player subclasses
Expand All @@ -291,6 +301,8 @@ def __init__(self, players, interaction_graph, reproduction_graph=None,
probability `mutation_rate`
mode: string, bd
Birth-Death (bd) or Death-Birth (db)
match_class: subclass of Match
The match type to use for scoring
"""
MoranProcess.__init__(self, players, turns=turns, noise=noise,
deterministic_cache=deterministic_cache,
Expand Down
97 changes: 97 additions & 0 deletions axelrod/tests/unit/test_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import unittest

from axelrod import graph


class TestGraph(unittest.TestCase):

def test_cycle(self):
g = graph.cycle(1, directed=False)
self.assertEqual(g.vertices(), [0])
self.assertEqual(g.edges(), [(0, 0)])
self.assertEqual(g.directed, False)
g = graph.cycle(1, directed=True)
self.assertEqual(g.vertices(), [0])
self.assertEqual(g.edges(), [(0, 0)])
self.assertEqual(g.directed, True)
g = graph.cycle(2, directed=True)
self.assertEqual(g.vertices(), [0, 1])
self.assertEqual(g.edges(), [(0, 1), (1, 0)])
g = graph.cycle(2, directed=False)
self.assertEqual(g.vertices(), [0, 1])
self.assertEqual(g.edges(), [(0, 1), (1, 0)])
g = graph.cycle(3, directed=True)
self.assertEqual(g.vertices(), [0, 1, 2])
self.assertEqual(g.edges(), [(0, 1), (1, 2), (2, 0)])
g = graph.cycle(3, directed=False)
edges = [(0, 1), (1, 0), (1, 2), (2, 1), (2, 0), (0, 2)]
self.assertEqual(g.vertices(), [0, 1, 2])
self.assertEqual(g.edges(), edges)
g = graph.cycle(4, directed=True)
self.assertEqual(g.vertices(), [0, 1, 2, 3])
self.assertEqual(g.edges(), [(0, 1), (1, 2), (2, 3), (3, 0)])
self.assertEqual(g.out_vertices(0), [1])
self.assertEqual(g.out_vertices(1), [2])
self.assertEqual(g.out_vertices(2), [3])
self.assertEqual(g.out_vertices(3), [0])
self.assertEqual(g.in_vertices(0), [3])
self.assertEqual(g.in_vertices(1), [0])
self.assertEqual(g.in_vertices(2), [1])
self.assertEqual(g.in_vertices(3), [2])
g = graph.cycle(4, directed=False)
edges = [(0, 1), (1, 0), (1, 2), (2, 1),
(2, 3), (3, 2), (3, 0), (0, 3)]
self.assertEqual(g.vertices(), [0, 1, 2, 3])
self.assertEqual(g.edges(), edges)
for vertex, neighbors in [
(0, (1, 3)), (1, (0, 2)), (2, (1, 3)), (3, (0, 2))]:
self.assertEqual(set(g.out_vertices(vertex)), set(neighbors))
for vertex, neighbors in [
(0, (1, 3)), (1, (0, 2)), (2, (1, 3)), (3, (0, 2))]:
self.assertEqual(set(g.in_vertices(vertex)), set(neighbors))

def test_complete(self):
g = graph.complete_graph(2, loops=False)
self.assertEqual(g.vertices(), [0, 1])
self.assertEqual(g.edges(), [(0, 1), (1, 0)])
self.assertEqual(g.directed, False)
g = graph.complete_graph(3, loops=False)
self.assertEqual(g.vertices(), [0, 1, 2])
edges = [(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)]
self.assertEqual(g.edges(), edges)
self.assertEqual(g.directed, False)
g = graph.complete_graph(4, loops=False )
self.assertEqual(g.vertices(), [0, 1, 2, 3])
edges = [(0, 1), (1, 0), (0, 2), (2, 0), (0, 3), (3, 0),
(1, 2), (2, 1), (1, 3), (3, 1), (2, 3), (3, 2)]
self.assertEqual(g.edges(), edges)
self.assertEqual(g.directed, False)
for vertex, neighbors in [
(0, (1, 2, 3)), (1, (0, 2, 3)), (2, (0, 1, 3)), (3, (0, 1, 2))]:
self.assertEqual(set(g.out_vertices(vertex)), set(neighbors))
for vertex, neighbors in [
(0, (1, 2, 3)), (1, (0, 2, 3)), (2, (0, 1, 3)), (3, (0, 1, 2))]:
self.assertEqual(set(g.in_vertices(vertex)), set(neighbors))

def test_complete_with_loops(self):
g = graph.complete_graph(2, loops=True)
self.assertEqual(g.vertices(), [0, 1])
self.assertEqual(g.edges(), [(0, 0), (0, 1), (1, 0), (1, 1)])
self.assertEqual(g.directed, False)
g = graph.complete_graph(3, loops=True)
self.assertEqual(g.vertices(), [0, 1, 2])
edges = [(0, 0), (0, 1), (1, 0), (0, 2), (2, 0), (1, 1),
(1, 2), (2, 1), (2, 2)]
self.assertEqual(g.edges(), edges)
self.assertEqual(g.directed, False)
g = graph.complete_graph(4, loops=True)
self.assertEqual(g.vertices(), [0, 1, 2, 3])
edges = [(0, 0), (0, 1), (1, 0), (0, 2), (2, 0), (0, 3), (3, 0),
(1, 1), (1, 2), (2, 1), (1, 3), (3, 1),
(2, 2), (2, 3), (3, 2), (3, 3)]
self.assertEqual(g.edges(), edges)
self.assertEqual(g.directed, False)
neighbors = range(4)
for vertex in range(4):
self.assertEqual(set(g.out_vertices(vertex)), set(neighbors))
self.assertEqual(set(g.in_vertices(vertex)), set(neighbors))
34 changes: 33 additions & 1 deletion axelrod/tests/unit/test_moran.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,28 @@
from hypothesis import given, example, settings

import axelrod
from axelrod import MoranProcess, MoranProcessGraph
from axelrod import Match, MoranProcess, MoranProcessGraph
from axelrod.moran import fitness_proportionate_selection
from axelrod.tests.property import strategy_lists


class MockMatch(object):
"""Mock Match class to always return the same score for testing purposes."""

def __init__(self, players, *args, **kwargs):
self.players = players
score_dict = {"Cooperator": 2, "Defector": 1}
self.score_dict = score_dict

def play(self):
pass

def final_score_per_turn(self):
s = (self.score_dict[str(self.players[0])],
self.score_dict[str(self.players[1])])
return s


class TestMoranProcess(unittest.TestCase):

def test_fps(self):
Expand Down Expand Up @@ -136,6 +153,21 @@ def test_four_players(self):
self.assertEqual(populations, mp.populations)
self.assertEqual(mp.winning_strategy_name, str(axelrod.Defector()))

def test_standard_fixation(self):
"""Test a traditional Moran process with a MockMatch."""
axelrod.seed(0)
players = (axelrod.Cooperator(), axelrod.Cooperator(),
axelrod.Defector(), axelrod.Defector())
mp = MoranProcess(players, match_class=MockMatch)
winners = []
for i in range(100):
mp.play()
winner = mp.winning_strategy_name
winners.append(winner)
mp.reset()
winners = Counter(winners)
self.assertEqual(winners["Cooperator"], 82)

@given(strategies=strategy_lists(min_size=2, max_size=5))
@settings(max_examples=5, timeout=0) # Very low number of examples

Expand Down
1 change: 1 addition & 0 deletions docs/reference/bibliography.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ documentation.
.. [PRISON1998] LIFL (1998) PRISON. Available at: http://www.lifl.fr/IPD/ipd.frame.html (Accessed: 19 September 2016).
.. [Robson1989] Robson, Arthur, (1989), EFFICIENCY IN EVOLUTIONARY GAMES: DARWIN, NASH AND SECRET HANDSHAKE, Working Papers, Michigan - Center for Research on Economic & Social Theory, http://EconPapers.repec.org/RePEc:fth:michet:89-22.
.. [Singer-Clark2014] Singer-Clark, T. (2014). Morality Metrics On Iterated Prisoner’s Dilemma Players.
.. [Shakarian2013] Shakarian, P., Roos, P. & Moores, G. A Novel Analytical Method for Evolutionary Graph Theory Problems.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this: could you cite it in the moran.rst file?

.. [Slany2007] Slany W. and Kienreich W., On some winning strategies for the iterated prisoner’s dilemma, in Kendall G., Yao X. and Chong S. (eds.) The iterated prisoner’s dilemma: 20 years on. World Scientific, chapter 8, pp. 171-204, 2007.
.. [Stewart2012] Stewart, a. J., & Plotkin, J. B. (2012). Extortion and cooperation in the Prisoner’s Dilemma. Proceedings of the National Academy of Sciences, 109(26), 10134–10135. http://doi.org/10.1073/pnas.1208087109
.. [Szabó1992] Szabó, G., & Fáth, G. (2007). Evolutionary games on graphs. Physics Reports, 446(4-6), 97–216. http://doi.org/10.1016/j.physrep.2007.04.004
Expand Down