Jain-Krishna autocatalytic networks
The system of Jain and Krishna, where selection of the fittest builds autocatalytic sets of molecules.
- Name:
jain_krishna - Field:
biology/evolution - Formalism: discrete map
- References: Jain and Krishna 1998; Jain and Krishna 2001; Thurner et al. 2018
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('jain_krishna')
entry = describe('jain_krishna')
trajectory = create_simulation('jain_krishna').prepare().run().trajectory_
Factory
@register_system(
field='biology/evolution',
tags=('adaptive network', 'autocatalytic set', 'Perron eigenvalue', 'origin of life', 'stochastic map'),
references=('Jain and Krishna 1998', 'Jain and Krishna 2001', 'Thurner et al. 2018'),
defaults={'until': 1500, 'dt': 5.0},
)
def jain_krishna(
n: int = 40,
*,
p: float = 0.02,
h: float = 0.5,
period: int = 10,
seed: int | None = 1,
) -> System:
"""Return the system of Jain and Krishna, where selection of the fittest builds autocatalytic sets of molecules.
The populations of `n` molecular species approach the Perron eigenvector of their catalytic network between graph
updates, every `period` steps. At every graph update the least populated species is replaced by a new one, linked
to the others with probability `p`. Once a cycle of catalysts forms by chance, the species it supports win, and the
autocatalytic set grows until it spans the network, so the number of links and the populated species jump.
Args:
n:
The number of species.
p:
The probability of every catalytic link.
h:
The growth of a step from the catalysts.
period:
The number of steps between two graph updates.
seed:
The seed of the initial network.
Returns:
system:
The system, with the populations `species.x` of the variable group `species` on the nodes and the variable
group `links` on the edges, where an edge from `i` to `j` means that `j` catalyses `i`.
Examples:
>>> from skcomplex.systems.biology.evolution import jain_krishna
>>> round(float(jain_krishna(n=10).elements[0].values['x'].sum()), 6)
1.0
"""
rng = np.random.default_rng(seed)
graph = nx.gnp_random_graph(n, p, seed=int(rng.integers(2**31)), directed=True)
space = NetworkSpace(graph)
species = GroupElement(
'species',
element=ChemicalElement,
space=space,
on='sites',
variable=True,
capacity=2 * n,
values={
'x': np.full(n, 1 / n),
'tie': rng.random(n),
'fresh': 0.0,
'h': h,
'p': p,
'period': float(period),
'k': max(1, round(p * n)),
},
visual=VisualSpec('sites', {'states': ['x']}),
)
links = GroupElement(
'links',
element=Element,
space=space,
on='edges',
variable=True,
capacity=n * n,
visual=HIDDEN,
)
return System(elements=[species, links], time=DiscreteTime())
Components
Elements:
ChemicalElement, fromElementElement
class ChemicalElement(Element):
"""Molecular species of a pool whose relative population grows with the populations of its catalysts.
The populations follow the catalytic network: every species gains `h` times the populations of the species that
catalyse it, and the populations are normalised, so they approach the Perron eigenvector of the network. Every
`period` steps the least populated species, with ties broken at random, is replaced by a new species with `k`
random catalysts, and every species catalyses the new one with probability `p`.
Args:
label:
The label of the species or of the group of species.
visual:
How the element is drawn, or `None` for `default_visual_`.
x:
The relative population.
tie:
The random key that breaks ties between the least populated species.
fresh:
Whether the species appeared at the previous graph update.
h:
The growth of a step from the catalysts.
p:
The probability of every catalytic link of a new species.
period:
The number of steps between two graph updates.
k:
The number of catalysts of a new species.
Examples:
>>> from skcomplex.systems.biology.evolution import ChemicalElement
>>> ChemicalElement('species').period
10.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
x: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
tie: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
fresh: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
h: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
p: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.02,
period: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
k: int = 1,
) -> None:
super().__init__(label, visual=visual)
self.x = x
self.tie = tie
self.fresh = fresh
self.h = h
self.p = p
self.period = period
self.k = k
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the catalytic growth, and the replacement of the least populated species at every graph update.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
grown = s.x + s.h * neighbors(s.other.x) + 1e-6
key = s.x + 1e-9 * s.tie
update = mod(s.t, s.period) > s.period - 3 / 2
least = where(both(key <= minimum(key), update), 1, 0)
newcomer = where(total(s.fresh) > 1 / 2, 1, 0) * (1 - s.fresh)
def fresh(node: SimpleNamespace) -> object:
"""Return the weight of a candidate catalysed species, 1 for the new species and 0 otherwise."""
return node.fresh
return [
UpdateLaw(s.x, grown / total(grown)),
UpdateLaw(s.tie, uniform('tie')),
UpdateLaw(s.fresh, 0),
UpdateLaw(TopologyRequestSpec('add-node', edges=self.k), least),
UpdateLaw(TopologyRequestSpec('remove-node'), least),
UpdateLaw(TopologyRequestSpec('add-edge', toward=fresh), s.p * newcomer),
]
class Element(BaseElement):
"""Base class of entities that carry state, such as particles, populations, agents, or firms.
A subclass takes its states, its parameters, and `visual` as arguments of its own `__init__`, annotating each state
with a `StateSpec` and each parameter with a `ParameterSpec` inside `Annotated`, and returns its intrinsic dynamics
from `_get_laws`, which returns no laws by default. A subclass drawn in its own way gives `visual` a default in its
signature.
An element whose laws have the agent trait, such as rules, alone or in a group, is an agent: under discrete time it
runs on the agent stepper, whose rules receive every member as an agent with its states as attributes. Its states
then hold any value, not only numbers. A member is removed only when its states tagged with a quantity are settled
to zero, and its states and parameters cannot take the names of the agent view, such as `location` or `alive`, or
of the attributes Mesa reads, such as `position` or `system`.
A subclass that connects to other elements declares its ports in the class variable `ports`, a mapping from the name
of a port to its `ConnectorSpec`, and takes one algebraic state per variable of a port, named `<port>_<variable>`.
A flow is positive into the element.
Args:
label:
The label of the element, unique among the parts of its system.
visual:
How the element is drawn, or `None` for `default_visual_`.
Attributes:
default_visual_:
The visual of the element when it sets none.
Examples:
>>> from typing import Annotated
>>> from skcomplex.core.components.elements import Element
>>> from skcomplex.core.specs import StateSpec
>>> from skcomplex.core.specs import VisualSpec
>>> from sage.all__sagemath_symbolics import oo
>>> from sage.sets.real_set import RealSet
>>> class PopulationElement(Element):
... def __init__(
... self,
... label,
... n: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
... visual=VisualSpec('line'),
... ):
... super().__init__(label, visual=visual)
... self.n = n
>>> PopulationElement('rabbits', n=40)
PopulationElement(label='rabbits', n=40)
"""
ports: ClassVar[Mapping[str, ConnectorSpec]] = {}
@classmethod
def _get_view(cls) -> type | None:
"""Return the class of the views of the element's agents, or `None` for the engine's plain view.
An agent element names a subclass of the engine's `AgentView` that annotates its states and parameters, such as
`class WordView(AgentView): count: float`, so its rules read them with checked types.
"""
return None
def _get_laws(self, namespace: SimpleNamespace) -> Sequence[BasePart]:
"""Return the laws of the element for a namespace of symbols, none by default.
Args:
namespace:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the element.
"""
return []
Equations
species.x[i]: x_species(t + 1) == -(active_species_topology - 1)*x_species(t) + (_neighbours_0*h_species + x_species(t) + 1e-06)*active_species_topology/_reduced_0
species.tie[i]: tie_species(t + 1) == active_species_topology*uniform_tie_species - (active_species_topology - 1)*tie_species(t)
species.fresh[i]: fresh_species(t + 1) == -(active_species_topology - 1)*fresh_species(t)