Source code for gameanalysis.fixedpoint
"""Module for finding fixed points of functions on a simplex"""
import numpy as np
from gameanalysis import utils
[docs]def fixed_point(func, init, **kwargs):
"""Compute an approximate fixed point of a function
Parameters
----------
func : ndarray -> ndarray
A continuous function mapping from the d-simplex to itself.
init : ndarray
An initial guess for the fixed point. Since many may exist, the choice
of starting point will affect the solution.
kwargs : options
Additional options to pass on to labeled_subsimplex. See other options
for details.
"""
def fixed_func(mix):
"""Labeling function for a fixed point"""
return np.argmin((mix == 0) - mix + func(mix))
return labeled_subsimplex(fixed_func, init, **kwargs)
[docs]def labeled_subsimplex(
label_func, init, disc
): # pylint: disable=too-many-locals,too-many-statements
"""Find approximate center of a fully labeled subsimplex
This runs once at the discretization provided. It is recommended that this
be run several times with successively finer discretization and warm
started with the past result.
Parameters
----------
label_func : ndarray -> int
A proper lableing function. A labeling function takes an element of the
d-simplex and returns a label in [0, d). It is proper if the label
always coresponds to a dimension in support.
init : ndarray
An initial guess for where the fully labeled element might be. This
will be projected onto the simplex if it is not already.
disc : int
The discretization to use. Fixed points will be approximated by the
reciprocal this much.
Returns
-------
ret : ndarray
A discretized simplex with 1 coarser resolution (i.e. ret.sum() + 1 ==
init.sum()) that is fully labeled.
Notes
-----
This is an implementation of the sandwhich method from [5]_ and [6]_
.. [5] Kuhn and Mackinnon 1975. Sandwich Method for Finding Fixed Points.
.. [6] Kuhn 1968. Simplicial Approximation Of Fixed Points.
"""
init = np.asarray(init, float)
dim = init.size
# Base vertex of the subsimplex currently being used
dinit = _discretize_mixture(init, disc)
base = np.append(dinit, 0)
base[0] += 1
# permutation array of [1,dim] where v0 = base,
# v{i+1} = [..., vi_{perms[i] - 1} - 1, vi_{perms[i]} + 1, ...]
perms = np.arange(1, dim + 1)
# Array of labels for each vertex
labels = np.arange(dim + 1)
labels[dim] = label_func(dinit / disc)
# Vertex used to label initial vertices (vertex[-1] == 0)
label_vertex = base[:-1].copy()
# Last index moved
index = dim
# Most recent created index, should be set to
new_vertex = None
while labels[index] < dim:
# Find duplicate index. this is O(dim) but not a bottleneck
(dup_labels,) = np.nonzero(labels == labels[index])
(index,) = dup_labels[dup_labels != index]
# Flip simplex over at index
if index == 0:
base[perms[0]] += 1
base[perms[0] - 1] -= 1
perms = np.roll(perms, -1)
labels = np.roll(labels, -1)
index = dim
elif index == dim:
base[perms[-1] - 1] += 1
base[perms[-1]] -= 1
perms = np.roll(perms, 1)
labels = np.roll(labels, 1)
index = 0
else: # 0 < index < dim
perms[index - 1], perms[index] = perms[index], perms[index - 1]
# Compute actual value of flipped vertex
new_vertex = base.copy()
new_vertex[perms[:index]] += 1
new_vertex[perms[:index] - 1] -= 1
utils.check(
np.all(new_vertex >= 0) and new_vertex.sum() == disc + 1,
"vertex rotation failed, check labeling function",
)
# Update label of new vertex
if new_vertex[-1] == 2:
labels[index] = dim
elif new_vertex[-1] == 0:
labels[index] = np.argmax(new_vertex[:-1] - label_vertex)
else: # == 1
labels[index] = label_func(new_vertex[:-1] / disc)
utils.check(
0 <= labels[index] < dim and new_vertex[labels[index]],
"labeling function was not proper (see help)",
)
# Average out all vertices in simplex we care about
current = base
if index == 0: # pragma: no cover
count = 0
mean = np.zeros(dim)
else: # pragma: no cover
count = 1
mean = current.astype(float)
for i, j in enumerate(perms, 1):
current[j] += 1
current[j - 1] -= 1
if i != index:
count += 1
mean += (current - mean) / count
return mean[:-1] / disc
def _discretize_mixture(mix, k):
"""Discretize a mixture
The returned value will have all integer components that sum to k, with the
minimum error. Thus, discretizing the mixture.
"""
disc = np.floor(mix * k).astype(int)
inds = np.argsort(disc - mix * k)[: k - disc.sum()]
disc[inds] += 1
return disc