Skip to main content

wafrift_evolution/evolution/
engine.rs

1use crate::evolution::fitness::{evolutionary_fitness, update_gene_stats};
2use crate::evolution::{
3    Chromosome, GenePool,
4    population::{baseline_chromosome, random_chromosome},
5};
6use crate::lineage::{BypassCorpus, BypassEntry};
7use crate::search::SearchAlgorithm;
8use crate::types::{
9    Budget, EvolutionError, OracleVerdict, SearchStats, TargetHealthMonitor, load_checkpoint,
10    save_checkpoint,
11};
12use lru::LruCache;
13use rand::{SeedableRng, rngs::StdRng};
14use std::collections::HashMap;
15use std::num::NonZeroUsize;
16use std::path::{Path, PathBuf};
17use std::time::Instant;
18
19/// The evolutionary engine that maintains a population and evolves it.
20#[derive(Debug)]
21pub struct EvolutionEngine {
22    /// Search algorithm implementation.
23    algorithm: Box<dyn SearchAlgorithm>,
24    /// Gene pool for creating/mutating chromosomes.
25    pub gene_pool: GenePool,
26    /// Seeded random number generator.
27    pub rng: StdRng,
28    /// Payload→verdict LRU cache.
29    pub cache: LruCache<String, OracleVerdict>,
30    /// Hard budget limits.
31    pub budget: Budget,
32    /// Candidates currently being evaluated: ID → (Chromosome, sent_at).
33    pub in_flight: HashMap<u64, (Chromosome, Instant)>,
34    /// Search statistics.
35    pub stats: SearchStats,
36    /// Target health monitor.
37    pub target_health: TargetHealthMonitor,
38    /// Optional path for automatic checkpointing.
39    pub checkpoint_path: Option<PathBuf>,
40    /// Total oracle requests issued.
41    pub request_count: usize,
42    /// Per-gene success tracking: `(gene_name, gene_value, successes, attempts)`.
43    pub gene_stats: Vec<(String, String, u32, u32)>,
44    /// Fitness history: average fitness per generation (sliding window).
45    pub fitness_history: Vec<f64>,
46    /// Number of consecutive generations with no improvement.
47    pub stagnation_counter: u32,
48    /// Saved bypass corpus.
49    pub corpus: BypassCorpus,
50    /// Evaluations this generation.
51    generation_evals: usize,
52    /// Next candidate ID.
53    next_id: u64,
54    /// Pending single candidate for legacy sequential API.
55    pending_single: Option<(usize, Chromosome)>,
56}
57
58impl Clone for EvolutionEngine {
59    fn clone(&self) -> Self {
60        // Clone via checkpoint/restore for the algorithm, and re-seed RNG from state
61        let alg_bytes = self.algorithm.checkpoint().unwrap_or_default();
62        let mut restored = Self::with_algorithm(
63            self.algorithm.name(),
64            self.gene_pool.clone(),
65            self.rng.clone(),
66            self.budget,
67        )
68        .unwrap_or_else(|_| Self::new(20));
69        let _ = restored.algorithm.restore(&alg_bytes);
70        restored.cache = LruCache::new(self.cache.cap());
71        // Copy simple fields
72        restored.gene_stats = self.gene_stats.clone();
73        restored.fitness_history = self.fitness_history.clone();
74        restored.stagnation_counter = self.stagnation_counter;
75        restored.corpus = self.corpus.clone();
76        restored.request_count = self.request_count;
77        restored.stats = self.stats;
78        restored.next_id = self.next_id;
79        restored.pending_single = None;
80        restored
81    }
82}
83
84impl EvolutionEngine {
85    /// Create a new engine with the given algorithm and population size.
86    #[must_use]
87    pub fn new(population_size: usize) -> Self {
88        Self::new_seeded(population_size, 0)
89    }
90
91    /// Create a new engine with a seeded RNG.
92    #[must_use]
93    pub fn new_seeded(population_size: usize, seed: u64) -> Self {
94        let gene_pool = GenePool::default_wafrift();
95        let mut rng = StdRng::seed_from_u64(seed);
96        let mut population: Vec<Chromosome> = (0..population_size)
97            .map(|_| random_chromosome(&gene_pool, &mut rng))
98            .collect();
99        if population_size > 0 {
100            population[0] = baseline_chromosome(&gene_pool);
101        }
102
103        let mut engine = Self::with_algorithm("hill_climbing", gene_pool, rng, Budget::default())
104            .expect("hill_climbing is built-in");
105        engine
106            .algorithm
107            .initialize(population, &engine.gene_pool, &mut engine.rng.clone());
108        // Re-initialize with the same RNG to avoid double-use
109        let mut population2: Vec<Chromosome> = (0..population_size)
110            .map(|_| random_chromosome(&engine.gene_pool, &mut engine.rng))
111            .collect();
112        if population_size > 0 {
113            population2[0] = baseline_chromosome(&engine.gene_pool);
114        }
115        engine
116            .algorithm
117            .initialize(population2, &engine.gene_pool, &mut engine.rng);
118        engine
119    }
120
121    /// Create an engine with a specific algorithm by name.
122    pub fn with_algorithm(
123        algorithm_name: &str,
124        gene_pool: GenePool,
125        rng: StdRng,
126        budget: Budget,
127    ) -> Result<Self, EvolutionError> {
128        let algorithm: Box<dyn SearchAlgorithm> = match algorithm_name {
129            "hill_climbing" => Box::new(crate::search::HillClimbing::new()),
130            "simulated_annealing" => Box::new(crate::search::SimulatedAnnealing::new()),
131            "tabu_search" => Box::new(crate::search::TabuSearch::new(20)),
132            "novelty_search" => Box::new(crate::search::NoveltySearch::new(15, 0.3)),
133            "map_elites" => Box::new(crate::search::MapElites::new()),
134            _ => {
135                return Err(EvolutionError::AlgorithmError(format!(
136                    "unknown algorithm: {algorithm_name}"
137                )));
138            }
139        };
140
141        Ok(Self {
142            algorithm,
143            gene_pool,
144            rng,
145            cache: LruCache::new(NonZeroUsize::new(10_000).unwrap()),
146            budget,
147            in_flight: HashMap::new(),
148            stats: SearchStats::new(),
149            target_health: TargetHealthMonitor::new(),
150            checkpoint_path: None,
151            request_count: 0,
152            gene_stats: Vec::new(),
153            fitness_history: Vec::new(),
154            stagnation_counter: 0,
155            corpus: BypassCorpus::new(),
156            generation_evals: 0,
157            next_id: 0,
158            pending_single: None,
159        })
160    }
161
162    fn cache_key(chromosome: &Chromosome) -> String {
163        let mut parts: Vec<_> = chromosome
164            .genes
165            .iter()
166            .map(|(n, v)| format!("{n}={v}"))
167            .collect();
168        parts.sort();
169        parts.join(";")
170    }
171
172    fn next_eval_id(&mut self) -> u64 {
173        self.next_id += 1;
174        self.next_id
175    }
176
177    /// Get the next candidate to try (legacy sequential API).
178    ///
179    /// Returns a synthetic index and a reference to the stored candidate.
180    #[must_use]
181    pub fn next_candidate(&mut self) -> Option<(usize, &Chromosome)> {
182        if self.should_terminate() {
183            return None;
184        }
185        if self.pending_single.is_none() {
186            let batch = self.batch_candidates(1);
187            if batch.is_empty() {
188                return None;
189            }
190            self.pending_single = Some(batch.into_iter().next().unwrap());
191        }
192        self.pending_single
193            .as_ref()
194            .map(|(idx, chrom)| (*idx, chrom))
195    }
196
197    /// Request a batch of up to `n` candidates for parallel evaluation.
198    ///
199    /// Checks cache, budget, and target health before returning candidates.
200    pub fn batch_candidates(&mut self, n: usize) -> Vec<(usize, Chromosome)> {
201        if self.should_terminate() || n == 0 {
202            return Vec::new();
203        }
204
205        let mut result = Vec::with_capacity(n);
206        let mut cached_results = Vec::new();
207        let requested = self.algorithm.request_evaluations(n, &mut self.rng);
208
209        for candidate in requested {
210            let key = Self::cache_key(&candidate.chromosome);
211            if let Some(verdict) = self.cache.get(&key).copied() {
212                cached_results.push((candidate.id, verdict));
213            } else {
214                let eval_id = self.next_eval_id();
215                self.in_flight
216                    .insert(eval_id, (candidate.chromosome.clone(), Instant::now()));
217                result.push((eval_id as usize, candidate.chromosome));
218            }
219        }
220
221        if !cached_results.is_empty() {
222            self.algorithm.submit_evaluations(cached_results);
223        }
224
225        self.request_count += result.len();
226        result
227    }
228
229    /// Submit a batch of evaluation results.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if an evaluation ID is not in the in-flight set.
234    pub fn submit_batch(
235        &mut self,
236        results: Vec<(usize, OracleVerdict)>,
237    ) -> Result<(), EvolutionError> {
238        let mut to_submit: Vec<(u64, OracleVerdict)> = Vec::with_capacity(results.len());
239        for (id_usize, verdict) in results {
240            let id = id_usize as u64;
241            let (mut chromosome, _sent_at) = self
242                .in_flight
243                .remove(&id)
244                .ok_or(EvolutionError::InvalidChromosomeIndex(id_usize))?;
245
246            chromosome.record_verdict(&verdict);
247            let key = Self::cache_key(&chromosome);
248            self.cache.put(key, verdict);
249
250            update_gene_stats(&mut self.gene_stats, &chromosome.genes, verdict.passed);
251            let adjusted = evolutionary_fitness(&chromosome, &self.gene_stats);
252            chromosome.fitness = adjusted;
253
254            // Save high-fitness bypasses to corpus
255            if chromosome.fitness >= 0.85
256                && !self
257                    .corpus
258                    .entries
259                    .iter()
260                    .any(|e| e.payload_hash == format!("{:016x}", chromosome.hash()))
261            {
262                self.corpus
263                    .add(BypassEntry::from_chromosome(&chromosome, None));
264            }
265
266            to_submit.push((id, verdict));
267            self.generation_evals += 1;
268            self.stats.evaluations += 1;
269
270            if verdict.passed {
271                self.target_health.record_success();
272            } else if verdict.status_delta >= 500 {
273                self.target_health.record_error();
274            }
275        }
276
277        self.algorithm.submit_evaluations(to_submit);
278        Ok(())
279    }
280
281    /// Record legacy boolean feedback for a candidate.
282    pub fn record_feedback(
283        &mut self,
284        chromosome_index: usize,
285        passed: bool,
286    ) -> Result<(), EvolutionError> {
287        // Clear pending_single if it matches the index
288        if let Some((idx, _)) = self.pending_single
289            && idx == chromosome_index
290        {
291            self.pending_single = None;
292        }
293        self.record_verdict(chromosome_index, &OracleVerdict::from_bool(passed))
294    }
295
296    /// Record rich oracle verdict feedback.
297    pub fn record_verdict(
298        &mut self,
299        chromosome_index: usize,
300        verdict: &OracleVerdict,
301    ) -> Result<(), EvolutionError> {
302        self.submit_batch(vec![(chromosome_index, *verdict)])
303    }
304
305    /// Record target-error feedback.
306    pub fn record_target_error(&mut self, error: String) -> Result<(), EvolutionError> {
307        self.target_health.record_error();
308        if !self.target_health.is_healthy() {
309            return Err(EvolutionError::TargetHealthCritical(error));
310        }
311        Ok(())
312    }
313
314    /// Evolve the population to the next generation.
315    pub fn evolve(&mut self) {
316        if self.algorithm.best().is_none() {
317            return;
318        }
319
320        // Update fitness history with sliding window
321        if let Some(best) = self.algorithm.best() {
322            self.fitness_history.push(best.fitness);
323        }
324        if self.fitness_history.len() > 1000 {
325            self.fitness_history.remove(0);
326        }
327
328        // Detect stagnation
329        let window = 10_usize;
330        if self.fitness_history.len() >= window {
331            let recent = &self.fitness_history[self.fitness_history.len() - window..];
332            let improved = recent.windows(2).any(|w| w[1] > w[0] + 0.001);
333            if !improved {
334                self.stagnation_counter += 1;
335            } else {
336                self.stagnation_counter = 0;
337            }
338        }
339
340        self.stats.generation += 1;
341        self.generation_evals = 0;
342
343        if let Some(ref path) = self.checkpoint_path {
344            let _ = self.save_checkpoint(path);
345        }
346    }
347
348    /// Check if evolution should terminate.
349    #[must_use]
350    pub fn should_terminate(&self) -> bool {
351        if !self.target_health.is_healthy() {
352            return true;
353        }
354        self.algorithm.should_terminate(&self.stats, &self.budget)
355            || self.request_count >= self.budget.max_requests
356            || self.stats.stagnation_counter >= self.budget.stagnation_limit
357    }
358
359    /// Get the best-performing chromosome.
360    #[must_use]
361    pub fn best(&self) -> Option<&Chromosome> {
362        self.algorithm.best()
363    }
364
365    /// Save engine state to disk.
366    pub fn save_checkpoint(&self, path: &Path) -> Result<(), EvolutionError> {
367        let state = EngineState {
368            algorithm_name: self.algorithm.name().to_string(),
369            algorithm_state: self.algorithm.checkpoint()?,
370            gene_pool: self.gene_pool.clone(),
371            rng_seed: 0, // We can't easily extract seed; we rely on algorithm state
372            budget: self.budget,
373            gene_stats: self.gene_stats.clone(),
374            fitness_history: self.fitness_history.clone(),
375            stagnation_counter: self.stagnation_counter,
376            request_count: self.request_count,
377            stats: self.stats,
378            schema_version: 1,
379        };
380        save_checkpoint(path, &state)
381    }
382
383    /// Load engine state from disk.
384    pub fn load_checkpoint(&mut self, path: &Path) -> Result<(), EvolutionError> {
385        let mut state: EngineState = load_checkpoint(path)?;
386        state.stats.fixup_start_time();
387        self.algorithm.restore(&state.algorithm_state)?;
388        self.gene_pool = state.gene_pool;
389        self.budget = state.budget;
390        self.gene_stats = state.gene_stats;
391        self.fitness_history = state.fitness_history;
392        self.stagnation_counter = state.stagnation_counter;
393        self.request_count = state.request_count;
394        self.stats = state.stats;
395        Ok(())
396    }
397
398    /// Get per-gene success rates.
399    #[must_use]
400    pub fn gene_success_rates(&self) -> Vec<(&str, &str, f64)> {
401        crate::evolution::fitness::gene_success_rates(&self.gene_stats)
402    }
403
404    /// Get a human-readable summary.
405    #[must_use]
406    pub fn learned_summary(&self) -> String {
407        crate::evolution::fitness::learned_summary(
408            self.stats.generation,
409            self.algorithm.best(),
410            &self.gene_stats,
411            self.request_count,
412        )
413    }
414
415    /// Compute diversity score using algorithm population if available.
416    #[must_use]
417    pub fn diversity_score(&self) -> f64 {
418        // Fallback: use gene stats diversity heuristic
419        0.5
420    }
421}
422
423/// Serializable engine state.
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct EngineState {
426    pub algorithm_name: String,
427    pub algorithm_state: Vec<u8>,
428    pub gene_pool: GenePool,
429    pub rng_seed: u64,
430    pub budget: Budget,
431    pub gene_stats: Vec<(String, String, u32, u32)>,
432    pub fitness_history: Vec<f64>,
433    pub stagnation_counter: u32,
434    pub request_count: usize,
435    pub stats: SearchStats,
436    pub schema_version: u32,
437}
438
439use serde::{Deserialize, Serialize};