Source code for gameanalysis.AGGFN

import numpy as np
import scipy.special as sps
from scipy.misc import comb
from scipy.special import gammaln
from numpy.random import uniform, normal, choice
from itertools import combinations_with_replacement as CwR
from functools import partial

from gameanalysis import rsgame
import sys

_TINY = float(np.finfo(np.float64).tiny)

[docs]class Sym_AGG_FNA(rsgame.BaseGame): """Action Graph Game with Function Nodes. Represented games are symmetric. Action node utilities have additive structure. Function nodes are contribution-independent. Graph is bipartite so that function nodes have in-edges only from action nodes and vise versa. """ def __init__(self, num_players, num_strategies, action_weights, function_inputs, node_functions): """ Parameters ---------- num_players num_strategies action_weights: floating point matrix with |node_functions| rows num_strategies columns. Each entry specifies the incoming weight in the action graph for the action node (column). function_inputs: boolean matrix with num_strategies rows and |node_functions| columns. Each entry specifies whether the action node (row) is an input to the function node (col). node_functions: Activation functions for each function node. This can either be a list of python functions that work correctly when applied to a vector of inputs, or an array already storing the correct tabular representation. Class Variables --------------- self.action_weights self.function_inputs self.configs self.log_dev_reps self.func_table """ super().__init__(num_players, num_strategies) self.action_weights = np.array(action_weights, dtype=float) self.function_inputs = np.array(function_inputs, dtype=bool) self.configs = np.arange(num_players+1) self.log_dev_reps = gammaln(num_players) - gammaln(self.configs[1:]) -\ gammaln(num_players - self.configs[:-1]) if isinstance(node_functions, np.ndarray): # already a table self.func_table = node_functions else: # list of python functions, need to be applied to configurations self.func_table = np.array([f(self.configs) for f in node_functions], dtype=float) self.num_functions = self.func_table.shape[0] self._min_payoffs = None self._max_payoffs = None
[docs] def is_complete(self): return True
@staticmethod
[docs] def from_json(j_): """ Build a game from the info stored in a dictionary in the json format. """ num_players = j_["num_players"] strategy_names = sorted(j_["strategy_names"]) num_strats = len(strategy_names) function_names = sorted(j_["function_names"]) num_functions = len(function_names) functions = np.array([j_["function_tables"][f] for f in function_names]).T action_weights = np.empty([num_functions, num_strats], dtype=float) function_inputs = np.zeros([num_strats, num_functions], dtype=bool) for s,strat in enumerate(strategy_names): for f,func in enumerate(function_names): action_weights[f,s] = j_["action_weights"][strat].get(func,0) for f,func in enumerate(function_names): if strat in j_["function_inputs"][func]: function_inputs[s,f] = True return Sym_AGG_FNA(num_players, num_strats, action_weights, function_inputs, functions)
[docs] def to_json(self, strategy_names=None, function_names=None): """ Creates a json format of the game for storage """ j_ = {} j_["num_players"] = int(self.num_players[0]) if strategy_names is None: strategy_names = ["s" + str(i) for i in range(self.num_strategies[0])] if function_names is None: function_names = ["f" + str(i) for i in range(self.num_functions)] j_["strategy_names"] = strategy_names j_["function_names"] = function_names j_["function_inputs"] = {} for f, func in enumerate(function_names): inputs = [] for s, strat in enumerate(strategy_names): if self.function_inputs[s,f]: inputs.append(strat) j_["function_inputs"][func] = inputs j_["action_weights"] = {} for s, strat in enumerate(strategy_names): weights = {} for f, func in enumerate(function_names): if self.action_weights[f,s]: weights[func] = self.action_weights[f,s] j_["action_weights"][strat] = weights j_["function_tables"] = dict(zip(function_names, map(list, self.func_table))) return j_
[docs] def min_payoffs(self): """Returns a lower bound on the payoffs.""" if self._min_payoffs is None: minima = self.func_table.min(1, keepdims=True).repeat( self.num_strategies[0], axis=1) minima[self.action_weights <= 0] = 0 maxima = self.func_table.max(1, keepdims=True).repeat( self.num_strategies[0], axis=1) maxima[self.action_weights >= 0] = 0 self._min_payoffs = ((minima + maxima) * self.action_weights).sum(0).min(keepdims=True) self._min_payoffs.setflags(write=False) return self._min_payoffs.view()
[docs] def max_payoffs(self): """Returns an upper bound on the payoffs.""" if self._max_payoffs is None: minima = self.func_table.min(1, keepdims=True).repeat( self.num_strategies[0], axis=1) minima[self.action_weights >= 0] = 0 maxima = self.func_table.max(1, keepdims=True).repeat( self.num_strategies[0], axis=1) maxima[self.action_weights <= 0] = 0 self._max_payoffs = ((minima + maxima) * self.action_weights).sum(0).max(keepdims=True) self._max_payoffs.setflags(write=False) return self._max_payoffs.view()
[docs] def deviation_payoffs(self, mix, assume_complete=True, jacobian=False): # TODO To add jacobian support. assert not jacobian, "Sym_AGG_FNA doesn't support jacobian" func_node_probs = mix[:,None].repeat(self.num_functions, axis=1) func_node_probs[np.logical_not(self.function_inputs)] = 0 func_node_probs = func_node_probs.sum(0) log_input_probs = np.outer(np.log(func_node_probs + _TINY), self.configs[:-1]) log_non_input_probs = np.outer(np.log(1 - func_node_probs + _TINY), self.num_players - self.configs[1:]) config_probs = np.exp(log_input_probs + log_non_input_probs + self.log_dev_reps) EVs = np.empty(self.num_strategies) for s in range(self.num_strategies[0]): # function_outputs is func_table for 0 to N-1 for functions that # don't have s in their neighborhood and 1 to N for those that do. function_outputs = np.array(self.func_table[:,:-1]) function_outputs[self.function_inputs[s]] = \ self.func_table[:,1:][self.function_inputs[s]] node_EVs = (config_probs * function_outputs).sum(1) EVs[s] = np.dot(node_EVs, self.action_weights[:,s]) return EVs
[docs] def get_payoffs(self, profile): """Returns an array of profile payoffs.""" function_inputs = (profile * self.function_inputs.T).sum(1) function_outputs = self.func_table[np.arange(self.num_functions), function_inputs] payoffs = (self.action_weights.T * function_outputs).sum(1) payoffs[np.logical_not(profile)] = 0 return payoffs
[docs] def to_rsgame(self): """Builds an rsgame.Game object that represents the same game.""" profiles = self.all_profiles() payoffs = np.array([self.get_payoffs(p) for p in profiles]) return rsgame.Game(self.num_players, self.num_strategies, profiles, payoffs)