Skip to content

Commit 75e12dc

Browse files
authored
Merge pull request #273 from QuantEcon/compute_fp_lemke_howson
Implement the "imitation game algorithm" by McLennan-Tourky
2 parents 9004b35 + 9463b33 commit 75e12dc

10 files changed

+818
-103
lines changed

.gitignore

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ quantecon.egg-info/
1111
*.h5
1212
examples/solow_model/depreciation_rates.dta
1313
examples/solow_model/pwt80.dta
14-
examples/*.png
14+
examples/*.png
15+
16+
# Numba cache files
17+
*.nbc
18+
*.nbi

quantecon/compute_fp.py

+301-17
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
"""
22
Filename: compute_fp.py
3-
Authors: Thomas Sargent, John Stachurski
3+
Authors: Thomas Sargent, John Stachurski, Daisuke Oyama
44
5-
Compute the fixed point of a given operator T, starting from
5+
Compute an approximate fixed point of a given operator T, starting from
66
specified initial condition v.
77
88
"""
99
import time
10+
import warnings
1011
import numpy as np
12+
from numba import jit, generated_jit, types
13+
from .game_theory.lemke_howson import lemke_howson_tbl, get_mixed_actions
1114

1215

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

3538

36-
def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=1,
37-
print_skip=5, *args, **kwargs):
39+
_convergence_msg = 'Converged in {iterate} steps'
40+
_non_convergence_msg = \
41+
'max_iter attained before convergence in compute_fixed_point'
42+
43+
44+
def _is_approx_fp(T, v, error_tol, *args, **kwargs):
45+
error = np.max(np.abs(T(v, *args, **kwargs) - v))
46+
return error <= error_tol
47+
48+
49+
def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=2,
50+
print_skip=5, method='iteration', *args, **kwargs):
3851
"""
39-
Computes and returns :math:`T^k v`, an approximate fixed point.
52+
Computes and returns an approximate fixed point of the function `T`.
53+
54+
The default method `'iteration'` simply iterates the function given
55+
an initial condition `v` and returns :math:`T^k v` when the
56+
condition :math:`\lVert T^k v - T^{k-1} v\rVert \leq
57+
\mathrm{error_tol}` is satisfied or the number of iterations
58+
:math:`k` reaches `max_iter`. Provided that `T` is a contraction
59+
mapping or similar, :math:`T^k v` will be an approximation to the
60+
fixed point.
4061
41-
Here T is an operator, v is an initial condition and k is the number
42-
of iterates. Provided that T is a contraction mapping or similar,
43-
:math:`T^k v` will be an approximation to the fixed point.
62+
The method `'imitation_game'` uses the "imitation game algorithm"
63+
developed by McLennan and Tourky [1]_, which internally constructs
64+
a sequence of two-player games called imitation games and utilizes
65+
their Nash equilibria, computed by the Lemke-Howson algorithm
66+
routine. It finds an approximate fixed point of `T`, a point
67+
:math:`v^*` such that :math:`\lVert T(v) - v\rVert \leq
68+
\mathrm{error_tol}`, provided `T` is a function that satisfies the
69+
assumptions of Brouwer's fixed point theorm, i.e., a continuous
70+
function that maps a compact and convex set to itself.
4471
4572
Parameters
4673
----------
4774
T : callable
4875
A callable object (e.g., function) that acts on v
4976
v : object
50-
An object such that T(v) is defined
77+
An object such that T(v) is defined; modified in place if
78+
`method='iteration' and `v` is an array
5179
error_tol : scalar(float), optional(default=1e-3)
5280
Error tolerance
5381
max_iter : scalar(int), optional(default=50)
5482
Maximum number of iterations
55-
verbose : bool, optional(default=True)
56-
If True then print current error at each iterate.
83+
verbose : scalar(int), optional(default=2)
84+
Level of feedback (0 for no output, 1 for warnings only, 2 for
85+
warning and residual error reports during iteration)
86+
print_skip : scalar(int), optional(default=5)
87+
How many iterations to apply between print messages (effective
88+
only when `verbose=2`)
89+
method : str in {'iteration', 'imitation_game'},
90+
optional(default='iteration')
91+
Method of computing an approximate fixed point
5792
args, kwargs :
5893
Other arguments and keyword arguments that are passed directly
5994
to the function T each time it is called
@@ -63,25 +98,274 @@ def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=1,
6398
v : object
6499
The approximate fixed point
65100
101+
References
102+
----------
103+
.. [1] A. McLennan and R. Tourky, "From Imitation Games to
104+
Kakutani," 2006.
105+
66106
"""
107+
if max_iter < 1:
108+
raise ValueError('max_iter must be a positive integer')
109+
110+
if verbose not in (0, 1, 2):
111+
raise ValueError('verbose should be 0, 1 or 2')
112+
113+
if method not in ['iteration', 'imitation_game']:
114+
raise ValueError('invalid method')
115+
116+
if method == 'imitation_game':
117+
is_approx_fp = \
118+
lambda v: _is_approx_fp(T, v, error_tol, *args, **kwargs)
119+
v_star, converged, iterate = \
120+
_compute_fixed_point_ig(T, v, max_iter, verbose, print_skip,
121+
is_approx_fp, *args, **kwargs)
122+
return v_star
123+
124+
# method == 'iteration'
67125
iterate = 0
68126
error = error_tol + 1
69127

70-
if verbose:
128+
if verbose == 2:
71129
start_time = time.time()
72130
_print_after_skip(print_skip, it=None)
73131

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

79-
if verbose:
80-
etime = time.time() - start_time
81-
_print_after_skip(print_skip, iterate, error, etime)
82-
83137
try:
84138
v[:] = new_v
85139
except TypeError:
86140
v = new_v
141+
142+
if error <= error_tol or iterate >= max_iter:
143+
break
144+
145+
if verbose == 2:
146+
etime = time.time() - start_time
147+
_print_after_skip(print_skip, iterate, error, etime)
148+
149+
if verbose == 2:
150+
etime = time.time() - start_time
151+
print_skip = 1
152+
_print_after_skip(print_skip, iterate, error, etime)
153+
if verbose >= 1:
154+
if error > error_tol:
155+
warnings.warn(_non_convergence_msg, RuntimeWarning)
156+
elif verbose == 2:
157+
print(_convergence_msg.format(iterate=iterate))
158+
87159
return v
160+
161+
162+
def _compute_fixed_point_ig(T, v, max_iter, verbose, print_skip, is_approx_fp,
163+
*args, **kwargs):
164+
"""
165+
Implement the imitation game algorithm by McLennan and Tourky (2006)
166+
for computing an approximate fixed point of `T`.
167+
168+
Parameters
169+
----------
170+
is_approx_fp : callable
171+
A callable with signature `is_approx_fp(v)` which determines
172+
whether `v` is an approximate fixed point with a bool return
173+
value (i.e., True or False)
174+
175+
For the other parameters, see Parameters in compute_fixed_point.
176+
177+
Returns
178+
-------
179+
x_new : scalar(float) or ndarray(float)
180+
Approximate fixed point.
181+
182+
converged : bool
183+
Whether the routine has converged.
184+
185+
iterate : scalar(int)
186+
Number of iterations.
187+
188+
"""
189+
if verbose == 2:
190+
start_time = time.time()
191+
_print_after_skip(print_skip, it=None)
192+
193+
x_new = v
194+
y_new = T(x_new, *args, **kwargs)
195+
iterate = 1
196+
converged = is_approx_fp(x_new)
197+
198+
if converged or iterate >= max_iter:
199+
if verbose == 2:
200+
error = np.max(np.abs(y_new - x_new))
201+
etime = time.time() - start_time
202+
print_skip = 1
203+
_print_after_skip(print_skip, iterate, error, etime)
204+
if verbose >= 1:
205+
if not converged:
206+
warnings.warn(_non_convergence_msg, RuntimeWarning)
207+
elif verbose == 2:
208+
print(_convergence_msg.format(iterate=iterate))
209+
return x_new, converged, iterate
210+
211+
if verbose == 2:
212+
error = np.max(np.abs(y_new - x_new))
213+
etime = time.time() - start_time
214+
_print_after_skip(print_skip, iterate, error, etime)
215+
216+
# Length of the arrays to store the computed sequences of x and y.
217+
# If exceeded, reset to min(max_iter, buff_size*2).
218+
buff_size = 2**8
219+
buff_size = min(max_iter, buff_size)
220+
221+
shape = (buff_size,) + np.asarray(x_new).shape
222+
X, Y = np.empty(shape), np.empty(shape)
223+
X[0], Y[0] = x_new, y_new
224+
x_new = Y[0]
225+
226+
tableaux = tuple(np.empty((buff_size, buff_size*2+1)) for i in range(2))
227+
bases = tuple(np.empty(buff_size, dtype=int) for i in range(2))
228+
max_piv = 10**6 # Max number of pivoting steps in lemke_howson_tbl
229+
230+
while True:
231+
y_new = T(x_new, *args, **kwargs)
232+
iterate += 1
233+
converged = is_approx_fp(x_new)
234+
235+
if converged or iterate >= max_iter:
236+
break
237+
238+
if verbose == 2:
239+
error = np.max(np.abs(y_new - x_new))
240+
etime = time.time() - start_time
241+
_print_after_skip(print_skip, iterate, error, etime)
242+
243+
try:
244+
X[iterate-1] = x_new
245+
Y[iterate-1] = y_new
246+
except IndexError:
247+
buff_size = min(max_iter, buff_size*2)
248+
shape = (buff_size,) + X.shape[1:]
249+
X_tmp, Y_tmp = X, Y
250+
X, Y = np.empty(shape), np.empty(shape)
251+
X[:X_tmp.shape[0]], Y[:Y_tmp.shape[0]] = X_tmp, Y_tmp
252+
X[iterate-1], Y[iterate-1] = x_new, y_new
253+
254+
tableaux = tuple(np.empty((buff_size, buff_size*2+1))
255+
for i in range(2))
256+
bases = tuple(np.empty(buff_size, dtype=int) for i in range(2))
257+
258+
m = iterate
259+
tableaux_curr = tuple(tableau[:m, :2*m+1] for tableau in tableaux)
260+
bases_curr = tuple(basis[:m] for basis in bases)
261+
_initialize_tableaux_ig(X[:m], Y[:m], tableaux_curr, bases_curr)
262+
converged, num_iter = lemke_howson_tbl(
263+
tableaux_curr, bases_curr, init_pivot=m-1, max_iter=max_piv
264+
)
265+
_, rho = get_mixed_actions(tableaux_curr, bases_curr)
266+
267+
if Y.ndim <= 2:
268+
x_new = rho.dot(Y[:m])
269+
else:
270+
shape_Y = Y.shape
271+
Y_2d = Y.reshape(shape_Y[0], np.prod(shape_Y[1:]))
272+
x_new = rho.dot(Y_2d[:m]).reshape(shape_Y[1:])
273+
274+
if verbose == 2:
275+
error = np.max(np.abs(y_new - x_new))
276+
etime = time.time() - start_time
277+
print_skip = 1
278+
_print_after_skip(print_skip, iterate, error, etime)
279+
if verbose >= 1:
280+
if not converged:
281+
warnings.warn(_non_convergence_msg, RuntimeWarning)
282+
elif verbose == 2:
283+
print(_convergence_msg.format(iterate=iterate))
284+
285+
return x_new, converged, iterate
286+
287+
288+
@jit(nopython=True)
289+
def _initialize_tableaux_ig(X, Y, tableaux, bases):
290+
"""
291+
Given sequences `X` and `Y` of ndarrays, initialize the tableau and
292+
basis arrays in place for the "geometric" imitation game as defined
293+
in McLennan and Tourky (2006), to be passed to `lemke_howson_tbl`.
294+
295+
Parameters
296+
----------
297+
X, Y : ndarray(float)
298+
Arrays of the same shape (m, n).
299+
300+
tableaux : tuple(ndarray(float, ndim=2))
301+
Tuple of two arrays to be used to store the tableaux, of shape
302+
(2m, 2m). Modified in place.
303+
304+
bases : tuple(ndarray(int, ndim=1))
305+
Tuple of two arrays to be used to store the bases, of shape
306+
(m,). Modified in place.
307+
308+
Returns
309+
-------
310+
tableaux : tuple(ndarray(float, ndim=2))
311+
View to `tableaux`.
312+
313+
bases : tuple(ndarray(int, ndim=1))
314+
View to `bases`.
315+
316+
"""
317+
m = X.shape[0]
318+
min_ = np.zeros(m)
319+
320+
# Mover
321+
for i in range(m):
322+
for j in range(2*m):
323+
if j == i or j == i + m:
324+
tableaux[0][i, j] = 1
325+
else:
326+
tableaux[0][i, j] = 0
327+
# Right hand side
328+
tableaux[0][i, 2*m] = 1
329+
330+
# Imitator
331+
for i in range(m):
332+
# Slack variables
333+
for j in range(m):
334+
if j == i:
335+
tableaux[1][i, j] = 1
336+
else:
337+
tableaux[1][i, j] = 0
338+
# Payoff variables
339+
for j in range(m):
340+
d = X[i] - Y[j]
341+
tableaux[1][i, m+j] = _square_sum(d) * (-1)
342+
if tableaux[1][i, m+j] < min_[j]:
343+
min_[j] = tableaux[1][i, m+j]
344+
# Right hand side
345+
tableaux[1][i, 2*m] = 1
346+
# Shift the payoff values
347+
for i in range(m):
348+
for j in range(m):
349+
tableaux[1][i, m+j] -= min_[j]
350+
tableaux[1][i, m+j] += 1
351+
352+
for pl, start in enumerate([m, 0]):
353+
for i in range(m):
354+
bases[pl][i] = start + i
355+
356+
return tableaux, bases
357+
358+
359+
@generated_jit(nopython=True, cache=True)
360+
def _square_sum(a):
361+
if isinstance(a, types.Number):
362+
return lambda a: a**2
363+
elif isinstance(a, types.Array):
364+
return _square_sum_array
365+
366+
367+
def _square_sum_array(a):
368+
sum_ = 0
369+
for x in a.flat:
370+
sum_ += x**2
371+
return sum_

quantecon/game_theory/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .normal_form_game import Player, NormalFormGame
66
from .normal_form_game import pure2mixed, best_response_2p
77
from .random import random_game, covariance_game
8+
from .pure_nash import pure_nash_brute, pure_nash_brute_gen
89
from .support_enumeration import support_enumeration, support_enumeration_gen
910
from .lemke_howson import lemke_howson
10-
from .pure_nash import pure_nash_brute
11+
from .mclennan_tourky import mclennan_tourky

0 commit comments

Comments
 (0)