Source code for gameanalysis.matgame

"""module for complete independent games"""
import functools
import itertools

import numpy as np

from gameanalysis import rsgame
from gameanalysis import utils


[docs]class MatrixGame(rsgame.CompleteGame): """Matrix game representation This represents a complete independent game more compactly than a Game, but only works for complete independent games. Parameters ---------- role_names : (str,) The name of each role. strat_names : ((str,),) The name of each strategy per role. payoff_matrix : ndarray The matrix of payoffs for an asymmetric game. The last axis is the payoffs for each player, the first axes are the strategies for each player. matrix.shape[:-1] must correspond to the number of strategies for each player. matrix.ndim - 1 must equal matrix.shape[-1]. """ def __init__(self, role_names, strat_names, payoff_matrix): super().__init__(role_names, strat_names, np.ones(len(role_names), int)) self._payoff_matrix = payoff_matrix self._payoff_matrix.setflags(write=False) self._prof_offset = np.zeros(self.num_strats, int) self._prof_offset[self.role_starts] = 1 self._prof_offset.setflags(write=False) self._payoff_view = self._payoff_matrix.view() self._payoff_view.shape = (self.num_profiles, self.num_roles)
[docs] def payoff_matrix(self): """Return the payoff matrix""" return self._payoff_matrix.view()
[docs] @utils.memoize def min_strat_payoffs(self): """Returns the minimum payoff for each role""" mpays = np.empty(self.num_strats) for r, (pays, min_pays, n) in enumerate(zip( np.rollaxis(self._payoff_matrix, -1), np.split(mpays, self.role_starts[1:]), self.num_role_strats)): np.rollaxis(pays, r).reshape((n, -1)).min(1, min_pays) mpays.setflags(write=False) return mpays
[docs] @utils.memoize def max_strat_payoffs(self): """Returns the minimum payoff for each role""" mpays = np.empty(self.num_strats) for r, (pays, max_pays, n) in enumerate(zip( np.rollaxis(self._payoff_matrix, -1), np.split(mpays, self.role_starts[1:]), self.num_role_strats)): np.rollaxis(pays, r).reshape((n, -1)).max(1, max_pays) mpays.setflags(write=False) return mpays
[docs] @functools.lru_cache(maxsize=1) def payoffs(self): profiles = self.profiles() payoffs = np.zeros(profiles.shape) payoffs[profiles > 0] = self._payoff_matrix.flat return payoffs
[docs] def compress_profile(self, profile): """Compress profile in array of ints Normal profiles are an array of number of players playing a strategy. Since matrix games always have one player per role, this compresses each roles counts into a single int representing the played strategy per role. """ assert self.is_profile(profile).all() profile = np.asarray(profile, int) return np.add.reduceat(np.cumsum(self._prof_offset - profile, -1), self.role_starts, -1)
[docs] def uncompress_profile(self, comp_prof): comp_prof = np.asarray(comp_prof, int) assert np.all(comp_prof >= 0) and np.all( comp_prof < self.num_role_strats) profile = np.zeros(comp_prof.shape[:-1] + (self.num_strats,), int) inds = (comp_prof.reshape((-1, self.num_roles)) + self.role_starts + self.num_strats * np.arange(int(np.prod(comp_prof.shape[:-1])))[:, None]) profile.flat[inds] = 1 return profile
[docs] def get_payoffs(self, profile): """Returns an array of profile payoffs""" profile = np.asarray(profile, int) ids = self.profile_to_id(profile) payoffs = np.zeros_like(profile, float) payoffs[profile > 0] = self._payoff_view[ids].flat return payoffs
[docs] def deviation_payoffs(self, mix, *, jacobian=False): """Computes the expected value of each pure strategy played against all opponents playing mix. Parameters ---------- mix : ndarray The mix all other players are using jacobian : bool If true, the second returned argument will be the jacobian of the deviation payoffs with respect to the mixture. The first axis is the deviating strategy, the second axis is the strategy in the mix the jacobian is taken with respect to. """ rmix = [] for r, m in enumerate(np.split(mix, self.role_starts[1:])): shape = [1] * self.num_roles shape[r] = -1 rmix.append(m.reshape(shape)) devpays = np.empty(self.num_strats) for r, (out, n) in enumerate(zip( np.split(devpays, self.role_starts[1:]), self.num_role_strats)): pays = self._payoff_matrix[..., r].copy() for m in rmix[:r]: pays *= m for m in rmix[r + 1:]: pays *= m np.rollaxis(pays, r).reshape((n, -1)).sum(1, out=out) if not jacobian: return devpays jac = np.zeros((self.num_strats, self.num_strats)) for r, (jout, nr) in enumerate(zip( np.split(jac, self.role_starts[1:]), self.num_role_strats)): for d, (out, nd) in enumerate(zip( np.split(jout, self.role_starts[1:], 1), self.num_role_strats)): if r == d: continue pays = self._payoff_matrix[..., r].copy() f, s = min(r, d), max(r, d) for m in rmix[:f]: pays *= m for m in rmix[f + 1:s]: pays *= m for m in rmix[s + 1:]: pays *= m np.rollaxis(np.rollaxis(pays, r), d + (r > d), 1).reshape((nr, nd, -1)).sum(2, out=out) return devpays, jac
[docs] def restrict(self, rest): base = rsgame.emptygame_copy(self).restrict(rest) matrix = self._payoff_matrix for i, mask in enumerate(np.split(rest, self.role_starts[1:])): matrix = matrix[(slice(None),) * i + (mask,)] return MatrixGame(base.role_names, base.strat_names, matrix.copy())
[docs] def normalize(self): """Return a normalized MatGame""" scale = self.max_role_payoffs() - self.min_role_payoffs() scale[np.isclose(scale, 0)] = 1 return MatrixGame( self.role_names, self.strat_names, (self._payoff_matrix - self.min_role_payoffs()) / scale)
def _mat_to_json(self, matrix, role_index): """Convert a sub matrix into json representation""" if role_index == self.num_roles: return {role: float(pay) for role, pay in zip(self.role_names, matrix)} else: strats = self.strat_names[role_index] role_index += 1 return {strat: self._mat_to_json(mat, role_index) for strat, mat in zip(strats, matrix)}
[docs] def to_json(self): res = super().to_json() res['payoffs'] = self._mat_to_json(self._payoff_matrix, 0) res['type'] = 'matrix.1' return res
@utils.memoize def __hash__(self): return super().__hash__() def __eq__(self, other): return (super().__eq__(other) and # Identical payoffs np.allclose(self._payoff_matrix, other._payoff_matrix)) def __repr__(self): return '{}({})'.format( self.__class__.__name__, self.num_role_strats)
[docs]def matgame(payoff_matrix): """Create a game from a dense matrix with default names Parameters ---------- payoff_matrix : ndarray-like The matrix of payoffs for an asymmetric game. """ payoff_matrix = np.ascontiguousarray(payoff_matrix, float) return matgame_replace( rsgame.emptygame( np.ones(payoff_matrix.ndim - 1, int), np.array(payoff_matrix.shape[:-1], int)), payoff_matrix)
[docs]def matgame_names(role_names, strat_names, payoff_matrix): """Create a game from a payoff matrix with names Parameters ---------- role_names : [str] The name of each role. strat_names : [[str]] The name of each strategy for each role. payoff_matrix : ndarray-like The matrix mapping strategy indices to payoffs for each player. """ return matgame_replace( rsgame.emptygame_names( role_names, np.ones(len(role_names), int), strat_names), payoff_matrix)
def _mat_from_json(base, dic, matrix, depth): """Copy roles to a matrix representation""" if depth == base.num_roles: for role, payoff in dic.items(): matrix[base.role_index(role)] = payoff else: role = base.role_names[depth] offset = base.role_starts[depth] depth += 1 for strat, subdic in dic.items(): ind = base.role_strat_index(role, strat) - offset _mat_from_json(base, subdic, matrix[ind], depth)
[docs]def matgame_json(json): """Read a matrix game from json In general, the json will have 'type': 'matrix...' to indicate that it's a matrix game, but if the other fields are correct, this will still succeed. """ # This uses the fact that roles are always in lexicographic order base = rsgame.emptygame_json(json) matrix = np.empty(tuple(base.num_role_strats) + (base.num_roles,), float) _mat_from_json(base, json['payoffs'], matrix, 0) return matgame_replace(base, matrix)
[docs]def matgame_copy(copy_game): """Copy a matrix game from an existing game Parameters ---------- copy_game : RsGame Game to copy payoff data out of. This game must be complete. """ assert copy_game.is_complete() if hasattr(copy_game, 'payoff_matrix'): return matgame_replace(copy_game, copy_game.payoff_matrix()) # Get payoff matrix num_role_strats = copy_game.num_role_strats.repeat( copy_game.num_role_players) shape = tuple(num_role_strats) + (num_role_strats.size,) payoff_matrix = np.empty(shape, float) offset = copy_game.role_starts.repeat(copy_game.num_role_players) for profile, payoffs in zip(copy_game.profiles(), copy_game.payoffs()): inds = itertools.product(*[ set(itertools.permutations(np.arange(s.size).repeat(s))) for s in np.split(profile, copy_game.role_starts[1:])]) for nested in inds: ind = tuple(itertools.chain.from_iterable(nested)) payoff_matrix[ind] = payoffs[ind + offset] # Get role names if np.all(copy_game.num_role_players == 1): roles = copy_game.role_names strats = copy_game.strat_names else: # When we expand names, we need to make sure they stay sorted if utils.is_sorted(r + 'p' for r in copy_game.role_names): # We can naively append player numbers role_names = copy_game.role_names else: # We have to prefix to preserve role order maxlen = max(map(len, copy_game.role_names)) role_names = ( p + '_' * (maxlen - len(r)) + r for r, p in zip(copy_game.role_names, utils.prefix_strings('', copy_game.num_roles))) roles = tuple(itertools.chain.from_iterable( (r + s for s in utils.prefix_strings('p', p)) for r, p in zip(role_names, copy_game.num_role_players))) strats = tuple(itertools.chain.from_iterable( itertools.repeat(s, p) for s, p in zip(copy_game.strat_names, copy_game.num_role_players))) return MatrixGame(roles, strats, payoff_matrix)
[docs]def matgame_replace(base, payoff_matrix): """Replace an existing game with a new payoff matrix Parameters ---------- base : RsGame Game to take structure out of. payoff_matrix : ndarray-like The new payoff matrix.""" payoff_matrix = np.ascontiguousarray(payoff_matrix, float) assert np.all(base.num_role_players == 1), \ "replaced game must be independent" assert payoff_matrix.shape == (tuple(base.num_role_strats) + (base.num_roles,)), \ "payoff matrix not consistent shape with game" return MatrixGame(base.role_names, base.strat_names, payoff_matrix)