Source code for gameanalysis.reduction.deviation_preserving

"""Deviation preserving reduction"""
import numpy as np

from gameanalysis import paygame
from gameanalysis import restrict
from gameanalysis import rsgame
from gameanalysis import utils
from gameanalysis.reduction import _common
from gameanalysis.reduction import hierarchical


def _devs(game, num_profs):
    """Return an array of the player counts after deviation"""
    return np.tile(
        np.repeat(
            game.num_role_players - np.eye(game.num_roles, dtype=int),
            game.num_role_strats,
            0,
        ),
        (num_profs, 1),
    )


[docs]def reduce_game(full_game, red_players): # pylint: disable=too-many-locals """Reduce a game using deviation preserving reduction Parameters ---------- full_game : Game The game to reduce. red_players : ndarray-like The reduced number of players for each role. This will be coerced into the proper shape if necessary. """ red_game = rsgame.empty_names( full_game.role_names, red_players, full_game.strat_names ) utils.check( np.all((red_game.num_role_players > 1) | (full_game.num_role_players == 1)), "all reduced players must be greater than zero", ) utils.check( np.all(full_game.num_role_players >= red_game.num_role_players), "all full counts must not be less than reduced counts", ) if full_game.is_empty(): return red_game elif full_game.num_profiles < red_game.num_all_dpr_profiles: full_profiles = full_game.profiles() full_payoffs = full_game.payoffs() else: full_profiles = expand_profiles(full_game, red_game.all_profiles()) full_payoffs = full_game.get_payoffs(full_profiles) valid = ~np.all(np.isnan(full_payoffs) | (full_profiles == 0), 1) full_profiles = full_profiles[valid] full_payoffs = full_payoffs[valid] # Reduce red_profiles, red_inds, full_inds, strat_inds = _reduce_profiles( red_game, full_profiles, True ) if red_profiles.size == 0: # Empty reduction return red_game # Build mapping from payoffs to reduced profiles, and use bincount # to count the number of payoffs mapped to a specific location, and # sum the number of payoffs mapped to a specific location cum_inds = red_inds * full_game.num_strats + strat_inds payoff_vals = full_payoffs[full_inds, strat_inds] red_payoffs = np.bincount(cum_inds, payoff_vals, red_profiles.size).reshape( red_profiles.shape ) red_payoff_counts = np.bincount(cum_inds, minlength=red_profiles.size).reshape( red_profiles.shape ) mask = red_payoff_counts > 1 red_payoffs[mask] /= red_payoff_counts[mask] unknown = (red_profiles > 0) & (red_payoff_counts == 0) red_payoffs[unknown] = np.nan valid = ~np.all((red_profiles == 0) | np.isnan(red_payoffs), 1) return paygame.game_replace(red_game, red_profiles[valid], red_payoffs[valid])
[docs]def expand_profiles(full_game, profiles): # pylint: disable=too-many-locals """Expand profiles using dpr Parameters ---------- full_game : Game Game that expanded profiles will be valid for. profiles : ndarray-like The profiles to expand return_contributions : bool, optional If specified, returns a boolean array matching the shape is returned indicating the payoffs that are needed for the initial profiles. """ profiles = np.asarray(profiles, int) utils.check( profiles.shape[-1] == full_game.num_strats, "profiles not a valid shape" ) if not profiles.size: return np.empty((0, full_game.num_strats), int) profiles = profiles.reshape((-1, full_game.num_strats)) all_red_players = np.add.reduceat(profiles, full_game.role_starts, 1) red_players = all_red_players[0] utils.check(np.all(all_red_players == red_players), "profiles must be valid") num_profs = profiles.shape[0] dev_profs = profiles[:, None] - np.eye(full_game.num_strats, dtype=int) dev_profs = np.reshape(dev_profs, (-1, full_game.num_strats)) dev_full_players = _devs(full_game, num_profs) mask = ~np.any(dev_profs < 0, 1) devs = ( np.eye(full_game.num_strats, dtype=bool)[None] .repeat(num_profs, 0) .reshape((-1, full_game.num_strats))[mask] ) dev_full_profs = ( _common.expand_profiles(full_game, dev_full_players[mask], dev_profs[mask]) + devs ) ids = utils.axis_to_elem(dev_full_profs) return dev_full_profs[np.unique(ids, return_index=True)[1]]
[docs]def reduce_profiles(red_game, profiles): """Reduce profiles using dpr Parameters ---------- red_game : Game Game that reduced profiles will be profiles for. profiles : ndarray-like The profiles to reduce. """ return _reduce_profiles(red_game, profiles, False)
def _reduce_profiles( red_game, profiles, return_contributions ): # pylint: disable=too-many-locals """Reduce profiles using dpr Parameters ---------- red_game : Game Game that reduced profiles will be profiles for. profiles : ndarray-like The profiles to reduce. return_contributions : bool, optional If true return ancillary information about where the payoffs come from. """ profiles = np.asarray(profiles, int) utils.check(profiles.shape[-1] == red_game.num_strats, "profiles not a valid shape") if not profiles.size: return np.empty((0, red_game.num_strats), int) profiles = profiles.reshape((-1, red_game.num_strats)) all_full_players = np.add.reduceat(profiles, red_game.role_starts, 1) full_players = all_full_players[0] utils.check(np.all(all_full_players == full_players), "profiles must be valid") num_profs = profiles.shape[0] dev_profs = profiles.repeat(np.sum(profiles > 0, 1), 0) _, strat_inds = profiles.nonzero() dev_profs[np.arange(dev_profs.shape[0]), strat_inds] -= 1 dev_red_players = _devs(red_game, num_profs) mask = (profiles > 0).ravel() red_profs, reduced = _common.reduce_profiles( red_game, dev_red_players[mask], dev_profs ) rstrat_inds = strat_inds[reduced] red_profs[np.arange(red_profs.shape[0]), rstrat_inds] += 1 red_profs, red_inds = np.unique(utils.axis_to_elem(red_profs), return_inverse=True) red_profs = utils.axis_from_elem(red_profs) if not return_contributions: return red_profs full_inds = np.arange(num_profs).repeat(red_game.num_strats)[mask][reduced] return red_profs, red_inds, full_inds, rstrat_inds
[docs]def expand_deviation_profiles(full_game, rest, red_players, role_index=None): """Expand all deviation profiles from a restriction Parameters ---------- full_game : Game The game the deviations profiles will be valid for. rest : [bool] The restriction to get deviations from. red_players : [int] The number of players in each role in the reduced game. role_index : int, optional If specified , only expand deviations for the role selected. """ rest = np.asarray(rest, bool) rdev = np.eye(full_game.num_roles, dtype=int) red_players = np.broadcast_to(np.asarray(red_players, int), full_game.num_roles) support = np.add.reduceat(rest, full_game.role_starts) def dev_profs(red_players, full_players, mask, rst): """Deviation profiles for a particular role""" rgame = rsgame.empty(red_players, support) sub_profs = restrict.translate(rgame.all_profiles(), rest) game = rsgame.empty(full_players, full_game.num_role_strats) non_devs = hierarchical.expand_profiles(game, sub_profs) ndevs = np.sum(~mask) devs = np.zeros((ndevs, full_game.num_strats), int) devs[:, rst : rst + mask.size][:, ~mask] = np.eye(ndevs, dtype=int) profs = non_devs[:, None] + devs profs.shape = (-1, full_game.num_strats) return profs if role_index is None: # pylint: disable=no-else-return expanded_profs = [ dev_profs(red_players, full_players, mask, rs) for red_players, full_players, mask, rs in zip( red_players - rdev, full_game.num_role_players - rdev, np.split(rest, full_game.role_starts[1:]), full_game.role_starts, ) ] return np.concatenate(expanded_profs) else: full_players = full_game.num_role_players.copy() full_players[role_index] -= 1 red_players = red_players.copy() red_players[role_index] -= 1 mask = np.split(rest, full_game.role_starts[1:])[role_index] rstart = full_game.role_starts[role_index] return dev_profs(red_players, full_players, mask, rstart)