Skip to main content

henad_core/explore/search/
random.rs

1//! Random search. Every candidate is drawn uniformly from the space, and the best is kept.
2
3use crate::explore::design_rng::DesignRng;
4use crate::explore::search::evaluation_log::EvaluationLog;
5use crate::explore::search::genome::SearchSpace;
6use crate::explore::search::{
7    Candidate, CandidateOrigin, CandidateTracker, Evaluation, Objective, Proposal, RankingEntry, SearchReport, Searcher,
8};
9
10/// A search that draws every candidate uniformly.
11#[derive(Debug, Clone)]
12pub struct RandomSearch {
13    space: SearchSpace,
14    rng: DesignRng,
15    tracker: CandidateTracker,
16    log: EvaluationLog,
17}
18
19impl RandomSearch {
20    /// Returns a search of `max_evaluations` evaluations over `space`, drawing from `seed`.
21    pub fn new(space: SearchSpace, objective: Objective, max_evaluations: u64, seed: u64) -> Self {
22        Self {
23            space,
24            rng: DesignRng::new(seed),
25            tracker: CandidateTracker::new(max_evaluations),
26            log: EvaluationLog::new(objective),
27        }
28    }
29}
30
31impl Searcher for RandomSearch {
32    fn ask(&mut self, max: usize) -> Vec<Candidate> {
33        let proposals = (0..self.tracker.capacity(max))
34            .map(|_| Proposal::first_evaluation(self.space.random_genome(&mut self.rng), CandidateOrigin::Random))
35            .collect();
36        self.tracker.issue(proposals)
37    }
38
39    fn tell(&mut self, evaluations: &[Evaluation]) {
40        for evaluation in evaluations {
41            if let Some((candidate, batch)) = self.tracker.settle(evaluation.candidate_id) {
42                self.log.record(&candidate, batch, evaluation);
43            }
44        }
45    }
46
47    fn is_done(&self) -> bool {
48        self.tracker.is_done()
49    }
50
51    fn report(&self) -> SearchReport {
52        SearchReport {
53            ranking: self.log.ranking(),
54            ..SearchReport::default()
55        }
56    }
57
58    fn best(&self) -> Option<RankingEntry> {
59        self.log.best()
60    }
61
62    fn ranking_entry(&self, candidate_id: u64) -> Option<RankingEntry> {
63        self.log.ranking_entry(candidate_id)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::RandomSearch;
70    use crate::explore::search::tests::support::{drive, unit_space};
71    use crate::explore::search::{Aggregate, Candidate, Goal, Objective, Searcher as _};
72
73    fn search(seed: u64) -> RandomSearch {
74        let objective = Objective {
75            column: "Infected:max".to_owned(),
76            goal: Goal::Maximize,
77            aggregate: Aggregate::Mean,
78        };
79        RandomSearch::new(unit_space(3), objective, 50, seed)
80    }
81
82    #[test]
83    fn random_search_is_reproducible_from_its_seed() {
84        let sum = |candidate: &Candidate, _| vec![Some(candidate.genome.genes().iter().sum())];
85        let (mut first, mut again, mut other) = (search(7), search(7), search(8));
86        let first_asked = drive(&mut first, 6, 2, sum);
87        assert_eq!(first_asked.len(), 50);
88        assert_eq!(drive(&mut again, 6, 2, sum), first_asked);
89        assert_eq!(first.report(), again.report());
90        assert_ne!(drive(&mut other, 6, 2, sum), first_asked);
91
92        let report = first.report();
93        let best = report.best().expect("the search scored candidates");
94        let best_sum: f64 = first_asked[best.candidate_id as usize].genome.genes().iter().sum();
95        let top = first_asked
96            .iter()
97            .map(|candidate| candidate.genome.genes().iter().sum::<f64>())
98            .fold(f64::NEG_INFINITY, f64::max);
99        assert_eq!(best_sum, top, "the best candidate has the largest sum");
100        assert_eq!((best.replicate_count, best.evaluations), (2, 1));
101    }
102}