diff --git a/quantecon/game_theory/normal_form_game.py b/quantecon/game_theory/normal_form_game.py index edfd323d0..2e3463700 100644 --- a/quantecon/game_theory/normal_form_game.py +++ b/quantecon/game_theory/normal_form_game.py @@ -288,7 +288,7 @@ def best_response(self, opponents_actions, tie_breaking='smallest', array of floats (mixed action). tie_breaking : str, optional(default='smallest') - str in {'smallest', 'random', False}. Control how, or + str in {'smallest', 'random', False}. Control how, or whether, to break a tie (see Returns for details). payoff_perturbation : array_like(float), optional(default=None) @@ -380,6 +380,77 @@ def random_choice(self, actions=None, random_state=None): else: return idx + def is_dominated(self, action, tol=None, method=None): + """ + Determine whether `action` is strictly dominated by some mixed + action. + + Parameters + ---------- + action : scalar(int) + Integer representing a pure action. + + tol : scalar(float), optional(default=None) + Tolerance level used in determining domination. If None, + default to the value of the `tol` attribute. + + method : str, optional(default=None) + If None, `lemke_howson` from `quantecon.game_theory` is used + to solve for a Nash equilibrium of an auxiliary zero-sum + game. If `method='simplex'`, `scipy.optimize.linprog` is + used with `method='simplex'`. + + Returns + ------- + bool + True if `action` is strictly dominated by some mixed action; + False otherwise. + + """ + if tol is None: + tol = self.tol + + payoff_array = self.payoff_array + + if self.num_opponents == 0: + return payoff_array.max() > payoff_array[action] + tol + + ind = np.ones(self.num_actions, dtype=bool) + ind[action] = False + D = payoff_array[ind] + D -= payoff_array[action] + if self.num_opponents >= 2: + D.shape = (D.shape[0], np.prod(D.shape[1:])) + + if method is None: + from .lemke_howson import lemke_howson + g_zero_sum = NormalFormGame([Player(D), Player(-D.T)]) + NE = lemke_howson(g_zero_sum) + return NE[0] @ D @ NE[1] > tol + elif method in ['simplex']: + from scipy.optimize import linprog + m, n = D.shape + A = np.empty((n+2, m+1)) + A[:n, :m] = -D.T + A[:n, -1] = 1 # Slack variable + A[n, :m], A[n+1, :m] = 1, -1 # Equality constraint + A[n:, -1] = 0 + b = np.empty(n+2) + b[:n] = 0 + b[n], b[n+1] = 1, -1 + c = np.zeros(m+1) + c[-1] = -1 + res = linprog(c, A_ub=A, b_ub=b, method=method) + if res.success: + return res.x[-1] > tol + elif res.status == 2: # infeasible + return False + else: # pragma: no cover + msg = 'scipy.optimize.linprog returned {0}'.format(res.status) + raise RuntimeError(msg) + else: + raise ValueError('Unknown method {0}'.format(method)) + class NormalFormGame: """ diff --git a/quantecon/game_theory/tests/test_normal_form_game.py b/quantecon/game_theory/tests/test_normal_form_game.py index f62d9f29a..4793e8bea 100644 --- a/quantecon/game_theory/tests/test_normal_form_game.py +++ b/quantecon/game_theory/tests/test_normal_form_game.py @@ -69,6 +69,11 @@ def test_is_best_response_against_pure(self): def test_is_best_response_against_mixed(self): ok_(self.player.is_best_response([1/2, 1/2], [2/3, 1/3])) + def test_is_dominated(self): + for action in range(self.player.num_actions): + for method in [None, 'simplex']: + eq_(self.player.is_dominated(action, method=method), False) + class TestPlayer_2opponents: """Test the methods of Player with two opponent players""" @@ -101,6 +106,11 @@ def test_best_response_list_when_tie(self): sorted([0, 1]) ) + def test_is_dominated(self): + for action in range(self.player.num_actions): + for method in [None, 'simplex']: + eq_(self.player.is_dominated(action, method=method), False) + def test_random_choice(): n, m = 5, 4 @@ -113,6 +123,24 @@ def test_random_choice(): ok_(player.random_choice() in actions) +def test_player_corner_cases(): + n, m = 3, 4 + player = Player(np.zeros((n, m))) + for action in range(n): + eq_(player.is_best_response(action, [1/m]*m), True) + for method in [None, 'simplex']: + eq_(player.is_dominated(action, method=method), False) + + e = 1e-8 + player = Player([[-e, -e], [1, -1], [-1, 1]]) + action = 0 + eq_(player.is_best_response(action, [1/2, 1/2], tol=e), True) + eq_(player.is_best_response(action, [1/2, 1/2], tol=e/2), False) + for method in [None, 'simplex']: + eq_(player.is_dominated(action, tol=e, method=method), False) + eq_(player.is_dominated(action, tol=e/2, method=method), True) + + # NormalFormGame # class TestNormalFormGame_Sym2p: @@ -268,6 +296,11 @@ def test_best_response(self): """Trivial player: best_response""" eq_(self.player.best_response(None), 1) + def test_is_dominated(self): + """Trivial player: is_dominated""" + eq_(self.player.is_dominated(0), True) + eq_(self.player.is_dominated(1), False) + class TestNormalFormGame_1p: """Test for trivial NormalFormGame with a single player"""