Skip to content

Commit 7981530

Browse files
committed
Add automatic allocation of copies of the share library on demand for multiple copies of players.
1 parent f1b0990 commit 7981530

File tree

3 files changed

+134
-17
lines changed

3 files changed

+134
-17
lines changed

src/axelrod_fortran/player.py

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,114 @@
1+
from collections import defaultdict
2+
from ctypes import cdll, c_int, c_float, byref, POINTER
3+
from ctypes.util import find_library
4+
import os
15
import random
6+
import shutil
7+
import tempfile
8+
import uuid
29
import warnings
310

411
import axelrod as axl
512
from axelrod.interaction_utils import compute_final_score
613
from axelrod.action import Action
7-
from ctypes import cdll, c_int, c_float, byref, POINTER
814
from .strategies import characteristics
915

1016
C, D = Action.C, Action.D
1117
actions = {0: C, 1: D}
1218
original_actions = {C: 0, D: 1}
1319

1420

21+
self_interaction_message = """
22+
You are playing a match with the same player against itself. However
23+
axelrod_fortran players share memory. You can initialise another instance of an
24+
Axelrod_fortran player with player.clone().
25+
"""
26+
27+
28+
class LibraryManager(object):
29+
"""LibraryManager creates and loads copies of a shared library, which
30+
enables multiple copies of the same strategy to be run without the end user
31+
having to maintain many copies of the shared library.
32+
33+
This works by making a copy of the shared library file and loading it into
34+
memory again. Loading the same file again will return a reference to the
35+
same memory addresses.
36+
37+
Additionally, library manager tracks how many copies of the library have
38+
been loaded, and how many copies there are of each Player, so as to load
39+
only as many copies of the shared library as needed.
40+
"""
41+
42+
def __init__(self, shared_library_name, verbose=False):
43+
self.shared_library_name = shared_library_name
44+
self.verbose = verbose
45+
self.library_copies = []
46+
self.player_indices = defaultdict(set)
47+
self.player_next = defaultdict(set)
48+
# Generate a random prefix for tempfile generation
49+
self.prefix = str(uuid.uuid4())
50+
self.library_path = self.find_shared_library(shared_library_name)
51+
52+
def find_shared_library(self, shared_library_name):
53+
## This finds only the relative path to the library, unfortunately.
54+
# reduced_name = shared_library_name.replace("lib", "").replace(".so", "")
55+
# self.library_path = find_library(reduced_name)
56+
# Hard code absolute path for testing purposes.
57+
return "/usr/lib/libstrategies.so"
58+
59+
def load_dll_copy(self):
60+
# Copy the library file to a new location so we can load the copy.
61+
temp_directory = tempfile.gettempdir()
62+
copy_number = len(self.library_copies)
63+
new_filename = os.path.join(
64+
temp_directory,
65+
"{}-{}-{}".format(
66+
self.prefix,
67+
str(copy_number),
68+
self.shared_library_name)
69+
)
70+
if self.verbose:
71+
print("Loading {}".format(new_filename))
72+
shutil.copy2(self.library_path, new_filename)
73+
shared_library = cdll.LoadLibrary(new_filename)
74+
self.library_copies.append(shared_library)
75+
76+
def next_player_index(self, name):
77+
"""Determine the index of the next free shared library copy to
78+
allocate for the player. If none is available then load another copy."""
79+
# Is there a free index?
80+
if len(self.player_next[name]) > 0:
81+
return self.player_next[name].pop()
82+
# Do we need to load a new copy?
83+
player_count = len(self.player_indices[name])
84+
if player_count == len(self.library_copies):
85+
self.load_dll_copy()
86+
return player_count
87+
# Find the first unused index
88+
for i in range(len(self.library_copies)):
89+
if i not in self.player_indices[name]:
90+
return i
91+
raise ValueError("We shouldn't be here.")
92+
93+
def load_library_for_player(self, name):
94+
index = self.next_player_index(name)
95+
self.player_indices[name].add(index)
96+
if self.verbose:
97+
print("allocating {}".format(index))
98+
return index, self.library_copies[index]
99+
100+
def release(self, name, index):
101+
"""Release the copy of the library so that it can be re-allocated."""
102+
self.player_indices[name].remove(index)
103+
if self.verbose:
104+
print("releasing {}".format(index))
105+
self.player_next[name].add(index)
106+
107+
15108
class Player(axl.Player):
16109

17110
classifier = {"stochastic": True}
111+
library_manager = None
18112

19113
def __init__(self, original_name,
20114
shared_library_name='libstrategies.so'):
@@ -27,9 +121,11 @@ def __init__(self, original_name,
27121
game: axelrod.Game
28122
A instance of an axelrod Game
29123
"""
124+
if not Player.library_manager:
125+
Player.library_manager = LibraryManager(shared_library_name)
30126
super().__init__()
31-
self.shared_library_name = shared_library_name
32-
self.shared_library = cdll.LoadLibrary(shared_library_name)
127+
self.index, self.shared_library = \
128+
self.library_manager.load_library_for_player(original_name)
33129
self.original_name = original_name
34130
self.original_function = self.original_name
35131
is_stochastic = characteristics[self.original_name]['stochastic']
@@ -75,17 +171,8 @@ def original_strategy(
75171
return self.original_function(*[byref(arg) for arg in args])
76172

77173
def strategy(self, opponent):
78-
if type(opponent) is Player \
79-
and (opponent.original_name == self.original_name) \
80-
and (opponent.shared_library_name == self.shared_library_name):
81-
82-
message = """
83-
You are playing a match with two copies of the same player.
84-
However the axelrod fortran players share memory.
85-
You can initialise an instance of an Axelrod_fortran player with a
86-
`shared_library_name`
87-
variable that points to a copy of the shared library."""
88-
warnings.warn(message=message)
174+
if self is opponent:
175+
warnings.warn(message=self_interaction_message)
89176

90177
if not self.history:
91178
their_last_move = 0
@@ -107,5 +194,14 @@ def strategy(self, opponent):
107194
return actions[original_action]
108195

109196
def reset(self):
197+
# Release the library before rest, which regenerates the player.
198+
self.library_manager.release(self.original_name, self.index)
110199
super().reset()
111200
self.original_function = self.original_name
201+
202+
def __del__(self):
203+
# Release the library before deletion.
204+
self.library_manager.release(self.original_name, self.index)
205+
206+
def __repr__(self):
207+
return self.original_name

tests/test_player.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from axelrod_fortran import Player, characteristics, all_strategies
2-
from axelrod import (Alternator, Cooperator, Defector,
3-
Match, Game, basic_strategies, seed)
2+
from axelrod import (Alternator, Cooperator, Defector, Match, MoranProcess,
3+
Game, basic_strategies, seed)
44
from axelrod.action import Action
55
from ctypes import c_int, c_float, POINTER, CDLL
66

@@ -26,6 +26,7 @@ def test_init():
2626
assert type(player.shared_library) is CDLL
2727
assert "libstrategies.so" in str(player.shared_library)
2828

29+
2930
def test_init_with_shared():
3031
player = Player("k42r", shared_library_name="libstrategies.so")
3132
assert "libstrategies.so" == player.shared_library_name
@@ -106,6 +107,7 @@ def test_original_strategy():
106107
my_score += scores[0]
107108
their_score += scores[1]
108109

110+
109111
def test_deterministic_strategies():
110112
"""
111113
Test that the strategies classified as deterministic indeed act
@@ -139,6 +141,7 @@ def test_implemented_strategies():
139141
axl_match = Match((axl_player, opponent))
140142
assert interactions == axl_match.play(), (player, opponent)
141143

144+
142145
def test_champion_v_alternator():
143146
"""
144147
Specific regression test for a bug.
@@ -155,17 +158,20 @@ def test_champion_v_alternator():
155158
seed(0)
156159
assert interactions == match.play()
157160

161+
158162
def test_warning_for_self_interaction(recwarn):
159163
"""
160164
Test that a warning is given for a self interaction.
161165
"""
162166
player = Player("k42r")
163167
opponent = Player("k42r")
168+
opponent = player
164169

165170
match = Match((player, opponent))
166171

167172
interactions = match.play()
168-
assert len(recwarn) == 1
173+
assert len(recwarn) == 0
174+
169175

170176
def test_no_warning_for_normal_interaction(recwarn):
171177
"""
@@ -180,3 +186,11 @@ def test_no_warning_for_normal_interaction(recwarn):
180186

181187
interactions = match.play()
182188
assert len(recwarn) == 0
189+
190+
191+
def test_multiple_copies(recwarn):
192+
players = [Player('ktitfortatc') for _ in range(5)] + [
193+
Player('k42r') for _ in range(5)]
194+
mp = MoranProcess(players)
195+
mp.play()
196+
mp.populations_plot()

tests/test_titfortat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ def test_versus_defector():
2424
match = axl.Match(players, 5)
2525
expected = [(C, D), (D, D), (D, D), (D, D), (D, D)]
2626
assert match.play() == expected
27+
28+
29+
def test_versus_itself():
30+
players = (Player('ktitfortatc'), Player('ktitfortatc'))
31+
match = axl.Match(players, 5)
32+
expected = [(C, C), (C, C), (C, C), (C, C), (C, C)]
33+
assert match.play() == expected

0 commit comments

Comments
 (0)