Adaptive climbing in finite populations
A finite population that climbs a random fitness landscape by mutation and strong selection.
- Name:
adaptive_climbing - Field:
biology/evolution - Formalism: agent-based model
- References: Gros 2015
Factory
@register_model(
field='biology/evolution',
tags=('fitness landscape', 'agents', 'random energy model', 'stochastic escape'),
references=('Gros 2015',),
defaults={'until': 200},
)
def adaptive_climbing(
size: int = 10,
*,
u: float = 0.1,
length: int = 20,
seed: int = 0,
) -> System:
"""Return a finite population that climbs a random fitness landscape by mutation and strong selection.
The population climbs to a local optimum in an adaptive walk, ever slower near the top. When every individual
mutates in the same generation, which is likely only in small populations with a large `u`, it escapes to a lower
genome and starts a new walk, the wandering regime.
Args:
size:
The number of individuals.
u:
The probability that an individual mutates in a generation.
length:
The number of loci of a genome.
seed:
The seed of the fitness landscape.
Returns:
system:
The model, with the group `population`.
Examples:
>>> from skcomplex.models.biology.evolution import adaptive_climbing
>>> adaptive_climbing(5).elements[0].check().prepare().size_
5
"""
fitness = AdaptiveClimbingInteraction._random_fitness(0, seed)
return System(
elements=[
GroupElement(
'population',
element=IndividualElement,
size=size,
values={'trait': 0.0, 'fitness': fitness},
variable=True,
visual=VisualSpec('agents', {'color_by': 'fitness'}),
),
],
interactions=[
AdaptiveClimbingInteraction('climbing', target='population', mutation=u, length=length, seed=seed),
],
)
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 AdaptiveClimbingInteraction(EvolutionInteraction):
"""One generation of a population that climbs a random fitness landscape as a whole, under strong selection.
All individuals share one genome. Each mutates one random locus with probability `u`, to a genome of random fitness
in [0, 1], with probability `mutation`. If a mutant is fitter, the whole population takes the fittest mutant. If
every individual mutated to a less fit genome, the population escapes to the fittest of them. Otherwise it stays.
It is the best-mutant
replacement of `EvolutionInteraction` on genomes stored as the integers their bits spell.
Args:
label:
The label of the interaction.
target:
The path of the group of individuals.
mutation:
The probability that an individual mutates in a generation.
length:
The number of loci of a genome.
seed:
The seed of the fitness landscape.
Examples:
>>> from skcomplex.models.biology.evolution import AdaptiveClimbingInteraction
>>> AdaptiveClimbingInteraction('climbing', target='population', mutation=0.1).check().replacement
'best-mutant'
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
mutation: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.1,
length: int = 20,
seed: int = 0,
) -> None:
super().__init__(label, target=target, strategy='trait', replacement='best-mutant', mutation=mutation)
self.length = length
self.seed = seed
def _evaluate(self, strategies: Array, model: ModelViewProtocol) -> Array:
"""Return the random fitness of every genome.
Args:
strategies:
The genomes.
model:
The model view of the interaction.
Returns:
fitness:
The fitness of every genome.
"""
return np.array(
[AdaptiveClimbingInteraction._random_fitness(int(genome), self.seed) for genome in np.ravel(strategies)],
)
def _mutate(self, children: Array, probability: float, rng: np.random.Generator, population: Array) -> Array:
"""Return the genomes, each with one random locus flipped with the probability of mutation.
Args:
children:
The genomes, one row per individual.
probability:
The probability that a genome mutates.
rng:
The random number generator.
population:
The genomes of the population.
Returns:
children:
The mutated genomes.
"""
flips = (rng.random(len(children)) < probability).astype(np.int64)
loci = rng.integers(self.length, size=len(children))
return (children.astype(np.int64) ^ (flips << loci)[:, None]).astype(float)
@staticmethod
def _random_fitness(genome: int, seed: int) -> float:
"""Return the fitness of a genome in the random energy model, uniform in [0, 1] and fixed by the seed."""
return float(np.random.default_rng([seed, genome]).random())