Skip to main content

converge_optimization/gate/
determinism.rs

1//! Determinism specification for reproducible results
2
3use serde::{Deserialize, Serialize};
4use std::hash::{Hash, Hasher};
5
6/// Determinism specification for reproducible results
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct DeterminismSpec {
9    /// Random seed for reproducibility
10    pub seed: u64,
11    /// Tie-breaking strategy
12    pub tie_break: TieBreakStrategy,
13    /// Whether to enforce stable sorting
14    pub stable_sort: bool,
15    /// Version of determinism rules (for forward compatibility)
16    pub rules_version: String,
17}
18
19impl Default for DeterminismSpec {
20    fn default() -> Self {
21        Self {
22            seed: 0,
23            tie_break: TieBreakStrategy::LexicographicFirst,
24            stable_sort: true,
25            rules_version: "v1".to_string(),
26        }
27    }
28}
29
30impl DeterminismSpec {
31    /// Create with explicit seed
32    pub fn with_seed(seed: u64) -> Self {
33        Self {
34            seed,
35            ..Default::default()
36        }
37    }
38
39    /// Create with seed and tie-break strategy
40    pub fn new(seed: u64, tie_break: TieBreakStrategy) -> Self {
41        Self {
42            seed,
43            tie_break,
44            stable_sort: true,
45            rules_version: "v1".to_string(),
46        }
47    }
48
49    /// Generate a sub-seed for a specific phase
50    ///
51    /// This ensures different phases get different but deterministic seeds
52    /// derived from the main seed.
53    pub fn sub_seed(&self, phase: &str) -> u64 {
54        let mut hasher = std::collections::hash_map::DefaultHasher::new();
55        self.seed.hash(&mut hasher);
56        phase.hash(&mut hasher);
57        hasher.finish()
58    }
59
60    /// Generate a sub-seed for a numbered iteration
61    pub fn iter_seed(&self, iteration: usize) -> u64 {
62        let mut hasher = std::collections::hash_map::DefaultHasher::new();
63        self.seed.hash(&mut hasher);
64        iteration.hash(&mut hasher);
65        hasher.finish()
66    }
67}
68
69/// Strategy for breaking ties when multiple solutions are equally good
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum TieBreakStrategy {
73    /// Choose first in lexicographic order of identifiers
74    LexicographicFirst,
75    /// Choose last in lexicographic order
76    LexicographicLast,
77    /// Choose by smallest index/id
78    SmallestIndex,
79    /// Choose by largest index/id
80    LargestIndex,
81    /// Use seeded random selection
82    SeededRandom,
83}
84
85impl TieBreakStrategy {
86    /// Apply tie-breaking to select from candidates
87    pub fn select<T: Ord + Clone>(&self, candidates: &[T], seed: u64) -> Option<T> {
88        if candidates.is_empty() {
89            return None;
90        }
91        match self {
92            Self::LexicographicFirst | Self::SmallestIndex => candidates.iter().min().cloned(),
93            Self::LexicographicLast | Self::LargestIndex => candidates.iter().max().cloned(),
94            Self::SeededRandom => {
95                // Simple seeded selection using modulo
96                let idx = (seed as usize) % candidates.len();
97                Some(candidates[idx].clone())
98            }
99        }
100    }
101
102    /// Apply tie-breaking with a custom comparator
103    pub fn select_by<'a, T, F>(&self, candidates: &'a [T], seed: u64, compare: F) -> Option<&'a T>
104    where
105        F: Fn(&T, &T) -> std::cmp::Ordering,
106    {
107        if candidates.is_empty() {
108            return None;
109        }
110        match self {
111            Self::LexicographicFirst | Self::SmallestIndex => {
112                candidates.iter().min_by(|a, b| compare(a, b))
113            }
114            Self::LexicographicLast | Self::LargestIndex => {
115                candidates.iter().max_by(|a, b| compare(a, b))
116            }
117            Self::SeededRandom => {
118                let idx = (seed as usize) % candidates.len();
119                Some(&candidates[idx])
120            }
121        }
122    }
123
124    /// Sort candidates according to this strategy
125    pub fn sort<T: Ord>(&self, candidates: &mut [T]) {
126        match self {
127            Self::LexicographicFirst | Self::SmallestIndex => candidates.sort(),
128            Self::LexicographicLast | Self::LargestIndex => {
129                candidates.sort();
130                candidates.reverse();
131            }
132            Self::SeededRandom => {
133                // For random, we don't reorder - caller should use select
134            }
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_default_determinism() {
145        let spec = DeterminismSpec::default();
146        assert_eq!(spec.seed, 0);
147        assert_eq!(spec.tie_break, TieBreakStrategy::LexicographicFirst);
148        assert!(spec.stable_sort);
149    }
150
151    #[test]
152    fn test_sub_seed_consistency() {
153        let spec = DeterminismSpec::with_seed(12345);
154
155        // Same phase should always produce same sub-seed
156        let seed1 = spec.sub_seed("phase1");
157        let seed2 = spec.sub_seed("phase1");
158        assert_eq!(seed1, seed2);
159
160        // Different phases should produce different sub-seeds
161        let seed3 = spec.sub_seed("phase2");
162        assert_ne!(seed1, seed3);
163    }
164
165    #[test]
166    fn test_tie_break_lexicographic() {
167        let candidates = vec!["charlie", "alice", "bob"];
168
169        let first = TieBreakStrategy::LexicographicFirst.select(&candidates, 0);
170        assert_eq!(first, Some("alice"));
171
172        let last = TieBreakStrategy::LexicographicLast.select(&candidates, 0);
173        assert_eq!(last, Some("charlie"));
174    }
175
176    #[test]
177    fn test_tie_break_seeded() {
178        let candidates = vec![1, 2, 3, 4, 5];
179
180        // Same seed should select same element
181        let sel1 = TieBreakStrategy::SeededRandom.select(&candidates, 42);
182        let sel2 = TieBreakStrategy::SeededRandom.select(&candidates, 42);
183        assert_eq!(sel1, sel2);
184
185        // Different seeds may select different elements
186        let sel3 = TieBreakStrategy::SeededRandom.select(&candidates, 43);
187        // (they might happen to be equal, but usually won't be for arbitrary seeds)
188        let _ = sel3; // Just ensure it compiles
189    }
190
191    #[test]
192    fn test_tie_break_empty() {
193        let empty: Vec<i32> = vec![];
194        assert_eq!(TieBreakStrategy::LexicographicFirst.select(&empty, 0), None);
195    }
196}