-
Notifications
You must be signed in to change notification settings - Fork 266
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
Added player assumption classification #1418
Added player assumption classification #1418
Conversation
Is the size of the action set the right thing to key on? Did you consider having tighter agreement between the player's outputs actions and the expectations of the Game rather than just mathematical compatibility? I.e. IPD_actions = {C, D} (where C, D come from the IPD Game), then we could programmatically check if the Game's domain matches the Player's range. As other games are added, we could do something similar. For a game like Hawk-Dove, the actions would be {escalate, withdraw}, for rock paper scissors they would be {R, P, S}, and so on. |
@marcharper I did consider this; however my main concern was a use case e.g. if a user wanted to play around with the Hawk-Dove game but still use strategies like For strategies specific to one game, I trust that when the user is importing some Also note that (contrary to what I said in the docs - I made a mistake on the rock-paper-scissors docs and have submitted #1419 to fix it) because of how the ordering of actions is defined (two actions are equal if their values are equal), any set of actions with a total ordering will be interoperable. For example, |
Let's think carefully about this. A strategy could be technically playable with any 2x2 game but designed for / intended for a specific game. For example, many of the IPD strategies assume specific values of RPST. It's not clear to me that game action size is the determining factor, nor should we rely on a total ordering and equate axl.hawkdove.Action.Escalate == axl.Action.D. Different libraries map C and D to 0 and 1 differently, and one can swap rows in a game matrix to effectively change the specific ordering (total orders are not unique for finite indexing sets). It would be better for strategies to declare the games they are intended for, and perhaps also the games they are technically playable with, and we should warn the user when the player and the game are not aligned. A common rule of software development is that users can't be trusted to know what they are doing! In other words, we need to think carefully about the proper abstractions for the library rather than just push through what's technically feasible.
This isn't necessarily true. Hypothetically, with better abstractions, both strategies could derive from a "Play index 1 always of a finite game" strategy, retaining proper type alignment between Player and Game. Also, this is a research library focusing on usability and reproducibility, so minimizing code duplication isn't necessarily a privileged optimization metric. |
(Sorry! This ended up being a long comment, since I wanted to fully explain my thoughts on the design of this. TL;DR: Bullet point 4, and the final paragraph) I understand your point, and it may be worth adding a classifier for if a game really depends on the specific game being played. However, I can see a few points where I'm not sure that using this as our main classifier would really increase usability, and it would add complexity to development at the same time:
I just think that the incidence of "someone accidentally uses a strategy which doesn't work on their custom game" is lower than that of "someone wants to use an existing strategy on a different game of the same size", and gearing our classification system towards the first one at the expense of the latter just feels like it'll create more ignorable warnings than actually informative ones. In my experience with developing research software, when a less code-savvy user gets a warning then they usually treat it like the check engine light on a car! With this in mind I think a warning that's not relevant more often than it's relevant is problematic. As for development, my main rub is related to point 4. Enumerating exactly "this strategy works with game X, Y, Z and doesn't work with any other game" and then having to update it every time a game is added is almost certainly not the best abstraction (one could argue it isn't even an abstraction!) Perhaps for strategies relying on specific assumptions of a game, a better solution would be to add an "attributes" dict to the |
Reimplemented as a more general 'assumptions' model:
Some examples:
does this address your concerns @marcharper? I think it's a much more flexible model for maintaining alignment between |
I think I like the look of this @alexhroom, using the basic definition of a strategy as a way of mapping "information" to actions. I understand your point @marcharper though and really trust your instincts here. Would creating a specific kind of |
@drvinceknight one can see here https://github.com/Axelrod-Python/Axelrod/pull/1418/files#diff-0e43e29ffb206bbff9d1780ef3d83a2407c3ec58e893fd93c06bbcdd478a1278R275-R290 that currently a RuntimeError with a verbose message is raised, stating what assumption was violated. as far as I can see the only real benefit to using a unique exception type here is to be able to suppress it, but the |
This is better, thanks. Some notes:
|
I understand - maybe better to keep those strategies as 2x2 for now. Like I said, this would be done in a different PR (i'd prefer this PR to just focus on implementing the framework) so the details of that can be hashed out then.
See the discussion in #1414. Maybe it'd be useful here to implement a helper function which takes a |
Not against a classifier approach but I do like the sound of |
I guess my issue with |
Like the idea of something smart with namespaces (with the caveat for ease of maintenance...) -- I think the flat A thought: would a |
👍 Minor point: Did you want to grab the commit with the change to fix the readthedocs build? |
I'll just merge my fork's dev into this branch when #1419 is merged - trying to cherry-pick the commit without messing anything up is a nightmare in my experience (and suspiciously the read the docs build seems to now be succeeding without it) |
This reverts commit c4359bc.
Failing on coverage:
|
* added a read the docs config file * moved yaml to root directory
@@ -258,6 +258,64 @@ def reset(self): | |||
def update_history(self, play, coplay): | |||
self.history.append(play, coplay) | |||
|
|||
def check_assumptions(self, game_characteristics: dict, raise_error: bool=True): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the assumptions check be here or in the Match
class? It's in a Match
that Player
s and Game
s are combined (and also in the Moran process class, which uses the Match
class). The Player
class just needs to be initialized properly and be able to return values when .strategy()
is called, it's not involved in the scoring process and so it doesn't need to care about the Game
usually (unless it requires info about the game). Note that it's in a Match
that Player
instances are currently given info about the game or match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checking assumptions has use cases outside of matches - if it's in the Player class we can use it for filtering, e.g. if we wanted all strategies from a list strategies
that work on a game with assumptions my_assumptions
, we could do
[strategy() for strategy in strategies if strategy().assumptions_satisfy(my_assumptions)]
.
Note that in general, the methods just take a dict, not a Game
object specifically; the Game
itself is still completely hidden from the Player
class. In the Match class, the Player
s and Game
are combined in the way that you want, without either of them having their whole class exposed to the other.
it doesn't need to care about the Game usually (unless it requires info about the game).
"how should strategies handle requiring info about the game" is really the issue that we want to solve in this PR! :)
as far as I can tell the failure in the CI isn't caused by anything i've done - it seems to always almost pass except randomly fail sometimes (looking at both the test logs and running it locally a few times), unrelated to this PR |
This happens sometimes with hypothesis. Have kicked the CI, hopefully that sorts it but if not will look in to it. |
Do I understand correctly: with this PR every existing strategy assumes that the action set size is 2 and there are no other specified assumptions? |
@marcharper indeed - my aim for this PR is just to add the actual framework for strategy classification (which is not a breaking change so could be merged to dev) and then a follow-up issue would be to actually go through all the strategies and add assumptions, probably alongside a bigger refactoring of the strategies folder to account for non-ipd games - this would be a breaking API change (so would be part of 5.0.0). |
Should this go into the 5.0.0 branch now then, since we anticipate breaking behavior soon? |
@marcharper sure! changed base branch (note that means the most recent commit to dev has been added to this pr!) |
Thanks for all the work on this @alexhroom |
Fixes #1414 by adding a new 'assumptions' feature to strategy classifiers, which check the strategy's assumptions against 'attributes' defined for each game.
Changes:
-Player
s now have an'actions_size'
classifier, which is an integer, and defaults to 2.- ThePlayer
class now has a methodcheck_actions_size
which compares their actions size to a given integer. If the game is too small for their actions size, an error is raised (as otherwise it may cause anIndexError
if it plays an out-of-bounds action); if it is too big, a warning is raised (as it won't cause errors, but may cause unexpected behaviour).- TheMatch
class now checks these actions sizes against the game matrices when initialising.Note to reader: This PR originally just implemented game size checking, but as seen in the discussion below this was decided to be insufficient. The PR was then redone to introduce the following 'assumptions' model:
Action
class, things like RPST existing, or even specifically what the game isstrict_player_compatibility
parameter which, if set to False, downgrades these errors to warningsMockPlayer
accepts classifiers as an initialisation parameter to aid in the testing of this.Some examples:
axl.Cooperator
would not need to assume anything! It always chooses action 1, which always exists.axl.Adaptive
would assume its action set has size 2 exactly, as it is adapting according to that.axl.Cycler
's current implementation would need to assume that the actions are {C, D} as it specifically reads a string of C's and D's. (however it doesn't make assumptions on the actual game matrix itself!)axl.FirstByDowning
would need to assume the game has RPST, since it uses that in its implementation.axl.EvolvedANN
would assume precisely that the game is Prisoners' Dilemma, as it is trained on that game.