Moran process
The Moran process, a population of constant size where fitter traits fix by selection and drift.
- Name:
moran_process - Field:
biology/evolution - Formalism: agent-based model
- References: Moran 1958; Nowak 2006; Jensen 2023
Factory
@register_model(
field='biology/evolution',
tags=('population genetics', 'agents', 'fixation', 'genetic drift', 'mutation'),
references=('Moran 1958', 'Nowak 2006', 'Jensen 2023'),
defaults={'until': 100},
)
def moran_process(
size: int = 50,
*,
mutants: int = 1,
selection: float = 0.0,
mutation: float = 0.0,
) -> System:
"""Return the Moran process, a population of constant size where fitter traits fix by selection and drift.
The population starts with `mutants` individuals of trait 1 among individuals of trait 0. Without mutation one trait
eventually fixes, the mutant with probability `(1 - 1 / r^m) / (1 - 1 / r^N)` for the relative fitness
`r = 1 + selection`. With mutation the traits diffuse along the trait axis, in clusters rather than as a spreading
Gaussian, which distinguishes evolutionary diffusion from a random walk.
Args:
size:
The number of individuals.
mutants:
The initial number of individuals of trait 1.
selection:
The selective advantage of one unit of the trait.
mutation:
The probability that an offspring's trait mutates by one unit.
Returns:
system:
The model, with the group `population`.
Examples:
>>> from skcomplex.models.biology.evolution import moran_process
>>> float(moran_process(10, mutants=3).elements[0].values['trait'].sum())
3.0
"""
traits = np.zeros(size)
traits[:mutants] = 1.0
return System(
elements=[
GroupElement(
'population',
element=IndividualElement,
size=size,
values={'trait': traits, 'fitness': (1 + selection) ** traits},
variable=True,
),
],
interactions=[MoranStepInteraction('moran', target='population', advantage=selection, mutation=mutation)],
)
Components
class IndividualElement(Element):
"""Individual of a finite population, with an integer trait or genome.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
trait:
The trait, a type index or the bits of a genome.
fitness:
The fitness of the trait.
Examples:
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.biology.evolution import IndividualElement
>>> GroupElement('population', element=IndividualElement, size=10, values={'trait': 0.0}).size
10
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'trait'}),
*,
trait: Annotated[float, StateSpec(domain=ZZ)] = 0.0,
fitness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
super().__init__(label, visual=visual)
self.trait = trait
self.fitness = fitness
@classmethod
def _get_view(cls) -> type[IndividualView]:
"""Return the view of an individual, `IndividualView`."""
return IndividualView
class MoranStepInteraction(EvolutionInteraction):
"""One generation of the Moran process: as many birth and death events as there are individuals.
At every event an individual reproduces with probability proportional to its fitness `(1 + advantage)^trait`,
its offspring mutates to the trait one above or one below with probability `mutation / 2` each, and it replaces an
individual chosen uniformly at random, so the size of the population stays constant. It is the Moran replacement
of `EvolutionInteraction` with proportional selection and steps of the trait.
Args:
label:
The label of the interaction.
target:
The path of the group of individuals.
advantage:
The selective advantage of one unit of the trait.
mutation:
The probability that an offspring mutates.
phase:
The phase of the step the generation runs in, or `None` for the default phase.
Examples:
>>> from skcomplex.models.biology.evolution import MoranStepInteraction
>>> MoranStepInteraction('moran', target='population', advantage=0.1).check().replacement
'moran'
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
advantage: Annotated[float, ParameterSpec()] = 0.0,
mutation: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0,
phase: str | None = None,
) -> None:
super().__init__(
label,
target=target,
strategy='trait',
selection='proportional',
replacement='moran',
mutation=mutation,
alphabet=None,
change='step',
phase=phase,
)
self.advantage = advantage
def _evaluate(self, strategies: Array, model: ModelViewProtocol) -> Array:
"""Return the fitness `(1 + advantage)^trait` of every trait.
Args:
strategies:
The traits.
model:
The model view of the interaction.
Returns:
fitness:
The fitness of every trait.
"""
return (1 + model.parameter('advantage')) ** np.ravel(strategies)