Skip to content

Commit

Permalink
RFC, ENH: Implement GAMReader and GAMWriter (#758)
Browse files Browse the repository at this point in the history
* 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`
  • Loading branch information
oyamad authored Jan 23, 2025
1 parent 83a893d commit 87450f3
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 37 deletions.
1 change: 1 addition & 0 deletions quantecon/game_theory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
from .logitdyn import LogitDynamics
from .polymatrix_game import PolymatrixGame
from .howson_lcp import polym_lcp_solver
from .game_converters import GAMReader, GAMWriter, from_gam, to_gam
222 changes: 193 additions & 29 deletions quantecon/game_theory/game_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
Create a QuantEcon NormalFormGame from a gam file storing
a 3-player Minimum Effort Game
>>> filepath = "./tests/gam_files/minimum_effort_game.gam"
>>> nfg = qe_nfg_from_gam_file(filepath)
>>> import os
>>> import quantecon.game_theory as gt
>>> filepath = os.path.dirname(gt.__file__)
>>> filepath += "/tests/game_files/minimum_effort_game.gam"
>>> nfg = gt.from_gam(filepath)
>>> print(nfg)
3-player NormalFormGame with payoff profile array:
[[[[ 1., 1., 1.], [ 1., 1., -9.], [ 1., 1., -19.]],
Expand All @@ -24,12 +27,183 @@
[[-19., -19., 1.], [ -8., -8., 2.], [ 3., 3., 3.]]]]
"""
import numpy as np
from .normal_form_game import Player, NormalFormGame

from .normal_form_game import NormalFormGame
from itertools import product

def str2num(s):
if '.' in s:
return float(s)
return int(s)

def qe_nfg_from_gam_file(filename: str) -> NormalFormGame:

class GAMReader:
"""
Reader object that converts a game in GameTracer gam format into
a NormalFormGame.
"""
@classmethod
def from_file(cls, file_path):
"""
Read from a gam format file.
Parameters
----------
file_path : str
Path to gam file.
Returns
-------
NormalFormGame
Examples
--------
Save a gam format string in a temporary file:
>>> import tempfile
>>> fname = tempfile.mkstemp()[1]
>>> with open(fname, mode='w') as f:
... f.write(\"\"\"\\
... 2
... 3 2
...
... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\")
Read the file:
>>> g = GAMReader.from_file(fname)
>>> print(g)
2-player NormalFormGame with payoff profile array:
[[[3, 3], [3, 2]],
[[2, 2], [5, 6]],
[[0, 3], [6, 1]]]
"""
with open(file_path, 'r') as f:
string = f.read()
return cls._parse(string)

@classmethod
def from_url(cls, url):
"""
Read from a URL.
"""
import urllib.request
with urllib.request.urlopen(url) as response:
string = response.read().decode()
return cls._parse(string)

@classmethod
def from_string(cls, string):
"""
Read from a gam format string.
Parameters
----------
string : str
String in gam format.
Returns
-------
NormalFormGame
Examples
--------
>>> string = \"\"\"\\
... 2
... 3 2
...
... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\"
>>> g = GAMReader.from_string(string)
>>> print(g)
2-player NormalFormGame with payoff profile array:
[[[3, 3], [3, 2]],
[[2, 2], [5, 6]],
[[0, 3], [6, 1]]]
"""
return cls._parse(string)

@staticmethod
def _parse(string):
tokens = string.split()

N = int(tokens.pop(0))
nums_actions = tuple(int(tokens.pop(0)) for _ in range(N))
payoffs = np.array([str2num(s) for s in tokens])

na = np.prod(nums_actions)
payoffs2d = payoffs.reshape(N, na)
players = [
Player(
payoffs2d[i, :].reshape(nums_actions, order='F').transpose(
list(range(i, N)) + list(range(i))
)
) for i in range(N)
]

return NormalFormGame(players)


class GAMWriter:
"""
Writer object that converts a NormalFormgame into a game in
GameTracer gam format.
"""
@classmethod
def to_file(cls, g, file_path):
"""
Save the GameTracer gam format string representation of the
NormalFormGame `g` to a file.
Parameters
----------
g : NormalFormGame
file_path : str
Path to the file to write to.
"""
with open(file_path, 'w') as f:
f.write(cls._dump(g) + '\n')

@classmethod
def to_string(cls, g):
"""
Return a GameTracer gam format string representing the
NormalFormGame `g`.
Parameters
----------
g : NormalFormGame
Returns
-------
str
String representation in gam format.
"""
return cls._dump(g)

@staticmethod
def _dump(g):
s = str(g.N) + '\n'
s += ' '.join(map(str, g.nums_actions)) + '\n\n'

for i, player in enumerate(g.players):
payoffs = np.array2string(
player.payoff_array.transpose(
list(range(g.N-i, g.N)) + list(range(g.N-i))
).ravel(order='F'))[1:-1]
s += ' '.join(payoffs.split()) + ' '

return s.rstrip()


def from_gam(filename: str) -> NormalFormGame:
"""
Makes a QuantEcon Normal Form Game from a gam file.
Expand All @@ -51,32 +225,22 @@ def qe_nfg_from_gam_file(filename: str) -> NormalFormGame:
http://dags.stanford.edu/Games/gametracer.html
"""
with open(filename, 'r') as file:
lines = file.readlines()
combined = [
token
for line in lines
for token in line.split()
]
return GAMReader.from_file(filename)

i = iter(combined)
players = int(next(i))
actions = [int(next(i)) for _ in range(players)]

nfg = NormalFormGame(actions)
def to_gam(g, file_path=None):
"""
Write a NormalFormGame to a file in gam format.
entries = [
{
tuple(reversed(action_combination)): float(next(i))
for action_combination in product(
*[range(a) for a in actions])
}
for _ in range(players)
]
Parameters
----------
g : NormalFormGame
for action_combination in product(*[range(a) for a in actions]):
nfg[action_combination] = tuple(
entries[p][action_combination] for p in range(players)
)
file_path : str, optional(default=None)
Path to the file to write to. If None, the result is returned as
a string.
return nfg
"""
if file_path is None:
return GAMWriter.to_string(g)
return GAMWriter.to_file(g, file_path)
61 changes: 61 additions & 0 deletions quantecon/game_theory/tests/test_game_converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Tests for game_theory/game_converters.py
"""
import os
from tempfile import NamedTemporaryFile
from numpy.testing import assert_string_equal
from quantecon.game_theory import NormalFormGame, GAMWriter, to_gam


class TestGAMWriter:
def setup_method(self):
nums_actions = (2, 2, 2)
g = NormalFormGame(nums_actions)
g[0, 0, 0] = (0, 8, 16)
g[1, 0, 0] = (1, 9, 17)
g[0, 1, 0] = (2, 10, 18)
g[1, 1, 0] = (3, 11, 19)
g[0, 0, 1] = (4, 12, 20)
g[1, 0, 1] = (5, 13, 21)
g[0, 1, 1] = (6, 14, 22)
g[1, 1, 1] = (7, 15, 23)
self.g = g

self.s_desired = """\
3
2 2 2
0. 1. 2. 3. 4. 5. 6. 7. \
8. 9. 10. 11. 12. 13. 14. 15. \
16. 17. 18. 19. 20. 21. 22. 23."""

def test_to_file(self):
with NamedTemporaryFile(delete=False) as tmp_file:
temp_path = tmp_file.name
GAMWriter.to_file(self.g, temp_path)

with open(temp_path, 'r') as f:
s_actual = f.read()
assert_string_equal(s_actual, self.s_desired + '\n')

os.remove(temp_path)

def test_to_string(self):
s_actual = GAMWriter.to_string(self.g)

assert_string_equal(s_actual, self.s_desired)

def test_to_gam(self):
s_actual = to_gam(self.g)
assert_string_equal(s_actual, self.s_desired)

with NamedTemporaryFile(delete=False) as tmp_file:
temp_path = tmp_file.name
to_gam(self.g, temp_path)

with open(temp_path, 'r') as f:
s_actual = f.read()
assert_string_equal(s_actual, self.s_desired + '\n')

os.remove(temp_path)
8 changes: 4 additions & 4 deletions quantecon/game_theory/tests/test_howson_lcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import numpy as np
from numpy.testing import assert_, assert_allclose
from quantecon.game_theory.game_converters import qe_nfg_from_gam_file
from quantecon.game_theory.game_converters import from_gam
from quantecon.game_theory import (
Player,
NormalFormGame,
Expand All @@ -22,7 +22,7 @@

def test_polym_lcp_solver_where_solution_is_pure_NE():
filename = "big_polym.gam"
nfg = qe_nfg_from_gam_file(os.path.join(data_dir, filename))
nfg = from_gam(os.path.join(data_dir, filename))
polymg = PolymatrixGame.from_nf(nfg)
ne = polym_lcp_solver(polymg)
worked = nfg.is_nash(ne)
Expand All @@ -31,7 +31,7 @@ def test_polym_lcp_solver_where_solution_is_pure_NE():

def test_polym_lcp_solver_where_lcp_solver_must_backtrack():
filename = "triggers_back_case.gam"
nfg = qe_nfg_from_gam_file(os.path.join(data_dir, filename))
nfg = from_gam(os.path.join(data_dir, filename))
polymg = PolymatrixGame.from_nf(nfg)
ne = polym_lcp_solver(polymg)
worked = nfg.is_nash(ne)
Expand Down Expand Up @@ -305,7 +305,7 @@ def test_solves_multiplayer_rps_like():

def test_different_starting():
filename = "triggers_back_case.gam"
nfg = qe_nfg_from_gam_file(os.path.join(data_dir, filename))
nfg = from_gam(os.path.join(data_dir, filename))
polymg = PolymatrixGame.from_nf(nfg)
starting = [3, 2, 2, 0, 3]
# We also notice that changing the start
Expand Down
8 changes: 4 additions & 4 deletions quantecon/game_theory/tests/test_polymatrix_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from numpy.testing import assert_, assert_raises
from quantecon.game_theory.game_converters import qe_nfg_from_gam_file
from quantecon.game_theory.game_converters import from_gam
from quantecon.game_theory import NormalFormGame, PolymatrixGame
from numpy import allclose, zeros

Expand Down Expand Up @@ -39,13 +39,13 @@ class TestPolymatrixGame():
@classmethod
def setup_class(cls):
filename = "minimum_effort_game.gam"
cls.non_pmg = qe_nfg_from_gam_file(
cls.non_pmg = from_gam(
os.path.join(data_dir, filename))
filename = "big_polym.gam"
cls.pmg1 = qe_nfg_from_gam_file(
cls.pmg1 = from_gam(
os.path.join(data_dir, filename))
filename = "triggers_back_case.gam"
cls.pmg2 = qe_nfg_from_gam_file(
cls.pmg2 = from_gam(
os.path.join(data_dir, filename))
bimatrix = [[(54, 23), (72, 34)],
[(92, 32), (34, 36)],
Expand Down

0 comments on commit 87450f3

Please sign in to comment.