-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1300 from gaffney2010/classifiers
Move classifiers outside of Players
- Loading branch information
Showing
32 changed files
with
2,796 additions
and
561 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
import os | ||
from typing import ( | ||
Any, | ||
Callable, | ||
Generic, | ||
List, | ||
Optional, | ||
Set, | ||
Text, | ||
Type, | ||
TypeVar, | ||
Union, | ||
) | ||
import warnings | ||
import yaml | ||
|
||
from axelrod.player import Player | ||
|
||
ALL_CLASSIFIERS_PATH = "data/all_classifiers.yml" | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
class Classifier(Generic[T]): | ||
"""Describes a Player (strategy). | ||
User sets a name and function, f, at initialization. Through | ||
classify_player, looks for the classifier to be set in the passed Player | ||
class. If not set, then passes to f for calculation. | ||
f must operate on the class, and not an instance. If necessary, f may | ||
initialize an instance, but this shouldn't depend on runtime states, because | ||
the result gets stored in a file. If a strategy's classifier depends on | ||
runtime states, such as those created by transformers, then it can set the | ||
field in its classifier dict, and that will take precedent over saved | ||
values. | ||
Attributes | ||
---------- | ||
name: An identifier for the classifier, used as a dict key in storage and in | ||
'classifier' dicts of Player classes. | ||
player_class_classifier: A function that takes in a Player class (not an | ||
instance) and returns a value. | ||
""" | ||
|
||
def __init__( | ||
self, name: Text, player_class_classifier: Callable[[Type[Player]], T] | ||
): | ||
self.name = name | ||
self.player_class_classifier = player_class_classifier | ||
|
||
def classify_player(self, player: Type[Player]) -> T: | ||
"""Look for this classifier in the passed player's 'classifier' dict, | ||
otherwise pass to the player to f.""" | ||
try: | ||
return player.classifier[self.name] | ||
except: | ||
return self.player_class_classifier(player) | ||
|
||
|
||
stochastic = Classifier[bool]("stochastic", lambda _: False) | ||
memory_depth = Classifier[Union[float, int]]("memory_depth", lambda _: float("inf")) | ||
makes_use_of = Classifier[Optional[Set[Text]]]("makes_use_of", lambda _: None) | ||
long_run_time = Classifier[bool]("long_run_time", lambda _: False) | ||
inspects_source = Classifier[Optional[bool]]("inspects_source", lambda _: None) | ||
manipulates_source = Classifier[Optional[bool]]("manipulates_source", lambda _: None) | ||
manipulates_state = Classifier[Optional[bool]]("manipulates_state", lambda _: None) | ||
|
||
# Should list all known classifiers. | ||
all_classifiers = [ | ||
stochastic, | ||
memory_depth, | ||
makes_use_of, | ||
long_run_time, | ||
inspects_source, | ||
manipulates_source, | ||
manipulates_state, | ||
] | ||
|
||
|
||
def rebuild_classifier_table( | ||
classifiers: List[Classifier], | ||
players: List[Type[Player]], | ||
path: Text = ALL_CLASSIFIERS_PATH, | ||
) -> None: | ||
"""Builds the classifier table in data. | ||
Parameters | ||
---------- | ||
classifiers: A list of classifiers to calculate on the strategies | ||
players: A list of strategies (classes, not instances) to compute the | ||
classifiers for. | ||
path: Where to save the resulting yaml file. | ||
""" | ||
# Get absolute path | ||
dirname = os.path.dirname(__file__) | ||
filename = os.path.join(dirname, path) | ||
|
||
all_player_dicts = dict() | ||
for p in players: | ||
new_player_dict = dict() | ||
for c in classifiers: | ||
new_player_dict[c.name] = c.classify_player(p) | ||
all_player_dicts[p.name] = new_player_dict | ||
|
||
with open(filename, "w") as f: | ||
yaml.dump(all_player_dicts, f) | ||
|
||
|
||
class _Classifiers(object): | ||
"""A singleton used to calculate any known classifier. | ||
Attributes | ||
---------- | ||
all_player_dicts: A local copy of the dict saved in the classifier table. | ||
The keys are player names, and the values are 'classifier' dicts (keyed | ||
by classifier name). | ||
""" | ||
|
||
_instance = None | ||
all_player_dicts = dict() | ||
|
||
# Make this a singleton | ||
def __new__(cls): | ||
if cls._instance is None: | ||
cls._instance = super(_Classifiers, cls).__new__(cls) | ||
# When this is first created, read from the classifier table file. | ||
# Get absolute path | ||
dirname = os.path.dirname(__file__) | ||
filename = os.path.join(dirname, ALL_CLASSIFIERS_PATH) | ||
with open(filename, "r") as f: | ||
cls.all_player_dicts = yaml.load(f, Loader=yaml.FullLoader) | ||
|
||
return cls._instance | ||
|
||
@classmethod | ||
def known_classifier(cls, classifier_name: Text) -> bool: | ||
"""Returns True if the passed classifier_name is known.""" | ||
global all_classifiers | ||
return classifier_name in (c.name for c in all_classifiers) | ||
|
||
@classmethod | ||
def __getitem__( | ||
cls, key: Union[Classifier, Text] | ||
) -> Callable[[Union[Player, Type[Player]]], Any]: | ||
"""Looks up the classifier for the player. | ||
Given a passed classifier key, return a function that: | ||
Takes a player. If the classifier is found in the 'classifier' dict on | ||
the player, then return that. Otherwise look for the classifier for the | ||
player in the all_player_dicts. Returns None if the classifier is not | ||
found in either of those. | ||
The returned function expects Player instances, but if a Player class is | ||
passed, then it will create an instance by calling an argument-less | ||
initializer. If no such initializer exists on the class, then an error | ||
will result. | ||
Parameters | ||
---------- | ||
key: A classifier or classifier name that we want to calculate for the | ||
player. | ||
Returns | ||
------- | ||
A function that will map Player (or Player instances) to their value for | ||
this classification. | ||
""" | ||
# Key may be the name or an instance. Convert to name. | ||
if not isinstance(key, str): | ||
key = key.name | ||
|
||
if not cls.known_classifier(key): | ||
raise KeyError("Unknown classifier") | ||
|
||
def classify_player_for_this_classifier( | ||
player: Union[Player, Type[Player]] | ||
) -> Any: | ||
def try_lookup() -> Any: | ||
try: | ||
player_classifiers = cls.all_player_dicts[player.name] | ||
except: | ||
return None | ||
|
||
return player_classifiers.get(key, None) | ||
|
||
# If the passed player is not an instance, then try to initialize an | ||
# instance without arguments. | ||
if not isinstance(player, Player): | ||
try: | ||
player = player() | ||
warnings.warn( | ||
"Classifiers are intended to run on player instances. " | ||
"Passed player {} was initialized with default " | ||
"arguments.".format(player.name) | ||
) | ||
except: | ||
# All strategies must have trivial initializers. | ||
raise Exception( | ||
"Passed player class doesn't have a trivial initializer." | ||
) | ||
|
||
# Factory-generated players won't exist in the table. As well, some | ||
# players, like Random, may change classifiers at construction time; | ||
# this get() function takes a player instance, while the saved-values | ||
# are from operations on the player object itself. | ||
if key in player.classifier: | ||
return player.classifier[key] | ||
|
||
# Try to find the name in the all_player_dicts, read from disk. | ||
return try_lookup() | ||
|
||
return classify_player_for_this_classifier | ||
|
||
@classmethod | ||
def is_basic(cls, s: Union[Player, Type[Player]]): | ||
""" | ||
Defines criteria for a strategy to be considered 'basic' | ||
""" | ||
stochastic = cls.__getitem__("stochastic")(s) | ||
depth = cls.__getitem__("memory_depth")(s) | ||
inspects_source = cls.__getitem__("inspects_source")(s) | ||
manipulates_source = cls.__getitem__("manipulates_source")(s) | ||
manipulates_state = cls.__getitem__("manipulates_state")(s) | ||
return ( | ||
not stochastic | ||
and not inspects_source | ||
and not manipulates_source | ||
and not manipulates_state | ||
and depth in (0, 1) | ||
) | ||
|
||
@classmethod | ||
def obey_axelrod(cls, s: Union[Player, Type[Player]]): | ||
""" | ||
A function to check if a strategy obeys Axelrod's original tournament | ||
rules. | ||
""" | ||
for c in ["inspects_source", "manipulates_source", "manipulates_state"]: | ||
if cls.__getitem__(c)(s): | ||
return False | ||
return True | ||
|
||
|
||
Classifiers = _Classifiers() |
Oops, something went wrong.