Skip to content

Commit 87450f3

Browse files
authored
RFC, ENH: Implement GAMReader and GAMWriter (#758)
* ENH: Implement GAMReader * ENH: Implement GAMWriter * DOC: Revise Examples in game_converters.py * Rename `qe_nfg_from_gam_file` to `from_gam` * Add `to_gam`
1 parent 83a893d commit 87450f3

File tree

5 files changed

+263
-37
lines changed

5 files changed

+263
-37
lines changed

quantecon/game_theory/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@
2424
from .logitdyn import LogitDynamics
2525
from .polymatrix_game import PolymatrixGame
2626
from .howson_lcp import polym_lcp_solver
27+
from .game_converters import GAMReader, GAMWriter, from_gam, to_gam

quantecon/game_theory/game_converters.py

Lines changed: 193 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
Create a QuantEcon NormalFormGame from a gam file storing
88
a 3-player Minimum Effort Game
99
10-
>>> filepath = "./tests/gam_files/minimum_effort_game.gam"
11-
>>> nfg = qe_nfg_from_gam_file(filepath)
10+
>>> import os
11+
>>> import quantecon.game_theory as gt
12+
>>> filepath = os.path.dirname(gt.__file__)
13+
>>> filepath += "/tests/game_files/minimum_effort_game.gam"
14+
>>> nfg = gt.from_gam(filepath)
1215
>>> print(nfg)
1316
3-player NormalFormGame with payoff profile array:
1417
[[[[ 1., 1., 1.], [ 1., 1., -9.], [ 1., 1., -19.]],
@@ -24,12 +27,183 @@
2427
[[-19., -19., 1.], [ -8., -8., 2.], [ 3., 3., 3.]]]]
2528
2629
"""
30+
import numpy as np
31+
from .normal_form_game import Player, NormalFormGame
2732

28-
from .normal_form_game import NormalFormGame
29-
from itertools import product
3033

34+
def str2num(s):
35+
if '.' in s:
36+
return float(s)
37+
return int(s)
3138

32-
def qe_nfg_from_gam_file(filename: str) -> NormalFormGame:
39+
40+
class GAMReader:
41+
"""
42+
Reader object that converts a game in GameTracer gam format into
43+
a NormalFormGame.
44+
45+
"""
46+
@classmethod
47+
def from_file(cls, file_path):
48+
"""
49+
Read from a gam format file.
50+
51+
Parameters
52+
----------
53+
file_path : str
54+
Path to gam file.
55+
56+
Returns
57+
-------
58+
NormalFormGame
59+
60+
Examples
61+
--------
62+
Save a gam format string in a temporary file:
63+
64+
>>> import tempfile
65+
>>> fname = tempfile.mkstemp()[1]
66+
>>> with open(fname, mode='w') as f:
67+
... f.write(\"\"\"\\
68+
... 2
69+
... 3 2
70+
...
71+
... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\")
72+
73+
Read the file:
74+
75+
>>> g = GAMReader.from_file(fname)
76+
>>> print(g)
77+
2-player NormalFormGame with payoff profile array:
78+
[[[3, 3], [3, 2]],
79+
[[2, 2], [5, 6]],
80+
[[0, 3], [6, 1]]]
81+
82+
"""
83+
with open(file_path, 'r') as f:
84+
string = f.read()
85+
return cls._parse(string)
86+
87+
@classmethod
88+
def from_url(cls, url):
89+
"""
90+
Read from a URL.
91+
92+
"""
93+
import urllib.request
94+
with urllib.request.urlopen(url) as response:
95+
string = response.read().decode()
96+
return cls._parse(string)
97+
98+
@classmethod
99+
def from_string(cls, string):
100+
"""
101+
Read from a gam format string.
102+
103+
Parameters
104+
----------
105+
string : str
106+
String in gam format.
107+
108+
Returns
109+
-------
110+
NormalFormGame
111+
112+
Examples
113+
--------
114+
>>> string = \"\"\"\\
115+
... 2
116+
... 3 2
117+
...
118+
... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\"
119+
>>> g = GAMReader.from_string(string)
120+
>>> print(g)
121+
2-player NormalFormGame with payoff profile array:
122+
[[[3, 3], [3, 2]],
123+
[[2, 2], [5, 6]],
124+
[[0, 3], [6, 1]]]
125+
126+
"""
127+
return cls._parse(string)
128+
129+
@staticmethod
130+
def _parse(string):
131+
tokens = string.split()
132+
133+
N = int(tokens.pop(0))
134+
nums_actions = tuple(int(tokens.pop(0)) for _ in range(N))
135+
payoffs = np.array([str2num(s) for s in tokens])
136+
137+
na = np.prod(nums_actions)
138+
payoffs2d = payoffs.reshape(N, na)
139+
players = [
140+
Player(
141+
payoffs2d[i, :].reshape(nums_actions, order='F').transpose(
142+
list(range(i, N)) + list(range(i))
143+
)
144+
) for i in range(N)
145+
]
146+
147+
return NormalFormGame(players)
148+
149+
150+
class GAMWriter:
151+
"""
152+
Writer object that converts a NormalFormgame into a game in
153+
GameTracer gam format.
154+
155+
"""
156+
@classmethod
157+
def to_file(cls, g, file_path):
158+
"""
159+
Save the GameTracer gam format string representation of the
160+
NormalFormGame `g` to a file.
161+
162+
Parameters
163+
----------
164+
g : NormalFormGame
165+
166+
file_path : str
167+
Path to the file to write to.
168+
169+
"""
170+
with open(file_path, 'w') as f:
171+
f.write(cls._dump(g) + '\n')
172+
173+
@classmethod
174+
def to_string(cls, g):
175+
"""
176+
Return a GameTracer gam format string representing the
177+
NormalFormGame `g`.
178+
179+
Parameters
180+
----------
181+
g : NormalFormGame
182+
183+
Returns
184+
-------
185+
str
186+
String representation in gam format.
187+
188+
"""
189+
return cls._dump(g)
190+
191+
@staticmethod
192+
def _dump(g):
193+
s = str(g.N) + '\n'
194+
s += ' '.join(map(str, g.nums_actions)) + '\n\n'
195+
196+
for i, player in enumerate(g.players):
197+
payoffs = np.array2string(
198+
player.payoff_array.transpose(
199+
list(range(g.N-i, g.N)) + list(range(g.N-i))
200+
).ravel(order='F'))[1:-1]
201+
s += ' '.join(payoffs.split()) + ' '
202+
203+
return s.rstrip()
204+
205+
206+
def from_gam(filename: str) -> NormalFormGame:
33207
"""
34208
Makes a QuantEcon Normal Form Game from a gam file.
35209
@@ -51,32 +225,22 @@ def qe_nfg_from_gam_file(filename: str) -> NormalFormGame:
51225
http://dags.stanford.edu/Games/gametracer.html
52226
53227
"""
54-
with open(filename, 'r') as file:
55-
lines = file.readlines()
56-
combined = [
57-
token
58-
for line in lines
59-
for token in line.split()
60-
]
228+
return GAMReader.from_file(filename)
61229

62-
i = iter(combined)
63-
players = int(next(i))
64-
actions = [int(next(i)) for _ in range(players)]
65230

66-
nfg = NormalFormGame(actions)
231+
def to_gam(g, file_path=None):
232+
"""
233+
Write a NormalFormGame to a file in gam format.
67234
68-
entries = [
69-
{
70-
tuple(reversed(action_combination)): float(next(i))
71-
for action_combination in product(
72-
*[range(a) for a in actions])
73-
}
74-
for _ in range(players)
75-
]
235+
Parameters
236+
----------
237+
g : NormalFormGame
76238
77-
for action_combination in product(*[range(a) for a in actions]):
78-
nfg[action_combination] = tuple(
79-
entries[p][action_combination] for p in range(players)
80-
)
239+
file_path : str, optional(default=None)
240+
Path to the file to write to. If None, the result is returned as
241+
a string.
81242
82-
return nfg
243+
"""
244+
if file_path is None:
245+
return GAMWriter.to_string(g)
246+
return GAMWriter.to_file(g, file_path)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Tests for game_theory/game_converters.py
3+
4+
"""
5+
import os
6+
from tempfile import NamedTemporaryFile
7+
from numpy.testing import assert_string_equal
8+
from quantecon.game_theory import NormalFormGame, GAMWriter, to_gam
9+
10+
11+
class TestGAMWriter:
12+
def setup_method(self):
13+
nums_actions = (2, 2, 2)
14+
g = NormalFormGame(nums_actions)
15+
g[0, 0, 0] = (0, 8, 16)
16+
g[1, 0, 0] = (1, 9, 17)
17+
g[0, 1, 0] = (2, 10, 18)
18+
g[1, 1, 0] = (3, 11, 19)
19+
g[0, 0, 1] = (4, 12, 20)
20+
g[1, 0, 1] = (5, 13, 21)
21+
g[0, 1, 1] = (6, 14, 22)
22+
g[1, 1, 1] = (7, 15, 23)
23+
self.g = g
24+
25+
self.s_desired = """\
26+
3
27+
2 2 2
28+
29+
0. 1. 2. 3. 4. 5. 6. 7. \
30+
8. 9. 10. 11. 12. 13. 14. 15. \
31+
16. 17. 18. 19. 20. 21. 22. 23."""
32+
33+
def test_to_file(self):
34+
with NamedTemporaryFile(delete=False) as tmp_file:
35+
temp_path = tmp_file.name
36+
GAMWriter.to_file(self.g, temp_path)
37+
38+
with open(temp_path, 'r') as f:
39+
s_actual = f.read()
40+
assert_string_equal(s_actual, self.s_desired + '\n')
41+
42+
os.remove(temp_path)
43+
44+
def test_to_string(self):
45+
s_actual = GAMWriter.to_string(self.g)
46+
47+
assert_string_equal(s_actual, self.s_desired)
48+
49+
def test_to_gam(self):
50+
s_actual = to_gam(self.g)
51+
assert_string_equal(s_actual, self.s_desired)
52+
53+
with NamedTemporaryFile(delete=False) as tmp_file:
54+
temp_path = tmp_file.name
55+
to_gam(self.g, temp_path)
56+
57+
with open(temp_path, 'r') as f:
58+
s_actual = f.read()
59+
assert_string_equal(s_actual, self.s_desired + '\n')
60+
61+
os.remove(temp_path)

quantecon/game_theory/tests/test_howson_lcp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import numpy as np
66
from numpy.testing import assert_, assert_allclose
7-
from quantecon.game_theory.game_converters import qe_nfg_from_gam_file
7+
from quantecon.game_theory.game_converters import from_gam
88
from quantecon.game_theory import (
99
Player,
1010
NormalFormGame,
@@ -22,7 +22,7 @@
2222

2323
def test_polym_lcp_solver_where_solution_is_pure_NE():
2424
filename = "big_polym.gam"
25-
nfg = qe_nfg_from_gam_file(os.path.join(data_dir, filename))
25+
nfg = from_gam(os.path.join(data_dir, filename))
2626
polymg = PolymatrixGame.from_nf(nfg)
2727
ne = polym_lcp_solver(polymg)
2828
worked = nfg.is_nash(ne)
@@ -31,7 +31,7 @@ def test_polym_lcp_solver_where_solution_is_pure_NE():
3131

3232
def test_polym_lcp_solver_where_lcp_solver_must_backtrack():
3333
filename = "triggers_back_case.gam"
34-
nfg = qe_nfg_from_gam_file(os.path.join(data_dir, filename))
34+
nfg = from_gam(os.path.join(data_dir, filename))
3535
polymg = PolymatrixGame.from_nf(nfg)
3636
ne = polym_lcp_solver(polymg)
3737
worked = nfg.is_nash(ne)
@@ -305,7 +305,7 @@ def test_solves_multiplayer_rps_like():
305305

306306
def test_different_starting():
307307
filename = "triggers_back_case.gam"
308-
nfg = qe_nfg_from_gam_file(os.path.join(data_dir, filename))
308+
nfg = from_gam(os.path.join(data_dir, filename))
309309
polymg = PolymatrixGame.from_nf(nfg)
310310
starting = [3, 2, 2, 0, 3]
311311
# We also notice that changing the start

quantecon/game_theory/tests/test_polymatrix_game.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from numpy.testing import assert_, assert_raises
6-
from quantecon.game_theory.game_converters import qe_nfg_from_gam_file
6+
from quantecon.game_theory.game_converters import from_gam
77
from quantecon.game_theory import NormalFormGame, PolymatrixGame
88
from numpy import allclose, zeros
99

@@ -39,13 +39,13 @@ class TestPolymatrixGame():
3939
@classmethod
4040
def setup_class(cls):
4141
filename = "minimum_effort_game.gam"
42-
cls.non_pmg = qe_nfg_from_gam_file(
42+
cls.non_pmg = from_gam(
4343
os.path.join(data_dir, filename))
4444
filename = "big_polym.gam"
45-
cls.pmg1 = qe_nfg_from_gam_file(
45+
cls.pmg1 = from_gam(
4646
os.path.join(data_dir, filename))
4747
filename = "triggers_back_case.gam"
48-
cls.pmg2 = qe_nfg_from_gam_file(
48+
cls.pmg2 = from_gam(
4949
os.path.join(data_dir, filename))
5050
bimatrix = [[(54, 23), (72, 34)],
5151
[(92, 32), (34, 36)],

0 commit comments

Comments
 (0)