def test_game_size(): """Test game size""" assert utils.game_size(2000, 10) == 1442989326579174917694151 assert np.all(utils.game_size([10, 20, 100], 10) == [92378, 10015005, 4263421511271]) assert np.all(utils.game_size(10, [10, 20, 100]) == [92378, 20030010, 42634215112710]) assert np.all(utils.game_size([100, 20, 10], [10, 20, 100]) == [4263421511271, 68923264410, 42634215112710]) assert utils.game_size_inv(1442989326579174917694151, 2000) == 10 assert np.all(utils.game_size_inv( [92378, 10015005, 4263421511271], [10, 20, 100]) == 10) assert np.all(utils.game_size_inv( [92378, 20030010, 42634215112710], 10) == [10, 20, 100]) assert np.all(utils.game_size_inv( [4263421511271, 68923264410, 42634215112710], [100, 20, 10]) == [10, 20, 100]) assert np.all(utils.game_size_inv(100, [1, 5, 20]) == [100, 4, 2])
def profile_id(self, profiles): """Return a unique integer representing a profile""" profiles = -np.asarray(profiles, int) profiles[..., self.role_starts] += self.num_players profiles = profiles.cumsum(-1) rev_arange = -np.ones(self.num_role_strats, int) rev_arange[self.role_starts] += self.num_strategies rev_arange = rev_arange.cumsum() base = np.insert(self.role_sizes[:-1].cumprod(), 0, 1) return self.role_reduce(utils.game_size( rev_arange, profiles)).dot(base)
def test_game_size(): """Test game size""" assert utils.game_size(2000, 10) == 1442989326579174917694151 assert np.all( utils.game_size([10, 20, 100], 10) == [92378, 10015005, 4263421511271]) assert np.all( utils.game_size(10, [10, 20, 100]) == [92378, 20030010, 42634215112710]) assert np.all( utils.game_size([100, 20, 10], [10, 20, 100]) == [4263421511271, 68923264410, 42634215112710]) assert utils.game_size_inv(1442989326579174917694151, 2000) == 10 assert np.all( utils.game_size_inv([92378, 10015005, 4263421511271], [10, 20, 100]) == 10) assert np.all( utils.game_size_inv([92378, 20030010, 42634215112710], 10) == [10, 20, 100]) assert np.all( utils.game_size_inv([4263421511271, 68923264410, 42634215112710], [100, 20, 10]) == [10, 20, 100]) assert np.all(utils.game_size_inv(100, [1, 5, 20]) == [100, 4, 2])
def num_deviation_profiles(game, rest): """Returns the number of deviation profiles This is a closed form way to compute `deviation_profiles(game, rest).shape[0]`. """ rest = np.asarray(rest, bool) utils.check(game.is_restriction(rest), 'restriction must be valid') num_role_strats = np.add.reduceat(rest, game.role_starts) num_devs = game.num_role_strats - num_role_strats dev_players = game.num_role_players - np.eye(game.num_roles, dtype=int) return np.sum(utils.game_size(dev_players, num_role_strats).prod(1) * num_devs)
def num_deviation_profiles(game, rest): """Returns the number of deviation profiles This is a closed form way to compute `deviation_profiles(game, rest).shape[0]`. """ rest = np.asarray(rest, bool) utils.check(game.is_restriction(rest), 'restriction must be valid') num_role_strats = np.add.reduceat(rest, game.role_starts) num_devs = game.num_role_strats - num_role_strats dev_players = game.num_role_players - np.eye(game.num_roles, dtype=int) return np.sum( utils.game_size(dev_players, num_role_strats).prod(1) * num_devs)
def num_deviation_profiles(game, subgame_mask): """Returns the number of deviation profiles This is a closed form way to compute `deviation_profiles(game, subgame_mask).shape[0]`. """ subgame_mask = np.asarray(subgame_mask, bool) assert game.num_role_strats == subgame_mask.size num_strategies = game.role_reduce(subgame_mask) num_devs = game.num_strategies - num_strategies dev_players = game.num_players - np.eye(game.num_roles, dtype=int) return np.sum(utils.game_size(dev_players, num_strategies).prod(1) * num_devs)
def num_deviation_payoffs(game, subgame_mask): """Returns the number of deviation payoffs This is a closed form way to compute `np.sum(deviation_profiles(game, subgame_mask) > 0)`.""" subgame_mask = np.asarray(subgame_mask, bool) assert game.num_role_strats == subgame_mask.size num_strategies = game.role_reduce(subgame_mask) num_devs = game.num_strategies - num_strategies dev_players = (game.num_players - np.eye(game.num_roles, dtype=int) - np.eye(game.num_roles, dtype=int)[:, None]) temp = utils.game_size(dev_players, num_strategies).prod(2) non_deviators = np.sum(np.sum(temp * num_strategies, 1) * num_devs) return non_deviators + num_deviation_profiles(game, subgame_mask)
def num_deviation_payoffs(game, rest): """Returns the number of deviation payoffs This is a closed form way to compute `np.sum(deviation_profiles(game, rest) > 0)`.""" rest = np.asarray(rest, bool) utils.check(game.is_restriction(rest), 'restriction must be valid') num_role_strats = np.add.reduceat(rest, game.role_starts) num_devs = game.num_role_strats - num_role_strats dev_players = (game.num_role_players - np.eye(game.num_roles, dtype=int) - np.eye(game.num_roles, dtype=int)[:, None]) temp = utils.game_size(dev_players, num_role_strats).prod(2) non_deviators = np.sum(np.sum(temp * num_role_strats, 1) * num_devs) return non_deviators + num_deviation_profiles(game, rest)
def num_all_dpr_profiles(self): """The number of unique dpr profiles This calculation takes time exponential in the number of roles. """ # Get all combinations of "pure" roles and then filter by ones with # support at least 2. Thus, 0, 1, and 2 can be safely ignored pure = (np.arange(3, 1 << self.num_roles)[:, None] & (1 << np.arange(self.num_roles))).astype(bool) cards = pure.sum(1) pure = pure[cards > 1] cards = cards[cards > 1] - 1 # For each combination of pure roles, compute the number of profiles # conditioned on those roles being pure, then multiply them by the # cardinality of the pure roles. pure_counts = np.prod(self.num_strategies * pure + ~pure, 1) unpure_counts = np.prod((utils.game_size(self.num_players, self.num_strategies) - self.num_strategies) * ~pure + pure, 1) overcount = np.sum(cards * pure_counts * unpure_counts) return self.num_all_payoffs - overcount
def num_dpr_deviation_profiles(game, subgame_mask): """Returns the number of dpr deviation profiles""" subgame_mask = np.asarray(subgame_mask, bool) assert game.num_role_strats == subgame_mask.size num_strategies = game.role_reduce(subgame_mask) num_devs = game.num_strategies - num_strategies pure = (np.arange(3, 1 << game.num_roles)[:, None] & (1 << np.arange(game.num_roles))).astype(bool) cards = pure.sum(1) pure = pure[cards > 1] card_counts = cards[cards > 1, None] - 1 - ((game.num_players > 1) & pure) # For each combination of pure roles, compute the number of profiles # conditioned on those roles being pure, then multiply them by the # cardinality of the pure roles. sp_dev = np.eye(game.num_roles, dtype=bool) & (game.num_players == 1) pure_counts = num_strategies * ~sp_dev + sp_dev dev_players = game.num_players - np.eye(game.num_roles, dtype=int) unpure_counts = utils.game_size(dev_players, num_strategies) - pure_counts pure_counts = np.prod(pure_counts * pure[:, None] + ~pure[:, None], 2) unpure_counts = np.prod(unpure_counts * ~pure[:, None] + pure[:, None], 2) overcount = np.sum(card_counts * pure_counts * unpure_counts * num_devs) return num_deviation_payoffs(game, subgame_mask) - overcount
def num_dpr_deviation_profiles(game, rest): """Returns the number of dpr deviation profiles""" rest = np.asarray(rest, bool) utils.check(game.is_restriction(rest), 'restriction must be valid') num_role_strats = np.add.reduceat(rest, game.role_starts) num_devs = game.num_role_strats - num_role_strats pure = (np.arange(3, 1 << game.num_roles)[:, None] & (1 << np.arange(game.num_roles))).astype(bool) cards = pure.sum(1) pure = pure[cards > 1] card_counts = cards[cards > 1, None] - 1 - \ ((game.num_role_players > 1) & pure) # For each combination of pure roles, compute the number of profiles # conditioned on those roles being pure, then multiply them by the # cardinality of the pure roles. sp_dev = np.eye(game.num_roles, dtype=bool) & (game.num_role_players == 1) pure_counts = num_role_strats * ~sp_dev + sp_dev dev_players = game.num_role_players - np.eye(game.num_roles, dtype=int) unpure_counts = utils.game_size(dev_players, num_role_strats) - pure_counts pure_counts = np.prod(pure_counts * pure[:, None] + ~pure[:, None], 2) unpure_counts = np.prod(unpure_counts * ~pure[:, None] + pure[:, None], 2) overcount = np.sum(card_counts * pure_counts * unpure_counts * num_devs) return num_deviation_payoffs(game, rest) - overcount
def role_sizes(self): """The number of profiles in each role (independent of others)""" return utils.game_size(self.num_players, self.num_strategies)
def num_profiles(subgame, role_counts, **_): """Number of profiles in a subgame""" return utils.prod(utils.game_size(role_counts[role], len(strats)) for role, strats in subgame.items())
def num_all_payoffs(self): """The number of payoffs in all profiles""" dev_players = self.num_players - np.eye(self.num_roles, dtype=int) return np.sum(utils.game_size(dev_players, self.num_strategies) .prod(1) * self.num_strategies)
def test_game_size(): assert utils.game_size(2000, 10) == 1442989326579174917694151
def deviation_payoffs(self, mix, assume_complete=False, 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 assume_complete : bool If true, don't compute missing data and replace with nans. Just return the potentially inaccurate results. 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. The values that are marked nan are not very aggressive, so don't rely on accurate nan values in the jacobian. """ # TODO It wouldn't be hard to extend this to multiple mixtures, which # would allow array calculation of mixture regret. Support would have # to be iterative though. mix = np.asarray(mix, float) nan_mask = np.empty_like(mix, dtype=bool) # Fill out mask where we don't have data if self.is_complete() or assume_complete: nan_mask.fill(False) elif self.is_empty(): nan_mask.fill(True) else: # These calculations are approximate, but for games we can do # anything with, the size is bounded, and so numeric methods are # actually exact. support = mix > 0 strats = self.role_reduce(support) devs = self.profiles[:, ~support] num_supp = utils.game_size(self.num_players, strats).prod() dev_players = self.num_players - np.eye(self.num_roles, dtype=int) role_num_dev = utils.game_size(dev_players, strats).prod(1) num_dev = role_num_dev.repeat(self.num_strategies)[~support] nan_mask[support] = np.all(devs == 0, 1).sum() < num_supp nan_mask[~support] = devs[devs.sum(1) == 1].sum(0) < num_dev # Compute values if not nan_mask.all(): # _TINY effectively makes 0^0=1 and 0/0=0. log_mix = np.log(mix + _TINY) prof_prob = np.sum(self.profiles * log_mix, 1, keepdims=True) with np.errstate(under='ignore'): # Ignore underflow caused when profile probability is not # representable in floating point. probs = np.exp(prof_prob + self._dev_reps - log_mix) zero_prob = _TINY * self.num_players.sum() weighted_payoffs = probs * np.where(probs > zero_prob, self.payoffs, 0) values = np.sum(weighted_payoffs, 0) else: values = np.empty(self.num_role_strats) values[nan_mask] = np.nan if not jacobian: return values if not nan_mask.all(): tmix = mix + zero_prob product_rule = self.profiles[:, None] / tmix - np.diag(1 / tmix) dev_jac = np.sum(weighted_payoffs[..., None] * product_rule, 0) else: dev_jac = np.empty((self.num_role_strats, self.num_role_strats)) dev_jac[nan_mask] = np.nan return values, dev_jac