Evolutionary Sherrington-Kirkpatrick game
The evolutionary Sherrington-Kirkpatrick game, which self-organises to the edge of ergodicity breaking.
- Name:
evolutionary_sk_game - Field:
physics/criticality - Formalism: discrete map
- References: Lesmana, Feng, Chen, and Lai 2026; Bak and Sneppen 1993; Sherrington and Kirkpatrick 1975
Factory
@register_model(
field='physics/criticality',
tags=('self-organized criticality', 'reinforcement learning', 'spin glass', 'extremal dynamics', 'discrete map'),
references=('Lesmana, Feng, Chen, and Lai 2026', 'Bak and Sneppen 1993', 'Sherrington and Kirkpatrick 1975'),
defaults={'until': 3000, 'dt': 1},
observables=_observables,
)
def evolutionary_sk_game(
n_agents: int = 256,
*,
memory: float = 0.75,
temperature: npt.ArrayLike | None = None,
max_temperature: float = 2.0,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return the evolutionary Sherrington-Kirkpatrick game, which self-organises to the edge of ergodicity breaking.
After Lesmana et al. (2026, arXiv 2604.15669), `N` agents play ±1 on a Sherrington-Kirkpatrick landscape with
symmetric Gaussian couplings of variance `1 / N`. Each learns the incentive of its actions with memory `alpha` and
plays by a softmax policy at its own temperature, and the least fit agent is replaced, as in the Bak-Sneppen
model, by a newcomer whose temperature is drawn uniformly on `[0, max_temperature]`. The temperatures evolve to the
edge between ergodic and frozen learning, the replacement cascades have sizes distributed as `S^(-3/2)`, and the
mean reward exceeds that of the game at any fixed temperature. With a fixed temperature, no agent is replaced and
the model is the Sherrington-Kirkpatrick game of Galla and Farmer.
Args:
n_agents:
The number of agents `N`.
memory:
The weight `alpha` of the latest field in the incentive.
temperature:
The temperature of every agent, or one per agent, of the game without replacement, or `None` for the
evolving game.
max_temperature:
The largest temperature of a newcomer, and of the initial temperatures drawn uniformly.
random_state:
The seed or generator of the couplings, the initial actions, and the initial temperatures.
Returns:
system:
The model, with the group `agents` of states `incentive`, `spin`, `temperature`, `fitness`, and `reward`,
and the observables `mean_reward`, `mean_temperature`, and `least_fitness`.
Examples:
>>> from skcomplex.models.physics.criticality import evolutionary_sk_game
>>> agents = evolutionary_sk_game(16).elements[0]
>>> couplings = agents.values['couplings']
>>> agents.size, bool((couplings == couplings.T).all()), float(couplings.trace())
(16, True, 0.0)
"""
rng = np.random.default_rng(random_state)
couplings = _sk_couplings(n_agents, rng)
spins = rng.choice([-1.0, 1.0], size=n_agents)
evolving = temperature is None
if temperature is None:
temperatures = rng.uniform(0.0, max_temperature, n_agents)
else:
temperatures = np.broadcast_to(np.asarray(temperature, dtype=float), (n_agents,)).copy()
agents = GroupElement(
'agents',
element=LearnerElement,
size=n_agents,
values={
'spin': spins,
'temperature': temperatures,
'memory': memory,
'max_temperature': max_temperature,
'couplings': couplings,
'evolving': evolving,
},
visual=TEMPERATURE_VISUAL,
)
return System(elements=[agents])
Components
class LearnerElement(Element):
"""Agent of the evolutionary Sherrington-Kirkpatrick game, who learns a binary action and may be replaced.
The agent keeps an incentive `Q`, the value of action +1, and plays the action `s = ±1` by the softmax policy at
its temperature `T`. Every step the incentive becomes `(1 - alpha) Q + alpha h` for the local field
`h = Σ_j J_ij s_j`, and the fitness is `s Q`. When the agent evolves and its fitness is the least of the group,
it is replaced by a newcomer with no incentive and a temperature drawn uniformly up to `max_temperature`, who
learns from the field at once. The reward `s h` of the last actions is kept.
Args:
label:
The label of the agent or the group.
visual:
How the element is drawn, or `None` for `default_visual_`.
incentive:
The initial incentive `Q`.
spin:
The initial action, -1 or 1.
temperature:
The initial temperature `T` of the softmax policy.
fitness:
The initial fitness `s Q`.
reward:
The initial reward `s h`.
memory:
The weight `alpha` of the latest field in the incentive, so small values keep a long memory.
max_temperature:
The largest temperature of a newcomer.
couplings:
The coupling matrix `J`, one row and one column per agent.
evolving:
Whether the least fit agent is replaced.
settling:
The number of steps of learning alone after every replacement, so replacements happen when `t` is a
multiple of `settling + 1`.
Examples:
>>> from skcomplex.models.physics.criticality import LearnerElement
>>> LearnerElement('agent').memory
0.75
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
incentive: Annotated[float, StateSpec()] = 0.0,
spin: Annotated[float, StateSpec(domain=ZZ)] = 1.0,
temperature: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
fitness: Annotated[float, StateSpec()] = 0.0,
reward: Annotated[float, StateSpec()] = 0.0,
memory: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.75,
max_temperature: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
couplings: npt.ArrayLike | None = None,
evolving: bool = True,
settling: int = 2,
) -> None:
super().__init__(label, visual=visual)
self.incentive = incentive
self.spin = spin
self.temperature = temperature
self.fitness = fitness
self.reward = reward
self.memory = memory
self.max_temperature = max_temperature
self.couplings = couplings
self.evolving = evolving
self.settling = settling
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the replacement of the least fit agent, the learning of the incentive, and the softmax action.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
field = total(s.spin, weights=self.couplings)
incentive, temperature = s.incentive, s.temperature
if self.evolving:
least = both(s.fitness <= minimum(s.fitness), mod(s.t, self.settling + 1) < 1 / 2)
incentive = where(least, 0, s.incentive)
temperature = where(least, uniform('newcomer', 0, s.max_temperature), s.temperature)
learned = (1 - s.memory) * incentive + s.memory * field
action = _softmax_action(learned, temperature)
return [
UpdateLaw(s.incentive, learned),
UpdateLaw(s.temperature, temperature),
UpdateLaw(s.spin, action),
UpdateLaw(s.fitness, action * learned),
UpdateLaw(s.reward, s.spin * field),
]
Equations
agents.incentive[i]: incentive_agents(t + 1) == _reduced_1*memory_agents - (memory_agents - 1)*where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), 0, incentive_agents(t))
agents.spin[i]: spin_agents(t + 1) == where(uniform_action_agents < 1/2*tanh((_reduced_1*memory_agents - (memory_agents - 1)*where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), 0, incentive_agents(t)))/where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), max_temperature_agents*uniform_newcomer_agents, temperature_agents(t))) + 1/2, 1, -1)
agents.temperature[i]: temperature_agents(t + 1) == where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), max_temperature_agents*uniform_newcomer_agents, temperature_agents(t))
agents.fitness[i]: fitness_agents(t + 1) == (_reduced_1*memory_agents - (memory_agents - 1)*where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), 0, incentive_agents(t)))*where(uniform_action_agents < 1/2*tanh((_reduced_1*memory_agents - (memory_agents - 1)*where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), 0, incentive_agents(t)))/where(both(fitness_agents(t) <= _reduced_0, mod(t, 3) < 0.5), max_temperature_agents*uniform_newcomer_agents, temperature_agents(t))) + 1/2, 1, -1)
agents.reward[i]: reward_agents(t + 1) == _reduced_1*spin_agents(t)