Skip to content

Commit

Permalink
Merge pull request #1300 from gaffney2010/classifiers
Browse files Browse the repository at this point in the history
Move classifiers outside of Players
  • Loading branch information
drvinceknight committed Apr 7, 2020
2 parents d4735cb + 53a6371 commit ecad2a1
Show file tree
Hide file tree
Showing 32 changed files with 2,796 additions and 561 deletions.
3 changes: 2 additions & 1 deletion axelrod/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from axelrod.plot import Plot
from axelrod.game import DefaultGame, Game
from axelrod.history import History, LimitedHistory
from axelrod.player import is_basic, obey_axelrod, Player
from axelrod.player import Player
from axelrod.classifier import Classifiers
from axelrod.evolvable_player import EvolvablePlayer
from axelrod.mock_player import MockPlayer
from axelrod.match import Match
Expand Down
246 changes: 246 additions & 0 deletions axelrod/classifier.py
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()
Loading

0 comments on commit ecad2a1

Please sign in to comment.