Skip to main content

zeph_experiments/
generator.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `VariationGenerator` trait for parameter variation strategies.
5
6use std::collections::HashSet;
7
8use super::snapshot::ConfigSnapshot;
9use super::types::Variation;
10
11/// A strategy for generating parameter variations one at a time.
12///
13/// Each call to [`VariationGenerator::next`] must produce a variation that
14/// changes exactly one parameter from the baseline. The caller is responsible
15/// for tracking visited variations and passing them to `next`.
16///
17/// Implementations hold mutable state (position cursor, RNG seed) and must be
18/// both `Send` and `Sync` so that [`ExperimentEngine`] can be used with
19/// `tokio::spawn`. The engine loop accesses the generator exclusively via
20/// `&mut self`, so no concurrent access occurs in practice.
21pub trait VariationGenerator: Send + Sync {
22    /// Produce the next untested variation, or `None` if the space is exhausted.
23    ///
24    /// `baseline` is the current best-known configuration snapshot.
25    /// `visited` is the set of all variations already tested in this run.
26    fn next(
27        &mut self,
28        baseline: &ConfigSnapshot,
29        visited: &HashSet<Variation>,
30    ) -> Option<Variation>;
31
32    /// Strategy name for logging and metrics.
33    fn name(&self) -> &'static str;
34}
35
36#[cfg(test)]
37mod tests {
38    use super::super::types::{ParameterKind, VariationValue};
39    use super::*;
40    use ordered_float::OrderedFloat;
41
42    struct AlwaysOne;
43
44    impl VariationGenerator for AlwaysOne {
45        fn next(
46            &mut self,
47            _baseline: &ConfigSnapshot,
48            visited: &HashSet<Variation>,
49        ) -> Option<Variation> {
50            let v = Variation {
51                parameter: ParameterKind::Temperature,
52                value: VariationValue::Float(OrderedFloat(1.0)),
53            };
54            if visited.contains(&v) { None } else { Some(v) }
55        }
56
57        fn name(&self) -> &'static str {
58            "always_one"
59        }
60    }
61
62    #[test]
63    fn generator_returns_variation_when_not_visited() {
64        let mut generator = AlwaysOne;
65        let baseline = ConfigSnapshot::default();
66        let visited = HashSet::new();
67        let v = generator.next(&baseline, &visited);
68        assert!(v.is_some());
69        assert_eq!(v.unwrap().parameter, ParameterKind::Temperature);
70    }
71
72    #[test]
73    fn generator_returns_none_when_visited() {
74        let mut generator = AlwaysOne;
75        let baseline = ConfigSnapshot::default();
76        let mut visited = HashSet::new();
77        visited.insert(Variation {
78            parameter: ParameterKind::Temperature,
79            value: VariationValue::Float(OrderedFloat(1.0)),
80        });
81        assert!(generator.next(&baseline, &visited).is_none());
82    }
83
84    #[test]
85    fn generator_name_is_static_str() {
86        let generator = AlwaysOne;
87        assert_eq!(generator.name(), "always_one");
88    }
89
90    #[test]
91    fn generator_is_send() {
92        fn assert_send<T: Send>() {}
93        assert_send::<AlwaysOne>();
94    }
95}