Skip to content

game_theory.Player: Add is_dominated #327

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

Merged
merged 1 commit into from
Aug 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 72 additions & 1 deletion quantecon/game_theory/normal_form_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
"""
Expand Down
33 changes: 33 additions & 0 deletions quantecon/game_theory/tests/test_normal_form_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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"""
Expand Down