Skip to main content

red_queen_core/
archive.rs

1//! Quality-Diversity archives.
2
3use crate::genome::{BehaviorDescriptor, Genome};
4use crate::population::Individual;
5use rand::Rng;
6use std::collections::HashMap;
7
8/// Trait for QD archives.
9pub trait Archive<G: Genome>: Send + Sync {
10    /// Try to add an individual to the archive.
11    /// Returns true if the individual was added.
12    fn add(&mut self, individual: Individual<G>) -> bool;
13
14    /// Get all archived individuals.
15    fn get_all(&self) -> Vec<&Individual<G>>;
16
17    /// Get coverage statistics.
18    fn coverage(&self) -> ArchiveCoverage;
19
20    /// Number of individuals in the archive.
21    fn len(&self) -> usize;
22
23    /// Check if archive is empty.
24    fn is_empty(&self) -> bool {
25        self.len() == 0
26    }
27}
28
29/// Coverage statistics for an archive.
30#[derive(Debug, Clone)]
31pub struct ArchiveCoverage {
32    /// Number of cells filled.
33    pub filled: usize,
34    /// Total number of cells.
35    pub total: usize,
36}
37
38impl ArchiveCoverage {
39    /// Coverage as a percentage.
40    pub fn percentage(&self) -> f64 {
41        self.filled as f64 / self.total as f64
42    }
43}
44
45/// Behavior dimension configuration.
46#[derive(Debug, Clone)]
47pub struct BehaviorDimension {
48    /// Name of the dimension.
49    pub name: String,
50    /// Minimum value.
51    pub min: f64,
52    /// Maximum value.
53    pub max: f64,
54    /// Number of bins.
55    pub bins: usize,
56}
57
58/// MAP-Elites archive.
59pub struct MapElitesArchive<G: Genome> {
60    /// The grid of cells.
61    grid: HashMap<Vec<usize>, Individual<G>>,
62    /// Dimension configurations.
63    dimensions: Vec<BehaviorDimension>,
64}
65
66impl<G: Genome> std::fmt::Debug for MapElitesArchive<G> {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("MapElitesArchive")
69            .field("cells_filled", &self.grid.len())
70            .field("dimensions", &self.dimensions)
71            .finish()
72    }
73}
74
75impl<G: Genome> MapElitesArchive<G> {
76    /// Create a new MAP-Elites archive.
77    pub fn new(dimensions: Vec<BehaviorDimension>) -> Self {
78        Self {
79            grid: HashMap::new(),
80            dimensions,
81        }
82    }
83
84    /// Convert behavior to cell indices.
85    fn behavior_to_cell(&self, behavior: &BehaviorDescriptor) -> Vec<usize> {
86        self.dimensions
87            .iter()
88            .zip(behavior.values.iter())
89            .map(|(dim, value)| {
90                let normalized = (value - dim.min) / (dim.max - dim.min);
91                let bin = (normalized * dim.bins as f64).floor() as usize;
92                bin.min(dim.bins - 1)
93            })
94            .collect()
95    }
96
97    /// Get total number of cells.
98    fn total_cells(&self) -> usize {
99        self.dimensions.iter().map(|d| d.bins).product()
100    }
101}
102
103impl<G: Genome> Archive<G> for MapElitesArchive<G> {
104    fn add(&mut self, individual: Individual<G>) -> bool {
105        let Some(behavior) = &individual.behavior else {
106            return false;
107        };
108
109        let cell = self.behavior_to_cell(behavior);
110
111        // Check if cell is empty or new individual is better
112        let dominated = self
113            .grid
114            .get(&cell)
115            .map(|existing| individual.fitness_value() > existing.fitness_value())
116            .unwrap_or(true);
117
118        if dominated {
119            self.grid.insert(cell, individual);
120            true
121        } else {
122            false
123        }
124    }
125
126    fn get_all(&self) -> Vec<&Individual<G>> {
127        self.grid.values().collect()
128    }
129
130    fn coverage(&self) -> ArchiveCoverage {
131        ArchiveCoverage {
132            filled: self.grid.len(),
133            total: self.total_cells(),
134        }
135    }
136
137    fn len(&self) -> usize {
138        self.grid.len()
139    }
140}
141
142impl<G: Genome> MapElitesArchive<G> {
143    /// Select a random individual from the archive.
144    pub fn select_random<R: Rng>(&self, rng: &mut R) -> Option<&Individual<G>> {
145        if self.grid.is_empty() {
146            return None;
147        }
148        let idx = rng.gen_range(0..self.grid.len());
149        self.grid.values().nth(idx)
150    }
151
152    /// Get the best individual across all cells.
153    pub fn best(&self) -> Option<&Individual<G>> {
154        self.grid
155            .values()
156            .max_by(|a, b| {
157                a.fitness_value()
158                    .partial_cmp(&b.fitness_value())
159                    .unwrap_or(std::cmp::Ordering::Equal)
160            })
161    }
162
163    /// Get top N individuals across all cells.
164    pub fn top_n(&self, n: usize) -> Vec<&Individual<G>> {
165        let mut individuals: Vec<_> = self.grid.values().collect();
166        individuals.sort_by(|a, b| {
167            b.fitness_value()
168                .partial_cmp(&a.fitness_value())
169                .unwrap_or(std::cmp::Ordering::Equal)
170        });
171        individuals.truncate(n);
172        individuals
173    }
174
175    /// Get the dimension configurations.
176    pub fn dimensions(&self) -> &[BehaviorDimension] {
177        &self.dimensions
178    }
179
180    /// Get cell contents for visualization/analysis.
181    pub fn get_cell(&self, indices: &[usize]) -> Option<&Individual<G>> {
182        self.grid.get(indices)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::fitness::FitnessValue;
190    use rand::Rng;
191
192    // Simple test genome
193    #[derive(Clone, Debug)]
194    struct TestGenome {
195        value: f64,
196    }
197
198    impl Genome for TestGenome {
199        type Phenotype = f64;
200
201        fn random<R: Rng>(rng: &mut R) -> Self {
202            Self {
203                value: rng.gen_range(0.0..1.0),
204            }
205        }
206
207        fn mutate<R: Rng>(&mut self, rng: &mut R, _rate: f64) {
208            self.value = rng.gen_range(0.0..1.0);
209        }
210
211        fn crossover<R: Rng>(&self, other: &Self, _rng: &mut R) -> Self {
212            Self {
213                value: (self.value + other.value) / 2.0,
214            }
215        }
216
217        fn to_phenotype(&self) -> f64 {
218            self.value
219        }
220    }
221
222    fn make_individual(value: f64, fitness: f64, behavior: Vec<f64>) -> Individual<TestGenome> {
223        Individual {
224            genome: TestGenome { value },
225            fitness: Some(FitnessValue::Single(fitness)),
226            behavior: Some(BehaviorDescriptor::new(behavior)),
227            birth_generation: 0,
228        }
229    }
230
231    #[test]
232    fn test_archive_coverage_percentage() {
233        let coverage = ArchiveCoverage {
234            filled: 25,
235            total: 100,
236        };
237        assert!((coverage.percentage() - 0.25).abs() < 1e-10);
238    }
239
240    #[test]
241    fn test_map_elites_empty() {
242        let dimensions = vec![
243            BehaviorDimension {
244                name: "dim1".to_string(),
245                min: 0.0,
246                max: 1.0,
247                bins: 10,
248            },
249            BehaviorDimension {
250                name: "dim2".to_string(),
251                min: 0.0,
252                max: 1.0,
253                bins: 10,
254            },
255        ];
256        let archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
257
258        assert_eq!(archive.get_all().len(), 0);
259        let coverage = archive.coverage();
260        assert_eq!(coverage.filled, 0);
261        assert_eq!(coverage.total, 100); // 10 * 10
262    }
263
264    #[test]
265    fn test_map_elites_add_individual() {
266        let dimensions = vec![BehaviorDimension {
267            name: "dim1".to_string(),
268            min: 0.0,
269            max: 1.0,
270            bins: 10,
271        }];
272        let mut archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
273
274        let ind = make_individual(0.5, 1.0, vec![0.5]);
275        assert!(archive.add(ind));
276
277        assert_eq!(archive.get_all().len(), 1);
278        assert_eq!(archive.coverage().filled, 1);
279    }
280
281    #[test]
282    fn test_map_elites_reject_no_behavior() {
283        let dimensions = vec![BehaviorDimension {
284            name: "dim1".to_string(),
285            min: 0.0,
286            max: 1.0,
287            bins: 10,
288        }];
289        let mut archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
290
291        let ind = Individual {
292            genome: TestGenome { value: 0.5 },
293            fitness: Some(FitnessValue::Single(1.0)),
294            behavior: None, // No behavior
295            birth_generation: 0,
296        };
297
298        assert!(!archive.add(ind));
299        assert_eq!(archive.get_all().len(), 0);
300    }
301
302    #[test]
303    fn test_map_elites_replace_with_better() {
304        let dimensions = vec![BehaviorDimension {
305            name: "dim1".to_string(),
306            min: 0.0,
307            max: 1.0,
308            bins: 10,
309        }];
310        let mut archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
311
312        // Add first individual
313        let ind1 = make_individual(0.5, 0.3, vec![0.55]);
314        assert!(archive.add(ind1));
315
316        // Add better individual to same cell
317        let ind2 = make_individual(0.6, 0.8, vec![0.55]);
318        assert!(archive.add(ind2));
319
320        // Should still have 1 individual (replaced)
321        assert_eq!(archive.get_all().len(), 1);
322        assert!((archive.get_all()[0].fitness_value() - 0.8).abs() < 1e-10);
323    }
324
325    #[test]
326    fn test_map_elites_reject_worse() {
327        let dimensions = vec![BehaviorDimension {
328            name: "dim1".to_string(),
329            min: 0.0,
330            max: 1.0,
331            bins: 10,
332        }];
333        let mut archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
334
335        // Add first individual with high fitness
336        let ind1 = make_individual(0.5, 0.9, vec![0.55]);
337        assert!(archive.add(ind1));
338
339        // Try to add worse individual to same cell
340        let ind2 = make_individual(0.6, 0.3, vec![0.55]);
341        assert!(!archive.add(ind2));
342
343        // Should still have original
344        assert!((archive.get_all()[0].fitness_value() - 0.9).abs() < 1e-10);
345    }
346
347    #[test]
348    fn test_map_elites_different_cells() {
349        let dimensions = vec![BehaviorDimension {
350            name: "dim1".to_string(),
351            min: 0.0,
352            max: 1.0,
353            bins: 10,
354        }];
355        let mut archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
356
357        // Add to different cells
358        let ind1 = make_individual(0.1, 0.5, vec![0.15]); // bin 1
359        let ind2 = make_individual(0.5, 0.5, vec![0.55]); // bin 5
360        let ind3 = make_individual(0.9, 0.5, vec![0.95]); // bin 9
361
362        assert!(archive.add(ind1));
363        assert!(archive.add(ind2));
364        assert!(archive.add(ind3));
365
366        assert_eq!(archive.get_all().len(), 3);
367        assert_eq!(archive.coverage().filled, 3);
368    }
369
370    #[test]
371    fn test_map_elites_2d_grid() {
372        let dimensions = vec![
373            BehaviorDimension {
374                name: "x".to_string(),
375                min: 0.0,
376                max: 1.0,
377                bins: 5,
378            },
379            BehaviorDimension {
380                name: "y".to_string(),
381                min: 0.0,
382                max: 1.0,
383                bins: 5,
384            },
385        ];
386        let mut archive: MapElitesArchive<TestGenome> = MapElitesArchive::new(dimensions);
387
388        // Add to corner cells
389        archive.add(make_individual(0.1, 0.5, vec![0.1, 0.1])); // (0,0)
390        archive.add(make_individual(0.2, 0.5, vec![0.9, 0.1])); // (4,0)
391        archive.add(make_individual(0.3, 0.5, vec![0.1, 0.9])); // (0,4)
392        archive.add(make_individual(0.4, 0.5, vec![0.9, 0.9])); // (4,4)
393
394        assert_eq!(archive.coverage().total, 25); // 5 * 5
395        assert_eq!(archive.coverage().filled, 4);
396    }
397}