Skip to content

Commit

Permalink
Merge pull request #1197 from langner/master
Browse files Browse the repository at this point in the history
Spruce up deterministic_cache, ecosystem and eigen
  • Loading branch information
meatballs authored Aug 22, 2018
2 parents c83fcaa + 1c088ef commit 695a3e3
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 63 deletions.
46 changes: 42 additions & 4 deletions axelrod/ecosystem.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
"""Tools for simulating population dynamics of immutable players.
An ecosystem runs in the context of a previous tournament, and takes the
results as input. That means no matches are run by the ecosystem, and a
tournament needs to happen before it is created. For example:
players = [axelrod.Cooperator(), axlerod.Defector()]
tournament = axelrod.Tournament(players=players)
results = tournament.play()
ecosystem = axelrod.Ecosystem(results)
ecosystem.reproduce(100)
"""

import random
from axelrod.result_set import ResultSet

from typing import List, Callable

from axelrod.result_set import ResultSet


class Ecosystem(object):
"""Create an ecosystem based on the payoff matrix from an Axelrod
tournament."""
"""An ecosystem based on the payoff matrix from a tournament.
Attributes
----------
num_players: int
The number of players
"""

def __init__(self, results: ResultSet,
fitness: Callable[[float], float] = None,
population: List[int] = None) -> None:
"""Create a new ecosystem.
Parameters
----------
results: ResultSet
The results of the tournament run beforehand to use.
fitness: List of callables
The reproduction rate at which populations reproduce.
population: List of ints.
The initial populations of the players, corresponding to the
payoff matrix in results.
"""

self.results = results
self.num_players = self.results.num_players
Expand Down Expand Up @@ -45,7 +77,13 @@ def __init__(self, results: ResultSet,
self.fitness = lambda p: p

def reproduce(self, turns: int):

"""Reproduce populations according to the payoff matrix.
Parameters
----------
turns: int
The number of turns to run.
"""
for iturn in range(turns):
plist = list(range(self.num_players))
pops = self.population_sizes[-1]
Expand Down
19 changes: 13 additions & 6 deletions axelrod/eigen.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@
from typing import Tuple


def normalise(nvec: numpy.ndarray) -> numpy.ndarray:
def _normalise(nvec: numpy.ndarray) -> numpy.ndarray:
"""Normalises the given numpy array."""
with numpy.errstate(invalid='ignore'):
result = nvec / numpy.sqrt(numpy.dot(nvec, nvec))
return result


def squared_error(vector_1: numpy.ndarray, vector_2: numpy.ndarray) -> float:
def _squared_error(vector_1: numpy.ndarray, vector_2: numpy.ndarray) -> float:
"""Computes the squared error between two numpy arrays."""
diff = vector_1 - vector_2
s = numpy.dot(diff, diff)
return numpy.sqrt(s)


def power_iteration(mat: numpy.matrix, initial: numpy.ndarray) -> numpy.ndarray:
def _power_iteration(mat: numpy.matrix, initial: numpy.ndarray) -> numpy.ndarray:
"""
Generator of successive approximations.
Expand All @@ -41,7 +41,7 @@ def power_iteration(mat: numpy.matrix, initial: numpy.ndarray) -> numpy.ndarray:

vec = initial
while True:
vec = normalise(numpy.dot(mat, vec))
vec = _normalise(numpy.dot(mat, vec))
yield vec


Expand All @@ -60,6 +60,13 @@ def principal_eigenvector(mat: numpy.matrix, maximum_iterations=1000,
The maximum number of iterations of the approximation
max_error: float, 1e-8
Exit criterion -- error threshold of the difference of successive steps
Returns
-------
ndarray
Eigenvector estimate for the input matrix
float
Eigenvalue corresonding to the returned eigenvector
"""

mat_ = numpy.matrix(mat)
Expand All @@ -70,10 +77,10 @@ def principal_eigenvector(mat: numpy.matrix, maximum_iterations=1000,
if not maximum_iterations:
maximum_iterations = float('inf')
last = initial
for i, vector in enumerate(power_iteration(mat, initial=initial)):
for i, vector in enumerate(_power_iteration(mat, initial=initial)):
if i > maximum_iterations:
break
if squared_error(vector, last) < max_error:
if _squared_error(vector, last) < max_error:
break
last = vector
# Compute the eigenvalue (Rayleigh quotient)
Expand Down
42 changes: 17 additions & 25 deletions axelrod/tests/unit/test_deterministic_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,15 @@ def setUp(self):
self.cache = DeterministicCache()

def test_basic_init(self):
cache = DeterministicCache()
self.assertTrue(cache.mutable)
self.assertTrue(self.cache.mutable)

def test_init_from_file(self):
cache = DeterministicCache(file_name=self.test_load_file)
self.assertEqual(cache[self.test_key], self.test_value)
loaded_cache = DeterministicCache(file_name=self.test_load_file)
self.assertEqual(loaded_cache[self.test_key], self.test_value)

def test_setitem(self):
cache = DeterministicCache()
cache[self.test_key] = self.test_value
self.assertEqual(cache[self.test_key], self.test_value)
self.cache[self.test_key] = self.test_value
self.assertEqual(self.cache[self.test_key], self.test_value)

def test_setitem_invalid_key_not_tuple(self):
invalid_key = 'test'
Expand Down Expand Up @@ -87,41 +85,35 @@ def test_setitem_invalid_key_stochastic_player(self):
self.cache[invalid_key] = self.test_value

def test_setitem_invalid_value_not_list(self):
cache = DeterministicCache()
with self.assertRaises(ValueError):
cache[self.test_key] = 5
self.cache[self.test_key] = 5

def test_setitem_with_immutable_cache(self):
cache = DeterministicCache()
cache.mutable = False
self.cache.mutable = False
with self.assertRaises(ValueError):
cache[self.test_key] = self.test_value
self.cache[self.test_key] = self.test_value

def test_save(self):
cache = DeterministicCache()
cache[self.test_key] = self.test_value
cache.save(self.test_save_file)
self.cache[self.test_key] = self.test_value
self.cache.save(self.test_save_file)
with open(self.test_save_file, 'rb') as f:
text = f.read()
self.assertEqual(text, self.test_pickle)

def test_load(self):
cache = DeterministicCache()
cache.load(self.test_load_file)
self.assertEqual(cache[self.test_key], self.test_value)
self.cache.load(self.test_load_file)
self.assertEqual(self.cache[self.test_key], self.test_value)

def test_load_error_for_inccorect_format(self):
filename = "test_outputs/test.cache"
with open(filename, 'wb') as io:
pickle.dump(range(5), io)

with self.assertRaises(ValueError):
cache = DeterministicCache()
cache.load(filename)
self.cache.load(filename)

def test_del_item(self):
cache = DeterministicCache()
cache[self.test_key] = self.test_value
self.assertTrue(self.test_key in cache)
del cache[self.test_key]
self.assertFalse(self.test_key in cache)
self.cache[self.test_key] = self.test_value
self.assertTrue(self.test_key in self.cache)
del self.cache[self.test_key]
self.assertFalse(self.test_key in self.cache)
23 changes: 8 additions & 15 deletions axelrod/tests/unit/test_ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ def setUpClass(cls):
cls.res_cooperators = cooperators.play()
cls.res_defector_wins = defector_wins.play()

def test_init(self):
"""Are the populations created correctly?"""

# By default create populations of equal size
def test_default_population_sizes(self):
eco = axelrod.Ecosystem(self.res_cooperators)
pops = eco.population_sizes
self.assertEqual(eco.num_players, 4)
Expand All @@ -36,7 +33,7 @@ def test_init(self):
self.assertAlmostEqual(sum(pops[0]), 1.0)
self.assertEqual(list(set(pops[0])), [0.25])

# Can pass list of initial population distributions
def test_non_default_population_sizes(self):
eco = axelrod.Ecosystem(self.res_cooperators, population=[.7, .25, .03, .02])
pops = eco.population_sizes
self.assertEqual(eco.num_players, 4)
Expand All @@ -45,7 +42,7 @@ def test_init(self):
self.assertAlmostEqual(sum(pops[0]), 1.0)
self.assertEqual(pops[0], [.7, .25, .03, .02])

# Distribution will automatically normalise
def test_population_normalization(self):
eco = axelrod.Ecosystem(self.res_cooperators, population=[70, 25, 3, 2])
pops = eco.population_sizes
self.assertEqual(eco.num_players, 4)
Expand All @@ -54,22 +51,20 @@ def test_init(self):
self.assertAlmostEqual(sum(pops[0]), 1.0)
self.assertEqual(pops[0], [.7, .25, .03, .02])

# If passed list is of incorrect size get error
def test_results_and_population_of_different_sizes(self):
self.assertRaises(TypeError, axelrod.Ecosystem, self.res_cooperators,
population=[.7, .2, .03, .1, .1])

# If passed list has negative values
def test_negative_populations(self):
self.assertRaises(TypeError, axelrod.Ecosystem, self.res_cooperators,
population=[.7, -.2, .03, .2])

def test_fitness(self):
def test_fitness_function(self):
fitness = lambda p: 2 * p
eco = axelrod.Ecosystem(self.res_cooperators, fitness=fitness)
self.assertTrue(eco.fitness(10), 20)

def test_cooperators(self):
"""Are cooperators stable over time?"""

def test_cooperators_are_stable_over_time(self):
eco = axelrod.Ecosystem(self.res_cooperators)
eco.reproduce(100)
pops = eco.population_sizes
Expand All @@ -79,9 +74,7 @@ def test_cooperators(self):
self.assertEqual(sum(p), 1.0)
self.assertEqual(list(set(p)), [0.25])

def test_defector_wins(self):
"""Does one defector win over time?"""

def test_defector_wins_with_only_cooperators(self):
eco = axelrod.Ecosystem(self.res_defector_wins)
eco.reproduce(1000)
pops = eco.population_sizes
Expand Down
22 changes: 9 additions & 13 deletions axelrod/tests/unit/test_eigen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,37 @@
import numpy
from numpy.testing import assert_array_almost_equal

from axelrod.eigen import normalise, principal_eigenvector
from axelrod.eigen import _normalise, principal_eigenvector


class FunctionCases(unittest.TestCase):

def test_eigen_1(self):
# Test identity matrices
def test_identity_matrices(self):
for size in range(2, 6):
mat = numpy.identity(size)
evector, evalue = principal_eigenvector(mat)
self.assertAlmostEqual(evalue, 1)
assert_array_almost_equal(evector, normalise(numpy.ones(size)))
assert_array_almost_equal(evector, _normalise(numpy.ones(size)))

def test_eigen_2(self):
# Test a 2x2 matrix
def test_2x2_matrix(self):
mat = [[2, 1], [1, 2]]
evector, evalue = principal_eigenvector(mat)
self.assertAlmostEqual(evalue, 3)
assert_array_almost_equal(evector, numpy.dot(mat, evector) / evalue)
assert_array_almost_equal(evector, normalise([1, 1]))
assert_array_almost_equal(evector, _normalise([1, 1]))

def test_eigen_3(self):
# Test a 3x3 matrix
def test_3x3_matrix(self):
mat = [[1, 2, 0], [-2, 1, 2], [1, 3, 1]]
evector, evalue = principal_eigenvector(mat, maximum_iterations=None,
max_error=1e-10)
self.assertAlmostEqual(evalue, 3)
assert_array_almost_equal(evector, numpy.dot(mat, evector) / evalue)
assert_array_almost_equal(evector, normalise([0.5, 0.5, 1]))
assert_array_almost_equal(evector, _normalise([0.5, 0.5, 1]))

def test_eigen_4(self):
# Test a 4x4 matrix
def test_4x4_matrix(self):
mat = [[2, 0, 0, 0], [1, 2, 0, 0], [0, 1, 3, 0], [0, 0, 1, 3]]
evector, evalue = principal_eigenvector(mat, maximum_iterations=None,
max_error=1e-10)
self.assertAlmostEqual(evalue, 3, places=3)
assert_array_almost_equal(evector, numpy.dot(mat, evector) / evalue)
assert_array_almost_equal(evector, normalise([0, 0, 0, 1]), decimal=4)
assert_array_almost_equal(evector, _normalise([0, 0, 0, 1]), decimal=4)

0 comments on commit 695a3e3

Please sign in to comment.