Skip to content

Spruce up deterministic_cache, ecosystem and eigen #1197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)