Skip to content

Hypercycles

A network of self-replicating molecules whose catalysis keeps a set of them alive, a hypercycle.

  • Name: hypercycles
  • Field: biology/evolution
  • Formalism: ordinary differential equations
  • References: Eigen and Schuster 1977; Gros 2015

Factory

@register_model(
    field='biology/evolution',
    tags=('hypercycle', 'autocatalysis', 'prebiotic', 'ode'),
    references=('Eigen and Schuster 1977', 'Gros 2015'),
    defaults={'until': 50.0, 'dt': 0.5},
)
def hypercycles(
    molecules: int = 4,
    *,
    alpha: float = 0.1,
    omega: float = 1.0,
    topology: str = 'cycle',
) -> System:
    """Return a network of self-replicating molecules whose catalysis keeps a set of them alive, a hypercycle.

    The growth rates rise as `lambda_i = alpha i`. In a `'cycle'`, every molecule catalyses the next one. Up to four
    molecules the hypercycle keeps all of them at a fixed point, and from five some members die out and the others
    settle at a fixed point. In a `'complete'` network every molecule catalyses every other, and about
    `sqrt(2 omega / alpha)` of the fastest replicators survive while the others die out.

    Args:
        molecules:
            The number of molecules.

        alpha:
            The step of the autocatalytic growth rates.

        omega:
            The catalytic rate of a link.

        topology:
            `'cycle'` or `'complete'`.

    Returns:
        system:
            The model, with the concentrations `soup.x` of the molecules, members of the group `soup`.

    Raises:
        ValidationError:
            The topology is unknown.

    Examples:
        >>> from skcomplex.models.biology.evolution import hypercycles
        >>> hypercycles(3).interactions[0].topology
        ((0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0))
    """
    if topology == 'cycle':
        links = np.roll(np.eye(molecules), 1, axis=0)
    elif topology == 'complete':
        links = 1 - np.eye(molecules)
    else:
        error_msg = f'Unknown topology {topology!r}. Use cycle or complete.'
        raise ValidationError(error_msg)
    soup = GroupElement(
        'soup',
        element=MoleculeElement,
        size=molecules,
        values={
            'x': [1 / molecules] * molecules,
            'growth': [float(alpha * index) for index in range(1, molecules + 1)],
        },
    )
    catalysis = CatalysisInteraction(
        'catalysis',
        target='soup',
        omega=omega,
        topology=tuple(tuple(float(value) for value in row) for row in links),
    )
    return System(elements=[soup], interactions=[catalysis])

Components

class MoleculeElement(Element):
    """Self-replicating molecule of a prebiotic soup, with its concentration `x` and its own growth rate.

    Args:
        label:
            The label of the molecule or of the group of molecules.

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

        x:
            The initial concentration.

        growth:
            The rate of replication without catalysis.

    Examples:
        >>> from skcomplex.models.biology.evolution import MoleculeElement
        >>> MoleculeElement('molecule', growth=0.1).growth
        0.1
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
        growth: Annotated[object, ParameterSpec()] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.growth = growth

class CatalysisInteraction(ReplicatorInteraction):
    """Catalysed replication of the molecules of a group, with a common outflow that keeps the total at 1.

    Molecule `i` replicates at the rate `f_i = lambda_i + omega sum_j T_ij x_j`, with its own growth `lambda_i` and the
    0 or 1 topology `T` of the catalytic links read as the weights of a total over the molecules. The outflow is the
    total replication, so the concentrations follow the replicator equation `dx_i/dt = x_i (f_i - sum_j x_j f_j)`.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of molecules.

        omega:
            The catalytic rate of a link.

        topology:
            The catalytic links, 1 where molecule `j` catalyses molecule `i`.

    Examples:
        >>> from skcomplex.models.biology.evolution import CatalysisInteraction
        >>> CatalysisInteraction('catalysis', target='soup', topology=((0, 1), (1, 0))).topology
        ((0, 1), (1, 0))
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        omega: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        topology: Sequence[Sequence[float]] = ((0.0, 1.0), (1.0, 0.0)),
    ) -> None:
        super().__init__(label, target=target)
        self.omega = omega
        self.topology = topology

    def _check_fitness(self) -> None:
        """Leave the fitness unchecked, since the catalysis derives it from its topology."""

    def _get_fitness(self, namespace: SimpleNamespace) -> Expression:
        """Return the catalysed growth `lambda_i + omega sum_j T_ij x_j` of a molecule.

        Args:
            namespace:
                The namespace of the symbols the fitness can use, with the namespace of the target.

        Returns:
            fitness:
                The fitness of the molecule.
        """
        molecule = namespace.target
        fitness: Expression = molecule.growth + namespace.omega * total(
            molecule.x,
            weights=np.asarray(self.topology, dtype=float),
        )
        return fitness

Equations

soup.x[i]: diff(x_soup(t), t) == (_reduced_0*omega_catalysis - _reduced_1 + growth_soup)*x_soup(t)