Skip to content

Commit

Permalink
Merge pull request #77 from Axelrod-Python/indexing
Browse files Browse the repository at this point in the history
WIP: Add automatic allocation of copies of the share library on demand for multiple copies of players.
  • Loading branch information
meatballs authored Aug 21, 2018
2 parents f1b0990 + 181c08c commit 1ca2e91
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 32 deletions.
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ before_install:
- git clone https://github.com/Axelrod-Python/TourExec.git /tmp/TourExec
- cd /tmp/TourExec
- sudo make install
- export LD_LIBRARY_PATH=/usr/local/lib
- echo "/usr/lib/libstrategies.so" | sudo tee /etc/ld.so.conf.d/strategies-lib.conf
- sudo ldconfig
- ldconfig -p | grep libstrategies.so
- cd $TRAVIS_BUILD_DIR
install:
- pip install -r requirements.txt
Expand Down
58 changes: 42 additions & 16 deletions src/axelrod_fortran/player.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
from ctypes import byref, c_float, c_int, POINTER
import random
import warnings

import axelrod as axl
from axelrod.interaction_utils import compute_final_score
from axelrod.action import Action
from ctypes import cdll, c_int, c_float, byref, POINTER

from .strategies import characteristics
from .shared_library_manager import MultiprocessManager, load_library

C, D = Action.C, Action.D
actions = {0: C, 1: D}
original_actions = {C: 0, D: 1}


self_interaction_message = """
You are playing a match with the same player against itself. However
axelrod_fortran players share memory. You can initialise another instance of an
Axelrod_fortran player with player.clone().
"""


# Initialize a module-wide manager for loading copies of the shared library.
manager = MultiprocessManager()
manager.start()
shared_library_manager = manager.SharedLibraryManager("libstrategies.so")


class Player(axl.Player):

classifier = {"stochastic": True}

def __init__(self, original_name,
shared_library_name='libstrategies.so'):
def __init__(self, original_name):
"""
Parameters
----------
Expand All @@ -28,14 +42,16 @@ def __init__(self, original_name,
A instance of an axelrod Game
"""
super().__init__()
self.shared_library_name = shared_library_name
self.shared_library = cdll.LoadLibrary(shared_library_name)
self.index, self.shared_library_filename = \
shared_library_manager.get_filename_for_player(original_name)
self.shared_library = load_library(self.shared_library_filename)
self.original_name = original_name
self.original_function = self.original_name
is_stochastic = characteristics[self.original_name]['stochastic']
if is_stochastic is not None:
self.classifier['stochastic'] = is_stochastic


def __enter__(self):
return self

Expand Down Expand Up @@ -75,17 +91,8 @@ def original_strategy(
return self.original_function(*[byref(arg) for arg in args])

def strategy(self, opponent):
if type(opponent) is Player \
and (opponent.original_name == self.original_name) \
and (opponent.shared_library_name == self.shared_library_name):

message = """
You are playing a match with two copies of the same player.
However the axelrod fortran players share memory.
You can initialise an instance of an Axelrod_fortran player with a
`shared_library_name`
variable that points to a copy of the shared library."""
warnings.warn(message=message)
if self is opponent:
warnings.warn(message=self_interaction_message)

if not self.history:
their_last_move = 0
Expand All @@ -106,6 +113,25 @@ def strategy(self, opponent):
my_last_move)
return actions[original_action]

def _release_shared_library(self):
# While this looks like we're checking that the shared library file
# isn't deleted, the exception is actually thrown if the manager
# thread closes before the player class is garbage collected, which
# tends to happen at the end of a script.
try:
shared_library_manager.release(self.original_name, self.index)
except FileNotFoundError:
pass

def reset(self):
# Release the shared library since the object is rebuilt on reset.
self._release_shared_library()
super().reset()
self.original_function = self.original_name

def __del__(self):
# Release the library before deletion.
self._release_shared_library()

def __repr__(self):
return self.original_name
120 changes: 120 additions & 0 deletions src/axelrod_fortran/shared_library_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from collections import defaultdict
from ctypes import cdll
from ctypes.util import find_library
from multiprocessing.managers import BaseManager
from pathlib import Path
import platform
import shutil
import subprocess
import tempfile
import uuid


def load_library(filename):
"""Loads a shared library."""
lib = None
if Path(filename).exists():
lib = cdll.LoadLibrary(filename)
return lib


class SharedLibraryManager(object):
"""LibraryManager creates (and deletes) copies of a shared library, which
enables multiple copies of the same strategy to be run without the end user
having to maintain many copies of the shared library.
This works by making a copy of the shared library file and loading it into
memory again. Loading the same file again will return a reference to the
same memory addresses. To be thread-safe, this class just passes filenames
back to the Player class (which actually loads a reference to the library),
ensuring that multiple copies of a given player type do not use the same
copy of the shared library.
"""

def __init__(self, shared_library_name, verbose=False):
self.shared_library_name = shared_library_name
self.verbose = verbose
self.filenames = []
self.player_indices = defaultdict(set)
self.player_next = defaultdict(set)
# Generate a random prefix for tempfile generation
self.prefix = str(uuid.uuid4())
self.library_path = self.find_shared_library(shared_library_name)

def find_shared_library(self, shared_library_name):
# Hack for Linux since find_library doesn't return the full path.
if 'Linux' in platform.system():
output = subprocess.check_output(["ldconfig", "-p"])
for line in str(output).split(r"\n"):
rhs = line.split(" => ")[-1]
if shared_library_name in rhs:
return rhs
raise ValueError("{} not found".format(shared_library_name))
else:
return find_library(
shared_library_name.replace("lib", "").replace(".so", ""))

def create_library_copy(self):
"""Create a new copy of the shared library."""
# Copy the library file to a new (temp) location.
temp_directory = tempfile.gettempdir()
copy_number = len(self.filenames)
filename = "{}-{}-{}".format(
self.prefix,
str(copy_number),
self.shared_library_name)
new_filename = str(Path(temp_directory, filename))
if self.verbose:
print("Loading {}".format(new_filename))
shutil.copy2(self.library_path, new_filename)
self.filenames.append(new_filename)

def next_player_index(self, name):
"""Determine the index of the next free shared library copy to
allocate for the player. If none is available then make another copy."""
# Is there a free index?
if len(self.player_next[name]) > 0:
return self.player_next[name].pop()
# Do we need to load a new copy?
player_count = len(self.player_indices[name])
if player_count == len(self.filenames):
self.create_library_copy()
return player_count
# Find the first unused index
for i in range(len(self.filenames)):
if i not in self.player_indices[name]:
return i
raise ValueError("We shouldn't be here.")

def get_filename_for_player(self, name):
"""For a given player return a filename for a copy of the shared library
for use in a Player class, along with an index for later releasing."""
index = self.next_player_index(name)
self.player_indices[name].add(index)
if self.verbose:
print("allocating {}".format(index))
return index, self.filenames[index]

def release(self, name, index):
"""Release the copy of the library so that it can be re-allocated."""
self.player_indices[name].remove(index)
if self.verbose:
print("releasing {}".format(index))
self.player_next[name].add(index)

def __del__(self):
"""Cleanup temp files on object deletion."""
for filename in self.filenames:
path = Path(filename)
if path.exists():
if self.verbose:
print("deleting", str(path))
path.unlink()


# Setup up thread safe library manager.
class MultiprocessManager(BaseManager):
pass


MultiprocessManager.register('SharedLibraryManager', SharedLibraryManager)
35 changes: 20 additions & 15 deletions tests/test_player.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from axelrod_fortran import Player, characteristics, all_strategies
from axelrod import (Alternator, Cooperator, Defector,
Match, Game, basic_strategies, seed)
from axelrod.action import Action
from ctypes import c_int, c_float, POINTER, CDLL

import itertools

import pytest

from axelrod_fortran import Player, characteristics, all_strategies
from axelrod import (Alternator, Cooperator, Defector, Match, MoranProcess,
Game, basic_strategies, seed)
from axelrod.action import Action


C, D = Action.C, Action.D


Expand All @@ -22,15 +24,6 @@ def test_init():
assert player.original_function.restype == c_int
with pytest.raises(ValueError):
player = Player('test')
assert "libstrategies.so" == player.shared_library_name
assert type(player.shared_library) is CDLL
assert "libstrategies.so" in str(player.shared_library)

def test_init_with_shared():
player = Player("k42r", shared_library_name="libstrategies.so")
assert "libstrategies.so" == player.shared_library_name
assert type(player.shared_library) is CDLL
assert "libstrategies.so" in str(player.shared_library)


def test_matches():
Expand Down Expand Up @@ -106,6 +99,7 @@ def test_original_strategy():
my_score += scores[0]
their_score += scores[1]


def test_deterministic_strategies():
"""
Test that the strategies classified as deterministic indeed act
Expand Down Expand Up @@ -139,6 +133,7 @@ def test_implemented_strategies():
axl_match = Match((axl_player, opponent))
assert interactions == axl_match.play(), (player, opponent)


def test_champion_v_alternator():
"""
Specific regression test for a bug.
Expand All @@ -155,18 +150,20 @@ def test_champion_v_alternator():
seed(0)
assert interactions == match.play()


def test_warning_for_self_interaction(recwarn):
"""
Test that a warning is given for a self interaction.
"""
player = Player("k42r")
opponent = Player("k42r")
opponent = player

match = Match((player, opponent))

interactions = match.play()
assert len(recwarn) == 1


def test_no_warning_for_normal_interaction(recwarn):
"""
Test that a warning is not given for a normal interaction
Expand All @@ -180,3 +177,11 @@ def test_no_warning_for_normal_interaction(recwarn):

interactions = match.play()
assert len(recwarn) == 0


def test_multiple_copies(recwarn):
players = [Player('ktitfortatc') for _ in range(5)] + [
Player('k42r') for _ in range(5)]
mp = MoranProcess(players)
mp.play()
mp.populations_plot()
7 changes: 7 additions & 0 deletions tests/test_titfortat.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ def test_versus_defector():
match = axl.Match(players, 5)
expected = [(C, D), (D, D), (D, D), (D, D), (D, D)]
assert match.play() == expected


def test_versus_itself():
players = (Player('ktitfortatc'), Player('ktitfortatc'))
match = axl.Match(players, 5)
expected = [(C, C), (C, C), (C, C), (C, C), (C, C)]
assert match.play() == expected

0 comments on commit 1ca2e91

Please sign in to comment.