Skip to content

Co-evolution

Competitive co-evolution: evolve an attacker population against a defender population. Each generation scores attackers against the champion defender and defenders against the champion attacker, then breeds both. Deterministic and seedable.

coevolve

Co-evolve attacker and defender populations.

Parameters:

Name Type Description Default
attacker_class type[A]

attacker genome class to evolve.

required
defender_class type[D]

defender genome class to evolve.

required
attacker_fitness_vs Callable[[D], Fitness]

given a defender, builds the attacker Fitness.

required
defender_fitness_vs Callable[[A], Fitness]

given an attacker, builds the defender Fitness.

required
Source code in src/rotalabs_redqueen/core/coevolution.py
async def coevolve(
    attacker_class: type[A],
    defender_class: type[D],
    attacker_fitness_vs: Callable[[D], Fitness],
    defender_fitness_vs: Callable[[A], Fitness],
    generations: int,
    population_size: int = 20,
    elitism: int = 1,
    mutation_rate: float = 0.3,
    crossover_rate: float = 0.7,
    tournament_size: int = 3,
    seed: int | None = None,
) -> CoevolutionResult[A, D]:
    """Co-evolve attacker and defender populations.

    Args:
        attacker_class: attacker genome class to evolve.
        defender_class: defender genome class to evolve.
        attacker_fitness_vs: given a defender, builds the attacker Fitness.
        defender_fitness_vs: given an attacker, builds the defender Fitness.
    """
    rng = Rng(seed)
    attackers: list[Genome] = [attacker_class.random(rng) for _ in range(population_size)]
    defenders: list[Genome] = [defender_class.random(rng) for _ in range(population_size)]
    champ_attacker: Genome = attackers[0]
    champ_defender: Genome = defenders[0]
    best_a = 0.0
    best_d = 0.0
    history: list[dict] = []

    for gen in range(generations):
        a_results = await attacker_fitness_vs(champ_defender).evaluate_batch(attackers)
        d_results = await defender_fitness_vs(champ_attacker).evaluate_batch(defenders)
        a_inds = [Individual.from_result(g, r, gen) for g, r in zip(attackers, a_results)]
        d_inds = [Individual.from_result(g, r, gen) for g, r in zip(defenders, d_results)]

        best_attacker_ind = _best(a_inds)
        best_defender_ind = _best(d_inds)
        champ_attacker = best_attacker_ind.genome
        champ_defender = best_defender_ind.genome
        best_a = best_attacker_ind.fitness.value
        best_d = best_defender_ind.fitness.value
        history.append(
            {
                "generation": gen,
                "best_attacker_fitness": best_a,
                "best_defender_fitness": best_d,
            }
        )

        attackers = _breed(
            a_inds, rng, population_size, elitism, mutation_rate, crossover_rate, tournament_size
        )
        defenders = _breed(
            d_inds, rng, population_size, elitism, mutation_rate, crossover_rate, tournament_size
        )

    return CoevolutionResult(
        best_attacker=champ_attacker,
        best_defender=champ_defender,
        attacker_fitness=best_a,
        defender_fitness=best_d,
        generations=generations,
        history=history,
    )

CoevolutionResult

Bases: Generic[A, D]

Outcome of a co-evolution run.

Source code in src/rotalabs_redqueen/core/coevolution.py
@dataclass
class CoevolutionResult(Generic[A, D]):
    """Outcome of a co-evolution run."""

    best_attacker: A
    best_defender: D
    attacker_fitness: float
    defender_fitness: float
    generations: int
    history: list[dict] = field(default_factory=list)