import numpy as np
from gameanalysis import aggfn
from gameanalysis import rsgame
from gameanalysis import utils
def _random_inputs(prob, num_strats, num_funcs):
"""Returns a random mask without all true or all false per function"""
vals = np.random.random((num_strats, num_funcs))
mask = vals < prob
inds = np.arange(num_funcs)
mask[vals.argmin(0), inds] = True
mask[vals.argmax(0), inds] = False
return mask
def _random_mask(prob, num_funcs, num_strats):
"""Returns a random mask with at least one true in every row and col"""
vals = np.random.random((num_funcs, num_strats))
mask = vals < prob
mask[np.arange(num_funcs), vals.argmin(1)] = True
mask[vals.argmin(0), np.arange(num_strats)] = True
return mask
def _random_weights(prob, num_funcs, num_strats):
"""Returns random action weights"""
return (_random_mask(prob, num_funcs, num_strats) *
np.random.normal(0, 1, (num_funcs, num_strats)))
[docs]def normal_aggfn(role_players, role_strats, functions, *, input_prob=0.2,
weight_prob=0.2):
"""Generate a random normal AgfnGame
Each function value is an i.i.d Gaussian random walk.
Parameters
----------
role_players : int or ndarray
The number of players per role.
role_strats : int or ndarray
The number of strategies per role.
functions : int
The number of functions to generate.
input_prob : float, optional
The probability of a strategy counting towards a function value.
weight_prob : float, optional
The probability of a function producing non-zero payoffs to a strategy.
"""
base = rsgame.emptygame(role_players, role_strats)
inputs = _random_inputs(input_prob, base.num_strats, functions)
weights = _random_weights(weight_prob, functions, base.num_strats)
shape = (functions,) + tuple(base.num_role_players + 1)
funcs = np.random.normal(0, 1 / np.sqrt(base.num_players + 1), shape)
for r in range(1, base.num_roles + 1):
funcs.cumsum(r, out=funcs)
mean = funcs.mean(tuple(range(1, base.num_roles + 1)))
mean.shape = (functions,) + (1,) * base.num_roles
funcs -= mean
return aggfn.aggfn_replace(base, weights, inputs, funcs)
def _random_aggfn(role_players, role_strats, functions, input_prob,
weight_prob, role_dist):
"""Base form for structured random aggfn generation
role_dist takes a number of functions and a number of players and returns
an ndarray of the function values.
"""
base = rsgame.emptygame(role_players, role_strats)
inputs = _random_inputs(input_prob, base.num_strats, functions)
weights = _random_weights(weight_prob, functions, base.num_strats)
funcs = np.ones((functions,) + tuple(base.num_role_players + 1))
base_shape = [functions] + [1] * base.num_roles
for r, p in enumerate(base.num_role_players):
role_funcs = role_dist(functions, p)
shape = base_shape.copy()
shape[r + 1] = p + 1
role_funcs.shape = shape
funcs *= role_funcs
return aggfn.aggfn_replace(base, weights, inputs, funcs)
[docs]def poly_aggfn(role_players, role_strats, functions, *, input_prob=0.2,
weight_prob=0.2, degree=4):
"""Generate a random polynomial AgfnGame
Functions are generated by generating `degree` zeros in [0, num_players] to
serve as a polynomial functions.
Parameters
----------
role_players : int or ndarray
The number of players per role.
role_strats : int or ndarray
The number of strategies per role.
functions : int
The number of functions to generate.
input_prob : float, optional
The probability of a strategy counting towards a function value.
weight_prob : float, optional
The probability of a function producing non-zero payoffs to a strategy.
degree : int or [float], optional
Either an integer specifying the degree or a list of the probabilities
of degrees starting from one, e.g. 3 is the same as [0, 0, 1].
"""
if isinstance(degree, int):
degree = (0,) * (degree - 1) + (1,)
max_degree = len(degree)
def role_dist(functions, p):
zeros = (np.random.random((functions, max_degree)) * 1.5 - 0.25) * p
terms = np.arange(p + 1)[:, None] - zeros[:, None]
choices = np.random.choice(
max_degree, (functions, p + 1), True, degree)
terms[choices[..., None] < np.arange(max_degree)] = 1
poly = terms.prod(2) / p ** choices
# The prevents too many small polynomials from making functions
# effectively constant
scale = poly.max() - poly.min()
offset = poly.min() + 1
return (poly - offset) / (1 if np.isclose(scale, 0) else scale)
return _random_aggfn(role_players, role_strats, functions, input_prob,
weight_prob, role_dist)
[docs]def sine_aggfn(role_players, role_strats, functions, *, input_prob=0.2,
weight_prob=0.2, period=4):
"""Generate a random sinusodial AgfnGame
Functions are generated by generating sinusoids with uniform random shifts
and n periods in 0 to num_players, where n is chosen randomle between
min_period and max_period.
Parameters
----------
role_players : int or ndarray
The number of players per role.
role_strats : int or ndarray
The number of strategies per role.
functions : int
The number of functions to generate.
input_prob : float, optional
The probability of a strategy counting towards a function value.
weight_prob : float, optional
The probability of a function producing non-zero payoffs to a strategy.
period : float, optional
The loose number of periods in the payoff for each function.
"""
def role_dist(functions, p):
# This setup makes it so that the beat frequencies approach period
periods = ((np.arange(1, functions + 1) +
np.random.random(functions) / 2 - 1 / 4) *
period / functions)
offset = np.random.random((functions, 1))
return np.sin((np.linspace(0, 1, p + 1) * periods[:, None] + offset) *
2 * np.pi)
return _random_aggfn(role_players, role_strats, functions, input_prob,
weight_prob, role_dist)
def _random_monotone_polynomial(functions, players, degree):
"""Generates a random monotone polynomial table"""
coefs = (np.random.random((functions, degree + 1)) /
players ** np.arange(degree + 1))
powers = np.arange(players + 1) ** np.arange(degree + 1)[:, None]
return coefs.dot(powers)
[docs]def congestion(num_players, num_facilities, num_required, *, degree=2):
"""Generate a congestion game
A congestion game is a symmetric game, where there are a given number of
facilities, and each player must choose to use some amount of them. The
payoff for each facility decreases as more players use it, and a players
utility is the sum of the utilities for every facility.
In this formulation, facility payoffs are random polynomials of the number
of people using said facility.
Parameters
----------
num_players : int > 1
The number of players.
num_facilities : int > 1
The number of facilities.
num_required : 0 < int < num_facilities
The number of required facilities.
degree : int > 0, optional
Degree of payoff polynomials.
"""
assert num_players > 1, "must have more than one player"
assert num_facilities > 1, "must have more than one facility"
assert 0 < num_required < num_facilities, \
"must require more than zero but less than num_facilities"
assert degree > 0, "degree must be greater than zero"
function_inputs = utils.acomb(num_facilities, num_required)
functions = -_random_monotone_polynomial(num_facilities, num_players,
degree)
facs = tuple(utils.prefix_strings('', num_facilities))
strats = tuple('_'.join(facs[i] for i, m in enumerate(mask) if m)
for mask in function_inputs)
return aggfn.aggfn_names(['all'], num_players, [strats], facs,
function_inputs.T, function_inputs, functions)
[docs]def local_effect(num_players, num_strategies, *, edge_prob=0.2):
"""Generate a local effect game
In a local effect game, strategies are connected by a graph, and utilities
are a function of the number of players playing our strategy and the number
of players playing a neighboring strategy, hence local effect.
In this formulation, payoffs for others playing our strategy are negative
quadratics, and payoffs for playing other strategies are positive cubics.
Parameters
----------
num_players : int > 1
The number of players.
num_strategies : int > 1
The number of strategies.
edge_prob : float, optional
The probability that one strategy affects another.
"""
assert num_players > 1, "can't generate a single player game"
assert num_strategies > 1, "can't generate a single strategy game"
local_effect_graph = np.random.rand(
num_strategies, num_strategies) < edge_prob
np.fill_diagonal(local_effect_graph, False)
num_neighbors = local_effect_graph.sum()
num_functions = num_neighbors + num_strategies
action_weights = np.eye(num_functions, num_strategies, dtype=float)
function_inputs = np.eye(num_strategies, num_functions, dtype=bool)
in_act, out_act = local_effect_graph.nonzero()
func_inds = np.arange(num_strategies, num_functions)
function_inputs[in_act, func_inds] = True
action_weights[func_inds, out_act] = 1
function_table = np.empty((num_functions, num_players + 1), float)
function_table[:num_strategies] = -_random_monotone_polynomial(
num_strategies, num_players, 2)
function_table[num_strategies:] = _random_monotone_polynomial(
num_neighbors, num_players, 3)
return aggfn.aggfn(num_players, num_strategies, action_weights,
function_inputs, function_table)