Skip to content
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

RFC, ENH: Implement GAMReader and GAMWriter #758

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
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
Loading