Skip to content

Commit 62c2d55

Browse files
oyamadmmcky
andauthored
EHN: Add minmax solver (#579)
* EHN: Add minmax solver * MAINT: Player: Use minmax in is_dominated and dominated_actions Co-authored-by: mmcky <[email protected]>
1 parent dcd517d commit 62c2d55

File tree

6 files changed

+177
-14
lines changed

6 files changed

+177
-14
lines changed

docs/source/optimize.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Optimize
55
:maxdepth: 2
66

77
optimize/linprog_simplex
8+
optimize/minmax
89
optimize/nelder_mead
910
optimize/pivoting
1011
optimize/root_finding

docs/source/optimize/minmax.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
minmax
2+
======
3+
4+
.. automodule:: quantecon.optimize.minmax
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

quantecon/game_theory/normal_form_game.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,10 @@ def is_dominated(self, action, tol=None, method=None):
434434
default to the value of the `tol` attribute.
435435
436436
method : str, optional(default=None)
437-
If None, `lemke_howson` from `quantecon.game_theory` is used
438-
to solve for a Nash equilibrium of an auxiliary zero-sum
439-
game. If `method` is set to `'simplex'`, `'interior-point'`,
440-
or `'revised simplex'`, then `scipy.optimize.linprog` is
441-
used with the method as specified by `method`.
437+
If None, `minmax` from `quantecon.optimize` is used. If
438+
`method` is set to `'simplex'`, `'interior-point'`, or
439+
`'revised simplex'`, then `scipy.optimize.linprog` is used
440+
with the method as specified by `method`.
442441
443442
Returns
444443
-------
@@ -465,10 +464,9 @@ def is_dominated(self, action, tol=None, method=None):
465464
D.shape = (D.shape[0], np.prod(D.shape[1:]))
466465

467466
if method is None:
468-
from .lemke_howson import lemke_howson
469-
g_zero_sum = NormalFormGame([Player(D), Player(-D.T)])
470-
NE = lemke_howson(g_zero_sum)
471-
return NE[0] @ D @ NE[1] > tol
467+
from ..optimize.minmax import minmax
468+
v, _, _ = minmax(D)
469+
return v > tol
472470
elif method in ['simplex', 'interior-point', 'revised simplex']:
473471
from scipy.optimize import linprog
474472
m, n = D.shape
@@ -506,11 +504,10 @@ def dominated_actions(self, tol=None, method=None):
506504
default to the value of the `tol` attribute.
507505
508506
method : str, optional(default=None)
509-
If None, `lemke_howson` from `quantecon.game_theory` is used
510-
to solve for a Nash equilibrium of an auxiliary zero-sum
511-
game. If `method` is set to `'simplex'`, `'interior-point'`,
512-
or `'revised simplex'`, then `scipy.optimize.linprog` is
513-
used with the method as specified by `method`.
507+
If None, `minmax` from `quantecon.optimize` is used. If
508+
`method` is set to `'simplex'`, `'interior-point'`, or
509+
`'revised simplex'`, then `scipy.optimize.linprog` is used
510+
with the method as specified by `method`.
514511
515512
Returns
516513
-------

quantecon/optimize/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .linprog_simplex import (
66
linprog_simplex, solve_tableau, get_solution, PivOptions
77
)
8+
from .minmax import minmax
89
from .scalar_maximization import brent_max
910
from .nelder_mead import nelder_mead
1011
from .root_finding import newton, newton_halley, newton_secant, bisect, brentq

quantecon/optimize/minmax.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
Contain a minmax problem solver routine.
3+
4+
"""
5+
import numpy as np
6+
from numba import jit
7+
from .linprog_simplex import _set_criterion_row, solve_tableau, PivOptions
8+
from .pivoting import _pivoting
9+
10+
11+
@jit(nopython=True, cache=True)
12+
def minmax(A, max_iter=10**6, piv_options=PivOptions()):
13+
r"""
14+
Given an m x n matrix `A`, return the value :math:`v^*` of the
15+
minmax problem:
16+
17+
.. math::
18+
19+
v^* = \max_{x \in \Delta_m} \min_{y \in \Delta_n} x^T A y
20+
= \min_{y \in \Delta_n}\max_{x \in \Delta_m} x^T A y
21+
22+
and the optimal solutions :math:`x^* \in \Delta_m` and
23+
:math:`y^* \in \Delta_n`: :math:`v^* = x^{*T} A y^*`, where
24+
:math:`\Delta_k = \{z \in \mathbb{R}^k_+ \mid z_1 + \cdots + z_k =
25+
1\}`, :math:`k = m, n`.
26+
27+
This routine is jit-compiled by Numba, using
28+
`optimize.linprog_simplex` routines.
29+
30+
Parameters
31+
----------
32+
A : ndarray(float, ndim=2)
33+
ndarray of shape (m, n).
34+
35+
max_iter : int, optional(default=10**6)
36+
Maximum number of iteration in the linear programming solver.
37+
38+
piv_options : PivOptions, optional
39+
PivOptions namedtuple to set tolerance values used in the linear
40+
programming solver.
41+
42+
Returns
43+
-------
44+
v : float
45+
Value :math:`v^*` of the minmax problem.
46+
47+
x : ndarray(float, ndim=1)
48+
Optimal solution :math:`x^*`, of shape (,m).
49+
50+
y : ndarray(float, ndim=1)
51+
Optimal solution :math:`y^*`, of shape (,n).
52+
53+
"""
54+
m, n = A.shape
55+
56+
min_ = A.min()
57+
const = 0.
58+
if min_ <= 0:
59+
const = min_ * (-1) + 1
60+
61+
tableau = np.zeros((m+2, n+1+m+1))
62+
63+
for i in range(m):
64+
for j in range(n):
65+
tableau[i, j] = A[i, j] + const
66+
tableau[i, n] = -1
67+
tableau[i, n+1+i] = 1
68+
69+
tableau[-2, :n] = 1
70+
tableau[-2, -1] = 1
71+
72+
# Phase 1
73+
pivcol = 0
74+
75+
pivrow = 0
76+
max_ = tableau[0, pivcol]
77+
for i in range(1, m):
78+
if tableau[i, pivcol] > max_:
79+
pivrow = i
80+
max_ = tableau[i, pivcol]
81+
82+
_pivoting(tableau, n, pivrow)
83+
_pivoting(tableau, pivcol, m)
84+
85+
basis = np.arange(n+1, n+1+m+1)
86+
basis[pivrow] = n
87+
basis[-1] = 0
88+
89+
# Modify the criterion row for Phase 2
90+
c = np.zeros(n+1)
91+
c[-1] = -1
92+
_set_criterion_row(c, basis, tableau)
93+
94+
# Phase 2
95+
solve_tableau(tableau, basis, max_iter-2, skip_aux=False,
96+
piv_options=piv_options)
97+
98+
# Obtain solution
99+
x = np.empty(m)
100+
y = np.zeros(n)
101+
102+
for i in range(m+1):
103+
if basis[i] < n:
104+
y[basis[i]] = tableau[i, -1]
105+
106+
for j in range(m):
107+
x[j] = tableau[-1, n+1+j]
108+
if x[j] != 0:
109+
x[j] *= -1
110+
111+
v = tableau[-1, -1] - const
112+
113+
return v, x, y
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
Tests for minmax
3+
4+
"""
5+
import numpy as np
6+
from numpy.testing import assert_, assert_allclose
7+
8+
from quantecon.optimize import minmax
9+
from quantecon.game_theory import NormalFormGame, Player, lemke_howson
10+
11+
12+
class TestMinmax:
13+
def test_RPS(self):
14+
A = np.array(
15+
[[0, 1, -1],
16+
[-1, 0, 1],
17+
[1, -1, 0],
18+
[-1, -1, -1]]
19+
)
20+
v_expected = 0
21+
x_expected = [1/3, 1/3, 1/3, 0]
22+
y_expected = [1/3, 1/3, 1/3]
23+
24+
v, x, y = minmax(A)
25+
26+
assert_allclose(v, v_expected)
27+
assert_allclose(x, x_expected)
28+
assert_allclose(y, y_expected)
29+
30+
def test_random_matrix(self):
31+
seed = 12345
32+
rng = np.random.default_rng(seed)
33+
size = (10, 15)
34+
A = rng.normal(size=size)
35+
v, x, y = minmax(A)
36+
37+
for z in [x, y]:
38+
assert_((z >= 0).all())
39+
assert_allclose(z.sum(), 1)
40+
41+
g = NormalFormGame((Player(A), Player(-A.T)))
42+
NE = lemke_howson(g)
43+
assert_allclose(v, NE[0] @ A @ NE[1])
44+
assert_(g.is_nash((x, y)))

0 commit comments

Comments
 (0)