Skip to content

Commit d1d06b4

Browse files
GitTobymarcharper
authored andcommitted
Adding cycler (#45)
* Added initial Cycler & cycler tests * changing parameters and adding integration test * fixed the windows multi threading issue in an ugly way to fix details see todo comment * added integration test functionality * Added docstrings * Added docstrings, and extra content for custom initial populations * edited the bug for increasing pop sizes * Added integration test for pop sizes & seeding players utility * removed seeding player & windows checks. * removed numpy from the requirements * removed global population size var * adding multi threading fix * Outputter class changed + tests * Crossover & tests changed; added header to CSV printing * Crossover & tests changed; add header to CSV printing; added travis -v * removed GA output header * cleaned imports for GA * made changes to comments & crossover & mutation as requested in pr * Made changes requested in the PR * Removed Test for removed functionality * updated test logging - Moved print_output to run method - updated docs - removed logging in tests to make CI readable
1 parent 168da11 commit d1d06b4

File tree

14 files changed

+399
-58
lines changed

14 files changed

+399
-58
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ install:
1313
- pip install coveralls
1414
script:
1515
- python setup.py develop
16-
- coverage run --source=src -m unittest discover tests
16+
- coverage run --source=src -m unittest discover tests -v
1717
- coverage report -m
1818
- python doctests.py
1919
after_success:

docs/background/genetic_algorithm.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,20 @@ The crossover and mutation are implemented in the following way:
9292
being changed to another random state of
9393
:math:`\delta \times 10^{-1} \times N` (where :math:`N` is the number of
9494
states).
95+
96+
Cycler Sequence Calculator
97+
--------------------------
98+
99+
A Cycler Sequence is the sequence of C & D actions that are passed to the cycler player to follow when playing their
100+
tournament games.
101+
102+
the sequence is found using genetic feature selection:
103+
104+
- Crossover: By working with another cycler player, we take sections of each player and create a new cycler sequence
105+
from the following formula:
106+
let our two player being crossed be called p1 and p2 respectively. we then find the midpoint of both the sequences
107+
and take the first half from p1 and the second half from p2 to combine into the new cycler sequence.
108+
109+
- Mutation: we use a predictor :math:`\delta`to determine if we are going to mutate a
110+
single element in the current sequence. The element, or gene, we change in the sequence is uniformly selected using
111+
the random :code:`package`.

docs/tutorial/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ We can now evolve our population::
5353

5454
The :code:`run` command prints out the progress of the algorithm and this is
5555
also written to the output file (we passed :code:`output_filename` as an
56-
argument earlier).
56+
argument earlier). The printing can be turned off to keep logging to a minimum
57+
by passing :code:`print_output=False` to the :code:`run`.
5758

5859
The last best score is a finite state machine with representation
5960
:code:`0:C:0_C_0_C:0_D_1_D:1_C_1_D:1_D_1_D` which corresponds to a strategy that

src/axelrod_dojo/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .archetypes.fsm import FSMParams
33
from .archetypes.hmm import HMMParams
44
from .archetypes.gambler import GamblerParams
5+
from .archetypes.cycler import CyclerParams
56
from .algorithms.genetic_algorithm import Population
67
from .algorithms.particle_swarm_optimization import PSO
78
from .utils import (prepare_objective,

src/axelrod_dojo/algorithms/genetic_algorithm.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1-
from itertools import repeat
1+
from itertools import repeat, starmap
22
from multiprocessing import Pool, cpu_count
33
from operator import itemgetter
44
from random import randrange
55
from statistics import mean, pstdev
66

77
import axelrod as axl
8-
98
from axelrod_dojo.utils import Outputer, PlayerInfo, score_params
109

1110

1211
class Population(object):
1312
"""Population class that implements the evolutionary algorithm."""
13+
1414
def __init__(self, params_class, params_kwargs, size, objective, output_filename,
1515
bottleneck=None, mutation_probability=.1, opponents=None,
1616
processes=1, weights=None,
1717
sample_count=None, population=None):
18+
self.print_output = True
1819
self.params_class = params_class
1920
self.bottleneck = bottleneck
20-
2121
if processes == 0:
22-
processes = cpu_count()
23-
self.pool = Pool(processes=processes)
22+
self.processes = cpu_count()
23+
else:
24+
self.processes = processes
25+
26+
self.pool = Pool(processes=self.processes)
27+
2428
self.outputer = Outputer(output_filename, mode='a')
2529
self.size = size
2630
self.objective = objective
@@ -30,10 +34,10 @@ def __init__(self, params_class, params_kwargs, size, objective, output_filename
3034
self.bottleneck = bottleneck
3135
if opponents is None:
3236
self.opponents_information = [
33-
PlayerInfo(s, {}) for s in axl.short_run_time_strategies]
37+
PlayerInfo(s, {}) for s in axl.short_run_time_strategies]
3438
else:
3539
self.opponents_information = [
36-
PlayerInfo(p.__class__, p.init_kwargs) for p in opponents]
40+
PlayerInfo(p.__class__, p.init_kwargs) for p in opponents]
3741
self.generation = 0
3842

3943
self.params_kwargs = params_kwargs
@@ -50,13 +54,16 @@ def __init__(self, params_class, params_kwargs, size, objective, output_filename
5054
self.sample_count = sample_count
5155

5256
def score_all(self):
53-
starmap_params = zip(
57+
starmap_params_zip = zip(
5458
self.population,
5559
repeat(self.objective),
5660
repeat(self.opponents_information),
5761
repeat(self.weights),
5862
repeat(self.sample_count))
59-
results = self.pool.starmap(score_params, starmap_params)
63+
if self.processes == 1:
64+
results = list(starmap(score_params, starmap_params_zip))
65+
else:
66+
results = self.pool.starmap(score_params, starmap_params_zip)
6067
return results
6168

6269
def subset_population(self, indices):
@@ -77,22 +84,27 @@ def crossover(population, num_variants):
7784

7885
def evolve(self):
7986
self.generation += 1
80-
print("Scoring Generation {}".format(self.generation))
87+
if self.print_output:
88+
print("Scoring Generation {}".format(self.generation))
8189

8290
# Score population
8391
scores = self.score_all()
8492
results = list(zip(scores, range(len(scores))))
8593
results.sort(key=itemgetter(0), reverse=True)
8694

8795
# Report
88-
print("Generation", self.generation, "| Best Score:", results[0][0],
89-
repr(self.population[results[0][1]]))
96+
if self.print_output:
97+
print("Generation", self.generation, "| Best Score:", results[0][0], repr(self.population[results[0][
98+
1]])) # prints best result
9099
# Write the data
100+
# Note: if using this for analysis, for reproducability it may be useful to
101+
# pass type(opponent) for each of the opponents. This will allow verification of results post run
102+
91103
row = [self.generation, mean(scores), pstdev(scores), results[0][0],
92104
repr(self.population[results[0][1]])]
93-
self.outputer.write(row)
105+
self.outputer.write_row(row)
94106

95-
## Next Population
107+
# Next Population
96108
indices_to_keep = [p for (s, p) in results[0: self.bottleneck]]
97109
self.subset_population(indices_to_keep)
98110
# Add mutants of the best players
@@ -106,7 +118,7 @@ def evolve(self):
106118
params_to_modify = [params.copy() for params in self.population]
107119
params_to_modify += random_params
108120
# Crossover
109-
size_left = self.size - len(params_to_modify)
121+
size_left = self.size - len(self.population)
110122
params_to_modify = self.crossover(params_to_modify, size_left)
111123
# Mutate
112124
for p in params_to_modify:
@@ -119,7 +131,9 @@ def __iter__(self):
119131
def __next__(self):
120132
self.evolve()
121133

122-
def run(self, generations):
134+
def run(self, generations, print_output=True):
135+
self.print_output = print_output
136+
123137
for _ in range(generations):
124138
next(self)
125-
self.outputer.close()
139+

src/axelrod_dojo/archetypes/cycler.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import axelrod as axl
2+
from numpy import random
3+
4+
from axelrod_dojo.utils import Params
5+
6+
C, D = axl.Action
7+
8+
9+
class CyclerParams(Params):
10+
"""
11+
Cycler params is a class to aid with the processes of calculating the best sequence of moves for any given set of
12+
opponents. Each of the population in our algorithm will be an instance of this class for putting into our
13+
genetic lifecycle.
14+
"""
15+
16+
def __init__(self, sequence=None, sequence_length: int = 200, mutation_probability=0.1, mutation_potency=1):
17+
if sequence is None:
18+
# generates random sequence if none is given
19+
self.sequence = self.generate_random_sequence(sequence_length)
20+
else:
21+
# when passing a sequence, make a copy of the sequence to ensure mutation is for the instance only.
22+
self.sequence = list(sequence)
23+
24+
self.sequence_length = len(self.sequence)
25+
self.mutation_probability = mutation_probability
26+
self.mutation_potency = mutation_potency
27+
28+
def __repr__(self):
29+
return "{}".format(self.sequence)
30+
31+
@staticmethod
32+
def generate_random_sequence(sequence_length):
33+
"""
34+
Generate a sequence of random moves
35+
36+
Parameters
37+
----------
38+
sequence_length - length of random moves to generate
39+
40+
Returns
41+
-------
42+
list - a list of C & D actions: list[Action]
43+
"""
44+
# D = axl.Action(0) | C = axl.Action(1)
45+
return list(map(axl.Action, random.randint(0, 2, (sequence_length, 1))))
46+
47+
def crossover(self, other_cycler, in_seed=0):
48+
"""
49+
creates and returns a new CyclerParams instance with a single crossover point in the middle
50+
51+
Parameters
52+
----------
53+
other_cycler - the other cycler where we get the other half of the sequence
54+
55+
Returns
56+
-------
57+
CyclerParams
58+
59+
"""
60+
seq1 = self.sequence
61+
seq2 = other_cycler.sequence
62+
63+
if not in_seed == 0:
64+
# only seed for when we explicitly give it a seed
65+
random.seed(in_seed)
66+
67+
midpoint = int(random.randint(0, len(seq1)) / 2)
68+
new_seq = seq1[:midpoint] + seq2[midpoint:]
69+
return CyclerParams(sequence=new_seq)
70+
71+
def mutate(self):
72+
"""
73+
Basic mutation which may change any random gene(s) in the sequence.
74+
"""
75+
if random.rand() <= self.mutation_probability:
76+
mutated_sequence = self.sequence
77+
for _ in range(self.mutation_potency):
78+
index_to_change = random.randint(0, len(mutated_sequence))
79+
mutated_sequence[index_to_change] = mutated_sequence[index_to_change].flip()
80+
self.sequence = mutated_sequence
81+
82+
def player(self):
83+
"""
84+
Create and return a Cycler player with the sequence that has been generated with this run.
85+
86+
Returns
87+
-------
88+
Cycler(sequence)
89+
"""
90+
return axl.Cycler(self.get_sequence_str())
91+
92+
def copy(self):
93+
"""
94+
Returns a copy of the current cyclerParams
95+
96+
Returns
97+
-------
98+
CyclerParams - a separate instance copy of itself.
99+
"""
100+
# seq length will be provided when copying, no need to pass
101+
return CyclerParams(sequence=self.sequence, mutation_probability=self.mutation_probability)
102+
103+
def get_sequence_str(self):
104+
"""
105+
Concatenate all the actions as a string for constructing Cycler players
106+
107+
[C,D,D,C,D,C] -> "CDDCDC"
108+
[C,C,D,C,C,C] -> "CCDCCC"
109+
[D,D,D,D,D,D] -> "DDDDDD"
110+
111+
Returns
112+
-------
113+
str
114+
"""
115+
string_sequence = ""
116+
for action in self.sequence:
117+
string_sequence += str(action)
118+
119+
return string_sequence

src/axelrod_dojo/utils.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from functools import partial
33
from statistics import mean
44
import csv
5-
import os
65

76
import numpy as np
87
import axelrod as axl
@@ -11,17 +10,14 @@
1110
## Output Evolutionary Algorithm results
1211

1312
class Outputer(object):
14-
def __init__(self, filename, mode='w'):
15-
self.output = open(filename, mode)
16-
self.writer = csv.writer(self.output)
13+
def __init__(self, filename, mode='a'):
14+
self.file = filename
15+
self.mode = mode
1716

18-
def write(self, row):
19-
self.writer.writerow(row)
20-
self.output.flush()
21-
os.fsync(self.output.fileno())
22-
23-
def close(self):
24-
self.output.close()
17+
def write_row(self, row):
18+
with open(self.file, self.mode, newline='') as file_writer:
19+
writer = csv.writer(file_writer)
20+
writer.writerow(row)
2521

2622

2723
## Objective functions for optimization
@@ -105,11 +101,13 @@ def objective_moran_win(me, other, turns, noise, repetitions, N=5,
105101
scores_for_this_opponent.append(0)
106102
return scores_for_this_opponent
107103

104+
108105
# Evolutionary Algorithm
109106

110107

111108
class Params(object):
112109
"""Abstract Base Class for Parameters Objects."""
110+
113111
def mutate(self):
114112
pass
115113

@@ -147,6 +145,7 @@ def create_vector_bounds(self):
147145
"""Creates the bounds for the decision variables."""
148146
pass
149147

148+
150149
PlayerInfo = namedtuple('PlayerInfo', ['strategy', 'init_kwargs'])
151150

152151

@@ -193,4 +192,3 @@ def load_params(params_class, filename, num):
193192
for score, rep in all_params[:num]:
194193
best_params.append(parser(rep))
195194
return best_params
196-

0 commit comments

Comments
 (0)