-
Notifications
You must be signed in to change notification settings - Fork 268
/
Copy pathdeterministic_cache.py
176 lines (132 loc) · 5.02 KB
/
deterministic_cache.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""Tools for caching the results of deterministic matches.
The cache, in most cases, can simply be treated as a dictionary:
cache = DeterministicCache()
cache[key1] = result1
cache[key2] = result2
...
if some_key in cache:
do_something(cache[some_key])
else:
...
"""
import pickle
from collections import UserDict
from typing import List, Optional, Tuple
from axelrod import Classifiers
from .action import Action
from .player import Player
CachePlayerKey = Tuple[Player, Player]
CacheKey = Tuple[str, str]
def _key_transform(key: CachePlayerKey) -> CacheKey:
"""Convert a CachePlayerKey to a CacheKey
Parameters
----------
key: tuple
A 3-tuple: (player instance, player instance)
"""
return key[0].name, key[1].name
def _is_valid_key(key: CachePlayerKey) -> bool:
"""Validate a deterministic cache player key.
The key should always be a 2-tuple, with a pair of axelrod.Player
instances and one integer. Both players should be deterministic.
Parameters
----------
key : object
Returns
-------
Boolean indicating if the key is valid
"""
if not isinstance(key, tuple) or len(key) != 2:
return False
if not (isinstance(key[0], Player) and isinstance(key[1], Player)):
return False
if Classifiers["stochastic"](key[0]) or Classifiers["stochastic"](key[1]):
return False
return True
def _is_valid_value(value: List) -> bool:
"""Validate a deterministic cache value.
The value just needs to be a list, with any contents.
Parameters
----------
value : object
Returns
-------
Boolean indicating if the value is valid
"""
return isinstance(value, list)
class DeterministicCache(UserDict):
"""A class to cache the results of deterministic matches.
For matches with no noise between pairs of deterministic players, the
results will always be the same. We can hold the results for the longest
run in this class, so as to avoid repeatedly generating them in tournaments
of multiple repetitions. If a shorter or equal-length match is run, we can
use the stored results.
By also storing those cached results in a file, we can re-use the cache
between multiple tournaments if necessary.
The cache is a dictionary mapping pairs of Player classes to a list of
resulting interactions. e.g. for a 3 turn Match between Cooperator and
Alternator, the dictionary entry would be:
(axelrod.Cooperator, axelrod.Alternator): [(C, C), (C, D), (C, C)]
Most of the functionality is provided by the UserDict class (which uses an
instance of dict as the 'data' attribute to hold the dictionary entries).
This class overrides the __init__ and __setitem__ methods in order to limit
and validate the keys and values to be as described above. It also adds
methods to save/load the cache to/from a file.
"""
def __init__(self, file_name: Optional[str] = None) -> None:
"""Initialize a new cache.
Parameters
----------
file_name : string
Path to a previously saved cache file
"""
super().__init__()
self.mutable = True
if file_name is not None:
self.load(file_name)
def __delitem__(self, key: CachePlayerKey):
return super().__delitem__(_key_transform(key))
def __getitem__(self, key: CachePlayerKey) -> List[Tuple[Action, Action]]:
return super().__getitem__(_key_transform(key))
def __contains__(self, key):
return super().__contains__(_key_transform(key))
def __setitem__(self, key: CachePlayerKey, value):
"""Validate the key and value before setting them."""
if not self.mutable:
raise ValueError("Cannot update cache unless mutable is True.")
if not _is_valid_key(key):
raise ValueError(
"Key must be a tuple of 2 deterministic axelrod Player classes"
)
if not _is_valid_value(value):
raise ValueError(
"Value must be a list with length equal to turns attribute"
)
super().__setitem__(_key_transform(key), value)
def save(self, file_name: str) -> bool:
"""Serialise the cache dictionary to a file.
Parameters
----------
file_name : string
File path to which the cache should be saved
"""
with open(file_name, "wb") as io:
pickle.dump(self.data, io)
return True
def load(self, file_name: str) -> bool:
"""Load a previously saved cache into the dictionary.
Parameters
----------
file_name : string
Path to a previously saved cache file
"""
with open(file_name, "rb") as io:
data = pickle.load(io)
if isinstance(data, dict):
self.data = data
else:
raise ValueError(
"Cache file exists but is not the correct format. "
"Try deleting and re-building the cache file."
)
return True