Skip to content

Commit d5cd93b

Browse files
committed
ENH: Add GamPayoffVector
1 parent 92cb823 commit d5cd93b

2 files changed

Lines changed: 238 additions & 17 deletions

File tree

quantecon/game_theory/game_converters.py

Lines changed: 138 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Examples
55
--------
66
7-
Create a QuantEcon NormalFormGame from a gam file storing
7+
Create a QuantEcon NormalFormGame from a .gam file storing
88
a 3-player Minimum Effort Game
99
1010
>>> import os
@@ -27,10 +27,133 @@
2727
[[-19., -19., 1.], [ -8., -8., 2.], [ 3., 3., 3.]]]]
2828
2929
"""
30+
import numbers
3031
import numpy as np
3132
from .normal_form_game import Player, NormalFormGame
3233

3334

35+
class GamPayoffVector:
36+
"""
37+
Internal intermediate representation that stores payoffs in a single
38+
flat 1-dim array.
39+
40+
Payoff values are ordered as in the GameTracer .gam format:
41+
1. Player-major blocks: player 0, ..., player N-1.
42+
2. Within each block, action profiles are ordered with player 0
43+
varying fastest, then player 1, ..., player N-1 (i.e.,
44+
Fortran/column-major order).
45+
46+
Attributes
47+
----------
48+
N : scalar(int)
49+
Number of players.
50+
51+
nums_actions : tuple(int)
52+
Tuple of the numbers of actions, one for each player.
53+
54+
payoffs : ndarray(ndim=1)
55+
Array storing payoffs in .gam order.
56+
57+
"""
58+
def __init__(self, nums_actions, payoffs):
59+
nums_actions = tuple(nums_actions)
60+
if len(nums_actions) == 0:
61+
raise ValueError('nums_actions must be a non-empty iterable ' +
62+
'of positive integers')
63+
64+
for n in nums_actions:
65+
if not isinstance(n, numbers.Integral):
66+
raise TypeError('nums_actions must contain only integers')
67+
if n <= 0:
68+
raise ValueError('all nums_actions must be positive')
69+
70+
self.nums_actions = tuple(int(n) for n in nums_actions)
71+
self.N = len(self.nums_actions)
72+
73+
payoffs = np.ascontiguousarray(payoffs)
74+
if payoffs.ndim != 1:
75+
raise ValueError('payoffs must be a 1-dim array_like')
76+
77+
expected = np.prod(self.nums_actions) * self.N
78+
if payoffs.size != expected:
79+
raise ValueError(
80+
f'payoffs length mismatch: expected {expected}, ' +
81+
f'got {payoffs.size}'
82+
)
83+
84+
self.payoffs = payoffs
85+
86+
@classmethod
87+
def from_nfg(cls, g, dtype=None):
88+
"""
89+
Construct a GamPayoffVector from a NormalFormGame `g`.
90+
91+
Examples
92+
--------
93+
>>> player0 = Player([[0, 3], [1, 4], [2, 5]])
94+
>>> player1 = Player([[6, 7, 8], [9, 10, 11]])
95+
>>> g = NormalFormGame((player0, player1))
96+
>>> print(g)
97+
2-player NormalFormGame with payoff profile array:
98+
[[[ 0, 6], [ 3, 9]],
99+
[[ 1, 7], [ 4, 10]],
100+
[[ 2, 8], [ 5, 11]]]
101+
>>> p = GamPayoffVector.from_nfg(g)
102+
>>> p.payoffs
103+
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
104+
105+
"""
106+
N = g.N
107+
nums_actions = g.nums_actions
108+
if dtype is None:
109+
dtype = g.dtype
110+
111+
na = np.prod(nums_actions)
112+
payoffs = np.empty(na*N, dtype=dtype)
113+
114+
for i, player in enumerate(g.players):
115+
payoffs[na*i:na*(i+1)].reshape(nums_actions, order='F')[:] = \
116+
player.payoff_array.transpose(
117+
(*range(N-i, g.N), *range(N-i))
118+
)
119+
120+
return cls(nums_actions, payoffs)
121+
122+
def to_nfg(self, dtype=None):
123+
"""
124+
Construct a NormalFormGame from self.
125+
126+
Examples
127+
--------
128+
>>> nums_actions = (3, 2)
129+
>>> payoffs = np.arange(12)
130+
>>> p = GamPayoffVector(nums_actions, payoffs)
131+
>>> g = p.to_nfg()
132+
>>> print(g)
133+
2-player NormalFormGame with payoff profile array:
134+
[[[ 0, 6], [ 3, 9]],
135+
[[ 1, 7], [ 4, 10]],
136+
[[ 2, 8], [ 5, 11]]]
137+
138+
"""
139+
N = self.N
140+
nums_actions = self.nums_actions
141+
142+
na = np.prod(nums_actions)
143+
payoffs2d = self.payoffs.reshape((na, N), order='F')
144+
players = tuple(
145+
Player(
146+
np.asarray(
147+
payoffs2d[:, i].reshape(nums_actions, order='F').transpose(
148+
(*range(i, N), *range(i))
149+
), dtype=dtype, order='C'
150+
)
151+
) for i in range(N)
152+
)
153+
154+
return NormalFormGame(players)
155+
156+
34157
def _str2num(s):
35158
"""
36159
Convert string to appropriate numeric type.
@@ -52,27 +175,27 @@ def _str2num(s):
52175

53176
class GAMReader:
54177
"""
55-
Reader object that converts a game in GameTracer gam format into
178+
Reader object that converts a game in GameTracer .gam format into
56179
a NormalFormGame.
57180
58181
"""
59182
@classmethod
60183
def from_file(cls, file_path):
61184
"""
62-
Read from a gam format file.
185+
Read from a .gam format file.
63186
64187
Parameters
65188
----------
66189
file_path : str
67-
Path to gam file.
190+
Path to .gam file.
68191
69192
Returns
70193
-------
71194
NormalFormGame
72195
73196
Examples
74197
--------
75-
Save a gam format string in a temporary file:
198+
Save a .gam format string in a temporary file:
76199
77200
>>> import tempfile
78201
>>> fname = tempfile.mkstemp()[1]
@@ -111,12 +234,12 @@ def from_url(cls, url):
111234
@classmethod
112235
def from_string(cls, string):
113236
"""
114-
Read from a gam format string.
237+
Read from a .gam format string.
115238
116239
Parameters
117240
----------
118241
string : str
119-
String in gam format.
242+
String in .gam format.
120243
121244
Returns
122245
-------
@@ -163,13 +286,13 @@ def _parse(string):
163286
class GAMWriter:
164287
"""
165288
Writer object that converts a NormalFormgame into a game in
166-
GameTracer gam format.
289+
GameTracer .gam format.
167290
168291
"""
169292
@classmethod
170293
def to_file(cls, g, file_path):
171294
"""
172-
Save the GameTracer gam format string representation of the
295+
Save the GameTracer .gam format string representation of the
173296
NormalFormGame `g` to a file.
174297
175298
Parameters
@@ -186,7 +309,7 @@ def to_file(cls, g, file_path):
186309
@classmethod
187310
def to_string(cls, g):
188311
"""
189-
Return a GameTracer gam format string representing the
312+
Return a GameTracer .gam format string representing the
190313
NormalFormGame `g`.
191314
192315
Parameters
@@ -196,7 +319,7 @@ def to_string(cls, g):
196319
Returns
197320
-------
198321
str
199-
String representation in gam format.
322+
String representation in .gam format.
200323
201324
"""
202325
return cls._dump(g)
@@ -218,19 +341,19 @@ def _dump(g):
218341

219342
def from_gam(filename: str) -> NormalFormGame:
220343
"""
221-
Makes a QuantEcon Normal Form Game from a gam file.
344+
Makes a QuantEcon Normal Form Game from a .gam file.
222345
223346
Gam files are described by GameTracer [1]_.
224347
225348
Parameters
226349
----------
227350
filename : str
228-
path to gam file.
351+
path to .gam file.
229352
230353
Returns
231354
-------
232355
NormalFormGame
233-
The QuantEcon Normal Form Game described by the gam file.
356+
The QuantEcon Normal Form Game described by the .gam file.
234357
235358
References
236359
----------
@@ -243,7 +366,7 @@ def from_gam(filename: str) -> NormalFormGame:
243366

244367
def to_gam(g, file_path=None):
245368
"""
246-
Write a NormalFormGame to a file in gam format.
369+
Write a NormalFormGame to a file in .gam format.
247370
248371
Parameters
249372
----------

quantecon/game_theory/tests/test_game_converters.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,108 @@
44
"""
55
import os
66
from tempfile import NamedTemporaryFile
7-
from numpy.testing import assert_string_equal
8-
from quantecon.game_theory import NormalFormGame, GAMWriter, to_gam
7+
import numpy as np
8+
from numpy.testing import (
9+
assert_, assert_array_equal, assert_string_equal, assert_raises
10+
)
11+
from quantecon.game_theory import (
12+
Player, NormalFormGame, random_game, GAMWriter, to_gam
13+
)
14+
from quantecon.game_theory.game_converters import GamPayoffVector
915

1016

17+
# GamPayoffVector #
18+
19+
class TestGamPayoffVector:
20+
"""Golden test for GamPayoffVector"""
21+
22+
def setup_method(self):
23+
nums_actions = (2, 3, 4)
24+
N = len(nums_actions)
25+
na = np.prod(nums_actions)
26+
27+
A0 = np.arange(na).reshape(nums_actions, order='F')
28+
A1 = np.arange(100, 100+na).reshape(nums_actions, order='F')
29+
A2 = np.arange(200, 200+na).reshape(nums_actions, order='F')
30+
31+
self.payoffs1d = np.hstack([A.ravel(order='F') for A in [A0, A1, A2]])
32+
self.payoffs4d = np.stack([A0, A1, A2], axis=N)
33+
34+
self.N = N
35+
self.nums_actions = nums_actions
36+
37+
def test_init(self):
38+
p = GamPayoffVector(self.nums_actions, self.payoffs1d)
39+
40+
assert_(p.N == self.N)
41+
assert_(p.nums_actions == self.nums_actions)
42+
assert_array_equal(p.payoffs, self.payoffs1d)
43+
44+
def test_from_nfg(self):
45+
g = NormalFormGame(self.payoffs4d)
46+
p = GamPayoffVector.from_nfg(g)
47+
48+
assert_(p.N == self.N)
49+
assert_(p.nums_actions == self.nums_actions)
50+
assert_array_equal(p.payoffs, self.payoffs1d)
51+
52+
def test_to_nfg(self):
53+
p = GamPayoffVector(self.nums_actions, self.payoffs1d)
54+
g = p.to_nfg()
55+
assert_array_equal(g.payoff_profile_array, self.payoffs4d)
56+
57+
58+
def test_gampayoffvector_roundtrip():
59+
for ns in [(4, 3), (2, 2, 3, 2)]:
60+
N = len(ns)
61+
seed = 12345
62+
rng = np.random.default_rng(seed)
63+
payoffs = rng.integers(low=0, high=100, size=(*ns, N), dtype=np.int64)
64+
g = NormalFormGame(payoffs)
65+
p = GamPayoffVector.from_nfg(g)
66+
g1 = p.to_nfg()
67+
68+
p_32 = GamPayoffVector.from_nfg(g, dtype=np.int32)
69+
g2 = p_32.to_nfg()
70+
g3 = p_32.to_nfg(dtype=np.int64)
71+
72+
assert_(p_32.payoffs.dtype == np.int32)
73+
assert_(g2.dtype == np.int32)
74+
assert_(g3.dtype == np.int64)
75+
76+
for g_new in [g1, g2, g3]:
77+
assert_(g_new.N == g.N)
78+
assert_(g_new.nums_actions == g.nums_actions)
79+
for i in range(N):
80+
assert_array_equal(g_new.players[i].payoff_array,
81+
g.players[i].payoff_array)
82+
83+
84+
def test_gampayoffvector_1p():
85+
payoffs = [1., 2., 3.]
86+
nums_actions = (3,)
87+
88+
p0 = GamPayoffVector(nums_actions, payoffs)
89+
90+
g = NormalFormGame((Player(payoffs),))
91+
p1 = GamPayoffVector.from_nfg(g)
92+
93+
for p in [p0, p1]:
94+
assert_(p.N == 1)
95+
assert_(p.nums_actions == nums_actions)
96+
assert_array_equal(p.payoffs, payoffs)
97+
98+
99+
def test_invalid_inputs():
100+
assert_raises(ValueError, GamPayoffVector, (), np.array([], dtype=float))
101+
assert_raises(TypeError, GamPayoffVector, (2, 2.0), np.zeros(8))
102+
assert_raises(ValueError, GamPayoffVector, (2, 0), np.zeros(0))
103+
assert_raises(ValueError, GamPayoffVector, (2, 2), np.zeros((2, 4)))
104+
assert_raises(ValueError, GamPayoffVector, (2, 2), np.zeros(7))
105+
106+
107+
# TestGAMWriter #
108+
11109
class TestGAMWriter:
12110
def setup_method(self):
13111
nums_actions = (2, 2, 2)

0 commit comments

Comments
 (0)