Co-evolving combinatorial critical model
The co-evolving combinatorial critical model, whose diversity shows punctuated equilibria.
- Name:
ccc_model - Field:
economics/innovation - Formalism: agent-based system
- References: Thurner et al. 2018
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('ccc_model')
entry = describe('ccc_model')
trajectory = create_simulation('ccc_model').prepare().run().trajectory_
Factory
@register_system(
field='economics/innovation',
tags=('innovation', 'combinatorial evolution', 'punctuated equilibria', 'self-organized criticality', 'agents'),
references=('Thurner et al. 2018',),
defaults={'until': 1000},
)
def ccc_model(
n_entities: int = 100,
*,
r_plus: float = 5.0,
r_minus: float = 10.0,
p: float = 0.0002,
initial: float = 0.5,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return the co-evolving combinatorial critical model, whose diversity shows punctuated equilibria.
Every entity exists or not. Pairs of existing entities produce other entities through the production tensor `M+`
and destroy them through the destruction tensor `M-`, random tensors with on average `r+` productive and `r-`
destructive sets per entity, fixed during the run. Every step, in random sequential order, an entity exists when
its active productive sets outnumber its active destructive ones, disappears in the opposite case, and keeps its
state on a tie, and then switches spontaneously with a small probability `p` (Thurner et al. 2018, Section 5.6.4).
The diversity stays nearly constant for long quasi-equilibria, whose lifetimes and the sizes of the changes between
them have approximate power laws, and restructures in bursts. Mostly productive tensors fill the system, mostly
destructive ones keep it nearly empty, and the punctuated regime lies in between without fine-tuning. The source
leaves the initial state open, so every entity exists at the start with the probability `initial`. With the
tensors of this implementation the setting `r+ = 10` and `r- = 15` of Fig. 5.14 restructures almost every step,
so the defaults take `r+ = 5` and `r- = 10`, which is punctuated.
Args:
n_entities:
The number of entities.
r_plus:
The mean number of productive sets per entity.
r_minus:
The mean number of destructive sets per entity.
p:
The probability per step that an entity switches its state spontaneously.
initial:
The probability that an entity exists at the start.
random_state:
The seed or generator of the tensors and the initial states.
Returns:
system:
The system, with the group `entities` and the update `update`, whose reporters are the number of existing
entities `update.diversity` and the entities that changed state in the step `update.changes`.
Examples:
>>> from skcomplex.systems.economics.innovation import ccc_model
>>> ccc_model(10).elements[0].prepare().size_
10
"""
rng = np.random.default_rng(random_state)
entities = GroupElement(
'entities',
element=CombinatorialEntityElement,
size=n_entities,
values={'exists': (rng.random(n_entities) < initial).astype(float)},
)
update = CombinatorialUpdateInteraction(
'update',
entities='entities',
production=_random_sets(n_entities, r_plus, rng),
destruction=_random_sets(n_entities, r_minus, rng),
p=p,
)
return System(elements=[entities], interactions=[update])
Components
Elements:
CombinatorialEntityElement, fromElement
Interactions:
CombinatorialUpdateInteraction, fromFeedbackInteraction
class CombinatorialEntityElement(Element):
"""Entity of a combinatorial evolution, a species, a good, or a technology, that exists or not.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
exists:
1 when the entity exists, 0 otherwise.
fitness:
The active productive minus the active destructive sets of the entity at its last update.
Examples:
>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.economics.innovation import CombinatorialEntityElement
>>> GroupElement('entities', element=CombinatorialEntityElement, size=3).size
3
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'exists'}),
*,
exists: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.0,
fitness: Annotated[float, StateSpec()] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.exists = exists
self.fitness = fitness
@classmethod
def _get_view(cls) -> type[CombinatorialEntityView]:
"""Return the view of the agents of the element."""
return CombinatorialEntityView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return no laws, since the update of the system moves every entity.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return []
class CombinatorialUpdateInteraction(FeedbackInteraction[SystemView]):
"""Random sequential update of the CCC model by the sign of the fitness, with spontaneous flips.
Every step every entity `i`, in random order, computes its fitness
`f_i = sum_jk (M+_ijk - M-_ijk) sigma_j sigma_k` from the current states, exists when it is positive, disappears
when it is negative, and keeps its state when it is zero (Eq. 5.61 of Thurner et al. 2018). Then it switches its
state with the probability `p`.
Args:
label:
The label of the update.
rule:
Unused, since the update overrides `act`.
phase:
The phase the update acts in, or `None` for the default phase.
entities:
The path of the group of entities.
production:
The productive sets, triples `(i, j, k)` of the nonzero entries of `M+`.
destruction:
The destructive sets, triples `(i, j, k)` of the nonzero entries of `M-`.
p:
The probability per step that an entity switches its state spontaneously.
Examples:
>>> from skcomplex.systems.economics.innovation import CombinatorialUpdateInteraction
>>> CombinatorialUpdateInteraction('update', entities='entities').reports
('diversity', 'changes')
"""
reports: ClassVar[tuple[str, ...]] = ('diversity', 'changes')
def __init__(
self,
label: str,
*,
rule: None = None,
phase: str | None = None,
entities: Annotated[str, RoleSpec()],
production: tuple[tuple[int, int, int], ...] = (),
destruction: tuple[tuple[int, int, int], ...] = (),
p: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0002,
) -> None:
super().__init__(label, rule=rule, phase=phase)
self.entities = entities
self.production = production
self.destruction = destruction
self.p = p
@staticmethod
def _sets(triples: tuple[tuple[int, int, int], ...], size: int) -> list[Array]:
"""Return the pairs `(j, k)` of the sets of every entity."""
array = np.asarray(triples, dtype=int).reshape(-1, 3)
return [array[array[:, 0] == entity, 1:] for entity in range(size)]
def act(self, system: SystemView) -> None:
"""Update every entity once in random order.
Args:
system:
The system view of the update.
"""
entities = sorted(system.agents('entities', CombinatorialEntityView), key=lambda entity: entity.id)
size, rng = len(entities), system.rng
states = np.array([entity.exists for entity in entities])
before = states.copy()
plus, minus = self._sets(self.production, size), self._sets(self.destruction, size)
fitness = np.array([entity.fitness for entity in entities])
flips = rng.random(size) < system.parameter('p')
for entity in rng.permutation(size):
gain = states[plus[entity][:, 0]] @ states[plus[entity][:, 1]]
loss = states[minus[entity][:, 0]] @ states[minus[entity][:, 1]]
fitness[entity] = gain - loss
if fitness[entity] != 0:
states[entity] = float(fitness[entity] > 0)
if flips[entity]:
states[entity] = 1 - states[entity]
for entity, state, value in zip(entities, states, fitness, strict=True):
entity.exists, entity.fitness = float(state), float(value)
system.report('diversity', float(states.sum()))
system.report('changes', float(np.sum(states != before)))