Skip to content

Commit 72a5148

Browse files
authored
Merge pull request #1256 from Axelrod-Python/integrate_dojo
Integrate dojo params into Axelrod
2 parents d71ebd2 + a489029 commit 72a5148

30 files changed

+1860
-118
lines changed

axelrod/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from axelrod.game import DefaultGame, Game
1111
from axelrod.history import History, LimitedHistory
1212
from axelrod.player import is_basic, obey_axelrod, Player
13+
from axelrod.evolvable_player import EvolvablePlayer
1314
from axelrod.mock_player import MockPlayer
1415
from axelrod.match import Match
1516
from axelrod.moran import MoranProcess, ApproximateMoranProcess

axelrod/evolvable_player.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from pickle import dumps, loads
2+
from random import randrange
3+
from typing import Dict, List
4+
from .player import Player
5+
6+
7+
class InsufficientParametersError(Exception):
8+
"""Error indicating that insufficient parameters were specified to initialize an Evolvable Player."""
9+
def __init__(self, *args):
10+
super().__init__(*args)
11+
12+
13+
class EvolvablePlayer(Player):
14+
"""A class for a player that can evolve, for use in the Moran process or with reinforcement learning algorithms.
15+
16+
This is an abstract base class, not intended to be used directly.
17+
"""
18+
19+
name = "EvolvablePlayer"
20+
parent_class = Player
21+
parent_kwargs = [] # type: List[str]
22+
23+
def overwrite_init_kwargs(self, **kwargs):
24+
"""Use to overwrite parameters for proper cloning and testing."""
25+
for k, v in kwargs.items():
26+
self.init_kwargs[k] = v
27+
28+
def create_new(self, **kwargs):
29+
"""Creates a new variant with parameters overwritten by kwargs."""
30+
init_kwargs = self.init_kwargs.copy()
31+
init_kwargs.update(kwargs)
32+
return self.__class__(**init_kwargs)
33+
34+
# Serialization and deserialization. You may overwrite to obtain more human readable serializations
35+
# but you must overwrite both.
36+
37+
def serialize_parameters(self):
38+
"""Serialize parameters."""
39+
return dumps(self.init_kwargs)
40+
41+
@classmethod
42+
def deserialize_parameters(cls, serialized):
43+
"""Deserialize parameters to a Player instance."""
44+
init_kwargs = loads(serialized)
45+
return cls(**init_kwargs)
46+
47+
# Optional methods for evolutionary algorithms and Moran processes.
48+
49+
def mutate(self):
50+
"""Optional method to allow Player to produce a variant (not in place)."""
51+
pass # pragma: no cover
52+
53+
def crossover(self, other):
54+
"""Optional method to allow Player to produce variants in combination with another player. Returns a new
55+
Player."""
56+
pass # pragma: no cover
57+
58+
# Optional methods for particle swarm algorithm.
59+
60+
def receive_vector(self, vector):
61+
"""Receive a vector of params and overwrite the Player."""
62+
pass # pragma: no cover
63+
64+
def create_vector_bounds(self):
65+
"""Creates the bounds for the decision variables for Particle Swarm Algorithm."""
66+
pass # pragma: no cover
67+
68+
69+
def copy_lists(lists: List[List]) -> List[List]:
70+
return list(map(list, lists))
71+
72+
73+
def crossover_lists(list1: List, list2: List) -> List:
74+
cross_point = randrange(len(list1))
75+
new_list = list(list1[:cross_point]) + list(list2[cross_point:])
76+
return new_list
77+
78+
79+
def crossover_dictionaries(table1: Dict, table2: Dict) -> Dict:
80+
keys = list(table1.keys())
81+
cross_point = randrange(len(keys))
82+
new_items = [(k, table1[k]) for k in keys[:cross_point]]
83+
new_items += [(k, table2[k]) for k in keys[cross_point:]]
84+
new_table = dict(new_items)
85+
return new_table

axelrod/fingerprint.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import csv
21
import os
32
from collections import namedtuple
43
from tempfile import mkstemp
54
from typing import Any, List, Union
65

7-
import dask as da
86
import dask.dataframe as dd
97
import matplotlib.pyplot as plt
108
import numpy as np

axelrod/graph.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ def _add_edges(self, edges):
6666
self._add_edge(*edge)
6767

6868
def add_loops(self):
69-
"""Add all loops to edges."""
69+
"""
70+
Add all loops to edges
71+
"""
7072
self._add_edges((x, x) for x in self.vertices)
7173

7274
@property
@@ -120,25 +122,44 @@ def cycle(length, directed=False):
120122
return Graph(edges=edges, directed=directed)
121123

122124

123-
def complete_graph(size, loops=True):
124-
""" Produces a complete graph of specificies size.
125-
126-
See https://en.wikipedia.org/wiki/Complete_graph for details.
125+
def complete_graph(size, loops=True, directed=False):
126+
"""
127+
Produces a complete graph of size `length`.
128+
https://en.wikipedia.org/wiki/Complete_graph
127129
128130
Parameters
129131
----------
130132
size: int
131133
Number of vertices in the cycle
132134
loops: bool, True
133-
Should the graph contain cycles?
135+
attach loops at each node?
136+
directed: bool, False
137+
Is the graph directed?
134138
135139
Returns
136140
-------
137141
a Graph object for the complete graph
138142
"""
139143
edges = [(i, j) for i in range(size) for j in range(i + 1, size)]
140-
graph = Graph(edges=edges, directed=False)
144+
graph = Graph(directed=directed, edges=edges)
145+
if loops:
146+
graph.add_loops()
147+
return graph
148+
141149

150+
def attached_complete_graphs(length, loops=True, directed=False):
151+
"""Creates two complete undirected graphs of size `length`
152+
attached by a single edge."""
153+
edges = []
154+
# Two complete graphs
155+
for cluster in range(2):
156+
for i in range(length):
157+
for j in range(i + 1, length):
158+
edges.append(("{}:{}".format(cluster, i),
159+
"{}:{}".format(cluster, j)))
160+
# Attach at one node
161+
edges.append(("0:0", "1:0"))
162+
graph = Graph(directed=directed, edges=edges)
142163
if loops:
143164
graph.add_loops()
144165

axelrod/moran.py

Lines changed: 41 additions & 28 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,6 +56,8 @@ def __init__(
5656
interaction_graph: Graph = None,
5757
reproduction_graph: Graph = None,
5858
fitness_transformation: Callable = None,
59+
mutation_method="transition",
60+
stop_on_fixation=True
5961
) -> None:
6062
"""
6163
An agent based Moran process class. In each round, each player plays a
@@ -108,6 +110,11 @@ def __init__(
108110
given
109111
fitness_transformation:
110112
A function mapping a score to a (non-negative) float
113+
mutation_method:
114+
A string indicating if the mutation method should be between original types ("transition")
115+
or based on the player's mutation method, if present ("atomic").
116+
stop_on_fixation:
117+
A bool indicating if the process should stop on fixation
111118
"""
112119
self.turns = turns
113120
self.prob_end = prob_end
@@ -120,6 +127,12 @@ def __init__(
120127
self.score_history = [] # type: List
121128
self.winning_strategy_name = None # type: Optional[str]
122129
self.mutation_rate = mutation_rate
130+
self.stop_on_fixation = stop_on_fixation
131+
m = mutation_method.lower()
132+
if m in ["atomic", "transition"]:
133+
self.mutation_method = m
134+
else:
135+
raise ValueError("Invalid mutation method {}".format(mutation_method))
123136
assert (mutation_rate >= 0) and (mutation_rate <= 1)
124137
assert (noise >= 0) and (noise <= 1)
125138
mode = mode.lower()
@@ -159,6 +172,7 @@ def __init__(
159172
# Map players to graph vertices
160173
self.locations = sorted(interaction_graph.vertices)
161174
self.index = dict(zip(sorted(interaction_graph.vertices), range(len(players))))
175+
self.fixated = self.fixation_check()
162176

163177
def set_players(self) -> None:
164178
"""Copy the initial players into the first population."""
@@ -176,17 +190,23 @@ def mutate(self, index: int) -> Player:
176190
index:
177191
The index of the player to be mutated
178192
"""
179-
# Choose another strategy at random from the initial population
180-
r = random.random()
181-
if r < self.mutation_rate:
182-
s = str(self.players[index])
183-
j = randrange(0, len(self.mutation_targets[s]))
184-
p = self.mutation_targets[s][j]
185-
new_player = p.clone()
186-
else:
187-
# Just clone the player
188-
new_player = self.players[index].clone()
189-
return new_player
193+
194+
if self.mutation_method == "atomic":
195+
if not issubclass(self.players[index].__class__, EvolvablePlayer):
196+
raise TypeError("Player is not evolvable. Use a subclass of EvolvablePlayer.")
197+
return self.players[index].mutate()
198+
199+
# Assuming mutation_method == "transition"
200+
if self.mutation_rate > 0:
201+
# Choose another strategy at random from the initial population
202+
r = random.random()
203+
if r < self.mutation_rate:
204+
s = str(self.players[index])
205+
j = randrange(0, len(self.mutation_targets[s]))
206+
p = self.mutation_targets[s][j]
207+
return p.clone()
208+
# Just clone the player
209+
return self.players[index].clone()
190210

191211
def death(self, index: int = None) -> int:
192212
"""
@@ -250,14 +270,13 @@ def fixation_check(self) -> bool:
250270
Boolean:
251271
True if fixation has occurred (population all of a single type)
252272
"""
253-
if self.mutation_rate > 0:
254-
return False
255273
classes = set(str(p) for p in self.players)
274+
self.fixated = False
256275
if len(classes) == 1:
257276
# Set the winning strategy name variable
258277
self.winning_strategy_name = str(self.players[0])
259-
return True
260-
return False
278+
self.fixated = True
279+
return self.fixated
261280

262281
def __next__(self) -> object:
263282
"""
@@ -275,7 +294,7 @@ def __next__(self) -> object:
275294
Returns itself with a new population
276295
"""
277296
# Check the exit condition, that all players are of the same type.
278-
if self.fixation_check():
297+
if self.stop_on_fixation and self.fixation_check():
279298
raise StopIteration
280299
if self.mode == "bd":
281300
# Birth then death
@@ -286,16 +305,10 @@ def __next__(self) -> object:
286305
i = self.death()
287306
self.players[i] = None
288307
j = self.birth(i)
289-
# Mutate
290-
if self.mutation_rate:
291-
new_player = self.mutate(j)
292-
else:
293-
new_player = self.players[j].clone()
294-
# Replace player i with clone of player j
295-
self.players[i] = new_player
308+
# Mutate and/or replace player i with clone of player j
309+
self.players[i] = self.mutate(j)
310+
# Record population.
296311
self.populations.append(self.population_distribution())
297-
# Check again for fixation
298-
self.fixation_check()
299312
return self
300313

301314
def _matchup_indices(self) -> Set[Tuple[int, int]]:
@@ -395,10 +408,10 @@ def play(self) -> List[Counter]:
395408
populations:
396409
Returns a list of all the populations
397410
"""
398-
if self.mutation_rate != 0:
411+
if not self.stop_on_fixation or self.mutation_rate != 0:
399412
raise ValueError(
400413
"MoranProcess.play() will never exit if mutation_rate is"
401-
"nonzero. Use iteration instead."
414+
"nonzero or stop_on_fixation is False. Use iteration instead."
402415
)
403416
while True:
404417
try:

axelrod/player.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ class Player(object):
7777
"manipulates_state": None,
7878
}
7979

80-
# def __new__(cls, *args, history=None, **kwargs):
8180
def __new__(cls, *args, **kwargs):
8281
"""Caches arguments for Player cloning."""
8382
obj = super().__new__(cls)

axelrod/random_.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import random
22

3-
import numpy
3+
import numpy as np
4+
from numpy.random import choice
5+
46
from axelrod.action import Action
57

68
C, D = Action.C, Action.D
79

810

11+
def seed(seed_):
12+
"""Sets a seed"""
13+
random.seed(seed_)
14+
np.random.seed(seed_)
15+
16+
917
def random_choice(p: float = 0.5) -> Action:
1018
"""
1119
Return C with probability `p`, else return D
@@ -63,10 +71,10 @@ def randrange(a: int, b: int) -> int:
6371
return a + int(r)
6472

6573

66-
def seed(seed_):
67-
"""Sets a seed"""
68-
random.seed(seed_)
69-
numpy.random.seed(seed_)
74+
def random_vector(size):
75+
"""Create a random vector of values in [0, 1] that sums to 1."""
76+
vector = np.random.random(size)
77+
return vector / np.sum(vector)
7078

7179

7280
class Pdf(object):
@@ -81,7 +89,7 @@ def __init__(self, counter):
8189

8290
def sample(self):
8391
"""Sample from the pdf"""
84-
index = numpy.random.choice(a=range(self.size), p=self.probability)
92+
index = choice(a=range(self.size), p=self.probability)
8593
# Numpy cannot sample from a list of n dimensional objects for n > 1,
8694
# need to sample an index.
8795
return self.sample_space[index]

axelrod/result_set.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1+
from collections import Counter, namedtuple
12
import csv
23
import itertools
3-
from collections import Counter, namedtuple
44
from multiprocessing import cpu_count
55

6-
import axelrod.interaction_utils as iu
76
import numpy as np
87
import tqdm
9-
from axelrod.action import Action, str_to_actions
8+
from axelrod.action import Action
109

1110
import dask as da
1211
import dask.dataframe as dd
1312

1413
from . import eigen
15-
from .game import Game
1614

1715
C, D = Action.C, Action.D
1816

@@ -416,7 +414,6 @@ def _build_normalised_cooperation(self):
416414

417415
@update_progress_bar
418416
def _build_initial_cooperation_rate(self, interactions_series):
419-
interactions_dict = interactions_series.to_dict()
420417
interactions_array = np.array(
421418
[
422419
interactions_series.get(player_index, 0)

0 commit comments

Comments
 (0)