Skip to content
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

FIX: support_enumeration: Use _numba_linalg_solve #311

Merged
merged 5 commits into from
May 31, 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
1 change: 1 addition & 0 deletions docs/source/util.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Utilities
util/array
util/common_messages
util/notebooks
util/numba
util/random
util/timing
7 changes: 7 additions & 0 deletions docs/source/util/numba.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
numba
=====

.. automodule:: quantecon.util.numba
:members:
:undoc-members:
:show-inheritance:
111 changes: 28 additions & 83 deletions quantecon/game_theory/support_enumeration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,9 @@
Tardos, and V. Vazirani eds., Algorithmic Game Theory, 2007.

"""
from distutils.version import LooseVersion
import numpy as np
import numba
from numba import jit


least_numba_version = LooseVersion('0.28')
is_numba_required_installed = True
if LooseVersion(numba.__version__) < least_numba_version:
is_numba_required_installed = False
nopython = is_numba_required_installed

EPS = np.finfo(float).eps
from ..util.numba import _numba_linalg_solve


def support_enumeration(g):
Expand All @@ -46,11 +36,6 @@ def support_enumeration(g):
list(tuple(ndarray(float, ndim=1)))
List containing tuples of Nash equilibrium mixed actions.

Notes
-----
This routine is jit-complied if Numba version 0.28 or above is
installed.

"""
return list(support_enumeration_gen(g))

Expand Down Expand Up @@ -80,7 +65,7 @@ def support_enumeration_gen(g):
g.players[1].payoff_array)


@jit(nopython=nopython) # cache=True raises _pickle.PicklingError
@jit(nopython=True) # cache=True raises _pickle.PicklingError
def _support_enumeration_gen(payoff_matrix0, payoff_matrix1):
"""
Main body of `support_enumeration_gen`.
Expand All @@ -105,32 +90,28 @@ def _support_enumeration_gen(payoff_matrix0, payoff_matrix1):

for k in range(1, n_min+1):
supps = (np.arange(k), np.empty(k, np.int_))
actions = (np.empty(k), np.empty(k))
actions = (np.empty(k+1), np.empty(k+1))
A = np.empty((k+1, k+1))
A[:-1, -1] = -1
A[-1, :-1] = 1
A[-1, -1] = 0
b = np.zeros(k+1)
b[-1] = 1

while supps[0][-1] < nums_actions[0]:
supps[1][:] = np.arange(k)
while supps[1][-1] < nums_actions[1]:
if _indiff_mixed_action(payoff_matrix0, supps[0], supps[1],
A, b, actions[1]):
A, actions[1]):
if _indiff_mixed_action(payoff_matrix1, supps[1], supps[0],
A, b, actions[0]):
A, actions[0]):
out = (np.zeros(nums_actions[0]),
np.zeros(nums_actions[1]))
for p, (supp, action) in enumerate(zip(supps,
actions)):
out[p][supp] = action
out[p][supp] = action[:-1]
yield out
_next_k_array(supps[1])
_next_k_array(supps[0])


@jit(nopython=nopython)
def _indiff_mixed_action(payoff_matrix, own_supp, opp_supp, A, b, out):
@jit(nopython=True, cache=True)
def _indiff_mixed_action(payoff_matrix, own_supp, opp_supp, A, out):
"""
Given a player's payoff matrix `payoff_matrix`, an array `own_supp`
of this player's actions, and an array `opp_supp` of the opponent's
Expand All @@ -139,8 +120,7 @@ def _indiff_mixed_action(payoff_matrix, own_supp, opp_supp, A, b, out):
among the actions in `own_supp`, if any such exists. Return `True`
if such a mixed action exists and actions in `own_supp` are indeed
best responses to it, in which case the outcome is stored in `out`;
`False` otherwise. Arrays `A` and `b` are used in intermediate
steps.
`False` otherwise. Array `A` is used in intermediate steps.

Parameters
----------
Expand All @@ -154,17 +134,11 @@ def _indiff_mixed_action(payoff_matrix, own_supp, opp_supp, A, b, out):
Array containing the opponent's action indices, of length k.

A : ndarray(float, ndim=2)
Array used in intermediate steps, of shape (k+1, k+1). The
following values must be assigned in advance: `A[:-1, -1] = -1`,
`A[-1, :-1] = 1`, and `A[-1, -1] = 0`.

b : ndarray(float, ndim=1)
Array used in intermediate steps, of shape (k+1,). The following
values must be assigned in advance `b[:-1] = 0` and `b[-1] = 1`.
Array used in intermediate steps, of shape (k+1, k+1).

out : ndarray(float, ndim=1)
Array of length k to store the k nonzero values of the desired
mixed action.
Array of length k+1 to store the k nonzero values of the desired
mixed action in `out[:-1]` (and the payoff value in `out[-1]`.)

Returns
-------
Expand All @@ -175,15 +149,22 @@ def _indiff_mixed_action(payoff_matrix, own_supp, opp_supp, A, b, out):
m = payoff_matrix.shape[0]
k = len(own_supp)

A[:-1, :-1] = payoff_matrix[own_supp, :][:, opp_supp]
if _is_singular(A):
return False

sol = np.linalg.solve(A, b)
if (sol[:-1] <= 0).any():
for i in range(k):
for j in range(k):
A[j, i] = payoff_matrix[own_supp[i], opp_supp[j]] # transpose
A[:-1, -1] = 1
A[-1, :-1] = -1
A[-1, -1] = 0
out[:-1] = 0
out[-1] = 1

r = _numba_linalg_solve(A, out)
if r != 0: # A: singular
return False
out[:] = sol[:-1]
val = sol[-1]
for i in range(k):
if out[i] <= 0:
return False
val = out[-1]

if k == m:
return True
Expand Down Expand Up @@ -280,39 +261,3 @@ def _next_k_array(a):
pos += 1

return a


if is_numba_required_installed:
@jit(nopython=True, cache=True)
def _is_singular(a):
s = numba.targets.linalg._compute_singular_values(a)
if s[-1] <= s[0] * EPS:
return True
else:
return False
else:
def _is_singular(a):
s = np.linalg.svd(a, compute_uv=False)
if s[-1] <= s[0] * EPS:
return True
else:
return False

_is_singular_docstr = \
"""
Determine whether matrix `a` is numerically singular, by checking
its singular values.

Parameters
----------
a : ndarray(float, ndim=2)
2-dimensional array of floats.

Returns
-------
bool
Whether `a` is numerically singular.

"""

_is_singular.__doc__ = _is_singular_docstr
31 changes: 31 additions & 0 deletions quantecon/game_theory/tests/test_support_enumeration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,33 @@
Tests for support_enumeration.py

"""
import numpy as np
from numpy.testing import assert_allclose
from nose.tools import eq_
from quantecon.util import check_random_state
from quantecon.game_theory import Player, NormalFormGame, support_enumeration


def random_skew_sym(n, m=None, random_state=None):
"""
Generate a random skew symmetric zero-sum NormalFormGame of the form
O B
-B.T O
where B is an n x m matrix.

"""
if m is None:
m = n
random_state = check_random_state(random_state)
B = random_state.random_sample((n, m))
A = np.empty((n+m, n+m))
A[:n, :n] = 0
A[n:, n:] = 0
A[:n, n:] = B
A[n:, :n] = -B.T
return NormalFormGame([Player(A) for i in range(2)])


class TestSupportEnumeration():
def setUp(self):
self.game_dicts = []
Expand Down Expand Up @@ -35,10 +58,18 @@ def setUp(self):
def test_support_enumeration(self):
for d in self.game_dicts:
NEs_computed = support_enumeration(d['g'])
eq_(len(NEs_computed), len(d['NEs']))
for actions_computed, actions in zip(NEs_computed, d['NEs']):
for action_computed, action in zip(actions_computed, actions):
assert_allclose(action_computed, action)

def test_no_error_skew_sym(self):
# Test no LinAlgError is raised.
n, m = 3, 2
seed = 7028
g = random_skew_sym(n, m, random_state=seed)
NEs = support_enumeration(g)


if __name__ == '__main__':
import sys
Expand Down
71 changes: 71 additions & 0 deletions quantecon/util/numba.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Utilities to support Numba jitted functions

"""
import numpy as np
from numba import generated_jit, types
from numba.targets.linalg import _LAPACK


# BLAS kinds as letters
_blas_kinds = {
types.float32: 's',
types.float64: 'd',
types.complex64: 'c',
types.complex128: 'z',
}


@generated_jit(nopython=True, cache=True)
def _numba_linalg_solve(a, b):
"""
Solve the linear equation ax = b directly calling a Numba internal
function. The data in `a` and `b` are interpreted in Fortran order,
and dtype of `a` and `b` must be the same, one of {float32, float64,
complex64, complex128}. `a` and `b` are modified in place, and the
solution is stored in `b`. *No error check is made for the inputs.*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyamad is it standard convention to return the solution in b and not explicitly return the solution and a status code from the function as a tuple?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I understand your question, but here is where this is used, where it is check whether matrix A is nonsingular (instead of try-except which is not available in nopython mode), and in case A is singular I don't know what is in there in b.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah. I see. In the code example b is out and it is modified in place (rather than returned). Thanks @oyamad


Parameters
----------
a : ndarray(ndim=2)
2-dimensional ndarray of shape (n, n).

b : ndarray(ndim=1 or 2)
1-dimensional ndarray of shape (n,) or 2-dimensional ndarray of
shape (n, nrhs).

Returns
-------
r : scalar(int)
r = 0 if successful.

Notes
-----
From github.com/numba/numba/blob/master/numba/targets/linalg.py

"""
numba_xgesv = _LAPACK().numba_xgesv(a.dtype)
kind = ord(_blas_kinds[a.dtype])

def _numba_linalg_solve_impl(a, b): # pragma: no cover
n = a.shape[-1]
if b.ndim == 1:
nrhs = 1
else: # b.ndim == 2
nrhs = b.shape[-1]
F_INT_nptype = np.int32
ipiv = np.empty(n, dtype=F_INT_nptype)

r = numba_xgesv(
kind, # kind
n, # n
nrhs, # nhrs
a.ctypes, # a
n, # lda
ipiv.ctypes, # ipiv
b.ctypes, # b
n # ldb
)
return r

return _numba_linalg_solve_impl
59 changes: 59 additions & 0 deletions quantecon/util/tests/test_numba.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Tests for Numba support utilities

"""
import numpy as np
from numpy.testing import assert_array_equal
from numba import jit
from nose.tools import eq_, ok_
from quantecon.util.numba import _numba_linalg_solve


@jit(nopython=True)
def numba_linalg_solve_orig(a, b):
return np.linalg.solve(a, b)


class TestNumbaLinalgSolve:
def setUp(self):
self.dtypes = [np.float32, np.float64]
self.a = np.array([[3, 2, 0], [1, -1, 0], [0, 5, 1]])
self.b_1dim = np.array([2, 4, -1])
self.b_2dim = np.array([[2, 3], [4, 1], [-1, 0]])
self.a_singular = np.array([[0, 1, 2], [3, 4, 5], [3, 5, 7]])

def test_b_1dim(self):
for dtype in self.dtypes:
a = np.asfortranarray(self.a, dtype=dtype)
b = np.asfortranarray(self.b_1dim, dtype=dtype)
sol_orig = numba_linalg_solve_orig(a, b)
r = _numba_linalg_solve(a, b)
eq_(r, 0)
assert_array_equal(b, sol_orig)

def test_b_2dim(self):
for dtype in self.dtypes:
a = np.asfortranarray(self.a, dtype=dtype)
b = np.asfortranarray(self.b_2dim, dtype=dtype)
sol_orig = numba_linalg_solve_orig(a, b)
r = _numba_linalg_solve(a, b)
eq_(r, 0)
assert_array_equal(b, sol_orig)

def test_singular_a(self):
for b in [self.b_1dim, self.b_2dim]:
for dtype in self.dtypes:
a = np.asfortranarray(self.a_singular, dtype=dtype)
b = np.asfortranarray(b, dtype=dtype)
r = _numba_linalg_solve(a, b)
ok_(r != 0)


if __name__ == '__main__':
import sys
import nose

argv = sys.argv[:]
argv.append('--verbose')
argv.append('--nocapture')
nose.main(argv=argv, defaultTest=__file__)