from __future__ import annotations
from abc import ABC, abstractmethod
import numpy as np
from popgames.utilities.input_validators import (
check_array_shape,
check_scalar_value_bounds,
)
__all__ = ["RevisionProtocolABC", "Softmax", "Smith", "BNN", "Replicator", "CCSmith"]
[docs]
class RevisionProtocolABC(ABC):
"""
Abstract base class for revision protocols.
"""
[docs]
@abstractmethod
def __call__(self, p: np.ndarray, x: np.ndarray) -> np.ndarray:
"""
Subclasses must implement this method to enable the revision protocol to be called as a function.
Args:
p (np.ndarray): The payoff vector with shape (n, 1).
x (np.ndarray): The population state vector with shape (n, 1).
Returns:
np.ndarray: The switching probabilities as a matrix with shape (n, n).
"""
[docs]
class Softmax(RevisionProtocolABC):
"""
Softmax revision protocol. Also known as Logit-Choice revision protocol.
"""
def __init__(self, eta: float) -> None:
"""
Initialize the Softmax revision protocol object.
Args:
eta (float): The `temperature` or `noise` parameter.
"""
check_scalar_value_bounds(arg=eta, arg_name="eta", strictly_positive=True)
self.eta = eta
[docs]
def __call__(self, p: np.ndarray, x: np.ndarray) -> np.ndarray:
"""
Evaluate the Softmax revision protocol.
Args:
p (np.ndarray): The payoff vector with shape (n, 1).
x (np.ndarray): The population state vector with shape (n, 1).
Returns:
np.ndarray: The switching probabilities as a matrix with shape (n, n).
Examples:
>>> import numpy as np
>>> from popgames.revision_protocol import Softmax
>>> softmax = Softmax(eta=1)
>>> p = np.array([1, -1, 2]).reshape(3, 1)
>>> x = np.array([0.1, 0.7, 0.2]).reshape(3, 1)
>>> softmax(p, x)
array([[0.25949646, 0.25949646, 0.25949646],
[0.03511903, 0.03511903, 0.03511903],
[0.70538451, 0.70538451, 0.70538451]])
"""
logits = np.exp((p / self.eta) - np.max(p / self.eta))
probabilities = logits / (logits.sum())
return np.dot(probabilities, np.ones_like(probabilities).T)
[docs]
class Smith(RevisionProtocolABC):
"""
Smith revision protocol.
"""
def __init__(self, scale: float) -> None:
"""
Initialize the Smith revision protocol object.
Args:
scale (float): The scale parameter to ensure well-posed probabilities.
"""
check_scalar_value_bounds(arg=scale, arg_name="scale", strictly_positive=True)
self.scale = scale
[docs]
def __call__(self, p: np.ndarray, x: np.ndarray) -> np.ndarray:
"""
Evaluate the Smith revision protocol.
Args:
p (np.ndarray): The payoff vector with shape (n, 1).
x (np.ndarray): The population state vector with shape (n, 1).
Returns:
np.ndarray: The switching probabilities as a matrix with shape (n, n).
Examples:
>>> import numpy as np
>>> from popgames.revision_protocol import Smith
>>> smith = Smith(scale=0.1)
>>> p = np.array([1, -1, 2]).reshape(3, 1)
>>> x = np.array([0.1, 0.7, 0.2]).reshape(3, 1)
>>> smith(p, x)
array([[0. , 0.2, 0. ],
[0. , 0. , 0. ],
[0.1, 0.3, 0. ]])
"""
return np.maximum(p - p.T, 0) * self.scale
[docs]
class BNN(RevisionProtocolABC):
"""
Brown-von Neumann-Nash (BNN) revision protocol.
"""
def __init__(self, scale: float) -> None:
"""
Initialize the BNN revision protocol object.
Args:
scale (float): The scale parameter to ensure well-posed probabilities.
"""
check_scalar_value_bounds(arg=scale, arg_name="scale", strictly_positive=True)
self.scale = scale
[docs]
def __call__(self, p: np.ndarray, x: np.ndarray) -> np.ndarray:
"""
Evaluate the BNN revision protocol.
Args:
p (np.ndarray): The payoff vector with shape (n, 1).
x (np.ndarray): The population state vector with shape (n, 1).
Returns:
np.ndarray: The switching probabilities as a matrix with shape (n, n).
Examples:
>>> import numpy as np
>>> from popgames.revision_protocol import BNN
>>> bnn = BNN(scale=0.1)
>>> p = np.array([1, -1, 2]).reshape(3, 1)
>>> x = np.array([0.1, 0.7, 0.2]).reshape(3, 1)
>>> bnn(p, x)
array([[0.12, 0.12, 0.12],
[0. , 0. , 0. ],
[0.22, 0.22, 0.22]])
"""
p_hat = np.dot(x.T, p) / (x.sum())
delta_p = p - p_hat[0]
return np.maximum(np.dot(delta_p, np.ones_like(delta_p).T), 0) * self.scale
[docs]
class Replicator(RevisionProtocolABC):
"""
Replicator revision protocol. Also known as Pairwise Proportional Imitation revision protocol.
A revising agent observes a randomly chosen opponent and imitates its strategy with probability
proportional to the payoff excess of the opponent's strategy over its own. The resulting mean
dynamics are the replicator dynamics.
Strategies that are absent from the population (i.e., ``x_j = 0``) are never imitated. Hence,
unused strategies remain unused.
"""
def __init__(self, scale: float) -> None:
"""
Initialize the Replicator revision protocol object.
Args:
scale (float): The scale parameter to ensure well-posed probabilities. A sufficient condition is
``scale <= 1 / (max(p) - min(p))``.
"""
check_scalar_value_bounds(arg=scale, arg_name="scale", strictly_positive=True)
self.scale = scale
[docs]
def __call__(self, p: np.ndarray, x: np.ndarray) -> np.ndarray:
"""
Evaluate the Replicator revision protocol.
Args:
p (np.ndarray): The payoff vector with shape (n, 1).
x (np.ndarray): The population state vector with shape (n, 1).
Returns:
np.ndarray: The switching probabilities as a matrix with shape (n, n).
Examples:
>>> import numpy as np
>>> from popgames.revision_protocol import Replicator
>>> replicator = Replicator(scale=0.1)
>>> p = np.array([1, -1, 2]).reshape(3, 1)
>>> x = np.array([0.1, 0.7, 0.2]).reshape(3, 1)
>>> replicator(p, x)
array([[0. , 0.02, 0. ],
[0. , 0. , 0. ],
[0.02, 0.06, 0. ]])
"""
return (x / x.sum()) * np.maximum(p - p.T, 0) * self.scale
class CCSmith(RevisionProtocolABC):
"""
Capacity constrained Smith revision protocol.
"""
def __init__(
self,
scale: float,
x_bar: np.ndarray,
) -> None:
"""
Initialize the CCSmith revision protocol object.
Args:
scale (float): The scale parameter to ensure well-posed probabilities.
x_bar (np.ndarray): The capacity upper bounds for the available strategies, with shape (n, 1).
"""
check_scalar_value_bounds(arg=scale, arg_name="scale", strictly_positive=True)
self.scale = scale
check_array_shape(arg=x_bar, expected_shape=(len(x_bar), 1), arg_name="x_bar")
self.x_bar = x_bar
def __call__(
self,
p: np.ndarray,
x: np.ndarray,
) -> np.ndarray:
"""
Evaluate the CCSmith revision protocol.
Args:
p (np.ndarray): The payoff vector with shape (n, 1).
x (np.ndarray): The population state vector with shape (n, 1).
Returns:
np.ndarray: The switching probabilities as a matrix with shape (n, n).
"""
return np.maximum(self.x_bar - x, 0) * np.maximum(p - p.T, 0) * self.scale