Skip to main content

rlevo_evolution/coevolution/
mod.rs

1//! Co-evolutionary algorithms.
2//!
3//! Two populations evolve **simultaneously**, and each individual's fitness
4//! depends on the *other* population. Two regimes ship in v1:
5//!
6//! - **Competitive** ([`CompetitiveCoEA`]) — populations are adversaries
7//!   (predator vs. prey, Hillis 1990). Each is scored by how well it does
8//!   against the other; the dynamic is an arms race.
9//! - **Cooperative** ([`CooperativeCoEA`], CCGA — Potter & De Jong 1994) — a
10//!   high-dimensional problem is decomposed across populations whose
11//!   individuals combine (via *representatives*) into a full candidate.
12//!
13//! # Why not [`Strategy`](crate::strategy::Strategy)?
14//!
15//! [`Strategy::tell`](crate::strategy::Strategy::tell) accepts a single
16//! fitness vector, but co-evolutionary fitness is inherently paired — each
17//! population receives a vector computed relative to the other. Rather than
18//! distort that contract, co-evolution gets its own [`CoEvolutionaryAlgorithm`]
19//! trait and a dedicated [`CoEvolutionaryHarness`] that adapts to the existing
20//! `rlevo-core::evaluation::BenchEnv` surface, exactly as
21//! [`EvolutionaryHarness`](crate::strategy::EvolutionaryHarness) does. Both
22//! co-evolutionary algorithms are built from ordinary inner
23//! [`Strategy`](crate::strategy::Strategy) instances, so every phase-1/2/3a/3b
24//! algorithm composes in unchanged.
25//!
26//! # Pathology mitigation
27//!
28//! Competitive co-evolution is prone to **cycling**; [`HallOfFameFitness`]
29//! wraps any [`CoupledFitness`] to anchor scores against an archive of past
30//! champions (Rosin & Belew 1997). It composes at construction — algorithms
31//! are hall-of-fame-agnostic.
32//!
33//! # Coupling shape
34//!
35//! [`CoupledFitness`] takes a slice of populations and is N-population-ready;
36//! v1 algorithms always pass exactly two. The harness exposes
37//! `min(best_a, best_b)` (canonical maximise) as the benchmark reward (the
38//! weaker population — lower canonical fitness — is the binding constraint).
39//!
40//! # References
41//!
42//! - Hillis (1990), *Co-evolving parasites improve simulated evolution as an
43//!   optimization procedure.*
44//! - Potter & De Jong (1994), *A cooperative coevolutionary approach to
45//!   function optimization* (CCGA).
46//! - Rosin & Belew (1997), *New methods for competitive coevolution* (hall of
47//!   fame).
48//! - Ficici (2004), *Solution concepts in coevolutionary algorithms* (cycling
49//!   / intransitive dominance).
50
51pub mod competitive;
52pub mod cooperative;
53pub mod fitness;
54pub mod harness;
55pub mod hof;
56
57pub use competitive::{CompetitiveCoEA, CompetitiveCoEAParams};
58pub use cooperative::{
59    CooperativeCoEA, CooperativeCoEAParams, CooperativeState, RepresentativePolicy,
60};
61pub use fitness::CoupledFitness;
62pub use harness::{CoEAMetrics, CoEvolutionaryHarness};
63pub use hof::{HallOfFame, HallOfFameFitness};
64
65use std::fmt::Debug;
66
67use burn::tensor::backend::Backend;
68use rand::Rng;
69
70/// A co-evolutionary algorithm driving two populations under simultaneous
71/// updates.
72///
73/// The analogue of [`Strategy`](crate::strategy::Strategy) for the coupled
74/// case: [`init`](Self::init) builds the joint state and [`step`](Self::step)
75/// advances one simultaneous-update generation (both populations `ask` → one
76/// [`CoupledFitness`] evaluation → both `tell`). Implementors hold their inner
77/// strategies and a [`CoupledFitness`] by value and carry no PRNG state — all
78/// randomness flows through the explicit `rng` argument, per the crate's
79/// host-RNG convention.
80///
81/// v1 ships [`CompetitiveCoEA`] and [`CooperativeCoEA`]; Stackelberg
82/// (alternating) turn order is deferred.
83pub trait CoEvolutionaryAlgorithm<B: Backend>: Send + Sync {
84    /// Static parameters for a run (inner strategy params plus any coupling
85    /// configuration).
86    type Params: Clone + Debug + Send + Sync;
87
88    /// Generation-to-generation joint state.
89    type State: Clone + Debug + Send;
90
91    /// Build the initial joint state (initializes both inner strategies).
92    fn init(
93        &self,
94        params: &Self::Params,
95        rng: &mut dyn Rng,
96        device: &<B as burn::tensor::backend::BackendTypes>::Device,
97    ) -> Self::State;
98
99    /// Advance one simultaneous-update generation, returning the next state
100    /// and this generation's [`CoEAMetrics`].
101    fn step(
102        &self,
103        params: &Self::Params,
104        state: Self::State,
105        rng: &mut dyn Rng,
106        device: &<B as burn::tensor::backend::BackendTypes>::Device,
107    ) -> (Self::State, CoEAMetrics);
108
109    /// Snapshot the current metrics without advancing a generation.
110    fn metrics(&self, state: &Self::State) -> CoEAMetrics;
111}
112
113/// Joined state carrying both sub-strategy states plus per-population
114/// best/mean trackers.
115///
116/// Generic over the two inner strategy *state* types (`StA`, `StB`) rather
117/// than the strategies themselves, so it derives [`Clone`]/[`Debug`] without
118/// spurious bounds on the strategy types. Used by [`CompetitiveCoEA`];
119/// [`CooperativeCoEA`] wraps it in [`CooperativeState`] to add
120/// representative archives.
121#[derive(Debug, Clone)]
122pub struct CoEAState<StA, StB> {
123    /// Inner state of population A's strategy.
124    pub state_a: StA,
125    /// Inner state of population B's strategy.
126    pub state_b: StB,
127    /// Number of completed simultaneous-update generations.
128    pub generation: u64,
129    /// Best (highest, canonical maximise) fitness population A has seen across
130    /// all generations.
131    pub best_a: f32,
132    /// Best (highest, canonical maximise) fitness population B has seen across
133    /// all generations.
134    pub best_b: f32,
135    /// Mean fitness of population A in the most recent generation.
136    pub mean_a: f32,
137    /// Mean fitness of population B in the most recent generation.
138    pub mean_b: f32,
139}
140
141impl<StA, StB> CoEAState<StA, StB> {
142    /// Build the initial joint state with empty best/mean trackers.
143    pub(crate) fn new(state_a: StA, state_b: StB) -> Self {
144        Self {
145            state_a,
146            state_b,
147            generation: 0,
148            best_a: f32::NEG_INFINITY,
149            best_b: f32::NEG_INFINITY,
150            mean_a: f32::NAN,
151            mean_b: f32::NAN,
152        }
153    }
154}