Skip to content

Commit

Permalink
Merge 244cf54 into 7daf72a
Browse files Browse the repository at this point in the history
  • Loading branch information
marcharper committed Oct 4, 2019
2 parents 7daf72a + 244cf54 commit d344129
Show file tree
Hide file tree
Showing 30 changed files with 1,856 additions and 118 deletions.
1 change: 1 addition & 0 deletions axelrod/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from axelrod.game import DefaultGame, Game
from axelrod.history import History, LimitedHistory
from axelrod.player import is_basic, obey_axelrod, Player
from axelrod.evolvable_player import EvolvablePlayer
from axelrod.mock_player import MockPlayer
from axelrod.match import Match
from axelrod.moran import MoranProcess, ApproximateMoranProcess
Expand Down
85 changes: 85 additions & 0 deletions axelrod/evolvable_player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from pickle import dumps, loads
from random import randrange
from typing import Dict, List
from .player import Player


class InsufficientParametersError(Exception):
"""Error indicating that insufficient parameters were specified to initialize an Evolvable Player."""
def __init__(self, *args):
super().__init__(*args)


class EvolvablePlayer(Player):
"""A class for a player that can evolve, for use in the Moran process or with reinforcement learning algorithms.
This is an abstract base class, not intended to be used directly.
"""

name = "EvolvablePlayer"
parent_class = Player
parent_kwargs = [] # type: List[str]

def overwrite_init_kwargs(self, **kwargs):
"""Use to overwrite parameters for proper cloning and testing."""
for k, v in kwargs.items():
self.init_kwargs[k] = v

def create_new(self, **kwargs):
"""Creates a new variant with parameters overwritten by kwargs."""
init_kwargs = self.init_kwargs.copy()
init_kwargs.update(kwargs)
return self.__class__(**init_kwargs)

# Serialization and deserialization. You may overwrite to obtain more human readable serializations
# but you must overwrite both.

def serialize_parameters(self):
"""Serialize parameters."""
return dumps(self.init_kwargs)

@classmethod
def deserialize_parameters(cls, serialized):
"""Deserialize parameters to a Player instance."""
init_kwargs = loads(serialized)
return cls(**init_kwargs)

# Optional methods for evolutionary algorithms and Moran processes.

def mutate(self):
"""Optional method to allow Player to produce a variant (not in place)."""
pass # pragma: no cover

def crossover(self, other):
"""Optional method to allow Player to produce variants in combination with another player. Returns a new
Player."""
pass # pragma: no cover

# Optional methods for particle swarm algorithm.

def receive_vector(self, vector):
"""Receive a vector of params and overwrite the Player."""
pass # pragma: no cover

def create_vector_bounds(self):
"""Creates the bounds for the decision variables for Particle Swarm Algorithm."""
pass # pragma: no cover


def copy_lists(lists: List[List]) -> List[List]:
return list(map(list, lists))


def crossover_lists(list1: List, list2: List) -> List:
cross_point = randrange(len(list1))
new_list = list(list1[:cross_point]) + list(list2[cross_point:])
return new_list


def crossover_dictionaries(table1: Dict, table2: Dict) -> Dict:
keys = list(table1.keys())
cross_point = randrange(len(keys))
new_items = [(k, table1[k]) for k in keys[:cross_point]]
new_items += [(k, table2[k]) for k in keys[cross_point:]]
new_table = dict(new_items)
return new_table
2 changes: 0 additions & 2 deletions axelrod/fingerprint.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import csv
import os
from collections import namedtuple
from tempfile import mkstemp
from typing import Any, List, Union

import dask as da
import dask.dataframe as dd
import matplotlib.pyplot as plt
import numpy as np
Expand Down
33 changes: 26 additions & 7 deletions axelrod/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def _add_edges(self, edges):
self._add_edge(*edge)

def add_loops(self):
"""Add all loops to edges."""
"""
Add all loops to edges
"""
self._add_edges((x, x) for x in self.vertices)

@property
Expand Down Expand Up @@ -120,25 +122,42 @@ def cycle(length, directed=False):
return Graph(edges=edges, directed=directed)


def complete_graph(size, loops=True):
""" Produces a complete graph of specificies size.
See https://en.wikipedia.org/wiki/Complete_graph for details.
def complete_graph(size, loops=True, directed=False):
"""
Produces a complete graph of size `length`.
https://en.wikipedia.org/wiki/Complete_graph
Parameters
----------
size: int
Number of vertices in the cycle
loops: bool, True
Should the graph contain cycles?
attach loops at each node?
directed: bool, False
Is the graph directed?
Returns
-------
a Graph object for the complete graph
"""
edges = [(i, j) for i in range(size) for j in range(i + 1, size)]
graph = Graph(edges=edges, directed=False)
graph = Graph(directed=directed, edges=edges)
if loops:
graph.add_loops()
return graph


def attached_complete_graphs(length, loops=True, directed=False):
edges = []
# Two complete graphs
for cluster in range(2):
for i in range(length):
for j in range(i + 1, length):
edges.append(("{}:{}".format(cluster, i),
"{}:{}".format(cluster, j)))
# Attach at one node
edges.append(("0:0", "1:0"))
graph = Graph(directed=directed, edges=edges)
if loops:
graph.add_loops()

Expand Down
69 changes: 41 additions & 28 deletions axelrod/moran.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import matplotlib.pyplot as plt
import numpy as np
from axelrod import DEFAULT_TURNS, Game, Player
from axelrod import EvolvablePlayer, DEFAULT_TURNS, Game, Player

from .deterministic_cache import DeterministicCache
from .graph import Graph, complete_graph
Expand Down Expand Up @@ -56,6 +56,8 @@ def __init__(
interaction_graph: Graph = None,
reproduction_graph: Graph = None,
fitness_transformation: Callable = None,
mutation_method="transition",
stop_on_fixation=True
) -> None:
"""
An agent based Moran process class. In each round, each player plays a
Expand Down Expand Up @@ -108,6 +110,11 @@ def __init__(
given
fitness_transformation:
A function mapping a score to a (non-negative) float
mutation_method:
A string indicating if the mutation method should be between original types ("transition")
or based on the player's mutation method, if present ("atomic").
stop_on_fixation:
A bool indicating if the process should stop on fixation
"""
self.turns = turns
self.prob_end = prob_end
Expand All @@ -120,6 +127,12 @@ def __init__(
self.score_history = [] # type: List
self.winning_strategy_name = None # type: Optional[str]
self.mutation_rate = mutation_rate
self.stop_on_fixation = stop_on_fixation
m = mutation_method.lower()
if m in ["atomic", "transition"]:
self.mutation_method = m
else:
raise ValueError("Invalid mutation method {}".format(mutation_method))
assert (mutation_rate >= 0) and (mutation_rate <= 1)
assert (noise >= 0) and (noise <= 1)
mode = mode.lower()
Expand Down Expand Up @@ -159,6 +172,7 @@ def __init__(
# Map players to graph vertices
self.locations = sorted(interaction_graph.vertices)
self.index = dict(zip(sorted(interaction_graph.vertices), range(len(players))))
self.fixated = self.fixation_check()

def set_players(self) -> None:
"""Copy the initial players into the first population."""
Expand All @@ -176,17 +190,23 @@ def mutate(self, index: int) -> Player:
index:
The index of the player to be mutated
"""
# Choose another strategy at random from the initial population
r = random.random()
if r < self.mutation_rate:
s = str(self.players[index])
j = randrange(0, len(self.mutation_targets[s]))
p = self.mutation_targets[s][j]
new_player = p.clone()
else:
# Just clone the player
new_player = self.players[index].clone()
return new_player

if self.mutation_method == "atomic":
if not issubclass(self.players[index].__class__, EvolvablePlayer):
raise TypeError("Player is not evolvable. Use a subclass of EvolvablePlayer.")
return self.players[index].mutate()

# Assuming mutation_method == "transition"
if self.mutation_rate > 0:
# Choose another strategy at random from the initial population
r = random.random()
if r < self.mutation_rate:
s = str(self.players[index])
j = randrange(0, len(self.mutation_targets[s]))
p = self.mutation_targets[s][j]
return p.clone()
# Just clone the player
return self.players[index].clone()

def death(self, index: int = None) -> int:
"""
Expand Down Expand Up @@ -250,14 +270,13 @@ def fixation_check(self) -> bool:
Boolean:
True if fixation has occurred (population all of a single type)
"""
if self.mutation_rate > 0:
return False
classes = set(str(p) for p in self.players)
self.fixated = False
if len(classes) == 1:
# Set the winning strategy name variable
self.winning_strategy_name = str(self.players[0])
return True
return False
self.fixated = True
return self.fixated

def __next__(self) -> object:
"""
Expand All @@ -275,7 +294,7 @@ def __next__(self) -> object:
Returns itself with a new population
"""
# Check the exit condition, that all players are of the same type.
if self.fixation_check():
if self.stop_on_fixation and self.fixation_check():
raise StopIteration
if self.mode == "bd":
# Birth then death
Expand All @@ -286,16 +305,10 @@ def __next__(self) -> object:
i = self.death()
self.players[i] = None
j = self.birth(i)
# Mutate
if self.mutation_rate:
new_player = self.mutate(j)
else:
new_player = self.players[j].clone()
# Replace player i with clone of player j
self.players[i] = new_player
# Mutate and/or replace player i with clone of player j
self.players[i] = self.mutate(j)
# Record population.
self.populations.append(self.population_distribution())
# Check again for fixation
self.fixation_check()
return self

def _matchup_indices(self) -> Set[Tuple[int, int]]:
Expand Down Expand Up @@ -395,10 +408,10 @@ def play(self) -> List[Counter]:
populations:
Returns a list of all the populations
"""
if self.mutation_rate != 0:
if not self.stop_on_fixation or self.mutation_rate != 0:
raise ValueError(
"MoranProcess.play() will never exit if mutation_rate is"
"nonzero. Use iteration instead."
"nonzero or stop_on_fixation is False. Use iteration instead."
)
while True:
try:
Expand Down
1 change: 0 additions & 1 deletion axelrod/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ class Player(object):
"manipulates_state": None,
}

# def __new__(cls, *args, history=None, **kwargs):
def __new__(cls, *args, **kwargs):
"""Caches arguments for Player cloning."""
obj = super().__new__(cls)
Expand Down
20 changes: 14 additions & 6 deletions axelrod/random_.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import random

import numpy
import numpy as np
from numpy.random import choice

from axelrod.action import Action

C, D = Action.C, Action.D


def seed(seed_):
"""Sets a seed"""
random.seed(seed_)
np.random.seed(seed_)


def random_choice(p: float = 0.5) -> Action:
"""
Return C with probability `p`, else return D
Expand Down Expand Up @@ -63,10 +71,10 @@ def randrange(a: int, b: int) -> int:
return a + int(r)


def seed(seed_):
"""Sets a seed"""
random.seed(seed_)
numpy.random.seed(seed_)
def random_vector(size):
"""Create a random vector of values in [0, 1] that sums to 1."""
vector = np.random.random(size)
return vector / np.sum(vector)


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

def sample(self):
"""Sample from the pdf"""
index = numpy.random.choice(a=range(self.size), p=self.probability)
index = choice(a=range(self.size), p=self.probability)
# Numpy cannot sample from a list of n dimensional objects for n > 1,
# need to sample an index.
return self.sample_space[index]
7 changes: 2 additions & 5 deletions axelrod/result_set.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
from collections import Counter, namedtuple
import csv
import itertools
from collections import Counter, namedtuple
from multiprocessing import cpu_count

import axelrod.interaction_utils as iu
import numpy as np
import tqdm
from axelrod.action import Action, str_to_actions
from axelrod.action import Action

import dask as da
import dask.dataframe as dd

from . import eigen
from .game import Game

C, D = Action.C, Action.D

Expand Down Expand Up @@ -416,7 +414,6 @@ def _build_normalised_cooperation(self):

@update_progress_bar
def _build_initial_cooperation_rate(self, interactions_series):
interactions_dict = interactions_series.to_dict()
interactions_array = np.array(
[
interactions_series.get(player_index, 0)
Expand Down
Loading

0 comments on commit d344129

Please sign in to comment.