diff --git a/quantecon/game_theory/__init__.py b/quantecon/game_theory/__init__.py index 03249c3c..7ca07de5 100644 --- a/quantecon/game_theory/__init__.py +++ b/quantecon/game_theory/__init__.py @@ -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 diff --git a/quantecon/game_theory/game_converters.py b/quantecon/game_theory/game_converters.py index 8218a3d6..c5b305a5 100644 --- a/quantecon/game_theory/game_converters.py +++ b/quantecon/game_theory/game_converters.py @@ -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.]], @@ -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. @@ -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) diff --git a/quantecon/game_theory/tests/test_game_converters.py b/quantecon/game_theory/tests/test_game_converters.py new file mode 100644 index 00000000..719fcd5d --- /dev/null +++ b/quantecon/game_theory/tests/test_game_converters.py @@ -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) diff --git a/quantecon/game_theory/tests/test_howson_lcp.py b/quantecon/game_theory/tests/test_howson_lcp.py index 444dbae4..de30fd5b 100644 --- a/quantecon/game_theory/tests/test_howson_lcp.py +++ b/quantecon/game_theory/tests/test_howson_lcp.py @@ -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, @@ -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) @@ -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) @@ -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 diff --git a/quantecon/game_theory/tests/test_polymatrix_game.py b/quantecon/game_theory/tests/test_polymatrix_game.py index dc878762..56523b4d 100644 --- a/quantecon/game_theory/tests/test_polymatrix_game.py +++ b/quantecon/game_theory/tests/test_polymatrix_game.py @@ -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 @@ -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)],