Skip to content

Jain-Krishna autocatalytic networks

The model 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

Factory

@register_model(
    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 model 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 model, 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.models.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=CatalyticLinkElement,
        space=space,
        on='edges',
        variable=True,
        capacity=n * n,
        visual=HIDDEN,
    )
    return System(elements=[species, links], time=DiscreteTime())

Components

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.models.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 CatalyticLinkElement(Element):
    """Catalytic link from a species to a species that catalyses it, which carries no state of its own.

    Args:
        label:
            The label of the link or of the group of links.

    Examples:
        >>> from skcomplex.models.biology.evolution import CatalyticLinkElement
        >>> CatalyticLinkElement('link').label
        'link'
    """

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)