Skip to content

Commit 9f2e8a6

Browse files
committed
Update Moran process for atomic mutation and add tests
1 parent d0b4d79 commit 9f2e8a6

File tree

2 files changed

+60
-44
lines changed

2 files changed

+60
-44
lines changed

axelrod/moran.py

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import matplotlib.pyplot as plt
88
import numpy as np
9-
from axelrod import DEFAULT_TURNS, Game, Player
9+
from axelrod import EvolvablePlayer, DEFAULT_TURNS, Game, Player
1010

1111
from .deterministic_cache import DeterministicCache
1212
from .graph import Graph, complete_graph
@@ -56,7 +56,8 @@ def __init__(
5656
interaction_graph: Graph = None,
5757
reproduction_graph: Graph = None,
5858
fitness_transformation: Callable = None,
59-
mutation_method="transition"
59+
mutation_method="transition",
60+
stop_on_fixation=True
6061
) -> None:
6162
"""
6263
An agent based Moran process class. In each round, each player plays a
@@ -109,6 +110,8 @@ def __init__(
109110
given
110111
fitness_transformation:
111112
A function mapping a score to a (non-negative) float
113+
stop_on_fixation:
114+
A bool indicating if the process should stop on fixation.
112115
"""
113116
self.turns = turns
114117
self.prob_end = prob_end
@@ -121,6 +124,7 @@ def __init__(
121124
self.score_history = [] # type: List
122125
self.winning_strategy_name = None # type: Optional[str]
123126
self.mutation_rate = mutation_rate
127+
self.stop_on_fixation = stop_on_fixation
124128
m = mutation_method.lower()
125129
if m in ["atomic", "transition"]:
126130
self.mutation_method = m
@@ -165,6 +169,7 @@ def __init__(
165169
# Map players to graph vertices
166170
self.locations = sorted(interaction_graph.vertices)
167171
self.index = dict(zip(sorted(interaction_graph.vertices), range(len(players))))
172+
self.fixated = self.fixation_check()
168173

169174
def set_players(self) -> None:
170175
"""Copy the initial players into the first population."""
@@ -184,21 +189,21 @@ def mutate(self, index: int) -> Player:
184189
"""
185190

186191
if self.mutation_method == "atomic":
187-
mutant = self.players[index].clone()
188-
mutant.mutate()
189-
return mutant
190-
191-
# Choose another strategy at random from the initial population
192-
r = random.random()
193-
if r < self.mutation_rate:
194-
s = str(self.players[index])
195-
j = randrange(0, len(self.mutation_targets[s]))
196-
p = self.mutation_targets[s][j]
197-
new_player = p.clone()
198-
else:
199-
# Just clone the player
200-
new_player = self.players[index].clone()
201-
return new_player
192+
if not issubclass(self.players[index].__class__, EvolvablePlayer):
193+
raise TypeError("Player is not evolvable. Use a subclass of EvolvablePlayer.")
194+
return self.players[index].mutate()
195+
196+
# Assuming mutation_method == "transition"
197+
if self.mutation_rate > 0:
198+
# Choose another strategy at random from the initial population
199+
r = random.random()
200+
if r < self.mutation_rate:
201+
s = str(self.players[index])
202+
j = randrange(0, len(self.mutation_targets[s]))
203+
p = self.mutation_targets[s][j]
204+
return p.clone()
205+
# Just clone the player
206+
return self.players[index].clone()
202207

203208
def death(self, index: int = None) -> int:
204209
"""
@@ -262,14 +267,13 @@ def fixation_check(self) -> bool:
262267
Boolean:
263268
True if fixation has occurred (population all of a single type)
264269
"""
265-
if self.mutation_rate > 0:
266-
return False
267270
classes = set(str(p) for p in self.players)
271+
self.fixated = False
268272
if len(classes) == 1:
269273
# Set the winning strategy name variable
270274
self.winning_strategy_name = str(self.players[0])
271-
return True
272-
return False
275+
self.fixated = True
276+
return self.fixated
273277

274278
def __next__(self) -> object:
275279
"""
@@ -287,7 +291,7 @@ def __next__(self) -> object:
287291
Returns itself with a new population
288292
"""
289293
# Check the exit condition, that all players are of the same type.
290-
if self.fixation_check():
294+
if self.stop_on_fixation and self.fixation_check():
291295
raise StopIteration
292296
if self.mode == "bd":
293297
# Birth then death
@@ -298,16 +302,10 @@ def __next__(self) -> object:
298302
i = self.death()
299303
self.players[i] = None
300304
j = self.birth(i)
301-
# Mutate
302-
if self.mutation_rate:
303-
new_player = self.mutate(j)
304-
else:
305-
new_player = self.players[j].clone()
306-
# Replace player i with clone of player j
307-
self.players[i] = new_player
305+
# Mutate and/or replace player i with clone of player j
306+
self.players[i] = self.mutate(j)
307+
# Record population.
308308
self.populations.append(self.population_distribution())
309-
# Check again for fixation
310-
self.fixation_check()
311309
return self
312310

313311
def _matchup_indices(self) -> Set[Tuple[int, int]]:
@@ -407,10 +405,10 @@ def play(self) -> List[Counter]:
407405
populations:
408406
Returns a list of all the populations
409407
"""
410-
if self.mutation_rate != 0:
408+
if not self.stop_on_fixation or self.mutation_rate != 0:
411409
raise ValueError(
412410
"MoranProcess.play() will never exit if mutation_rate is"
413-
"nonzero. Use iteration instead."
411+
"nonzero or stop_on_fixation is False. Use iteration instead."
414412
)
415413
while True:
416414
try:

axelrod/tests/unit/test_moran.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,10 @@ def test_death_birth(self):
182182
for seed in seeds:
183183
axelrod.seed(seed)
184184
mp = MoranProcess((p1, p2), mode="db")
185-
next(mp)
186-
self.assertIsNotNone(mp.winning_strategy_name)
185+
mp.play()
186+
self.assertIsNotNone(mp.winning_strategy_name)
187+
# Number of populations is 2: the original and the one after the first round.
188+
self.assertEqual(len(mp.populations), 2)
187189

188190
def test_death_birth_outcomes(self):
189191
"""Show that birth-death and death-birth can produce different
@@ -218,7 +220,7 @@ def test_two_random_players(self):
218220
def test_two_players_with_mutation(self):
219221
p1, p2 = axelrod.Cooperator(), axelrod.Defector()
220222
axelrod.seed(5)
221-
mp = MoranProcess((p1, p2), mutation_rate=0.2)
223+
mp = MoranProcess((p1, p2), mutation_rate=0.2, stop_on_fixation=False)
222224
self.assertDictEqual(mp.mutation_targets, {str(p1): [p2], str(p2): [p1]})
223225
# Test that mutation causes the population to alternate between
224226
# fixations
@@ -256,7 +258,7 @@ def test_three_players_with_mutation(self):
256258
p2 = axelrod.Random()
257259
p3 = axelrod.Defector()
258260
players = [p1, p2, p3]
259-
mp = MoranProcess(players, mutation_rate=0.2)
261+
mp = MoranProcess(players, mutation_rate=0.2, stop_on_fixation=False)
260262
self.assertDictEqual(
261263
mp.mutation_targets,
262264
{str(p1): [p3, p2], str(p2): [p1, p3], str(p3): [p1, p2]},
@@ -377,6 +379,28 @@ def test_cooperator_can_win_with_fitness_transformation(self):
377379
populations = mp.play()
378380
self.assertEqual(mp.winning_strategy_name, "Cooperator")
379381

382+
def test_atomic_mutation_fsm(self):
383+
axelrod.seed(12)
384+
players = [axelrod.EvolvableFSMPlayer(num_states=2, initial_state=1, initial_action=C)
385+
for _ in range(5)]
386+
mp = MoranProcess(players, turns=10, mutation_method="atomic")
387+
population = mp.play()
388+
self.assertEqual(
389+
mp.winning_strategy_name,
390+
'Evolvable FSM Player: ((0, C, 1, D), (0, D, 1, C), (1, C, 0, D), (1, D, 1, C)), 1, C, 2, 0.1')
391+
self.assertEqual(len(mp.populations), 31)
392+
self.assertTrue(mp.fixated)
393+
394+
def test_atomic_mutation_cycler(self):
395+
axelrod.seed(10)
396+
cycle_length = 5
397+
players = [axelrod.EvolvableCycler(cycle_length=cycle_length)
398+
for _ in range(5)]
399+
mp = MoranProcess(players, turns=10, mutation_method="atomic")
400+
population = mp.play()
401+
self.assertEqual(mp.winning_strategy_name, 'EvolvableCycler: CDCDD, 5, 0.2, 1')
402+
self.assertEqual(len(mp.populations), 19)
403+
self.assertTrue(mp.fixated)
380404

381405
class GraphMoranProcess(unittest.TestCase):
382406
def test_complete(self):
@@ -496,13 +520,7 @@ def test_init(self):
496520
"""Test the initialisation process"""
497521
self.assertEqual(
498522
set(self.amp.cached_outcomes.keys()),
499-
set(
500-
[
501-
("Cooperator", "Defector"),
502-
("Cooperator", "Cooperator"),
503-
("Defector", "Defector"),
504-
]
505-
),
523+
{("Cooperator", "Defector"), ("Cooperator", "Cooperator"), ("Defector", "Defector")},
506524
)
507525
self.assertEqual(self.amp.players, self.players)
508526
self.assertEqual(self.amp.turns, 0)

0 commit comments

Comments
 (0)