Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move classifiers outside of Players #1300

Merged
merged 43 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9a13961
Create classifier.py
gaffney2010 Mar 22, 2020
d42ef12
Change classifiers to composition instead of inheritance.
gaffney2010 Mar 22, 2020
374627a
Add template type and fix "def" -> "class"
gaffney2010 Mar 22, 2020
f47ba31
Write CalcAllClassifiersPlayers
gaffney2010 Mar 22, 2020
f8188a9
Add ClassifierManager
gaffney2010 Mar 22, 2020
f36a22e
Import Player and add YAML to requirements
gaffney2010 Mar 22, 2020
72683cc
Created rebuild_classifier_table file
gaffney2010 Mar 22, 2020
8516693
Fix path issue in rebuild_classifier_table
gaffney2010 Mar 22, 2020
fc53eee
Made ClassifierManager a shorter name
gaffney2010 Mar 22, 2020
b38a016
Updated classifier checker to new API
gaffney2010 Mar 22, 2020
acf2672
Added some tests
gaffney2010 Mar 27, 2020
acde074
Updated half of the documentation
gaffney2010 Mar 28, 2020
018795e
Update remaining documents
MXB6669 Mar 28, 2020
2696930
Fixed missing import
MXB6669 Mar 28, 2020
7fa9f2f
Change number of stochastic strategies
MXB6669 Mar 28, 2020
1601431
Fix bug in MemoryDecay that was changing the classifier.
MXB6669 Mar 28, 2020
d549cd4
Fix bugs in my own refactor
MXB6669 Mar 28, 2020
369e90b
Adjust test for lack of default values on test strategies.
MXB6669 Mar 28, 2020
6a78671
Fix more errors in updating code.
MXB6669 Mar 28, 2020
0a9b99d
Merge branch 'master' of https://github.com/Axelrod-Python/Axelrod in…
MXB6669 Mar 28, 2020
8dae189
Fix yaml requirement
MXB6669 Mar 29, 2020
ce90748
Fix a typo, and make path to test_outputs more robust
MXB6669 Mar 30, 2020
19257cc
Delete print statement on missing classifier. These will always be m…
MXB6669 Mar 30, 2020
634af0a
Update docs/tutorials/contributing/strategy/adding_the_new_strategy.rst
gaffney2010 Mar 31, 2020
dfd2603
Address many of the comments from the review
gaffney2010 Mar 31, 2020
eb1bb7d
Merge branch 'classifiers' of https://github.com/gaffney2010/Axelrod …
gaffney2010 Mar 31, 2020
698bf64
Fix meta test
gaffney2010 Mar 31, 2020
09a02f5
Add more tests to improve coverage.
gaffney2010 Mar 31, 2020
c0f6902
Move is_basic and obey_axelrod to Classifiers class.
gaffney2010 Mar 31, 2020
3cb6bb1
Fix typo in rst
gaffney2010 Mar 31, 2020
842cffb
Merge branch 'master' of https://github.com/Axelrod-Python/Axelrod in…
gaffney2010 Mar 31, 2020
1fe2431
Respond to comment on PR.
gaffney2010 Apr 1, 2020
09e48cb
Lookup classifiers from yaml when no trivial initializer
gaffney2010 Apr 1, 2020
0cdabd4
Fix non-strategy "strategies" in test_filters
gaffney2010 Apr 2, 2020
ae2bc51
Add warning
gaffney2010 Apr 2, 2020
f676025
Fix test for classification
gaffney2010 Apr 6, 2020
e83c8f7
Silence warnings on initialization.
gaffney2010 Apr 6, 2020
f719bbb
Silence warnings on test, and pass instances elsewhere
gaffney2010 Apr 6, 2020
34287b9
Reset standard warning behavior at end of tests, and add tests
gaffney2010 Apr 6, 2020
c024907
Fix broken test and suppress more warnings.
gaffney2010 Apr 6, 2020
122e4ce
Remove unused code in test to improve coverage
gaffney2010 Apr 6, 2020
21ca728
Merge from origin
gaffney2010 Apr 6, 2020
53a6371
Update axelrod/tests/unit/test_classification.py
gaffney2010 Apr 7, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]):
marcharper marked this conversation as resolved.
Show resolved Hide resolved
"""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()
gaffney2010 marked this conversation as resolved.
Show resolved Hide resolved
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)
gaffney2010 marked this conversation as resolved.
Show resolved Hide resolved
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