Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 64

Computational game theory in Sagemath

  • Hannah Lorrimore, Cardiff University

  • James Campbell, Cardiff University

  • Tobenna P. Igwe, University of Liverpool (Google Summer of Code)

  • Vincent Knight, Cardiff University


Game Theory

Battle of the sexes game:

(3102)(2103)\begin{pmatrix} 3&1\\ 0&2 \end{pmatrix} \quad \begin{pmatrix} 2&1\\ 0&3 \end{pmatrix}
import gambit gambit_game = gambit.Game.read_game("game.nfg") gambit_game
Error in lines 1-1 Traceback (most recent call last): File "/usr/local/lib/python2.7/dist-packages/smc_sagews/sage_server.py", line 905, in execute exec compile(block+'\n', '', 'single') in namespace, locals File "", line 1, in <module> ImportError: No module named gambit
2+2
4
g = NormalFormGame(gambit_game) g g.obtain_nash()
Normal Form Game with the following utilities: {(0, 1): [1.0, 1.0], (1, 0): [0.0, 0.0], (0, 0): [3.0, 2.0], (1, 1): [2.0, 3.0]} [[(0, 1), (0, 1)], [(3/4, 1/4), (1/4, 3/4)], [(1, 0), (1, 0)]]
# build game A = matrix([[3, 1], [0, 2]]) B = matrix([[2, 1], [0, 3]]) show(A, B) battle_of_the_sexes = NormalFormGame([A, B])
(3102)\displaystyle \left(\begin{array}{rr} 3 & 1 \\ 0 & 2 \end{array}\right) (2103)\displaystyle \left(\begin{array}{rr} 2 & 1 \\ 0 & 3 \end{array}\right)
# show game battle_of_the_sexes
Normal Form Game with the following utilities: {(0, 1): [1, 1], (1, 0): [0, 0], (0, 0): [3, 2], (1, 1): [2, 3]}
latex(battle_of_the_sexes)
\left(\left(\begin{array}{rr} 3 & 1 \\ 0 & 2 \end{array}\right), \left(\begin{array}{rr} 2 & 1 \\ 0 & 3 \end{array}\right)\right)
# solve game battle_of_the_sexes.obtain_nash()
[[(0, 1), (0, 1)], [(3/4, 1/4), (1/4, 3/4)], [(1, 0), (1, 0)]]
# different algorithms battle_of_the_sexes.obtain_nash?
import timeit n = 1 enum_data = [0, 0] lrs_data = [0, 0] LCP_data = [0, 0] for k in range(2, 8): enum_list = [] lrs_list = [] LCP_list = [] for t in range(5): stp = """from sage.game_theory.normal_form_game import NormalFormGame from sage.matrix.constructor import random_matrix from sage.rings.all import ZZ g = NormalFormGame([random_matrix(ZZ, %s), random_matrix(ZZ, %s)])""" % (k, k) enum_t = timeit.Timer("g.obtain_nash(algorithm='enumeration')", stp) lrs_t = timeit.Timer("g.obtain_nash(algorithm='lrs')", stp) LCP_t = timeit.Timer("g.obtain_nash(algorithm='LCP')", stp) enum_list.append(enum_t.timeit(number=n)/n) lrs_list.append(lrs_t.timeit(number=n)/n) LCP_list.append(LCP_t.timeit(number=n)/n) enum_data.append(mean(enum_list)) lrs_data.append(mean(lrs_list)) LCP_data.append(mean(LCP_list))
battle_of_the_sexes.obtain_nash(algorithm='enumeration') battle_of_the_sexes.obtain_nash(algorithm='lrs') battle_of_the_sexes.obtain_nash(algorithm='LCP')
[[(0, 1), (0, 1)], [(3/4, 1/4), (1/4, 3/4)], [(1, 0), (1, 0)]] [[(0, 1), (0, 1)], [(3/4, 1/4), (1/4, 3/4)], [(1, 0), (1, 0)]] [[(0.0, 1.0), (0.0, 1.0)], [(0.75, 0.25), (0.25, 0.75)], [(1.0, 0.0), (1.0, 0.0)]]
battle_of_the_sexes.obtain_nash(algorithm='enumeration') battle_of_the_sexes.obtain_nash(algorithm='lrs') e_of_the_sexes.obtain_nash(algorithm='LCP')
[[(0, 1), (0, 1)], [(3/4, 1/4), (1/4, 3/4)], [(1, 0), (1, 0)]] [[(0, 1), (0, 1)], [(3/4, 1/4), (1/4, 3/4)], [(1, 0), (1, 0)]]
Error in lines 3-3 Traceback (most recent call last): File "/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/.sagemathcloud/sage_server.py", line 879, in execute exec compile(block+'\n', '', 'single') in namespace, locals File "", line 1, in <module> NameError: name 'e_of_the_sexes' is not defined

Degeneracy

# catalog game_theory.normal_form_games.Pigs
x = game_theory.normal_form_games.RPSLS() y = game_theory.normal_form_games.RPS() show(x) print '' show(y)
((0111110111110111110111110),(0111110111110111110111110))\displaystyle \left(\left(\begin{array}{rrrrr} 0 & -1 & 1 & 1 & -1 \\ 1 & 0 & -1 & -1 & 1 \\ -1 & 1 & 0 & 1 & -1 \\ -1 & 1 & -1 & 0 & 1 \\ 1 & -1 & 1 & -1 & 0 \end{array}\right), \left(\begin{array}{rrrrr} 0 & 1 & -1 & -1 & 1 \\ -1 & 0 & 1 & 1 & -1 \\ 1 & -1 & 0 & -1 & 1 \\ 1 & -1 & 1 & 0 & -1 \\ -1 & 1 & -1 & 1 & 0 \end{array}\right)\right)
((011101110),(011101110))\displaystyle \left(\left(\begin{array}{rrr} 0 & -1 & 1 \\ 1 & 0 & -1 \\ -1 & 1 & 0 \end{array}\right), \left(\begin{array}{rrr} 0 & 1 & -1 \\ -1 & 0 & 1 \\ 1 & -1 & 0 \end{array}\right)\right)
x.is_degenerate() y.is_degenerate()
True False

Lemke-Howson

g = game_theory.normal_form_games.RPS() print g._solve_lh(1)
[[[0.333333333333333, 0.333333333333333, 0.333333333333333], [0.333333333333333, 0.333333333333333, 0.333333333333333]]]
print g._lh_find_all()
([Equ [[0, 0, 0], [0, 0, 0]] Labels[0, 0, 0, 0, 0, 0]], [Equ [[0.333333333333333, 0.333333333333333, 0.333333333333333], [0.333333333333333, 0.333333333333333, 0.333333333333333]] Labels[0, 0, 0, 0, 0, 0]])
A = matrix([[10, 0], [5, 5], [0, 9]]) B = matrix([[2, 0], [1, 2], [2, 0]]) A, B g = NormalFormGame([A, B]) neq, peq = g._lh_find_all() for i in neq: print i
([10 0] [ 5 5] [ 0 9], [2 0] [1 2] [2 0]) Equ [[0, 0, 0], [0, 0]] Labels[0, 1, 0, 0, 1] Equ [[0.333333333333333, 0.666666666666667, 0.0], [0.500000000000000, 0.500000000000000]] Labels[1, 0, 1, 1, 0]
for j in peq: print j
Equ [[1.00000000000000, 0.0, 0.0], [1.00000000000000, 0.0]] Labels[0, 1, 0, 0, 1] Equ [[0.0, 0.666666666666667, 0.333333333333333], [0.444444444444444, 0.555555555555556]] Labels[1, 0, 1, 1, 0]
b, n, p = g._lh_bipartite_graph() b.plot(edge_labels=True)

##N-player Games

threegame = NormalFormGame() threegame.add_player(2) # Adding first player with 2 strategies threegame.add_player(2) # Adding second player with 2 strategies threegame.add_player(2) # Adding third player with 2 strategies threegame[0, 0, 0][0] = 3 threegame[0, 0, 0][1] = 1 threegame[0, 0, 0][2] = 4 threegame[0, 0, 1] = [1, 5 ,9] threegame[0, 1, 0] = [2, 6, 5] threegame[0, 1, 1] = [3, 5, 8] threegame[1, 0, 0] = [9, 7, 9] threegame[1, 0, 1] = [3, 2, 3] threegame[1, 1, 0] = [8, 4, 6] threegame[1, 1, 1] = [2, 6, 4] print threegame
Normal Form Game with the following utilities: {(0, 1, 1): [3, 5, 8], (1, 1, 0): [8, 4, 6], (1, 0, 0): [9, 7, 9], (0, 0, 1): [1, 5, 9], (1, 0, 1): [3, 2, 3], (0, 0, 0): [3, 1, 4], (0, 1, 0): [2, 6, 5], (1, 1, 1): [2, 6, 4]}
for i in threegame._solve_gambit_enum_poly(): print i
[(0.0, 1.0), (1.0, 0.0), (1.0, 0.0)] [(0.4, 0.6), (0.0, 1.0), (0.0, 1.0)] [(1.0, 0.0), (0.0, 1.0), (0.0, 1.0)]
for i in threegame._solve_gambit_simpdiv(): print i
[(Fraction(0, 1), Fraction(1, 1)), (Fraction(1, 1), Fraction(0, 1)), (Fraction(1, 1), Fraction(0, 1))]
threeplayer = NormalFormGame() threeplayer.add_player(2) threeplayer.add_player(2) threeplayer.add_player(2) threeplayer
Normal Form Game with the following utilities: {(0, 1, 1): [False, False, False], (1, 1, 0): [False, False, False], (1, 0, 0): [False, False, False], (0, 0, 1): [False, False, False], (1, 0, 1): [False, False, False], (0, 0, 0): [False, False, False], (0, 1, 0): [False, False, False], (1, 1, 1): [False, False, False]}
for i in threeplayer._solve_gambit_enum_poly(): print i
[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)] [(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)] [(0.0, 1.0), (1.0, 0.0), (1.0, 0.0)] [(0.0, 1.0), (1.0, 0.0), (1.0, 0.0)] [(1.0, 0.0), (0.0, 1.0), (0.0, 1.0)] [(1.0, 0.0), (0.0, 1.0), (0.0, 1.0)] [(1.0, 0.0), (1.0, 0.0), (1.0, 0.0)] [(1.0, 0.0), (1.0, 0.0), (1.0, 0.0)]
threeplayer._solve_gambit_ipa()
Error in lines 1-1 Traceback (most recent call last): File "/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/.sagemathcloud/sage_server.py", line 879, in execute exec compile(block+'\n', '', 'single') in namespace, locals File "", line 1, in <module> File "/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/sage/local/lib/python2.7/site-packages/sage/game_theory/normal_form_game.py", line 1859, in _solve_gambit_ipa return self._solve_gambit(ExternalIteratedPolymatrixSolver()) File "/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/sage/local/lib/python2.7/site-packages/sage/game_theory/normal_form_game.py", line 1832, in _solve_gambit output = solver.solve(g) File "build/bdist.linux-x86_64/egg/gambit/nash.py", line 214, in solve game, rational=False) File "build/bdist.linux-x86_64/egg/gambit/nash.py", line 62, in _parse_output for line in stream: File "sage/ext/interrupt/interrupt.pyx", line 197, in sage.ext.interrupt.interrupt.sage_python_check_interrupt (/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/sage/src/build/cythonized/sage/ext/interrupt/interrupt.c:1742) sig_check() File "sage/ext/interrupt/interrupt.pyx", line 86, in sage.ext.interrupt.interrupt.sig_raise_exception (/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/sage/src/build/cythonized/sage/ext/interrupt/interrupt.c:883) raise KeyboardInterrupt KeyboardInterrupt

## Extensive form games

player_1 = EFG_Player('Bob') player_2 = EFG_Player('Celine') leaf_4 = EFG_Leaf({player_1: 2, player_2: 3}) leaf_3 = EFG_Leaf({player_1: 0, player_2: 0}) leaf_2 = EFG_Leaf({player_1: 1, player_2: 1}) leaf_1 = EFG_Leaf({player_1: 3, player_2: 2}) node_3 = EFG_Node({'Sports': leaf_3, 'Comedy': leaf_4}, 'Node 3', player_2) node_2 = EFG_Node({'Sports': leaf_1, 'Comedy': leaf_2}, 'Node 2', player_2) node_1 = EFG_Node({'Sports': node_2, 'Comedy': node_3}, 'Tree Root', player_1)
Error in lines 7-7 Traceback (most recent call last): File "/usr/local/lib/python2.7/dist-packages/smc_sagews/sage_server.py", line 905, in execute exec compile(block+'\n', '', 'single') in namespace, locals File "", line 1, in <module> File "sage/misc/lazy_import.pyx", line 383, in sage.misc.lazy_import.LazyImport.__call__ (/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/RW/RW-sage-game-theory/src/build/cythonized/sage/misc/lazy_import.c:3457) return self._get_object()(*args, **kwds) File "/projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/RW/RW-sage-game-theory/local/lib/python2.7/site-packages/sage/game_theory/extensive_form_game.py", line 4342, in __init__ raise TypeError("Node must be passed an args in the form of a " TypeError: Node must be passed an args in the form of a dictionary.
efg_battle_of_the_sexes = ExtensiveFormGame(node_1) efg_battle_of_the_sexes
Extensive Form Game with the following underlying tree: {Extensive form game node with name: Node 2: [Extensive form game leaf with utilities given by: (1, 1), Extensive form game leaf with utilities given by: (3, 2)], Extensive form game node with name: Node 3: [Extensive form game leaf with utilities given by: (2, 3), Extensive form game leaf with utilities given by: (0, 0)], Extensive form game node with name: Tree Root: [Extensive form game node with name: Node 3, Extensive form game node with name: Node 2]}
efg_battle_of_the_sexes.plot()
efg_battle_of_the_sexes.plot_info_sets()
efg_battle_of_the_sexes.set_info_set([node_2, node_3])
efg_battle_of_the_sexes.plot() efg_battle_of_the_sexes.plot_info_sets()
efg_battle_of_the_sexes.obtain_nash()
[[[{'Tree Root': {'Comedy': 0.0, 'Sports': 1.0}}], [{'Node 2': {'Comedy': 0.0, 'Sports': 1.0}, 'Node 3': {'Comedy': 0.0, 'Sports': 1.0}}]], [[{'Tree Root': {'Comedy': 0.25, 'Sports': 0.75}}], [{'Node 2': {'Comedy': 0.75, 'Sports': 0.25}, 'Node 3': {'Comedy': 0.75, 'Sports': 0.25}}]], [[{'Tree Root': {'Comedy': 1.0, 'Sports': 0.0}}], [{'Node 2': {'Comedy': 1.0, 'Sports': 0.0}, 'Node 3': {'Comedy': 1.0, 'Sports': 0.0}}]]]
efg_gambit_game = gambit.Game.read_game("efg_game.efg") efg_battle_of_the_sexes_2 = ExtensiveFormGame(efg_gambit_game) efg_battle_of_the_sexes_2.plot()
Error in lines 1-1 Traceback (most recent call last): File "/usr/local/lib/python2.7/dist-packages/smc_sagews/sage_server.py", line 905, in execute exec compile(block+'\n', '', 'single') in namespace, locals File "", line 1, in <module> NameError: name 'gambit' is not defined

Interface between EFG and NFG

Open Tickets

  • #18536: Solvers for constant sum games

  • #18679: Deprecation of maximization within NormalFormGame

  • #18712: Create test for Degeneracy in Normal Form Games

  • #18945: Game Theory: Build class for extensive form games

## Thanks

  • Travis Scrimshaw (UC Davis \to University of Minnesota)

  • Karl Dieters Krisman (Gordon College, Boston)

suitr_pref = {'J': ('A', 'D', 'C', 'B'), 'K': ('A', 'B', 'C', 'D'), 'L': ('B', 'D', 'C', 'A'), 'M': ('C', 'A', 'B', 'D')} reviewr_pref = {'A': ('L', 'J', 'K', 'M'), 'B': ('J', 'M', 'L', 'K'), 'C': ('K', 'M', 'L', 'J'), 'D': ('M', 'K', 'J', 'L')} m = MatchingGame([suitr_pref, reviewr_pref]) m.solve()
{'K': 'C', 'J': 'A', 'M': 'B', 'L': 'D'}
m.plot()
g = MatchingGame??
File: /projects/2a31f88b-4244-4bd1-8e3a-3169ff24daac/sage/src/sage/misc/lazy_import.pyx Source: class MatchingGame(SageObject): r""" A matching game. A matching game (also called a stable matching problem) models a situation in a population of `N` suitors and `N` reviewers. Suitors and reviewers rank their preferences and attempt to find a match. Formally, a matching game of size `N` is defined by two disjoint sets `S` and `R` of size `N`. Associated to each element of `S` and `R` is a preference list: .. MATH:: f : S \to R^N \text{ and } g : R \to S^N. Here is an example of matching game on 4 players: .. MATH:: S = \{J, K, L, M\}, \\ R = \{A, B, C, D\}. With preference functions: .. MATH:: f(s) = \begin{cases} (A, D, C, B) & \text{ if } s=J,\\ (A, B, C, D) & \text{ if } s=K,\\ (B, D, C, A) & \text{ if } s=L,\\ (C, A, B, D) & \text{ if } s=M,\\ \end{cases} g(s) = \begin{cases} (L, J, K, M) & \text{ if } s=A,\\ (J, M, L, K) & \text{ if } s=B,\\ (K, M, L, J) & \text{ if } s=C,\\ (M, K, J, L) & \text{ if } s=D.\\ \end{cases} INPUT: Two potential inputs are accepted (see below to see the effect of each): - ``reviewer/suitors_preferences`` -- a dictionary containing the preferences of all players: * key - each reviewer/suitors * value - a tuple of suitors/reviewers OR: - ``integer`` -- an integer simply representing the number of reviewers and suitors. To implement the above game in Sage:: sage: suitr_pref = {'J': ('A', 'D', 'C', 'B'), ....: 'K': ('A', 'B', 'C', 'D'), ....: 'L': ('B', 'D', 'C', 'A'), ....: 'M': ('C', 'A', 'B', 'D')} sage: reviewr_pref = {'A': ('L', 'J', 'K', 'M'), ....: 'B': ('J', 'M', 'L', 'K'), ....: 'C': ('K', 'M', 'L', 'J'), ....: 'D': ('M', 'K', 'J', 'L')} sage: m = MatchingGame([suitr_pref, reviewr_pref]) sage: m A matching game with 4 suitors and 4 reviewers sage: m.suitors() ('K', 'J', 'M', 'L') sage: m.reviewers() ('A', 'C', 'B', 'D') A matching `M` is any bijection between `S` and `R`. If `s \in S` and `r \in R` are matched by `M` we denote: .. MATH:: M(s) = r. On any given matching game, one intends to find a matching that is stable. In other words, so that no one individual has an incentive to break their current match. Formally, a stable matching is a matching that has no blocking pairs. A blocking pair is any pair `(s, r)` such that `M(s) \neq r` but `s` prefers `r` to `M(r)` and `r` prefers `s` to `M^{-1}(r)`. To obtain the stable matching in Sage we use the ``solve`` method which uses the extended Gale-Shapley algorithm [DI1989]_:: sage: m.solve() {'J': 'A', 'K': 'C', 'L': 'D', 'M': 'B'} Matchings have a natural representations as bipartite graphs:: sage: plot(m) Graphics object consisting of 13 graphics primitives The above plots the bipartite graph associated with the matching. This plot can be accessed directly:: sage: graph = m.bipartite_graph() sage: graph Bipartite graph on 8 vertices It is possible to initiate a matching game without having to name each suitor and reviewer:: sage: n = 10 sage: big_game = MatchingGame(n) sage: big_game.suitors() (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) sage: big_game.reviewers() (-1, -2, -3, -4, -5, -6, -7, -8, -9, -10) If we attempt to obtain the stable matching for the above game, without defining the preference function we obtain an error:: sage: big_game.solve() Traceback (most recent call last): ... ValueError: suitor preferences are not complete To continue we have to populate the preference dictionary. Here is one example where the preferences are simply the corresponding element of the permutation group:: sage: from itertools import permutations sage: suitr_preferences = list(permutations([-i-1 for i in range(n)])) sage: revr_preferences = list(permutations([i+1 for i in range(n)])) sage: for player in range(n): ....: big_game.suitors()[player].pref = suitr_preferences[player] ....: big_game.reviewers()[player].pref = revr_preferences[-player] sage: big_game.solve() {1: -1, 2: -8, 3: -9, 4: -10, 5: -7, 6: -6, 7: -5, 8: -4, 9: -3, 10: -2} Note that we can also combine the two ways of creating a game. For example here is an initial matching game:: sage: suitrs = {'Romeo': ('Juliet', 'Rosaline'), ....: 'Mercutio': ('Juliet', 'Rosaline')} sage: revwrs = {'Juliet': ('Romeo', 'Mercutio'), ....: 'Rosaline': ('Mercutio', 'Romeo')} sage: g = MatchingGame(suitrs, revwrs) Let us assume that all of a sudden a new pair of suitors and reviewers is added but their names are not known:: sage: g.add_reviewer() sage: g.add_suitor() sage: g.reviewers() ('Rosaline', 'Juliet', -3) sage: g.suitors() ('Mercutio', 'Romeo', 3) Note that when adding a reviewer or a suitor all preferences are wiped:: sage: [s.pref for s in g.suitors()] [[], [], []] sage: [r.pref for r in g.reviewers()] [[], [], []] If we now try to solve the game we will get an error as we have not specified the preferences which will need to be updated:: sage: g.solve() Traceback (most recent call last): ... ValueError: suitor preferences are not complete Here we update the preferences so that the new reviewers and suitors don't affect things too much (they prefer each other and are the least preferred of the others):: sage: g.suitors()[0].pref = suitrs['Mercutio'] + (-3,) sage: g.suitors()[1].pref = suitrs['Romeo'] + (-3,) sage: g.suitors()[2].pref = (-3, 'Juliet', 'Rosaline') sage: g.reviewers()[0].pref = revwrs['Rosaline'] + (3,) sage: g.reviewers()[1].pref = revwrs['Juliet'] + (3,) sage: g.reviewers()[2].pref = (3, 'Romeo', 'Mercutio') Now the game can be solved:: sage: D = g.solve() sage: D['Mercutio'] 'Rosaline' sage: D['Romeo'] 'Juliet' sage: D[3] -3 Note that the above could be equivalently (and more simply) carried out by simply updated the original preference dictionaries:: sage: for key in suitrs: ....: suitrs[key] = suitrs[key] + (-3,) sage: for key in revwrs: ....: revwrs[key] = revwrs[key] + (3,) sage: suitrs[3] = (-3, 'Juliet', 'Rosaline') sage: revwrs[-3] = (3, 'Romeo', 'Mercutio') sage: g = MatchingGame(suitrs, revwrs) sage: D = g.solve() sage: D['Mercutio'] 'Rosaline' sage: D['Romeo'] 'Juliet' sage: D[3] -3 It can be shown that the Gale-Shapley algorithm will return the stable matching that is optimal from the point of view of the suitors and is in fact the worst possible matching from the point of view of the reviewers. To quickly obtain the matching that is optimal for the reviewers we use the ``solve`` method with the ``invert=True`` option:: sage: left_dict = {'a': ('A', 'B', 'C'), ....: 'b': ('B', 'C', 'A'), ....: 'c': ('B', 'A', 'C')} sage: right_dict = {'A': ('b', 'c', 'a'), ....: 'B': ('a', 'c', 'b'), ....: 'C': ('a', 'b', 'c')} sage: quick_game = MatchingGame([left_dict, right_dict]) sage: quick_game.solve() {'a': 'A', 'b': 'C', 'c': 'B'} sage: quick_game.solve(invert=True) {'A': 'c', 'B': 'a', 'C': 'b'} EXAMPLES: 8 player letter game:: sage: suitr_pref = {'J': ('A', 'D', 'C', 'B'), ....: 'K': ('A', 'B', 'C', 'D'), ....: 'L': ('B', 'D', 'C', 'A'), ....: 'M': ('C', 'A', 'B', 'D')} sage: reviewr_pref = {'A': ('L', 'J', 'K', 'M'), ....: 'B': ('J', 'M', 'L', 'K'), ....: 'C': ('K', 'M', 'L', 'J'), ....: 'D': ('M', 'K', 'J', 'L')} sage: m = MatchingGame([suitr_pref, reviewr_pref]) sage: m._suitors ['K', 'J', 'M', 'L'] sage: m._reviewers ['A', 'C', 'B', 'D'] Also works for numbers:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) Can create a game from an integer. This gives default set of preference functions:: sage: g = MatchingGame(3) sage: g A matching game with 3 suitors and 3 reviewers We have an empty set of preferences for a default named set of preferences:: sage: for s in g.suitors(): ....: s, s.pref (1, []) (2, []) (3, []) sage: for r in g.reviewers(): ....: r, r.pref (-1, []) (-2, []) (-3, []) Before trying to solve such a game the algorithm will check if it is complete or not:: sage: g.solve() Traceback (most recent call last): ... ValueError: suitor preferences are not complete To be able to obtain the stable matching we must input the preferences:: sage: for s in g.suitors(): ....: s.pref = (-1, -2, -3) sage: for r in g.reviewers(): ....: r.pref = (1, 2, 3) sage: g.solve() {1: -1, 2: -2, 3: -3} REFERENCES: .. [DI1989] Dan Gusfield and Robert W. Irving. *The stable marriage problem: structure and algorithms*. Vol. 54. Cambridge: MIT press, 1989. """ def __init__(self, generator, revr=None): r""" Initialize a matching game and check the inputs. TESTS:: sage: suit = {0: (3, 4), 1: (3, 4)} sage: revr = {3: (0, 1), 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: TestSuite(g).run() sage: g = MatchingGame(3) sage: TestSuite(g).run() sage: g2 = MatchingGame(QQ(3)) sage: g == g2 True The above shows that the input can be either two dictionaries or an integer:: sage: g = MatchingGame(suit, 3) Traceback (most recent call last): ... TypeError: generator must be an integer or a pair of 2 dictionaries sage: g = MatchingGame(matrix(2, [1, 2, 3, 4])) Traceback (most recent call last): ... TypeError: generator must be an integer or a pair of 2 dictionaries sage: g = MatchingGame('1,2,3', 'A,B,C') Traceback (most recent call last): ... TypeError: generator must be an integer or a pair of 2 dictionaries """ self._suitors = [] self._reviewers = [] if revr is not None: generator = [generator, revr] if generator in ZZ: for i in range(generator): self.add_suitor() self.add_reviewer() elif isinstance(generator[0], dict) and isinstance(generator[1], dict): for i in generator[0]: self.add_suitor(i) for k in generator[1]: self.add_reviewer(k) for i in self._suitors: i.pref = generator[0][i._name] for k in self._reviewers: k.pref = generator[1][k._name] else: raise TypeError("generator must be an integer or a pair of 2 dictionaries") def _repr_(self): r""" Return a basic representation of the game stating how many players are in the game. EXAMPLES: Matching game with 2 reviewers and 2 suitors:: sage: M = MatchingGame(2) sage: M A matching game with 2 suitors and 2 reviewers """ return 'A matching game with {} suitors and {} reviewers'.format( len(self._suitors), len(self._reviewers)) def _latex_(self): r""" Create the LaTeX representation of the dictionaries for suitors and reviewers. EXAMPLES:: sage: suit = {0: (3, 4), 1: (3, 4)} sage: revr = {3: (0, 1), 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: latex(g) \text{Suitors:} \begin{aligned} \\ 0 & \to (3, 4) \\ 1 & \to (3, 4) \end{aligned} \text{Reviewers:} \begin{aligned} \\ 3 & \to (0, 1) \\ 4 & \to (1, 0) \end{aligned} """ output = "\\text{Suitors:}\n\\begin{aligned}" for suitor in self._suitors: output += "\n\\\\ %s & \\to %s"%(suitor, suitor.pref) output += "\n\\end{aligned}\n\\text{Reviewers:}\n\\begin{aligned}" for reviewer in self._reviewers: output += "\n\\\\ %s & \\to %s"%(reviewer, reviewer.pref) return output + "\n\\end{aligned}" def __eq__(self, other): """ Check equality. sage: suit = {0: (3, 4), 1: (3, 4)} sage: revr = {3: (0, 1), 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g2 = MatchingGame([suit, revr]) sage: g == g2 True Here the two sets of suitors have different preferences:: sage: suit1 = {0: (3, 4), 1: (3, 4)} sage: revr1 = {3: (1, 0), 4: (1, 0)} sage: g1 = MatchingGame([suit1, revr1]) sage: suit2 = {0: (4, 3), 1: (3, 4)} sage: revr2 = {3: (1, 0), 4: (1, 0)} sage: g2 = MatchingGame([suit2, revr2]) sage: g == g2 False Here the two sets of reviewers have different preferences:: sage: suit1 = {0: (3, 4), 1: (3, 4)} sage: revr1 = {3: (0, 1), 4: (1, 0)} sage: g1 = MatchingGame([suit1, revr1]) sage: suit2 = {0: (3, 4), 1: (3, 4)} sage: revr2 = {3: (1, 0), 4: (0, 1)} sage: g2 = MatchingGame([suit2, revr2]) sage: g == g2 False Note that if two games are created with players ordered differently they can still be equal:: sage: g1 = MatchingGame(1) sage: g1.add_reviewer(-2) sage: g1.add_reviewer(-3) sage: g1.add_suitor(3) sage: g1.add_suitor(2) sage: g1.reviewers() (-1, -2, -3) sage: g1.suitors() (1, 3, 2) sage: g2 = MatchingGame(1) sage: g2.add_reviewer(-2) sage: g2.add_reviewer(-3) sage: g2.add_suitor(2) sage: g2.add_suitor(3) sage: g2.reviewers() (-1, -2, -3) sage: g2.suitors() (1, 2, 3) sage: g1 == g2 True """ return (isinstance(other, MatchingGame) and set(self._suitors) == set(other._suitors) and set(self._reviewers) == set(other._reviewers) and all(r1.pref == r2.pref for r1, r2 in zip(set(self._reviewers), set(other._reviewers))) and all(s1.pref == s2.pref for s1, s2 in zip(set(self._suitors), set(other._suitors)))) def __hash__(self): """ Raise an error because this is mutable. EXAMPLES:: sage: hash(MatchingGame(3)) Traceback (most recent call last): ... TypeError: unhashable because matching games are mutable """ raise TypeError("unhashable because matching games are mutable") def plot(self): r""" Create the plot representing the stable matching for the game. Note that the game must be solved for this to work. EXAMPLES: An error is returned if the game is not solved:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: plot(g) Traceback (most recent call last): ... ValueError: game has not been solved yet sage: g.solve() {0: 3, 1: 4} sage: plot(g) Graphics object consisting of 7 graphics primitives """ pl = self.bipartite_graph() return pl.plot() def bipartite_graph(self): r""" Construct a ``BipartiteGraph`` Object of the game. This method is similar to the plot method. Note that the game must be solved for this to work. EXAMPLES: An error is returned if the game is not solved:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g.bipartite_graph() Traceback (most recent call last): ... ValueError: game has not been solved yet sage: g.solve() {0: 3, 1: 4} sage: g.bipartite_graph() Bipartite graph on 4 vertices """ self._is_solved() graph = BipartiteGraph(self._sol_dict) return graph def _is_solved(self): r""" Raise an error if the game has not been solved yet. EXAMPLES:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g._is_solved() Traceback (most recent call last): ... ValueError: game has not been solved yet sage: g.solve() {0: 3, 1: 4} sage: g._is_solved() """ suitor_check = all(s.partner for s in self._suitors) reviewer_check = all(r.partner for r in self._reviewers) if not suitor_check or not reviewer_check: raise ValueError("game has not been solved yet") def _is_complete(self): r""" Raise an error if all players do not have acceptable preferences. EXAMPLES: Not enough reviewers:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 1)} sage: g = MatchingGame([suit, revr]) sage: g._is_complete() Traceback (most recent call last): ... ValueError: must have the same number of reviewers as suitors Not enough suitors:: sage: suit = {0: (3, 4)} sage: revr = {1: (0, 2), ....: 3: (0, 1)} sage: g = MatchingGame([suit, revr]) sage: g._is_complete() Traceback (most recent call last): ... ValueError: must have the same number of reviewers as suitors Suitors preferences are incomplete:: sage: suit = {0: (3, 8), ....: 1: (0, 0)} sage: revr = {3: (0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g._is_complete() Traceback (most recent call last): ... ValueError: suitor preferences are not complete Reviewer preferences are incomplete:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 2, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g._is_complete() Traceback (most recent call last): ... ValueError: reviewer preferences are not complete Suitor preferences have repetitions:: sage: suit = {0: (3, 4), ....: 1: (3, 4)} sage: revr = {3: (0, 0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g._is_complete() Traceback (most recent call last): ... ValueError: reviewer preferences contain repetitions Reviewer preferences have repetitions:: sage: suit = {0: (3, 4, 3), ....: 1: (3, 4)} sage: revr = {3: (0, 1), ....: 4: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g._is_complete() Traceback (most recent call last): ... ValueError: suitor preferences contain repetitions """ if len(self._suitors) != len(self._reviewers): raise ValueError("must have the same number of reviewers as suitors") for suitor in self._suitors: if set(suitor.pref) != set(self._reviewers): raise ValueError("suitor preferences are not complete") for reviewer in self._reviewers: if set(reviewer.pref) != set(self._suitors): raise ValueError("reviewer preferences are not complete") for reviewer in self._reviewers: if len(set(reviewer.pref)) < len(reviewer.pref): raise ValueError("reviewer preferences contain repetitions") for suitor in self._suitors: if len(set(suitor.pref)) < len(suitor.pref): raise ValueError("suitor preferences contain repetitions") def add_suitor(self, name=None): r""" Add a suitor to the game. INPUTS: - ``name`` -- can be a string or a number; if left blank will automatically generate an integer EXAMPLES: Creating a two player game:: sage: g = MatchingGame(2) sage: g.suitors() (1, 2) Adding a suitor without specifying a name:: sage: g.add_suitor() sage: g.suitors() (1, 2, 3) Adding a suitor while specifying a name:: sage: g.add_suitor('D') sage: g.suitors() (1, 2, 3, 'D') Note that now our game is no longer complete:: sage: g._is_complete() Traceback (most recent call last): ... ValueError: must have the same number of reviewers as suitors Note that an error is raised if one tries to add a suitor with a name that already exists:: sage: g.add_suitor('D') Traceback (most recent call last): ... ValueError: a suitor with name "D" already exists If we add a suitor without passing a name then the name of the suitor will not use one that is already chosen:: sage: suit = {0: (-1, -2), ....: 2: (-2, -1)} sage: revr = {-1: (0, 1), ....: -2: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g.suitors() (0, 2) sage: g.add_suitor() sage: g.suitors() (0, 2, 3) """ if name is None: name = len(self._suitors) + 1 while name in self._suitors: name += 1 if any(s._name == name for s in self._suitors): raise ValueError('a suitor with name "{}" already exists'.format(name)) new_suitor = Player(name) self._suitors.append(new_suitor) for r in self._reviewers: r.pref = [] def add_reviewer(self, name=None): r""" Add a reviewer to the game. INPUTS: - ``name`` -- can be a string or number; if left blank will automatically generate an integer EXAMPLES: Creating a two player game:: sage: g = MatchingGame(2) sage: g.reviewers() (-1, -2) Adding a suitor without specifying a name:: sage: g.add_reviewer() sage: g.reviewers() (-1, -2, -3) Adding a suitor while specifying a name:: sage: g.add_reviewer(10) sage: g.reviewers() (-1, -2, -3, 10) Note that now our game is no longer complete:: sage: g._is_complete() Traceback (most recent call last): ... ValueError: must have the same number of reviewers as suitors Note that an error is raised if one tries to add a reviewer with a name that already exists:: sage: g.add_reviewer(10) Traceback (most recent call last): ... ValueError: a reviewer with name "10" already exists If we add a reviewer without passing a name then the name of the reviewer will not use one that is already chosen:: sage: suit = {0: (-1, -3), ....: 1: (-3, -1)} sage: revr = {-1: (0, 1), ....: -3: (1, 0)} sage: g = MatchingGame([suit, revr]) sage: g.reviewers() (-3, -1) sage: g.add_reviewer() sage: g.reviewers() (-3, -1, -4) """ if name is None: name = -len(self._reviewers) - 1 while name in self._reviewers: name -= 1 if any(r._name == name for r in self._reviewers): raise ValueError('a reviewer with name "{}" already exists'.format(name)) new_reviewer = Player(name) self._reviewers.append(new_reviewer) for s in self._suitors: s.pref = [] def suitors(self): """ Return the suitors of ``self``. EXAMPLES:: sage: g = MatchingGame(2) sage: g.suitors() (1, 2) """ return tuple(self._suitors) def reviewers(self): """ Return the reviewers of ``self``. EXAMPLES:: sage: g = MatchingGame(2) sage: g.reviewers() (-1, -2) """ return tuple(self._reviewers) def solve(self, invert=False): r""" Compute a stable matching for the game using the Gale-Shapley algorithm. EXAMPLES:: sage: suitr_pref = {'J': ('A', 'D', 'C', 'B'), ....: 'K': ('A', 'B', 'C', 'D'), ....: 'L': ('B', 'C', 'D', 'A'), ....: 'M': ('C', 'A', 'B', 'D')} sage: reviewr_pref = {'A': ('L', 'J', 'K', 'M'), ....: 'B': ('J', 'M', 'L', 'K'), ....: 'C': ('M', 'K', 'L', 'J'), ....: 'D': ('M', 'K', 'J', 'L')} sage: m = MatchingGame([suitr_pref, reviewr_pref]) sage: m.solve() {'J': 'A', 'K': 'D', 'L': 'B', 'M': 'C'} sage: suitr_pref = {'J': ('A', 'D', 'C', 'B'), ....: 'K': ('A', 'B', 'C', 'D'), ....: 'L': ('B', 'C', 'D', 'A'), ....: 'M': ('C', 'A', 'B', 'D')} sage: reviewr_pref = {'A': ('L', 'J', 'K', 'M'), ....: 'B': ('J', 'M', 'L', 'K'), ....: 'C': ('M', 'K', 'L', 'J'), ....: 'D': ('M', 'K', 'J', 'L')} sage: m = MatchingGame([suitr_pref, reviewr_pref]) sage: m.solve(invert=True) {'A': 'L', 'B': 'J', 'C': 'M', 'D': 'K'} sage: suitr_pref = {1: (-1,)} sage: reviewr_pref = {-1: (1,)} sage: m = MatchingGame([suitr_pref, reviewr_pref]) sage: m.solve() {1: -1} sage: suitr_pref = {} sage: reviewr_pref = {} sage: m = MatchingGame([suitr_pref, reviewr_pref]) sage: m.solve() {} TESTS: This also works for players who are both a suitor and reviewer:: sage: suit = {0: (3,4,2), 1: (3,4,2), 2: (2,3,4)} sage: revr = {2: (2,0,1), 3: (0,1,2), 4: (1,0,2)} sage: g = MatchingGame(suit, revr) sage: g.solve() {0: 3, 1: 4, 2: 2} """ self._is_complete() for s in self._suitors: s.partner = None for r in self._reviewers: r.partner = None if invert: reviewers = deepcopy(self._suitors) suitors = deepcopy(self._reviewers) else: suitors = deepcopy(self._suitors) reviewers = deepcopy(self._reviewers) while any(s.partner is None for s in suitors): s = None for x in suitors: if x.partner is None: s = x break r = next((x for x in reviewers if x == s.pref[0]), None) if r.partner is None: r.partner = s s.partner = r elif r.pref.index(s._name) < r.pref.index(r.partner._name): r.partner.partner = None r.partner = s s.partner = r else: s.pref = s.pref[1:] if invert: suitors, reviewers = reviewers, suitors for i, j in zip(self._suitors, suitors): i.partner = j.partner for i, j in zip(self._reviewers, reviewers): i.partner = j.partner self._sol_dict = {} for s in self._suitors: self._sol_dict[s] = [s.partner] for r in self._reviewers: self._sol_dict[r] = [r.partner] if invert: return {key:self._sol_dict[key][0] for key in self._reviewers} return {key:self._sol_dict[key][0] for key in self._suitors}