Skip to content

Implement the "imitation game algorithm" by McLennan-Tourky #273

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 12 commits into from
Dec 14, 2016
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ quantecon.egg-info/
*.h5
examples/solow_model/depreciation_rates.dta
examples/solow_model/pwt80.dta
examples/*.png
examples/*.png

# Numba cache files
*.nbc
*.nbi
318 changes: 301 additions & 17 deletions quantecon/compute_fp.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""
Filename: compute_fp.py
Authors: Thomas Sargent, John Stachurski
Authors: Thomas Sargent, John Stachurski, Daisuke Oyama

Compute the fixed point of a given operator T, starting from
Compute an approximate fixed point of a given operator T, starting from
specified initial condition v.

"""
import time
import warnings
import numpy as np
from numba import jit, generated_jit, types
from .game_theory.lemke_howson import lemke_howson_tbl, get_mixed_actions


def _print_after_skip(skip, it=None, dist=None, etime=None):
Expand All @@ -33,27 +36,59 @@ def _print_after_skip(skip, it=None, dist=None, etime=None):
return


def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=1,
print_skip=5, *args, **kwargs):
_convergence_msg = 'Converged in {iterate} steps'
_non_convergence_msg = \
'max_iter attained before convergence in compute_fixed_point'


def _is_approx_fp(T, v, error_tol, *args, **kwargs):
error = np.max(np.abs(T(v, *args, **kwargs) - v))
return error <= error_tol


def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=2,
print_skip=5, method='iteration', *args, **kwargs):
"""
Computes and returns :math:`T^k v`, an approximate fixed point.
Computes and returns an approximate fixed point of the function `T`.

The default method `'iteration'` simply iterates the function given
an initial condition `v` and returns :math:`T^k v` when the
condition :math:`\lVert T^k v - T^{k-1} v\rVert \leq
\mathrm{error_tol}` is satisfied or the number of iterations
:math:`k` reaches `max_iter`. Provided that `T` is a contraction
mapping or similar, :math:`T^k v` will be an approximation to the
fixed point.

Here T is an operator, v is an initial condition and k is the number
of iterates. Provided that T is a contraction mapping or similar,
:math:`T^k v` will be an approximation to the fixed point.
The method `'imitation_game'` uses the "imitation game algorithm"
developed by McLennan and Tourky [1]_, which internally constructs
a sequence of two-player games called imitation games and utilizes
their Nash equilibria, computed by the Lemke-Howson algorithm
routine. It finds an approximate fixed point of `T`, a point
:math:`v^*` such that :math:`\lVert T(v) - v\rVert \leq
\mathrm{error_tol}`, provided `T` is a function that satisfies the
assumptions of Brouwer's fixed point theorm, i.e., a continuous
function that maps a compact and convex set to itself.

Parameters
----------
T : callable
A callable object (e.g., function) that acts on v
v : object
An object such that T(v) is defined
An object such that T(v) is defined; modified in place if
`method='iteration' and `v` is an array
error_tol : scalar(float), optional(default=1e-3)
Error tolerance
max_iter : scalar(int), optional(default=50)
Maximum number of iterations
verbose : bool, optional(default=True)
If True then print current error at each iterate.
verbose : scalar(int), optional(default=2)
Level of feedback (0 for no output, 1 for warnings only, 2 for
warning and residual error reports during iteration)
print_skip : scalar(int), optional(default=5)
How many iterations to apply between print messages (effective
only when `verbose=2`)
method : str in {'iteration', 'imitation_game'},
optional(default='iteration')
Method of computing an approximate fixed point
args, kwargs :
Other arguments and keyword arguments that are passed directly
to the function T each time it is called
Expand All @@ -63,25 +98,274 @@ def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=1,
v : object
The approximate fixed point

References
----------
.. [1] A. McLennan and R. Tourky, "From Imitation Games to
Kakutani," 2006.

"""
if max_iter < 1:
raise ValueError('max_iter must be a positive integer')

if verbose not in (0, 1, 2):
raise ValueError('verbose should be 0, 1 or 2')

if method not in ['iteration', 'imitation_game']:
raise ValueError('invalid method')

if method == 'imitation_game':
is_approx_fp = \
lambda v: _is_approx_fp(T, v, error_tol, *args, **kwargs)
v_star, converged, iterate = \
_compute_fixed_point_ig(T, v, max_iter, verbose, print_skip,
is_approx_fp, *args, **kwargs)
return v_star

# method == 'iteration'
iterate = 0
error = error_tol + 1

if verbose:
if verbose == 2:
start_time = time.time()
_print_after_skip(print_skip, it=None)

while iterate < max_iter and error > error_tol:
while True:
new_v = T(v, *args, **kwargs)
iterate += 1
error = np.max(np.abs(new_v - v))

if verbose:
etime = time.time() - start_time
_print_after_skip(print_skip, iterate, error, etime)

try:
v[:] = new_v
except TypeError:
v = new_v

if error <= error_tol or iterate >= max_iter:
break

if verbose == 2:
etime = time.time() - start_time
_print_after_skip(print_skip, iterate, error, etime)

if verbose == 2:
etime = time.time() - start_time
print_skip = 1
_print_after_skip(print_skip, iterate, error, etime)
if verbose >= 1:
if error > error_tol:
warnings.warn(_non_convergence_msg, RuntimeWarning)
elif verbose == 2:
print(_convergence_msg.format(iterate=iterate))

return v


def _compute_fixed_point_ig(T, v, max_iter, verbose, print_skip, is_approx_fp,
*args, **kwargs):
"""
Implement the imitation game algorithm by McLennan and Tourky (2006)
for computing an approximate fixed point of `T`.

Parameters
----------
is_approx_fp : callable
A callable with signature `is_approx_fp(v)` which determines
whether `v` is an approximate fixed point with a bool return
value (i.e., True or False)

For the other parameters, see Parameters in compute_fixed_point.

Returns
-------
x_new : scalar(float) or ndarray(float)
Approximate fixed point.

converged : bool
Whether the routine has converged.

iterate : scalar(int)
Number of iterations.

"""
if verbose == 2:
start_time = time.time()
_print_after_skip(print_skip, it=None)

x_new = v
y_new = T(x_new, *args, **kwargs)
iterate = 1
converged = is_approx_fp(x_new)

if converged or iterate >= max_iter:
if verbose == 2:
error = np.max(np.abs(y_new - x_new))
etime = time.time() - start_time
print_skip = 1
_print_after_skip(print_skip, iterate, error, etime)
if verbose >= 1:
if not converged:
warnings.warn(_non_convergence_msg, RuntimeWarning)
elif verbose == 2:
print(_convergence_msg.format(iterate=iterate))
return x_new, converged, iterate

if verbose == 2:
error = np.max(np.abs(y_new - x_new))
etime = time.time() - start_time
_print_after_skip(print_skip, iterate, error, etime)

# Length of the arrays to store the computed sequences of x and y.
# If exceeded, reset to min(max_iter, buff_size*2).
buff_size = 2**8
buff_size = min(max_iter, buff_size)

shape = (buff_size,) + np.asarray(x_new).shape
X, Y = np.empty(shape), np.empty(shape)
X[0], Y[0] = x_new, y_new
x_new = Y[0]

tableaux = tuple(np.empty((buff_size, buff_size*2+1)) for i in range(2))
bases = tuple(np.empty(buff_size, dtype=int) for i in range(2))
max_piv = 10**6 # Max number of pivoting steps in lemke_howson_tbl

while True:
y_new = T(x_new, *args, **kwargs)
iterate += 1
converged = is_approx_fp(x_new)

if converged or iterate >= max_iter:
break

if verbose == 2:
error = np.max(np.abs(y_new - x_new))
etime = time.time() - start_time
_print_after_skip(print_skip, iterate, error, etime)

try:
X[iterate-1] = x_new
Y[iterate-1] = y_new
except IndexError:
buff_size = min(max_iter, buff_size*2)
shape = (buff_size,) + X.shape[1:]
X_tmp, Y_tmp = X, Y
X, Y = np.empty(shape), np.empty(shape)
X[:X_tmp.shape[0]], Y[:Y_tmp.shape[0]] = X_tmp, Y_tmp
X[iterate-1], Y[iterate-1] = x_new, y_new

tableaux = tuple(np.empty((buff_size, buff_size*2+1))
for i in range(2))
bases = tuple(np.empty(buff_size, dtype=int) for i in range(2))

m = iterate
tableaux_curr = tuple(tableau[:m, :2*m+1] for tableau in tableaux)
bases_curr = tuple(basis[:m] for basis in bases)
_initialize_tableaux_ig(X[:m], Y[:m], tableaux_curr, bases_curr)
converged, num_iter = lemke_howson_tbl(
tableaux_curr, bases_curr, init_pivot=m-1, max_iter=max_piv
)
_, rho = get_mixed_actions(tableaux_curr, bases_curr)

if Y.ndim <= 2:
x_new = rho.dot(Y[:m])
else:
shape_Y = Y.shape
Y_2d = Y.reshape(shape_Y[0], np.prod(shape_Y[1:]))
x_new = rho.dot(Y_2d[:m]).reshape(shape_Y[1:])

if verbose == 2:
error = np.max(np.abs(y_new - x_new))
etime = time.time() - start_time
print_skip = 1
_print_after_skip(print_skip, iterate, error, etime)
if verbose >= 1:
if not converged:
warnings.warn(_non_convergence_msg, RuntimeWarning)
elif verbose == 2:
print(_convergence_msg.format(iterate=iterate))

return x_new, converged, iterate


@jit(nopython=True)
def _initialize_tableaux_ig(X, Y, tableaux, bases):
"""
Given sequences `X` and `Y` of ndarrays, initialize the tableau and
basis arrays in place for the "geometric" imitation game as defined
in McLennan and Tourky (2006), to be passed to `lemke_howson_tbl`.

Parameters
----------
X, Y : ndarray(float)
Arrays of the same shape (m, n).

tableaux : tuple(ndarray(float, ndim=2))
Tuple of two arrays to be used to store the tableaux, of shape
(2m, 2m). Modified in place.

bases : tuple(ndarray(int, ndim=1))
Tuple of two arrays to be used to store the bases, of shape
(m,). Modified in place.

Returns
-------
tableaux : tuple(ndarray(float, ndim=2))
View to `tableaux`.

bases : tuple(ndarray(int, ndim=1))
View to `bases`.

"""
m = X.shape[0]
min_ = np.zeros(m)

# Mover
for i in range(m):
for j in range(2*m):
if j == i or j == i + m:
tableaux[0][i, j] = 1
else:
tableaux[0][i, j] = 0
# Right hand side
tableaux[0][i, 2*m] = 1

# Imitator
for i in range(m):
# Slack variables
for j in range(m):
if j == i:
tableaux[1][i, j] = 1
else:
tableaux[1][i, j] = 0
# Payoff variables
for j in range(m):
d = X[i] - Y[j]
tableaux[1][i, m+j] = _square_sum(d) * (-1)
if tableaux[1][i, m+j] < min_[j]:
min_[j] = tableaux[1][i, m+j]
# Right hand side
tableaux[1][i, 2*m] = 1
# Shift the payoff values
for i in range(m):
for j in range(m):
tableaux[1][i, m+j] -= min_[j]
tableaux[1][i, m+j] += 1

for pl, start in enumerate([m, 0]):
for i in range(m):
bases[pl][i] = start + i

return tableaux, bases


@generated_jit(nopython=True, cache=True)
def _square_sum(a):
if isinstance(a, types.Number):
return lambda a: a**2
elif isinstance(a, types.Array):
return _square_sum_array


def _square_sum_array(a):
sum_ = 0
for x in a.flat:
sum_ += x**2
return sum_
3 changes: 2 additions & 1 deletion quantecon/game_theory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .normal_form_game import Player, NormalFormGame
from .normal_form_game import pure2mixed, best_response_2p
from .random import random_game, covariance_game
from .pure_nash import pure_nash_brute, pure_nash_brute_gen
from .support_enumeration import support_enumeration, support_enumeration_gen
from .lemke_howson import lemke_howson
from .pure_nash import pure_nash_brute
from .mclennan_tourky import mclennan_tourky
Loading