Skip to content

Commit e31b00d

Browse files
committed
First pass on reproducible matches and parallel tournaments with random seeding
1 parent fe3bc42 commit e31b00d

37 files changed

+444
-300
lines changed

axelrod/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from axelrod.load_data_ import load_pso_tables, load_weights
66
from axelrod import graph
77
from axelrod.action import Action
8-
from axelrod.random_ import random_choice, random_flip, seed, Pdf
8+
from axelrod.random_ import Pdf, RandomGenerator, BulkRandomGenerator
99
from axelrod.plot import Plot
1010
from axelrod.game import DefaultGame, Game
1111
from axelrod.history import History, LimitedHistory

axelrod/match.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import random
21
from math import ceil, log
32

43
import axelrod.interaction_utils as iu
54
from axelrod import DEFAULT_TURNS
65
from axelrod.action import Action
76
from axelrod import Classifiers
87
from axelrod.game import Game
8+
from axelrod.random_ import RandomGenerator
99
from .deterministic_cache import DeterministicCache
1010

1111
C, D = Action.C, Action.D
@@ -30,6 +30,7 @@ def __init__(
3030
noise=0,
3131
match_attributes=None,
3232
reset=True,
33+
seed=None
3334
):
3435
"""
3536
Parameters
@@ -52,6 +53,8 @@ def __init__(
5253
but these can be overridden if desired.
5354
reset : bool
5455
Whether to reset players or not
56+
seed : int
57+
Random seed for reproducibility
5558
"""
5659

5760
defaults = {
@@ -65,6 +68,9 @@ def __init__(
6568
self.result = []
6669
self.noise = noise
6770

71+
self.seed = seed
72+
self._random = RandomGenerator(seed=self.seed)
73+
6874
if game is None:
6975
self.game = Game()
7076
else:
@@ -129,6 +135,19 @@ def _cached_enough_turns(self, cache_key, turns):
129135
return False
130136
return len(self._cache[cache_key]) >= turns
131137

138+
def simultaneous_play(self, player, coplayer, noise=0):
139+
"""This pits two players against each other."""
140+
s1, s2 = player.strategy(coplayer), coplayer.strategy(player)
141+
if noise:
142+
# Note this uses the Match classes random generator, not either
143+
# player's random generator. A player shouldn't be able to
144+
# predict the outcome of this noise flip.
145+
s1 = self.random_flip(s1, noise)
146+
s2 = self.random_flip(s2, noise)
147+
player.update_history(s1, s2)
148+
coplayer.update_history(s2, s1)
149+
return s1, s2
150+
132151
def play(self):
133152
"""
134153
The resulting list of actions from a match between two players.
@@ -147,17 +166,22 @@ def play(self):
147166
148167
i.e. One entry per turn containing a pair of actions.
149168
"""
150-
turns = min(sample_length(self.prob_end), self.turns)
169+
self._random = RandomGenerator(seed=self.seed)
170+
r = self._random.random()
171+
turns = min(sample_length(self.prob_end, r), self.turns)
151172
cache_key = (self.players[0], self.players[1])
152173

153174
if self._stochastic or not self._cached_enough_turns(cache_key, turns):
154175
for p in self.players:
155176
if self.reset:
156177
p.reset()
157178
p.set_match_attributes(**self.match_attributes)
179+
# Generate a random seed for each player
180+
p.set_seed(self._random.randint(0, 100000000))
158181
result = []
159182
for _ in range(turns):
160-
plays = self.players[0].play(self.players[1], self.noise)
183+
plays = self.simultaneous_play(
184+
self.players[0], self.players[1], self.noise)
161185
result.append(plays)
162186

163187
if self._cache_update_required:
@@ -216,7 +240,7 @@ def __len__(self):
216240
return self.turns
217241

218242

219-
def sample_length(prob_end):
243+
def sample_length(prob_end, random_value):
220244
"""
221245
Sample length of a game.
222246
@@ -249,5 +273,4 @@ def sample_length(prob_end):
249273
return float("inf")
250274
if prob_end == 1:
251275
return 1
252-
x = random.random()
253-
return int(ceil(log(1 - x) / log(1 - prob_end)))
276+
return int(ceil(log(1 - random_value) / log(1 - prob_end)))

axelrod/match_generator.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from axelrod.random_ import BulkRandomGenerator
2+
3+
14
class MatchGenerator(object):
25
def __init__(
36
self,
@@ -9,6 +12,7 @@ def __init__(
912
prob_end=None,
1013
edges=None,
1114
match_attributes=None,
15+
seed=None
1216
):
1317
"""
1418
A class to generate matches. This is used by the Tournament class which
@@ -43,6 +47,7 @@ def __init__(
4347
self.opponents = players
4448
self.prob_end = prob_end
4549
self.match_attributes = match_attributes
50+
self.random_generator = BulkRandomGenerator(seed)
4651

4752
self.edges = edges
4853
if edges is not None:
@@ -73,7 +78,8 @@ def build_match_chunks(self):
7378

7479
for index_pair in edges:
7580
match_params = self.build_single_match_params()
76-
yield (index_pair, match_params, self.repetitions)
81+
r = next(self.random_generator)
82+
yield (index_pair, match_params, self.repetitions, r)
7783

7884
def build_single_match_params(self):
7985
"""

axelrod/moran.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Implementation of the Moran process on Graphs."""
22

3-
import random
43
from collections import Counter
54
from typing import Callable, List, Optional, Set, Tuple
65

@@ -11,35 +10,7 @@
1110
from .deterministic_cache import DeterministicCache
1211
from .graph import Graph, complete_graph
1312
from .match import Match
14-
from .random_ import randrange
15-
16-
17-
def fitness_proportionate_selection(
18-
scores: List, fitness_transformation: Callable = None
19-
) -> int:
20-
"""Randomly selects an individual proportionally to score.
21-
22-
Parameters
23-
----------
24-
scores: Any sequence of real numbers
25-
fitness_transformation: A function mapping a score to a (non-negative) float
26-
27-
Returns
28-
-------
29-
An index of the above list selected at random proportionally to the list
30-
element divided by the total.
31-
"""
32-
if fitness_transformation is None:
33-
csums = np.cumsum(scores)
34-
else:
35-
csums = np.cumsum([fitness_transformation(s) for s in scores])
36-
total = csums[-1]
37-
r = random.random() * total
38-
39-
for i, x in enumerate(csums):
40-
if x >= r:
41-
break
42-
return i
13+
from .random_ import RandomGenerator
4314

4415

4516
class MoranProcess(object):
@@ -57,7 +28,8 @@ def __init__(
5728
reproduction_graph: Graph = None,
5829
fitness_transformation: Callable = None,
5930
mutation_method="transition",
60-
stop_on_fixation=True
31+
stop_on_fixation=True,
32+
seed = None
6133
) -> None:
6234
"""
6335
An agent based Moran process class. In each round, each player plays a
@@ -128,6 +100,7 @@ def __init__(
128100
self.winning_strategy_name = None # type: Optional[str]
129101
self.mutation_rate = mutation_rate
130102
self.stop_on_fixation = stop_on_fixation
103+
self._random = RandomGenerator(seed=seed)
131104
m = mutation_method.lower()
132105
if m in ["atomic", "transition"]:
133106
self.mutation_method = m
@@ -182,6 +155,32 @@ def set_players(self) -> None:
182155
self.players.append(player)
183156
self.populations = [self.population_distribution()]
184157

158+
def fitness_proportionate_selection(self,
159+
scores: List, fitness_transformation: Callable = None) -> int:
160+
"""Randomly selects an individual proportionally to score.
161+
162+
Parameters
163+
----------
164+
scores: Any sequence of real numbers
165+
fitness_transformation: A function mapping a score to a (non-negative) float
166+
167+
Returns
168+
-------
169+
An index of the above list selected at random proportionally to the list
170+
element divided by the total.
171+
"""
172+
if fitness_transformation is None:
173+
csums = np.cumsum(scores)
174+
else:
175+
csums = np.cumsum([fitness_transformation(s) for s in scores])
176+
total = csums[-1]
177+
r = self._random.random() * total
178+
179+
for i, x in enumerate(csums):
180+
if x >= r:
181+
break
182+
return i
183+
185184
def mutate(self, index: int) -> Player:
186185
"""Mutate the player at index.
187186
@@ -199,10 +198,10 @@ def mutate(self, index: int) -> Player:
199198
# Assuming mutation_method == "transition"
200199
if self.mutation_rate > 0:
201200
# Choose another strategy at random from the initial population
202-
r = random.random()
201+
r = self._random.random()
203202
if r < self.mutation_rate:
204203
s = str(self.players[index])
205-
j = randrange(0, len(self.mutation_targets[s]))
204+
j = self._random.randrange(0, len(self.mutation_targets[s]))
206205
p = self.mutation_targets[s][j]
207206
return p.clone()
208207
# Just clone the player
@@ -223,13 +222,13 @@ def death(self, index: int = None) -> int:
223222
"""
224223
if index is None:
225224
# Select a player to be replaced globally
226-
i = randrange(0, len(self.players))
225+
i = self._random.randrange(0, len(self.players))
227226
# Record internally for use in _matchup_indices
228227
self.dead = i
229228
else:
230229
# Select locally
231230
# index is not None in this case
232-
vertex = random.choice(
231+
vertex = self._random.choice(
233232
sorted(self.reproduction_graph.out_vertices(self.locations[index]))
234233
)
235234
i = self.index[vertex]

axelrod/player.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,11 @@
99
from axelrod.action import Action
1010
from axelrod.game import DefaultGame
1111
from axelrod.history import History
12-
from axelrod.random_ import random_flip
12+
from axelrod.random_ import RandomGenerator
1313

1414
C, D = Action.C, Action.D
1515

1616

17-
def simultaneous_play(player, coplayer, noise=0):
18-
"""This pits two players against each other."""
19-
s1, s2 = player.strategy(coplayer), coplayer.strategy(player)
20-
if noise:
21-
s1 = random_flip(s1, noise)
22-
s2 = random_flip(s2, noise)
23-
player.update_history(s1, s2)
24-
coplayer.update_history(s2, s1)
25-
return s1, s2
26-
27-
2817
class Player(object):
2918
"""A class for a player in the tournament.
3019
@@ -64,6 +53,7 @@ def __init__(self):
6453
self._history = History()
6554
self.classifier = copy.deepcopy(self.classifier)
6655
self.set_match_attributes()
56+
self.set_seed()
6757

6858
def __eq__(self, other):
6959
"""
@@ -77,6 +67,10 @@ def __eq__(self, other):
7767
value = getattr(self, attribute, None)
7868
other_value = getattr(other, attribute, None)
7969

70+
if attribute == "_random":
71+
# Don't compare the random seeds.
72+
continue
73+
8074
if isinstance(value, np.ndarray):
8175
if not (np.array_equal(value, other_value)):
8276
return False
@@ -123,6 +117,11 @@ def set_match_attributes(self, length=-1, game=None, noise=0):
123117
self.match_attributes = {"length": length, "game": game, "noise": noise}
124118
self.receive_match_attributes()
125119

120+
def set_seed(self, seed=None):
121+
"""Set a random seed for the player's random number
122+
generator."""
123+
self._random = RandomGenerator(seed=seed)
124+
126125
def __repr__(self):
127126
"""The string method for the strategy.
128127
Appends the `__init__` parameters to the strategy's name."""
@@ -147,9 +146,9 @@ def strategy(self, opponent):
147146
"""This is a placeholder strategy."""
148147
raise NotImplementedError()
149148

150-
def play(self, opponent, noise=0):
151-
"""This pits two players against each other."""
152-
return simultaneous_play(self, opponent, noise)
149+
# def play(self, opponent, noise=0):
150+
# """This pits two players against each other."""
151+
# return simultaneous_play(self, opponent, noise)
153152

154153
def clone(self):
155154
"""Clones the player without history, reapplying configuration

0 commit comments

Comments
 (0)