Skip to content

Commit 3315c9f

Browse files
Merge pull request #1229 from gaffney2010/speed-up-cache
Speed up caching logic
2 parents 0a281c5 + 48b65a5 commit 3315c9f

File tree

5 files changed

+81
-45
lines changed

5 files changed

+81
-45
lines changed

axelrod/deterministic_cache.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
from .action import Action
2020
from .player import Player
2121

22-
CachePlayerKey = Tuple[Player, Player, int]
23-
CacheKey = Tuple[str, str, int]
22+
CachePlayerKey = Tuple[Player, Player]
23+
CacheKey = Tuple[str, str]
2424

2525

2626
def _key_transform(key: CachePlayerKey) -> CacheKey:
@@ -29,15 +29,15 @@ def _key_transform(key: CachePlayerKey) -> CacheKey:
2929
Parameters
3030
----------
3131
key: tuple
32-
A 3-tuple: (player instance, player instance, match length)
32+
A 3-tuple: (player instance, player instance)
3333
"""
34-
return key[0].name, key[1].name, key[2]
34+
return key[0].name, key[1].name
3535

3636

3737
def _is_valid_key(key: CachePlayerKey) -> bool:
3838
"""Validate a deterministic cache player key.
3939
40-
The key should always be a 3-tuple, with a pair of axelrod.Player
40+
The key should always be a 2-tuple, with a pair of axelrod.Player
4141
instances and one integer. Both players should be deterministic.
4242
4343
Parameters
@@ -48,13 +48,12 @@ def _is_valid_key(key: CachePlayerKey) -> bool:
4848
-------
4949
Boolean indicating if the key is valid
5050
"""
51-
if not isinstance(key, tuple) or len(key) != 3:
51+
if not isinstance(key, tuple) or len(key) != 2:
5252
return False
5353

5454
if not (
5555
isinstance(key[0], Player)
5656
and isinstance(key[1], Player)
57-
and isinstance(key[2], int)
5857
):
5958
return False
6059

@@ -83,10 +82,11 @@ def _is_valid_value(value: List) -> bool:
8382
class DeterministicCache(UserDict):
8483
"""A class to cache the results of deterministic matches.
8584
86-
For fixed length matches with no noise between pairs of deterministic
87-
players, the results will always be the same. We can hold those results
88-
in this class so as to avoid repeatedly generating them in tournaments
89-
of multiple repetitions.
85+
For matches with no noise between pairs of deterministic players, the
86+
results will always be the same. We can hold the results for the longest
87+
run in this class, so as to avoid repeatedly generating them in tournaments
88+
of multiple repetitions. If a shorter or equal-length match is run, we can
89+
use the stored results.
9090
9191
By also storing those cached results in a file, we can re-use the cache
9292
between multiple tournaments if necessary.
@@ -134,8 +134,7 @@ def __setitem__(self, key: CachePlayerKey, value):
134134

135135
if not _is_valid_key(key):
136136
raise ValueError(
137-
"Key must be a tuple of 2 deterministic axelrod Player classes "
138-
"and an integer"
137+
"Key must be a tuple of 2 deterministic axelrod Player classes"
139138
)
140139

141140
if not _is_valid_value(value):

axelrod/match.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ def _cache_update_required(self):
116116
and not (any(p.classifier["stochastic"] for p in self.players))
117117
)
118118

119+
def _cached_enough_turns(self, cache_key, turns):
120+
"""
121+
Returns true iff there are is a entry in self._cache for the given key and
122+
it's at least turns long.
123+
"""
124+
if cache_key not in self._cache:
125+
return False
126+
return len(self._cache[cache_key]) >= turns
127+
119128
def play(self):
120129
"""
121130
The resulting list of actions from a match between two players.
@@ -135,9 +144,9 @@ def play(self):
135144
i.e. One entry per turn containing a pair of actions.
136145
"""
137146
turns = min(sample_length(self.prob_end), self.turns)
138-
cache_key = (self.players[0], self.players[1], turns)
147+
cache_key = (self.players[0], self.players[1])
139148

140-
if self._stochastic or (cache_key not in self._cache):
149+
if self._stochastic or not self._cached_enough_turns(cache_key, turns):
141150
for p in self.players:
142151
p.reset()
143152
p.set_match_attributes(**self.match_attributes)
@@ -148,7 +157,7 @@ def play(self):
148157
if self._cache_update_required:
149158
self._cache[cache_key] = result
150159
else:
151-
result = self._cache[cache_key]
160+
result = self._cache[cache_key][:turns]
152161

153162
self.result = result
154163
return result

axelrod/tests/unit/test_deterministic_cache.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
class TestDeterministicCache(unittest.TestCase):
1111
@classmethod
1212
def setUpClass(cls):
13-
cls.test_key = (TitForTat(), Defector(), 3)
13+
cls.test_key = (TitForTat(), Defector())
1414
cls.test_value = [(C, D), (D, D), (D, D)]
1515
cls.test_save_file = "test_cache_save.txt"
1616
cls.test_load_file = "test_cache_load.txt"
17-
test_data_to_pickle = {("Tit For Tat", "Defector", 3): [(C, D), (D, D), (D, D)]}
17+
test_data_to_pickle = {("Tit For Tat", "Defector"): [(C, D), (D, D), (D, D)]}
1818
cls.test_pickle = pickle.dumps(test_data_to_pickle)
1919

2020
with open(cls.test_load_file, "wb") as f:
@@ -44,40 +44,30 @@ def test_setitem_invalid_key_not_tuple(self):
4444
with self.assertRaises(ValueError):
4545
self.cache[invalid_key] = self.test_value
4646

47-
def test_setitem_invalid_key_too_short(self):
48-
invalid_key = self.test_key + (4,)
49-
with self.assertRaises(ValueError):
50-
self.cache[invalid_key] = self.test_value
51-
52-
def test_setitem_invalid_key_too_long(self):
53-
invalid_key = self.test_key[:2]
54-
with self.assertRaises(ValueError):
55-
self.cache[invalid_key] = self.test_value
56-
5747
def test_setitem_invalid_key_first_two_elements_not_player(self):
58-
invalid_key = ("test", "test", 2)
48+
invalid_key = ("test", "test")
5949
with self.assertRaises(ValueError):
6050
self.cache[invalid_key] = self.test_value
6151

62-
invalid_key = (TitForTat(), "test", 2)
52+
invalid_key = (TitForTat(), "test")
6353
with self.assertRaises(ValueError):
6454
self.cache[invalid_key] = self.test_value
6555

66-
invalid_key = ("test", TitForTat(), 2)
56+
invalid_key = ("test", TitForTat())
6757
with self.assertRaises(ValueError):
6858
self.cache[invalid_key] = self.test_value
6959

70-
def test_setitem_invalid_key_last_element_not_integer(self):
60+
def test_setitem_invalid_key_too_many_players(self):
7161
invalid_key = (TitForTat(), TitForTat(), TitForTat())
7262
with self.assertRaises(ValueError):
7363
self.cache[invalid_key] = self.test_value
7464

7565
def test_setitem_invalid_key_stochastic_player(self):
76-
invalid_key = (Random(), TitForTat(), 2)
66+
invalid_key = (Random(), TitForTat())
7767
with self.assertRaises(ValueError):
7868
self.cache[invalid_key] = self.test_value
7969

80-
invalid_key = (TitForTat(), Random(), 2)
70+
invalid_key = (TitForTat(), Random())
8171
with self.assertRaises(ValueError):
8272
self.cache[invalid_key] = self.test_value
8373

axelrod/tests/unit/test_match.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,8 @@ def test_example_prob_end(self):
8989
self.assertEqual(len(match.play()), expected_length)
9090
self.assertEqual(match.noise, 0)
9191
self.assertEqual(match.game.RPST(), (3, 1, 0, 5))
92-
self.assertEqual(len(match._cache), 3)
93-
94-
for expected_length in expected_lengths:
95-
self.assertEqual(
96-
match._cache[(p1, p2, expected_length)], [(C, C)] * expected_length
97-
)
92+
self.assertEqual(len(match._cache), 1)
93+
self.assertEqual(match._cache[(p1, p2)], [(C, C)] * 5)
9894

9995
@given(turns=integers(min_value=1, max_value=200), game=games())
10096
@example(turns=5, game=axelrod.DefaultGame)
@@ -167,14 +163,54 @@ def test_play(self):
167163
expected_result = [(C, D), (C, D), (C, D)]
168164
self.assertEqual(match.play(), expected_result)
169165
self.assertEqual(
170-
cache[(axelrod.Cooperator(), axelrod.Defector(), 3)], expected_result
166+
cache[(axelrod.Cooperator(), axelrod.Defector())], expected_result
171167
)
172168

173169
# a deliberately incorrect result so we can tell it came from the cache
174-
expected_result = [(C, C), (D, D), (D, C)]
175-
cache[(axelrod.Cooperator(), axelrod.Defector(), 3)] = expected_result
170+
expected_result = [(C, C), (D, D), (D, C), (C, C), (C, D)]
171+
cache[(axelrod.Cooperator(), axelrod.Defector())] = expected_result
176172
match = axelrod.Match(players, 3, deterministic_cache=cache)
177-
self.assertEqual(match.play(), expected_result)
173+
self.assertEqual(match.play(), expected_result[:3])
174+
175+
def test_cache_grows(self):
176+
"""
177+
We want to make sure that if we try to use the cache for more turns than
178+
what is stored, then it will instead regenerate the result and overwrite
179+
the cache.
180+
"""
181+
cache = DeterministicCache()
182+
players = (axelrod.Cooperator(), axelrod.Defector())
183+
match = axelrod.Match(players, 3, deterministic_cache=cache)
184+
expected_result_5_turn = [(C, D), (C, D), (C, D), (C, D), (C, D)]
185+
expected_result_3_turn = [(C, D), (C, D), (C, D)]
186+
self.assertEqual(match.play(), expected_result_3_turn)
187+
match.turns = 5
188+
self.assertEqual(match.play(), expected_result_5_turn)
189+
# The cache should now hold the 5-turn result..
190+
self.assertEqual(
191+
cache[(axelrod.Cooperator(), axelrod.Defector())],
192+
expected_result_5_turn
193+
)
194+
195+
def test_cache_doesnt_shrink(self):
196+
"""
197+
We want to make sure that when we access the cache looking for fewer
198+
turns than what is stored, then it will not overwrite the cache with the
199+
shorter result.
200+
"""
201+
cache = DeterministicCache()
202+
players = (axelrod.Cooperator(), axelrod.Defector())
203+
match = axelrod.Match(players, 5, deterministic_cache=cache)
204+
expected_result_5_turn = [(C, D), (C, D), (C, D), (C, D), (C, D)]
205+
expected_result_3_turn = [(C, D), (C, D), (C, D)]
206+
self.assertEqual(match.play(), expected_result_5_turn)
207+
match.turns = 3
208+
self.assertEqual(match.play(), expected_result_3_turn)
209+
# The cache should still hold the 5.
210+
self.assertEqual(
211+
cache[(axelrod.Cooperator(), axelrod.Defector())],
212+
expected_result_5_turn
213+
)
178214

179215
def test_scores(self):
180216
player1 = axelrod.TitForTat()

docs/tutorials/advanced/using_the_cache.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ Let us rerun the above match but using the cache::
4040
We can take a look at the cache::
4141

4242
>>> cache # doctest: +ELLIPSIS
43-
{('Soft Go By Majority', 'Alternator', 200): [(C, C), ..., (C, D)]}
43+
{('Soft Go By Majority', 'Alternator'): [(C, C), ..., (C, D)]}
4444
>>> len(cache)
4545
1
46+
>>> len(cache[(axl.GoByMajority(), axl.Alternator())])
47+
200
4648

4749
This maps a triplet of 2 player names and the match length to the resulting
4850
interactions. We can rerun the code and compare the timing::

0 commit comments

Comments
 (0)