Skip to content
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

Implement transitive fingerprint #1125

Merged
merged 19 commits into from
Aug 25, 2017
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion axelrod/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
from .tournament import Tournament
from .result_set import ResultSet, ResultSetFromFile
from .ecosystem import Ecosystem
from .fingerprint import AshlockFingerprint
from .fingerprint import AshlockFingerprint, TransitiveFingerprint

183 changes: 182 additions & 1 deletion axelrod/fingerprint.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from collections import namedtuple
import csv
import os
from collections import namedtuple
from tempfile import mkstemp

import matplotlib.pyplot as plt
import numpy as np
import tqdm
from mpl_toolkits.axes_grid1 import make_axes_locatable

import axelrod as axl
from axelrod import Player
Expand Down Expand Up @@ -379,3 +382,181 @@ def plot(self, cmap: str = 'seismic', interpolation: str = 'none',
if title is not None:
plt.title(title)
return fig


class TransitiveFingerprint(object):
def __init__(self, strategy, opponents=None, number_opponents=50):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we change this to number_of_opponents?

"""
Parameters
----------
strategy : class or instance
A class that must be descended from axelrod.Player or an instance of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you mean "axelrod.Player or a descendent" -- I don't believe an instance of axelrod.Player would work here.

axelrod.Player.
opponents : list of class or instance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same: List of Player classes or something similar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I see below that a class or instance can be given. I think we should stick to one or the other, ideally instance (since the parameters may be important).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree. This is just implemented to be analagous to the Ashlock fingerprint, which also allows both (this was a poor design decision mainly on my part).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could either leave them both in as is or remove them from Ashlock which adds a very minor backwards incompatibility.

A list that contains a list of opponents
Default: A spectrum of Random players
number_opponents: integer
An integer that defines the number of Random opponents
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of Random opponents

Default: 50
"""
self.strategy = strategy

if opponents is None:
self.opponents = [axl.Random(p) for p in
np.linspace(0, 1, number_opponents)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

else:
self.opponents = opponents

def fingerprint(self, turns: int = 50, repetitions: int = 10,
noise: float = None, processes: int = None,
filename: str = None,
progress_bar: bool = True) -> np.array:
"""Build and play the spatial tournament.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the docstring, how about something like "Creates a spatial tournament to run the necessary matches to obtain fingerprint data"?


Creates the opponents and their edges then builds a spatial tournament.

Parameters
----------
turns : integer, optional
The number of turns per match
repetitions : integer, optional
The number of times the round robin should be repeated
processes : integer, optional
The number of processes to be used for parallel processing
filename: string, optional
The name of the file for self.spatial_tournament's interactions.
if None and in_memory=False, will auto-generate a filename.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ... a filename will be generated.

progress_bar : bool
Whether or not to create a progress bar which will be updated

Returns
----------
self.data : np.array
A numpy array containing the mean cooperation rate against each
opponent in each turn. The ith row corresponds to the ith opponent
and the jth column the jth turn.
"""

if isinstance(self.strategy, axl.Player):
players = [self.strategy] + self.opponents
else:
players = [self.strategy()] + self.opponents

temp_file_descriptor = None
if filename is None:
temp_file_descriptor, filename = mkstemp()

edges = [(0, k + 1) for k in range(len(self.opponents))]
tournament = axl.Tournament(players=players,
edges=edges, turns=turns, noise=noise,
repetitions=repetitions)
tournament.play(filename=filename, build_results=False,
progress_bar=progress_bar, processes=processes)

self.data = self.analyse_cooperation_ratio(filename)

if temp_file_descriptor is not None:
os.close(temp_file_descriptor)
os.remove(filename)

return self.data

@staticmethod
def analyse_cooperation_ratio(filename):
"""Generates the data used from the tournament

Return an M by N array where M is the number of opponents and N is the
number of turns.

Parameters
----------
filename : str
The filename of the interactions

Returns
----------
self.data : np.array
A numpy array containing the mean cooperation rate against each
opponent in each turn. The ith row corresponds to the ith opponent
and the jth column the jth turn.
"""
did_c = np.vectorize(lambda action: int(action == 'C'))

cooperation_rates = {}
with open(filename, "r") as f:
reader = csv.reader(f)
for row in reader:
opponent_index, player_history = int(row[1]), list(row[4])
if opponent_index in cooperation_rates:
cooperation_rates[opponent_index].append(did_c(player_history))
else:
cooperation_rates[opponent_index] = [did_c(player_history)]

for index, rates in cooperation_rates.items():
cooperation_rates[index] = np.mean(rates, axis=0)

return np.array([cooperation_rates[index]
for index in cooperation_rates])

def plot(self, cmap: str = 'viridis', interpolation: str = 'none',
title: str = None, colorbar: bool = True, labels: bool = True,
display_names: bool = False) -> plt.Figure:
"""Plot the results of the spatial tournament.

Parameters
----------
cmap : str, optional
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above docstrings use integer and string -- can we chose either the full word or the python functions int and str (my preference is the latter) throughout?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nikoleta-v3 I expect these are also not quite right in the Ashlock fingerprint: can you change them there too? (to int and str)

A matplotlib colour map, full list can be found at
http://matplotlib.org/examples/color/colormaps_reference.html
interpolation : str, optional
A matplotlib interpolation, full list can be found at
http://matplotlib.org/examples/images_contours_and_fields/interpolation_methods.html
title : str, optional
A title for the plot
colorbar : bool, optional
Choose whether the colorbar should be included or not
labels : bool, optional
Choose whether the axis labels and ticks should be included
display_name : bool, optional
Choose whether to display the names of the strategies

Returns
----------
figure : matplotlib figure
A heat plot of the results of the spatial tournament
"""

fig, ax = plt.subplots()
mat = ax.imshow(self.data, cmap=cmap, interpolation=interpolation)

width = len(self.data) / 2
height = width
fig.set_size_inches(width, height)

plt.xlabel('turns')
ax.tick_params(axis='both', which='both', length=0)

if display_names:
plt.yticks(range(len(self.opponents)), [str(player) for player in
self.opponents])
else:
plt.yticks([0, len(self.opponents) - 1], [0, 1])
plt.ylabel("Probability of cooperation")

if not labels:
plt.axis('off')

if title is not None:
plt.title(title)

if colorbar:
max_score = 0
min_score = 1
ticks = [min_score, 1 / 2, max_score]

divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.2)
cbar = fig.colorbar(mat, cax=cax, ticks=ticks)

plt.tight_layout()
return fig
108 changes: 98 additions & 10 deletions axelrod/tests/unit/test_fingerprint.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import os
from tempfile import mkstemp
import unittest
from tempfile import mkstemp
from unittest.mock import patch

import numpy as np
import matplotlib.pyplot
from hypothesis import given, settings

import axelrod as axl
from axelrod.fingerprint import (create_points, create_jossann, create_probes,
create_edges, generate_data, reshape_data,
AshlockFingerprint, Point)
AshlockFingerprint, Point, TransitiveFingerprint)
from axelrod.tests.property import strategy_lists


matplotlib_installed = True
try:
import matplotlib.pyplot
except ImportError: # pragma: no cover
matplotlib_installed = False


C, D = axl.Action.C, axl.Action.D


Expand Down Expand Up @@ -399,3 +395,95 @@ def test_pair_fingerprints(self, strategy_pair):
data = af.fingerprint(turns=2, repetitions=2, step=0.5,
progress_bar=False)
self.assertIsInstance(data, dict)


class TestTransitiveFingerprint(unittest.TestCase):

def test_init(self):
player = axl.TitForTat()
fingerprint = axl.TransitiveFingerprint(strategy=player)
self.assertEqual(fingerprint.strategy, player)
self.assertEqual(fingerprint.opponents, [axl.Random(p) for p in
np.linspace(0, 1, 50)])

def test_init_with_opponents(self):
player = axl.TitForTat()
opponents = [s() for s in axl.demo_strategies]
fingerprint = axl.TransitiveFingerprint(strategy=player,
opponents=opponents)
self.assertEqual(fingerprint.strategy, player)
self.assertEqual(fingerprint.opponents, opponents)

def test_init_with_not_default_number(self):
player = axl.TitForTat()
number_opponents = 10
fingerprint = axl.TransitiveFingerprint(strategy=player,
number_opponents=number_opponents)
self.assertEqual(fingerprint.strategy, player)
self.assertEqual(fingerprint.opponents, [axl.Random(p) for p in
np.linspace(0, 1, 10)])

def test_fingerprint_with_filename(self):
filename = "test_outputs/test_fingerprint.csv"
strategy = axl.TitForTat()
tf = TransitiveFingerprint(strategy)
tf.fingerprint(turns=1, repetitions=1, progress_bar=False,
filename=filename)
with open(filename, 'r') as out:
data = out.read()
self.assertEqual(len(data.split("\n")), 50 + 1)

def test_serial_fingerprint(self):
strategy = axl.TitForTat()
tf = TransitiveFingerprint(strategy)
tf.fingerprint(repetitions=1, progress_bar=False,
filename='test_outputs/tran_fin.csv')
self.assertEqual(tf.data.shape, (50, 50))

def test_parallel_fingerprint(self):
strategy = axl.TitForTat()
tf = TransitiveFingerprint(strategy)
tf.fingerprint(repetitions=1, progress_bar=False, processes=2)

self.assertEqual(tf.data.shape, (50, 50))

def test_analyse_cooperation_ratio(self):
tf = TransitiveFingerprint(axl.TitForTat)
filename = "test_outputs/test_fingerprint.csv"
with open(filename, "w") as f:
f.write(
"""0,1,Player0,Player1,CCC,DDD
0,1,Player0,Player1,CCC,DDD
0,2,Player0,Player2,CCD,DDD
0,2,Player0,Player2,CCC,DDD
0,3,Player0,Player3,CCD,DDD
0,3,Player0,Player3,DCC,DDD
0,4,Player0,Player3,DDD,DDD
0,4,Player0,Player3,DDD,DDD""")
data = tf.analyse_cooperation_ratio(filename)
expected_data = np.array([[1, 1, 1],
[1, 1, 1 / 2],
[1 / 2, 1, 1 / 2],
[0, 0, 0]])
self.assertTrue(np.array_equal(data, expected_data))

def test_plot(self):
"""
Test that plot is created with various arguments.
"""
tf = TransitiveFingerprint(axl.TitForTat)
tf.fingerprint(turns=10, repetitions=2, progress_bar=False)
p = tf.plot()
self.assertIsInstance(p, matplotlib.pyplot.Figure)
p = tf.plot(cmap="jet")
self.assertIsInstance(p, matplotlib.pyplot.Figure)
p = tf.plot(interpolation='bicubic')
self.assertIsInstance(p, matplotlib.pyplot.Figure)
p = tf.plot(title="Title")
self.assertIsInstance(p, matplotlib.pyplot.Figure)
p = tf.plot(colorbar=False)
self.assertIsInstance(p, matplotlib.pyplot.Figure)
p = tf.plot(labels=False)
self.assertIsInstance(p, matplotlib.pyplot.Figure)
p = tf.plot(display_names=True)
self.assertIsInstance(p, matplotlib.pyplot.Figure)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 49 additions & 2 deletions docs/tutorials/further_topics/fingerprinting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Fingerprinting
==============

Ashlock Fingerprints
--------------------

In [Ashlock2008]_, [Ashlock2009]_ a methodology for obtaining visual
representation of a strategy's behaviour is described. The basic method is to
play the strategy against a probe strategy with varying noise parameters.
Expand Down Expand Up @@ -77,5 +80,49 @@ This allows for the fingerprinting of parametrized strategies::
>>> data[(0, 0)]
4.4...

Ashlock's fingerprint is currently the only fingerprint implemented in the
library.
Transitive Fingerprint
-----------------------

Another implemented fingerprint is the transitive fingerprint. The
transitive fingerprint represents the cooperation rate of a strategy against a
set of opponents over a number of turns.

By default the set of opponents consists of :code:`50` Random players that
cooperate with increasing probability. This is how to obtain the transitive
fingerprint for :code:`TitForTat`::

>>> axl.seed(0)
>>> player = axl.TitForTat()
>>> tf = axl.TransitiveFingerprint(player)
>>> data = tf.fingerprint(turns=40)

The data produced is a :code:`numpy` array showing the cooperation rate against
a given opponent (row) in a given turn (column)::

>>> data.shape
(50, 40)

It is also possible to visualise the fingerprint::

>>> p = tf.plot()
>>> p.show()

.. image:: _static/fingerprinting/transitive_TFT.png
:width: 100%
:align: center

It is also possible to fingerprint against a given set of opponents::

>>> axl.seed(1)
>>> opponents = [s() for s in axl.demo_strategies]
>>> tf = axl.TransitiveFingerprint(player, opponents=opponents)
>>> data = tf.fingerprint(turns=5, repetitions=10)

The name of the opponents can be displayed in the plot::

>>> p = tf.plot(display_names=True)
>>> p.show()

.. image:: _static/fingerprinting/transitive_TFT_against_demo.png
:width: 70%
:align: center