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

Add more abstract classes #122

Merged
merged 22 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
441f2ca
Write tests for new matching classes.
daffidwilde Jul 14, 2020
165eb93
Write tests for new base class structure.
daffidwilde Jul 15, 2020
3da9ed3
Clean up old tests.
daffidwilde Jul 15, 2020
c09bca7
Implement base classes for players and matchings.
daffidwilde Jul 15, 2020
b30330f
Add compatability to games and hospital classes.
daffidwilde Jul 15, 2020
953cdd4
Update game class tests.
daffidwilde Jul 15, 2020
a619ea3
Catch attribute typo in supervisor.
daffidwilde Jul 15, 2020
308268a
Add compatability to other player tests.
daffidwilde Jul 15, 2020
94cba09
Catch attribute typo in SR test.
daffidwilde Jul 15, 2020
12e319a
Implement composite strategies for HR tests.
daffidwilde Jul 15, 2020
57d9fd7
Update game class tests.
daffidwilde Jul 15, 2020
1ad2d84
Start implementing composites with SR.
daffidwilde Jul 15, 2020
2fdf2a7
Format codebase.
daffidwilde Jul 15, 2020
71523f3
Catch typo in SA tutorial.
daffidwilde Jul 15, 2020
5208d6b
Update classes in README
daffidwilde Jul 15, 2020
78ed067
Merge branch 'add-abstract-classes' into use-composite-strategies
daffidwilde Jul 15, 2020
62e5081
Merge branch 'use-composite-strategies' into add-abstract-classes
daffidwilde Jul 15, 2020
79f4f25
Format codebase with black>19
daffidwilde Oct 30, 2020
3e2f01a
Merge branch 'v1.4-prep' into add-abstract-classes
daffidwilde Oct 30, 2020
de29deb
Add first phase check to SR.
daffidwilde Nov 3, 2020
0aec0cf
Merge branch 'v1.4-prep' into add-abstract-classes
daffidwilde Nov 3, 2020
c0615a3
Fix (now-outdated) logic in tests.
daffidwilde Nov 3, 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
10 changes: 5 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ The ``Matching`` object
+++++++++++++++++++++++

This matching is not a standard Python dictionary, though it does largely look
and behave like one. It is in fact an instance of the ``Matching`` class:
and behave like one. It is in fact an instance of the ``SingleMatching`` class:

>>> matching = game.matching
>>> type(matching)
<class 'matching.matching.Matching'>
<class 'matching.matchings.SingleMatching'>

This dictionary-like object is primarily useful as a teaching device that eases
the process of manipulating a matching after a solution has been found.
Expand All @@ -116,9 +116,9 @@ Despite passing dictionaries of strings here, the matching displays instances of
>>> matching = game.matching
>>> for suitor in matching:
... print(type(suitor))
<class 'matching.player.Player'>
<class 'matching.player.Player'>
<class 'matching.player.Player'>
<class 'matching.players.player.Player'>
<class 'matching.players.player.Player'>
<class 'matching.players.player.Player'>

This is because ``create_from_dictionaries`` creates instances of the
appropriate player classes first and passes them to the game class. Using
Expand Down
8 changes: 4 additions & 4 deletions docs/tutorials/project_allocation/main.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x7f922586fc10>"
"<matplotlib.legend.Legend at 0x7fbcc9eadbb0>"
]
},
"execution_count": 27,
Expand Down Expand Up @@ -1172,7 +1172,7 @@
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x7f922723e670>"
"<matplotlib.legend.Legend at 0x7fbccb877cd0>"
]
},
"execution_count": 30,
Expand Down Expand Up @@ -1235,7 +1235,7 @@
"for project, project_students in matching.items():\n",
" for student in project_students:\n",
" inverted_matching[student.name] = project.name\n",
" student_preference_of_matching.append(student.pref_names.index(project.name))"
" student_preference_of_matching.append(student._pref_names.index(project.name))"
]
},
{
Expand Down Expand Up @@ -1547,7 +1547,7 @@
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x7f920ae2a820>"
"<matplotlib.legend.Legend at 0x7fbcaf634dc0>"
]
},
"execution_count": 36,
Expand Down
21 changes: 15 additions & 6 deletions src/matching/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@

import sys

from .game import BaseGame
from .matching import Matching
from .player import Player
from .version import __version__

if not sys.warnoptions:
import warnings

warnings.simplefilter("always")

__all__ = ["BaseGame", "Matching", "Player", "__version__"]
from .base import BaseGame, BaseMatching, BasePlayer
from .matchings import MultipleMatching, SingleMatching
from .players import Player
from .version import __version__

__all__ = [
"BaseGame",
"BaseMatching",
"BasePlayer",
"Matching",
"MultipleMatching",
"Player",
"SingleMatching",
"__version__",
]
1 change: 1 addition & 0 deletions src/matching/algorithms/stable_roommates.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def locate_all_or_nothing_cycle(player):
lasts.append(their_worst)

player = their_worst

if lasts.count(player) > 1:
break

Expand Down
260 changes: 260 additions & 0 deletions src/matching/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
""" Abstract base classes for inheritance. """
import abc
import warnings

from matching.exceptions import PlayerExcludedWarning, PreferencesChangedWarning


class BasePlayer:
"""An abstract base class to represent a player within a matching game.

Parameters
----------
name : object
An identifier. This should be unique and descriptive.

Attributes
----------
prefs : List[BasePlayer]
The player's preferences. Defaults to ``None`` and is updated using the
``set_prefs`` method.
matching : Optional[BasePlayer]
The current match of the player. ``None`` if not currently matched.
_pref_names : Optional[List]
A list of the names in ``prefs``. Updates with ``prefs`` via
``set_prefs`` method.
_original_prefs : Optional[List[BasePlayer]]
The original set of player preferences. Defaults to ``None`` and does
not update after the first ``set_prefs`` method call.
"""

def __init__(self, name):

self.name = name
self.prefs = []
self.matching = None

self._pref_names = []
self._original_prefs = None

def __repr__(self):

return str(self.name)

def _forget(self, other):
"""Forget another player by removing them from the player's preference
list."""

prefs = self.prefs[:]
prefs.remove(other)
self.prefs = prefs

def unmatched_message(self):

return f"{self} is unmatched."

def not_in_preferences_message(self, other):

return (
f"{self} is matched to {other} but they do not appear in their "
f"preference list: {self.prefs}."
)

def set_prefs(self, players):
""" Set the player's preferences to be a list of players. """

self.prefs = players
self._pref_names = [player.name for player in players]

if self._original_prefs is None:
self._original_prefs = players[:]

def prefers(self, player, other):
"""Determines whether the player prefers a player over some other
player."""

prefs = self._original_prefs
return prefs.index(player) < prefs.index(other)

@abc.abstractmethod
def _match(self, other):
"""A placeholder function for assigning the player to be matched to
some other player."""

@abc.abstractmethod
def _unmatch(self, other):
"""A placeholder function for unassigning the player from its match
with some other player."""

@abc.abstractmethod
def get_favourite(self):
"""A placeholder function for getting the player's favourite, feasible
player."""

@abc.abstractmethod
def get_successors(self):
"""A placeholder function for getting the logically feasible
'successors' of the player."""

@abc.abstractmethod
def check_if_match_is_unacceptable(self):
"""A placeholder for chacking the acceptability of the current
match(es) of the player."""


class BaseGame(metaclass=abc.ABCMeta):
"""An abstract base class for facilitating various matching games.

Parameters
----------
clean
Defaults to :code:`False`. If :code:`True`, when passing a set of
players to create a game instance, they will be automatically cleaned.

Attributes
----------
matching
After solving the game, a :code:`Matching` object is found here.
Otherwise, :code:`None`.
blocking_pairs
After checking the stability of the game instance, a list of any pairs
that block the stability of the matching is found here. Otherwise,
:code:`None`.
"""

def __init__(self, clean=False):

self.matching = None
self.blocking_pairs = None
self.clean = clean

def _remove_player(self, player, player_party, other_party):
"""Remove a player from the game instance as well as any relevant
player preference lists."""

party = vars(self)[player_party][:]
party.remove(player)
vars(self)[player_party].remove(player)
for other in vars(self)[other_party]:
if player in other.prefs:
other._forget(player)

def _check_inputs_player_prefs_unique(self, party):
"""Check that each player in :code:`party` has not ranked another
player more than once. If so, and :code:`clean` is :code:`True`, then
take the first instance they appear in the preference list."""

for player in vars(self)[party]:
unique_prefs = []
for other in player.prefs:
if other not in unique_prefs:
unique_prefs.append(other)
else:
warnings.warn(
PreferencesChangedWarning(
f"{player} has ranked {other} multiple times."
)
)

if self.clean:
player.set_prefs(unique_prefs)

def _check_inputs_player_prefs_all_in_party(self, party, other_party):
"""Check that each player in :code:`party` has ranked only players in
:code:`other_party`. If :code:`clean`, then forget any extra
preferences."""

players = vars(self)[party]
others = vars(self)[other_party]
for player in players:

for other in player.prefs:
if other not in others:
warnings.warn(
PreferencesChangedWarning(
f"{player} has ranked a non-{other_party[:-1]}: "
f"{other}."
)
)
if self.clean:
player._forget(other)

def _check_inputs_player_prefs_nonempty(self, party, other_party):
"""Make sure that each player in :code:`party` has a nonempty
preference list of players in :code:`other_party`. If :code:`clean`,
remove any such player."""

for player in vars(self)[party]:

if not player.prefs:
warnings.warn(
PlayerExcludedWarning(
f"{player} has an empty preference list."
)
)
if self.clean:
self._remove_player(player, party, other_party)

@abc.abstractmethod
def solve(self):
""" Placeholder for solving the given matching game. """

@abc.abstractmethod
def check_stability(self):
""" Placeholder for checking the stability of the current matching. """

@abc.abstractmethod
def check_validity(self):
""" Placeholder for checking the validity of the current matching. """


class BaseMatching(dict, metaclass=abc.ABCMeta):
"""An abstract base class for the storing and updating of a matching.

Attributes
----------
dictionary : dict or None
If not ``None``, a dictionary mapping a ``Player`` to one of: ``None``,
a single ``Player`` or a list of ``Player`` instances.
"""

def __init__(self, dictionary=None):

self._data = {}
if dictionary is not None:
self._data.update(dictionary)

super().__init__(self._data)

def __repr__(self):

return repr(self._data)

def keys(self):

return self._data.keys()

def values(self):

return self._data.values()

def __getitem__(self, player):

return self._data[player]

@abc.abstractmethod
def __setitem__(self, player, new_match):
""" A placeholder function for how to update the matching. """

def _check_player_in_keys(self, player):
""" Raise an error if :code:`player` is not in the dictionary. """

if player not in self._data.keys():
raise ValueError(f"{player} is not a key in this matching.")

def _check_new_valid_type(self, new, types):
"""Raise an error is :code:`new` is not an instance of one of
:code:`types`."""

if not isinstance(new, types):
raise ValueError(f"{new} is not one of {types} and is not valid.")
Loading