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)