Daisuke Oyama
Faculty of Economics, University of Tokyo
In [1]:
import numpy as np
In [2]:
np.set_printoptions(precision=5) # Reduce the number of digits printed
In [3]:
A = np.array([[3, 3],
[2, 5],
[0 ,6]])
B_T = np.array([[3, 2, 3],
[2, 6, 1]])
m, n = A.shape # Numbers of actions of the players
To be consistent with the 0-based indexing in Python, we call the players 0 and 1.
Build the tableau for each player:
In [4]:
# Player 0
tableau0 = np.empty((n, m+n+1))
tableau0[:, :m] = B_T
tableau0[:, m:m+n] = np.identity(n)
tableau0[:, -1] = 1
In [5]:
# One-line commamd
# tableau0 = np.hstack((B_T, np.identity(n), np.ones((n, 1))))
In [6]:
tableau0
Out[6]:
In [7]:
# Player 1
tableau1 = np.empty((m, n+m+1))
tableau1[:, :m] = np.identity(m)
tableau1[:, m:m+n] = A
tableau1[:, -1] = 1
In [8]:
# One-line command
# tableau1 = np.hstack((np.identity(m), A, np.ones((m, 1))))
In [9]:
tableau1
Out[9]:
Denote the player 0's variables by $x_0, x_1, x_2$ and $s_3, s_4$, and the player 1's variables by $r_0, r_1, r_2$ and $y_3, y_4$.
The initial basic variables are $s_3, s_4$ and $r_0, r_1, r_2$.
In [10]:
basic_vars0 = np.arange(m, m+n)
basic_vars0
Out[10]:
In [11]:
basic_vars1 = np.arange(0, m)
basic_vars1
Out[11]:
Let the initial pivot index be 1
,
so that $x_1$ is to enter the basis:
In [12]:
init_pivot = 1
In [13]:
# Current pivot
pivot = init_pivot
Determine the basic variable to leave by the minimum ratio test:
In [14]:
ratios = tableau0[:, -1] / tableau0[:, pivot]
ratios
Out[14]:
In [15]:
row_min = ratios.argmin()
row_min
Out[15]:
In [16]:
basic_vars0[row_min]
Out[16]:
$s_4$ is the basic variable that leaves the basis.
Update the tableau:
In [17]:
tableau0[row_min, :] /= tableau0[row_min, pivot]
In [18]:
tableau0
Out[18]:
In [19]:
for i in range(tableau0.shape[0]):
if i != row_min:
tableau0[i, :] -= tableau0[row_min, :] * tableau0[i, pivot]
In [20]:
# Another approach by a NumPy trick
# ind = np.ones(tableau0.shape[0], dtype=bool)
# ind[row_min] = False
# tableau0[ind, :] -= tableau0[row_min, :] * tableau0[ind, :][:, [pivot]]
In [21]:
tableau0
Out[21]:
Update the basic variables and the pivot for the next step:
In [22]:
basic_vars0[row_min], pivot = pivot, basic_vars0[row_min]
In [23]:
basic_vars0
Out[23]:
In [24]:
basic_vars0[row_min]
Out[24]:
In [25]:
pivot
Out[25]:
That is, $x_1$ has become a basic variable, while $s_4$ becomes a nonbasic variable (i.e., $s_4 = 0$).
If the new pivot is equal to the initial pivot, we are done.
In [26]:
pivot == init_pivot
Out[26]:
But this is False
, so we continue.
In the next step, the variable $y_4$ which is complementary to $s_4$ (i.e., $y_4 s_4 = 0$) becomes a basic variable.
In [27]:
tableau1
Out[27]:
In [28]:
ratios = tableau1[:, -1] / tableau1[:, pivot]
row_min = ratios.argmin()
row_min
Out[28]:
In [29]:
tableau1[row_min, :] /= tableau1[row_min, pivot]
In [30]:
tableau1
Out[30]:
In [31]:
ind = np.ones(tableau1.shape[0], dtype=bool)
ind[row_min] = False
tableau1[ind, :] -= tableau1[row_min, :] * tableau1[ind, :][:, [pivot]]
In [32]:
tableau1
Out[32]:
In [33]:
basic_vars1[row_min], pivot = pivot, basic_vars1[row_min]
In [34]:
pivot
Out[34]:
In [35]:
basic_vars1
Out[35]:
In [36]:
basic_vars1[row_min]
Out[36]:
That is, $y_4$ has become a basic variable, while $r_2$ becomes a nonbasic variable.
If the new pivot is equal to the initial pivot, we are done.
In [37]:
pivot == init_pivot
Out[37]:
But this is False
, so we continue.
In the next step, the variable $x_2$ which is complementary to $r_2$ becomes a basic variable.
In [38]:
tableau0
Out[38]:
In [39]:
ratios = tableau0[:, -1] / tableau0[:, pivot]
row_min = ratios.argmin()
row_min
Out[39]:
In [40]:
tableau0[row_min, :] /= tableau0[row_min, pivot]
ind = np.ones(tableau0.shape[0], dtype=bool)
ind[row_min] = False
tableau0[ind, :] -= tableau0[row_min, :] * tableau0[ind, :][:, [pivot]]
In [41]:
tableau0
Out[41]:
In [42]:
basic_vars0[row_min], pivot = pivot, basic_vars0[row_min]
In [43]:
pivot
Out[43]:
In [44]:
pivot == init_pivot
Out[44]:
In [45]:
tableau1
Out[45]:
In [46]:
ratios = tableau1[:, -1] / tableau1[:, pivot]
row_min = ratios.argmin()
row_min
Out[46]:
Note on the warning:
tableau1[:, pivot]
has a zero entry, so we get a "divide by zero" warning.
In [47]:
tableau1[:, pivot]
Out[47]:
We can just ignore it,
but we can also suppress it by
np.errstate
with a with
clause:
In [48]:
with np.errstate(divide='ignore'):
ratios = tableau1[:, -1] / tableau1[:, pivot]
In [49]:
ratios
Out[49]:
In [50]:
tableau1[row_min, :] /= tableau1[row_min, pivot]
ind = np.ones(tableau1.shape[0], dtype=bool)
ind[row_min] = False
tableau1[ind, :] -= tableau1[row_min, :] * tableau1[ind, :][:, [pivot]]
In [51]:
tableau1
Out[51]:
In [52]:
basic_vars1[row_min], pivot = pivot, basic_vars1[row_min]
In [53]:
pivot
Out[53]:
In [54]:
pivot == init_pivot
Out[54]:
Now we have complete labeling, so we are done.
The basic variables are:
In [55]:
basic_vars0
Out[55]:
$x_2$ and $x_1$, and
In [56]:
basic_vars1
Out[56]:
$r_0$, $y_3$, and $y_4$.
The indices of the basic variables corresponding to $x$:
In [57]:
basic_vars0[basic_vars0 < m]
Out[57]:
The indices of the basic variables corresponding to $y$:
In [58]:
basic_vars1[basic_vars1 >= m]
Out[58]:
The values of the basic variables are stored in the last columns of the tableaux.
The values of $x_2$ and $x_1$ are:
In [59]:
tableau0[basic_vars0 < m, -1]
Out[59]:
The values of $y_3$ and $y_4$ are:
In [60]:
tableau1[basic_vars1 >= m, -1]
Out[60]:
We need to normalize these values so that $x$ and $y$ are probability distributions.
In [61]:
x = np.zeros(m)
x[basic_vars0[basic_vars0 < m]] = tableau0[basic_vars0 < m, -1]
x /= x.sum()
In [62]:
x
Out[62]:
In [63]:
y = np.zeros(n)
y[basic_vars1[basic_vars1 >= m] - m] = tableau1[basic_vars1 >= m, -1]
y /= y.sum()
In [64]:
y
Out[64]:
The Nash equilibrium we have found is:
In [65]:
(x, y)
Out[65]:
In [66]:
def min_ratio_test(tableau, pivot):
ind_nonpositive = tableau[:, pivot] <= 0
with np.errstate(divide='ignore', invalid='ignore'):
ratios = tableau[:, -1] / tableau[:, pivot]
ratios[ind_nonpositive] = np.inf
row_min = ratios.argmin()
return row_min
In [67]:
def pivoting(tableau, pivot, pivot_row):
"""
Perform a pivoting step.
Modify `tableau` in place (and return its view).
"""
# Row indices except pivot_row
ind = np.ones(tableau.shape[0], dtype=bool)
ind[pivot_row] = False
# Store the values in the pivot column, except for row_min
# Made 2-dim by np.newaxis
multipliers = tableau[ind, pivot, np.newaxis]
# Update the tableau
tableau[pivot_row, :] /= tableau[pivot_row, pivot]
tableau[ind, :] -= tableau[pivot_row, :] * multipliers
return tableau
In [68]:
def lemke_howson_tbl(tableau0, tableau1, basic_vars0, basic_vars1, init_pivot):
m, n = tableau1.shape[0], tableau0.shape[0]
tableaux = (tableau0, tableau1)
basic_vars = (basic_vars0, basic_vars1)
init_player = int((basic_vars[0]==init_pivot).any())
players = [init_player, 1 - init_player]
pivot = init_pivot
while True:
for i in players:
# Determine the leaving variable
row_min = min_ratio_test(tableaux[i], pivot)
# Pivoting step: modify tableau in place
pivoting(tableaux[i], pivot, row_min)
# Update the basic variables and the pivot
basic_vars[i][row_min], pivot = pivot, basic_vars[i][row_min]
if pivot == init_pivot:
break
else:
continue
break
out_dtype = np.result_type(*tableaux)
out = np.zeros(m+n, dtype=out_dtype)
for i, (start, num) in enumerate(zip((0, m), (m, n))):
ind = basic_vars[i] < start + num if i == 0 else start <= basic_vars[i]
out[basic_vars[i][ind]] = tableaux[i][ind, -1]
return out
Note: There is no nested break
in Python;
see e.g., break two for loops.
In [69]:
def normalize(unnormalized, m, n):
normalized = np.empty(m+n)
for (start, num) in zip((0, m), (m, n)):
s = unnormalized[start:start+num].sum()
if s != 0:
normalized[start:start+num] = unnormalized[start:start+num] / s
else:
normalized[start:start+num] = 0
return normalized[:m], normalized[m:]
In [70]:
def lemke_howson(A, B_T, init_pivot=0, return_tableaux=False):
m, n = A.shape
tableaux = (np.hstack((B_T, np.identity(n), np.ones((n, 1)))),
np.hstack((np.identity(m), A, np.ones((m, 1)))))
basic_vars = (np.arange(m, m+n), np.arange(0, m))
unnormalized = lemke_howson_tbl(*tableaux, *basic_vars, init_pivot)
normalized = normalize(unnormalized, m, n)
if return_tableaux:
return normalized, tableaux, basic_vars
return normalized
In [71]:
init_pivot = 1
x, y = lemke_howson(A, B_T, init_pivot)
print("Nash equilibrium found\n", (x, y))
In [72]:
lemke_howson(A, B_T, init_pivot, return_tableaux=True)
Out[72]:
In [73]:
def lemke_howson_all(A, B_T):
m, n = A.shape
k = 0
NEs = []
basic_vars_list = []
player = (m <= n)
init_pivot = k
actions, tableaux, basic_vars = \
lemke_howson(A, B_T, init_pivot, return_tableaux=True)
NEs.append(actions)
basic_vars_list.append(np.sort(basic_vars[player]))
for a in range(m+n):
if a == k:
continue
init_pivot = a
actions, tableaux, basic_vars = \
lemke_howson(A, B_T, init_pivot, return_tableaux=True)
basic_vars_sorted = np.sort(basic_vars[player])
for arr in basic_vars_list:
if np.array_equal(basic_vars_sorted, arr):
break
else:
NEs.append(actions)
basic_vars_list.append(basic_vars_sorted)
unnormalized = \
lemke_howson_tbl(*tableaux, *basic_vars, init_pivot=k)
NEs.append(normalize(unnormalized, m, n))
basic_vars_list.append(np.sort(basic_vars[player]))
return NEs
In [74]:
lemke_howson_all(A, B_T)
Out[74]:
There are games in which some of the Nash equilibria cannot be reached by Lemke-Howson path from the origin, as in the game in (3.7) in von Stengel (2007).
In [75]:
payoff_matrix_3_7 = np.array([[3, 3, 0], [4, 0, 1], [0, 4, 5]])
lemke_howson_all(payoff_matrix_3_7, payoff_matrix_3_7)
Out[75]:
In [76]:
def pivoting_int(tableau, pivot, pivot_row, prev_pivot_el):
"""
Perform a pivoting step with integer input data.
Modify `tableau` in place (and return its view).
"""
# Row indices except pivot_row
ind = np.ones(tableau.shape[0], dtype=bool)
ind[pivot_row] = False
# Store the values in the pivot column, except for row_min
# Made 2-dim by np.newaxis
multipliers = tableau[ind, pivot, np.newaxis]
# Update the tableau
tableau[ind, :] *= tableau[pivot_row, pivot]
tableau[ind, :] -= tableau[pivot_row, :] * multipliers
tableau[ind, :] //= prev_pivot_el # Floor division: return int
return tableau
In [77]:
def lemke_howson_tbl_int(tableau0, tableau1, basic_vars0, basic_vars1, init_pivot):
m, n = tableau1.shape[0], tableau0.shape[0]
tableaux = (tableau0, tableau1)
basic_vars = (basic_vars0, basic_vars1)
init_player = int((basic_vars[0]==init_pivot).any())
players = [init_player, 1 - init_player]
pivot = init_pivot
prev_pivot_els = np.ones(2, dtype=np.int_)
while True:
for i in players:
# Determine the leaving variable
row_min = min_ratio_test(tableaux[i], pivot)
# Pivoting step: modify tableau in place
pivoting_int(tableaux[i], pivot, row_min, prev_pivot_els[i])
# Backup the pivot element
prev_pivot_els[i] = tableaux[i][row_min, pivot]
# Update the basic variables and the pivot
basic_vars[i][row_min], pivot = pivot, basic_vars[i][row_min]
if pivot == init_pivot:
break
else:
continue
break
out = np.zeros(m+n, dtype=np.int_)
for i, (start, num) in enumerate(zip((0, m), (m, n))):
ind = basic_vars[i] < start + num if i == 0 else start <= basic_vars[i]
out[basic_vars[i][ind]] = tableaux[i][ind, -1]
return out
In [78]:
import sympy
In [79]:
def normalize_rational(unnormalized, m, n):
"""
Normalize the integer array `unnormalized` with ratioal numbers.
"""
normalized = np.empty(m+n, np.object_)
for (start, num) in zip((0, m), (m, n)):
s = unnormalized[start:start+num].sum()
if s != 0:
for k in range(start, start+num):
normalized[k] = sympy.Rational(sympy.S(unnormalized[k]), sympy.S(s))
else:
normalized[start:start+num] = sympy.Rational(0)
return normalized[:m], normalized[m:]
In [80]:
def lemke_howson_int(A, B_T, init_pivot=0, rational=False, return_tableaux=False):
m, n = A.shape
tableaux = (np.hstack((B_T, np.identity(n, dtype=np.int_), np.ones((n, 1), dtype=np.int_))),
np.hstack((np.identity(m, dtype=np.int_), A, np.ones((m, 1), dtype=np.int_))))
basic_vars = (np.arange(m, m+n), np.arange(0, m))
unnormalized = lemke_howson_tbl_int(*tableaux, *basic_vars, init_pivot)
if rational:
normalized = normalize_rational(unnormalized, m, n)
else:
normalized = normalize(unnormalized, m, n)
if return_tableaux:
return normalized, tableaux, basic_vars
return normalized
In [81]:
def lemke_howson_all_int(A, B_T, rational=False):
m, n = A.shape
k = 0
NEs = []
basic_vars_list = []
player = (m <= n)
init_pivot = k
actions, tableaux, basic_vars = \
lemke_howson_int(A, B_T, init_pivot, rational=rational, return_tableaux=True)
NEs.append(actions)
basic_vars_list.append(np.sort(basic_vars[player]))
for a in range(m+n):
if a == k:
continue
init_pivot = a
actions, tableaux, basic_vars = \
lemke_howson_int(A, B_T, init_pivot, rational=rational, return_tableaux=True)
basic_vars_sorted = np.sort(basic_vars[player])
for arr in basic_vars_list:
if np.array_equal(basic_vars_sorted, arr):
break
else:
NEs.append(actions)
basic_vars_list.append(basic_vars_sorted)
unnormalized = \
lemke_howson_tbl_int(*tableaux, *basic_vars, init_pivot=k)
if rational:
normalized = normalize_rational(unnormalized, m, n)
else:
normalized = normalize(unnormalized, m, n)
NEs.append(normalized)
basic_vars_list.append(np.sort(basic_vars[player]))
return NEs
In [82]:
lemke_howson_int(A, B_T, init_pivot=1)
Out[82]:
In [83]:
lemke_howson_int(A, B_T, init_pivot=1, return_tableaux=True)
Out[83]:
In [84]:
lemke_howson_int(A, B_T, init_pivot=1, rational=True)
Out[84]:
In [85]:
lemke_howson_int(A, B_T, init_pivot=0, rational=True)
Out[85]:
In [86]:
lemke_howson_all_int(A, B_T)
Out[86]:
In [87]:
lemke_howson_all_int(A, B_T, rational=True)
Out[87]:
In [88]:
def min_ratio_test_no_tie_breaking(tableau, pivot, test_col, argmins, num_argmins):
idx = 0
i = argmins[idx]
if tableau[i, pivot] > 0:
min_ratio = tableau[i, test_col] / tableau[i, pivot]
else:
min_ratio = np.inf
for k in range(1, num_argmins):
i = argmins[k]
if tableau[i, pivot] <= 0:
continue
ratio = tableau[i, test_col] / tableau[i, pivot]
if ratio > min_ratio:
continue
elif ratio < min_ratio:
min_ratio = ratio
idx = 0
elif ratio == min_ratio:
idx += 1
argmins[idx] = k
return idx + 1
In [89]:
def lex_min_ratio_test(tableau, pivot, slack_start):
num_rows = tableau.shape[0]
argmins = np.arange(num_rows)
num_argmins = num_rows
num_argmins = min_ratio_test_no_tie_breaking(tableau, pivot, -1, argmins, num_argmins)
if num_argmins == 1:
return argmins[0]
for j in range(slack_start, slack_start+num_rows):
if j == pivot:
continue
num_argmins = min_ratio_test_no_tie_breaking(tableau, pivot, j, argmins, num_argmins)
if num_argmins == 1:
break
return argmins[0]
Caveat: Because of rounding errors, one should not rely on equality between floating point numbers. For example:
In [90]:
2/3
Out[90]:
In [91]:
1 - 1/3
Out[91]:
In [92]:
1 - 1/3 == 2/3
Out[92]:
Note: In comparing $\frac{a}{b}$ and $\frac{a'}{b'}$, one may instead compare $a b'$ and $a' b$: when these are integers, the latter involves only integers. (This, of course, does not apply only for lexico-minimum test.)
In [93]:
def lemke_howson_tbl_int_lex_min(tableau0, tableau1, basic_vars0, basic_vars1, init_pivot):
m, n = tableau1.shape[0], tableau0.shape[0]
tableaux = (tableau0, tableau1)
basic_vars = (basic_vars0, basic_vars1)
init_player = int((basic_vars[0]==init_pivot).any())
players = [init_player, 1 - init_player]
pivot = init_pivot
prev_pivot_els = np.ones(2, dtype=np.int_)
slack_starts = (m, 0)
while True:
for i in players:
# Determine the leaving variable
row_min = lex_min_ratio_test(tableaux[i], pivot, slack_starts[i])
# Pivoting step: modify tableau in place
pivoting_int(tableaux[i], pivot, row_min, prev_pivot_els[i])
# Backup the pivot element
prev_pivot_els[i] = tableaux[i][row_min, pivot]
# Update the basic variables and the pivot
basic_vars[i][row_min], pivot = pivot, basic_vars[i][row_min]
if pivot == init_pivot:
break
else:
continue
break
out = np.zeros(m+n, dtype=np.int_)
for i, (start, num) in enumerate(zip((0, m), (m, n))):
ind = basic_vars[i] < start + num if i == 0 else start <= basic_vars[i]
out[basic_vars[i][ind]] = tableaux[i][ind, -1]
return out
In [94]:
def lemke_howson_int_lex_min(A, B_T, init_pivot=0, rational=False, return_tableaux=False):
m, n = A.shape
tableaux = (np.hstack((B_T, np.identity(n, dtype=np.int_), np.ones((n, 1), dtype=np.int_))),
np.hstack((np.identity(m, dtype=np.int_), A, np.ones((m, 1), dtype=np.int_))))
basic_vars = (np.arange(m, m+n), np.arange(0, m))
unnormalized = lemke_howson_tbl_int_lex_min(*tableaux, *basic_vars, init_pivot)
if rational:
normalized = normalize_rational(unnormalized, m, n)
else:
normalized = normalize(unnormalized, m, n)
if return_tableaux:
return normalized, tableaux, basic_vars
return normalized
In [95]:
def lemke_howson_all_int_lex_min(A, B_T, rational=False):
m, n = A.shape
k = 0
NEs = []
basic_vars_list = []
player = (m <= n)
init_pivot = k
actions, tableaux, basic_vars = \
lemke_howson_int_lex_min(A, B_T, init_pivot, rational=rational, return_tableaux=True)
NEs.append(actions)
basic_vars_list.append(np.sort(basic_vars[player]))
for a in range(m+n):
if a == k:
continue
init_pivot = a
actions, tableaux, basic_vars = \
lemke_howson_int_lex_min(A, B_T, init_pivot, rational=rational, return_tableaux=True)
basic_vars_sorted = np.sort(basic_vars[player])
for arr in basic_vars_list:
if np.array_equal(basic_vars_sorted, arr):
break
else:
NEs.append(actions)
basic_vars_list.append(basic_vars_sorted)
unnormalized = \
lemke_howson_tbl_int_lex_min(*tableaux, *basic_vars, init_pivot=k)
if rational:
normalized = normalize_rational(unnormalized, m, n)
else:
normalized = normalize(unnormalized, m, n)
NEs.append(normalized)
basic_vars_list.append(np.sort(basic_vars[player]))
return NEs
Consider the following degenerate game:
In [96]:
C = np.array([[3, 3],
[2, 5],
[0 ,6]])
D_T = np.array([[3, 2, 3],
[3, 6, 1]])
lemke_howson_all
fails to work properly:
In [97]:
lemke_howson_all(C, D_T)
Out[97]:
With lexico-minimum test:
In [98]:
lemke_howson_all_int_lex_min(C, D_T)
Out[98]:
Due to the paricular fixed way of introducing the perturbations $(\varepsilon^1, \ldots, \varepsilon^k)$, the output does depend on the ordering of the actions:
In [99]:
# Change the order of the actions of player 1
E = np.array([[3, 3],
[5, 2],
[6 ,0]])
F_T = np.array([[3, 6, 1],
[3, 2, 3]])
In [100]:
lemke_howson_all_int_lex_min(E, F_T)
Out[100]:
Essentially, the exercise corresponds to considering $$ \begin{pmatrix} \dfrac{3}{1+\varepsilon_1} & \dfrac{2}{1+\varepsilon_1} & \dfrac{3}{1+\varepsilon_1} \\ \dfrac{3}{1+\varepsilon_2} & \dfrac{6}{1+\varepsilon_2} & \dfrac{1}{1+\varepsilon_2} \end{pmatrix}, $$ where $(\varepsilon_1, \varepsilon_2) =(\varepsilon^1, \varepsilon^2)$ or $(\varepsilon_1, \varepsilon_2) =(\varepsilon^2, \varepsilon^1)$. In fact:
In [101]:
eps = 0.01
D_T_eps = np.array([[3 / (1 + eps), 2 / (1 + eps), 3 / (1 + eps)],
[3 / (1 + eps**2), 6 / (1 + eps**2), 1 / (1 + eps**2)]])
F_T_eps = np.array([[3 / (1 + eps**2), 2 / (1 + eps**2), 3 / (1 + eps**2)],
[3 / (1 + eps), 6 / (1 + eps), 1 / (1 + eps)]])
In [102]:
lemke_howson_all(C, D_T_eps)
Out[102]:
In [103]:
lemke_howson_all(C, F_T_eps)
Out[103]:
In [ ]: