Skip to content

Commit 4932c98

Browse files
committed
Modications and Tests for Moran process
1 parent 4bec215 commit 4932c98

File tree

2 files changed

+72
-7
lines changed

2 files changed

+72
-7
lines changed

axelrod/moran.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ def __init__(self, players, turns=100, noise=0, deterministic_cache=None, mutati
4040
4141
If the mutation_rate is 0, the population will eventually fixate on exactly one player type. In this
4242
case a StopIteration exception is raised and the play stops. If mutation_rate is not zero, then
43-
the process will iterate indefinitely.
43+
the process will iterate indefinitely, so mp.play() will never exit, and you should use the class as an
44+
iterator instead.
45+
46+
When a player mutates, it chooses a random player type from the initial population. This is not the only
47+
method yet emulates the common method in the literature.
4448
4549
Parameters
4650
----------
@@ -65,11 +69,21 @@ def __init__(self, players, turns=100, noise=0, deterministic_cache=None, mutati
6569
self.mutation_rate = mutation_rate
6670
assert (mutation_rate >= 0) and (mutation_rate <= 1)
6771
assert (noise >= 0) and (noise <= 1)
68-
6972
if deterministic_cache is not None:
7073
self.deterministic_cache = deterministic_cache
7174
else:
7275
self.deterministic_cache = DeterministicCache()
76+
# Build the set of mutation targets
77+
# Determine the number of unique types (players)
78+
keys = set([str(p) for p in players])
79+
# Create a dictionary mapping each type to a set of representatives of the other types
80+
d = dict()
81+
for p in players:
82+
d[str(p)] = p
83+
mt = dict()
84+
for key in keys:
85+
mt[key] = [v for (k, v) in d.items() if k != key]
86+
self.mutation_targets = mt
7387

7488
def set_players(self):
7589
"""Copy the initial players into the first population."""
@@ -88,10 +102,23 @@ def _stochastic(self):
88102
"""
89103
return is_stochastic(self.players, self.noise) or (self.mutation_rate > 0)
90104

105+
def mutate(self, index):
106+
# If mutate, choose another strategy at random from the initial population
107+
r = random.random()
108+
if r < self.mutation_rate:
109+
s = str(self.players[index])
110+
p = random.choice(self.mutation_targets[s])
111+
new_player = p.clone()
112+
else:
113+
# Just clone the player
114+
new_player = self.players[index].clone()
115+
return new_player
116+
91117
def __next__(self):
92118
"""Iterate the population:
93119
- play the round's matches
94120
- chooses a player proportionally to fitness (total score) to reproduce
121+
- mutate, if appropriate
95122
- choose a player at random to be replaced
96123
- update the population
97124
"""
@@ -107,15 +134,15 @@ def __next__(self):
107134
j = fitness_proportionate_selection(scores)
108135
# Mutate?
109136
if self.mutation_rate:
110-
r = random.random()
111-
# If mutate, choose another strategy at random
112-
if r < self.mutation_rate:
113-
j = randrange(0, len(self.players))
137+
new_player = self.mutate(j)
138+
else:
139+
new_player = self.players[j].clone()
114140
# Randomly remove a strategy
115141
i = randrange(0, len(self.players))
116142
# Replace player i with clone of player j
117-
self.players[i] = self.players[j].clone()
143+
self.players[i] = new_player
118144
self.populations.append(self.population_distribution())
145+
return self
119146

120147
def _play_next_round(self):
121148
"""Plays the next round of the process. Every player is paired up

axelrod/tests/unit/test_moran.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# -*- coding: utf-8 -*-
2+
from collections import Counter
3+
import itertools
24
import random
35
import unittest
46

@@ -46,6 +48,24 @@ def test_two_players(self):
4648
self.assertEqual(populations, mp.populations)
4749
self.assertEqual(mp.winning_strategy_name, str(p2))
4850

51+
def test_two_players_with_mutation(self):
52+
p1, p2 = axelrod.Cooperator(), axelrod.Defector()
53+
random.seed(5)
54+
mp = MoranProcess((p1, p2), mutation_rate=0.2)
55+
self.assertEqual(mp._stochastic, True)
56+
self.assertEqual(mp.mutation_targets, {str(p1): [p2], str(p2): [p1]})
57+
# Test that mutation causes the population to alternate between fixations
58+
counters = [
59+
Counter({'Cooperator': 2}),
60+
Counter({'Defector': 2}),
61+
Counter({'Cooperator': 2}),
62+
Counter({'Defector': 2})
63+
]
64+
for counter in counters:
65+
for _ in itertools.takewhile(lambda x: x.population_distribution() != counter, mp):
66+
pass
67+
self.assertEqual(mp.population_distribution(), counter)
68+
4969
def test_three_players(self):
5070
players = [axelrod.Cooperator(), axelrod.Cooperator(),
5171
axelrod.Defector()]
@@ -57,6 +77,24 @@ def test_three_players(self):
5777
self.assertEqual(populations, mp.populations)
5878
self.assertEqual(mp.winning_strategy_name, str(axelrod.Defector()))
5979

80+
def test_three_players_with_mutation(self):
81+
p1 = axelrod.Cooperator()
82+
p2 = axelrod.Random()
83+
p3 = axelrod.Defector()
84+
players = [p1, p2, p3]
85+
mp = MoranProcess(players, mutation_rate=0.2)
86+
self.assertEqual(mp._stochastic, True)
87+
self.assertEqual(mp.mutation_targets, {str(p1): [p2, p3], str(p2): [p1, p3], str(p3): [p1, p2]})
88+
# Test that mutation causes the population to alternate between fixations
89+
counters = [
90+
Counter({'Cooperator': 3}),
91+
Counter({'Defector': 3}),
92+
]
93+
for counter in counters:
94+
for _ in itertools.takewhile(lambda x: x.population_distribution() != counter, mp):
95+
pass
96+
self.assertEqual(mp.population_distribution(), counter)
97+
6098
def test_four_players(self):
6199
players = [axelrod.Cooperator() for _ in range(3)]
62100
players.append(axelrod.Defector())

0 commit comments

Comments
 (0)