Skip to content

SK game of reinforcement learners

The SK game, reinforcement learners whose rewards come from quenched Sherrington-Kirkpatrick couplings.

  • Name: sk_game
  • Field: physics/criticality
  • Formalism: discrete map
  • References: Lesmana et al. 2026; Sherrington and Kirkpatrick 1975; Watkins and Dayan 1992

Factory

@register_model(
    field='physics/criticality',
    tags=('spin glass', 'reinforcement learning', 'game', 'disorder', 'discrete map'),
    references=('Lesmana et al. 2026', 'Sherrington and Kirkpatrick 1975', 'Watkins and Dayan 1992'),
    defaults={'until': 200, 'dt': 1},
    observables=_observables,
)
def sk_game(
    n_agents: int = 256,
    *,
    alpha: float = 0.5,
    temperature: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the SK game, reinforcement learners whose rewards come from quenched Sherrington-Kirkpatrick couplings.

    Every agent plays `sigma_i = ±1` and earns `sigma_i sum_j J_ij sigma_j`, with symmetric Gaussian couplings of mean
    0, variance `1 / N`, and zero diagonal. It discounts its rewards with the memory `alpha` into the incentive `Q_i`
    and plays from a softmax policy at the temperature `T` (Lesmana et al., Eqs. 1-3). All agents start with zero
    incentives, so their first actions are random, and update in parallel. At low temperature the agents freeze into
    one of many ordered states with a high collective reward, and at high temperature they explore noisily with a low
    one.

    Args:
        n_agents:
            The number of agents `N`.

        alpha:
            The memory parameter.

        temperature:
            The exploration temperature of every agent.

        random_state:
            The seed or generator of the couplings and of the initial actions.

    Returns:
        system:
            The model, with the actions `agents.sigma`, the incentives `agents.Q`, and the coupling matrix in the values
            of the group. Its standard observable `reward` is the mean reward per agent.

    Examples:
        >>> from skcomplex.models.physics.criticality import sk_game
        >>> couplings = sk_game(10).elements[0].values['couplings']
        >>> bool((couplings == couplings.T).all())
        True
    """
    rng = np.random.default_rng(random_state)
    upper = np.triu(rng.normal(0.0, 1.0 / np.sqrt(n_agents), (n_agents, n_agents)), 1)
    agents = GroupElement(
        'agents',
        element=LearningAgentElement,
        size=n_agents,
        values={
            'sigma': rng.choice([-1.0, 1.0], size=n_agents),
            'Q': 0.0,
            'alpha': alpha,
            'temperature': temperature,
            'couplings': upper + upper.T,
        },
    )
    return System(elements=[agents])

Components

class LearningAgentElement(Element):
    """Agent of the SK game that plays an action of ±1 from a softmax policy over its incentive to play +1.

    The incentive follows the reinforcement rule `Q(t + 1) = (1 - alpha) Q(t) + alpha sum_j J_ij sigma_j(t)`, the
    discounted reward of playing +1, and the agent plays `sigma = ±1` with probability `1 / (1 + exp(-+2 Q / T))`.

    Args:
        label:
            The label of the agent or the group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        sigma:
            The initial action, -1 or 1.

        Q:
            The initial incentive.

        alpha:
            The memory parameter, the weight of the latest reward.

        temperature:
            The exploration temperature `T` of the softmax policy.

        couplings:
            The coupling matrix `J`, one row and one column per agent.

    Examples:
        >>> from skcomplex.models.physics.criticality import LearningAgentElement
        >>> LearningAgentElement('agent').alpha
        0.5
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        sigma: Annotated[float, StateSpec(domain=ZZ)] = 1.0,
        Q: Annotated[float, StateSpec()] = 0.0,
        alpha: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
        temperature: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
        couplings: npt.ArrayLike | None = None,
    ) -> None:
        super().__init__(label, visual=visual)
        self.sigma = sigma
        self.Q = Q
        self.alpha = alpha
        self.temperature = temperature
        self.couplings = couplings

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the reinforcement of the incentive and the softmax draw of the action, both in parallel.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        reward = total(s.sigma, weights=self.couplings)
        up = 1 / (1 + exp(-2 * s.Q / s.temperature))
        return [
            UpdateLaw(s.Q, (1 - s.alpha) * s.Q + s.alpha * reward),
            UpdateLaw(s.sigma, where(uniform('u') < up, 1, -1)),
        ]

Equations

agents.sigma[i]: sigma_agents(t + 1) == where(uniform_u_agents < (1/(e^(-2*Q_agents(t)/temperature_agents) + 1)), 1, -1)
agents.Q[i]: Q_agents(t + 1) == _reduced_0*alpha_agents - (alpha_agents - 1)*Q_agents(t)