Source code for quantecon.game_theory.game_converters

"""
Utilities for converting between representations of games.

Currently supports reading and writing the GameTracer `.gam` text format
[1]_.

Examples
--------
Create a QuantEcon NormalFormGame from a .gam file storing a 3-player
Minimum Effort Game:

>>> import os
>>> import quantecon.game_theory as gt
>>> filepath = os.path.dirname(gt.__file__)
>>> filepath = os.path.join(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.]],
  [[  1.,  -9.,   1.],   [  1.,  -9.,  -9.],   [  1.,  -9., -19.]],
  [[  1., -19.,   1.],   [  1., -19.,  -9.],   [  1., -19., -19.]]],
<BLANKLINE>
 [[[ -9.,   1.,   1.],   [ -9.,   1.,  -9.],   [ -9.,   1., -19.]],
  [[ -9.,  -9.,   1.],   [  2.,   2.,   2.],   [  2.,   2.,  -8.]],
  [[ -9., -19.,   1.],   [  2.,  -8.,   2.],   [  2.,  -8.,  -8.]]],
<BLANKLINE>
 [[[-19.,   1.,   1.],   [-19.,   1.,  -9.],   [-19.,   1., -19.]],
  [[-19.,  -9.,   1.],   [ -8.,   2.,   2.],   [ -8.,   2.,  -8.]],
  [[-19., -19.,   1.],   [ -8.,  -8.,   2.],   [  3.,   3.,   3.]]]]

References
----------
.. [1] Ben Blum, Daphne Koller, Christian Shelton, "Game Theory:
   GameTracer," http://dags.stanford.edu/Games/gametracer.html

"""
import io
import sys
import numbers
import numpy as np
from .normal_form_game import Player, NormalFormGame


[docs]class GAMPayoffVector: """ Internal intermediate representation that stores payoffs in a single flat 1-dim array. Payoff values are ordered as in the GameTracer .gam format: 1. Player-major blocks: player 0, ..., player N-1. 2. Within each block, action profiles are ordered with player 0 varying fastest, then player 1, ..., player N-1 (i.e., Fortran/column-major order). Attributes ---------- N : scalar(int) Number of players. nums_actions : tuple(int) Tuple of the numbers of actions, one for each player. payoffs : ndarray(ndim=1) Array storing payoffs in .gam order. """ def __init__(self, nums_actions, payoffs): nums_actions = tuple(nums_actions) if len(nums_actions) == 0: raise ValueError('nums_actions must be a non-empty iterable ' + 'of positive integers') for n in nums_actions: if not isinstance(n, numbers.Integral): raise TypeError('nums_actions must contain only integers') if n <= 0: raise ValueError('all nums_actions must be positive') self.nums_actions = tuple(int(n) for n in nums_actions) self.N = len(self.nums_actions) payoffs = np.ascontiguousarray(payoffs) if payoffs.ndim != 1: raise ValueError('payoffs must be a 1-dim array_like') expected = np.prod(self.nums_actions) * self.N if payoffs.size != expected: raise ValueError( f'payoffs length mismatch: expected {expected}, ' + f'got {payoffs.size}' ) self.payoffs = payoffs
[docs] @classmethod def from_nfg(cls, g, dtype=None): """ Construct a GAMPayoffVector from a NormalFormGame `g`. Examples -------- >>> player0 = Player([[0, 3], [1, 4], [2, 5]]) >>> player1 = Player([[6, 7, 8], [9, 10, 11]]) >>> g = NormalFormGame((player0, player1)) >>> print(g) 2-player NormalFormGame with payoff profile array: [[[ 0, 6], [ 3, 9]], [[ 1, 7], [ 4, 10]], [[ 2, 8], [ 5, 11]]] >>> p = GAMPayoffVector.from_nfg(g) >>> p.payoffs array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) """ N = g.N nums_actions = g.nums_actions if dtype is None: dtype = g.dtype na = np.prod(nums_actions) payoffs = np.empty(na*N, dtype=dtype) for i, player in enumerate(g.players): payoffs[na*i:na*(i+1)].reshape(nums_actions, order='F')[:] = \ player.payoff_array.transpose( (*range(N-i, g.N), *range(N-i)) ) return cls(nums_actions, payoffs)
[docs] def to_nfg(self, dtype=None): """ Construct a NormalFormGame from self. Examples -------- >>> nums_actions = (3, 2) >>> payoffs = np.arange(12) >>> p = GAMPayoffVector(nums_actions, payoffs) >>> g = p.to_nfg() >>> print(g) 2-player NormalFormGame with payoff profile array: [[[ 0, 6], [ 3, 9]], [[ 1, 7], [ 4, 10]], [[ 2, 8], [ 5, 11]]] """ N = self.N nums_actions = self.nums_actions na = np.prod(nums_actions) payoffs2d = self.payoffs.reshape((na, N), order='F') players = tuple( Player( np.asarray( payoffs2d[:, i].reshape(nums_actions, order='F').transpose( (*range(i, N), *range(i)) ), dtype=dtype, order='C' ) ) for i in range(N) ) return NormalFormGame(players)
def _str2num(s): """ Convert string to appropriate numeric type. Parameters ---------- s : str String representation of a number. Returns ------- int or float Integer if no decimal point, otherwise float. """ if '.' in s: return float(s) return int(s)
[docs]class GAMReader: """ Parser for the GameTracer .gam format. """
[docs] @classmethod def from_file(cls, file_path): """ Read from a .gam format file. """ with open(file_path, 'r') as f: string = f.read() return cls._parse(string)
[docs] @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)
[docs] @classmethod def from_string(cls, string): """ Read from a .gam format string. """ return cls._parse(string)
@staticmethod def _parse(string): tokens = string.split() if not tokens: raise ValueError('empty .gam input') pos = 0 # N tok = tokens[pos] try: N = int(tok) except ValueError as err: raise ValueError(f'invalid N token: {tok!r}') from err pos += 1 if N <= 0: raise ValueError('N must be a positive integer') # nums_actions if len(tokens) < pos + N: got = max(0, len(tokens) - pos) raise ValueError( f'incomplete header: expected {N} action counts, got {got}' ) try: nums_actions = tuple(int(tok) for tok in tokens[pos:pos+N]) except ValueError as err: raise ValueError('invalid action count token in header') from err pos += N # payoffs payoffs = np.array([_str2num(tok) for tok in tokens[pos:]]) p = GAMPayoffVector(nums_actions, payoffs) return p.to_nfg()
[docs]class GAMWriter: """ Serializer for the GameTracer .gam format. """
[docs] @classmethod def to_file(cls, g, file_path): """ Write `g` to a file in GameTracer .gam format. """ with open(file_path, 'w') as f: f.write(cls._dump(g) + '\n')
[docs] @classmethod def to_string(cls, g): """ Return the GameTracer .gam string representation of `g`. """ return cls._dump(g)
@staticmethod def _dump(g): p = GAMPayoffVector.from_nfg(g) buf = io.StringIO() buf.write(str(p.N)) buf.write('\n') buf.write(' '.join(map(str, p.nums_actions))) buf.write('\n\n') payoffs_str = np.array2string( p.payoffs, separator=' ', threshold=sys.maxsize, # no truncation '...' # suppress_small helps avoid scientific notation for small |x|; # large |x| values may still print with e+... suppress_small=True )[1:-1] # strip brackets buf.write(' '.join(payoffs_str.split())) return buf.getvalue().rstrip()
[docs]def from_gam(filename: str) -> NormalFormGame: """ Read a GameTracer .gam file and return a NormalFormGame. Parameters ---------- filename : str Path to .gam file. Returns ------- NormalFormGame The game described by the .gam file. 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 = from_gam(fname) >>> print(g) 2-player NormalFormGame with payoff profile array: [[[3, 3], [3, 2]], [[2, 2], [5, 6]], [[0, 3], [6, 1]]] """ return GAMReader.from_file(filename)
[docs]def from_gam_string(string): """ Read a .gam format string and return a NormalFormGame. Parameters ---------- string : str String in .gam format. Returns ------- NormalFormGame The game described by the .gam string. Examples -------- >>> string = \"\"\"\\ ... 2 ... 3 2 ... ... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\" >>> g = from_gam_string(string) >>> print(g) 2-player NormalFormGame with payoff profile array: [[[3, 3], [3, 2]], [[2, 2], [5, 6]], [[0, 3], [6, 1]]] """ return GAMReader.from_string(string)
[docs]def from_gam_url(url): """ Read a GameTracer .gam file from a URL and return a NormalFormGame. Parameters ---------- url : str String containing a URL of the .gam file. Returns ------- NormalFormGame The game described by the .gam file. """ return GAMReader.from_url(url)
[docs]def to_gam(g, file_path=None): """ Write a NormalFormGame to a file in .gam format. Parameters ---------- g : NormalFormGame file_path : str, optional(default=None) Path to the file to write to. If None, the result is returned as a string. Returns ------- None or str """ if file_path is None: return GAMWriter.to_string(g) return GAMWriter.to_file(g, file_path)