Skip to main content

kcode_k1_kmap_selection/
lib.rs

1pub fn score(edge_weight: f64, inherited_strength: f64) -> f64 {
2    edge_weight * inherited_strength
3}
4
5pub fn choose<R>(
6    candidates: &[(usize, f64, u64)],
7    temperature: f64,
8    random: &mut R,
9) -> Result<usize, String>
10where
11    R: FnMut() -> Result<f64, String>,
12{
13    if candidates.is_empty() {
14        return Err("Kmap candidate set was empty".to_owned());
15    }
16    if temperature == 0.0 {
17        let maximum = candidates
18            .iter()
19            .map(|candidate| candidate.1)
20            .max_by(f64::total_cmp)
21            .ok_or_else(|| "Kmap candidate set was empty".to_owned())?;
22        let maxima: Vec<usize> = candidates
23            .iter()
24            .enumerate()
25            .filter(|(_, candidate)| candidate.1.total_cmp(&maximum).is_eq())
26            .map(|(index, _)| index)
27            .collect();
28        let index = (random_unit(random)? * maxima.len() as f64) as usize;
29        return maxima
30            .get(index.min(maxima.len() - 1))
31            .copied()
32            .ok_or_else(|| "Kmap maximum candidate set was empty".to_owned());
33    }
34
35    let maximum_log = candidates
36        .iter()
37        .map(|candidate| candidate.1.ln())
38        .max_by(f64::total_cmp)
39        .ok_or_else(|| "Kmap candidate set was empty".to_owned())?;
40    let weights: Vec<f64> = candidates
41        .iter()
42        .map(|candidate| ((candidate.1.ln() - maximum_log) / temperature).exp())
43        .collect();
44    let total: f64 = weights.iter().sum();
45    let threshold = random_unit(random)? * total;
46    let mut cumulative = 0.0;
47    for (index, weight) in weights.into_iter().enumerate() {
48        cumulative += weight;
49        if threshold < cumulative {
50            return Ok(index);
51        }
52    }
53    Ok(candidates.len() - 1)
54}
55
56fn random_unit<R>(random: &mut R) -> Result<f64, String>
57where
58    R: FnMut() -> Result<f64, String>,
59{
60    let value = random()?;
61    if value.is_finite() && (0.0..1.0).contains(&value) {
62        Ok(value)
63    } else {
64        Err("Kmap random value must be finite and in [0, 1)".to_owned())
65    }
66}
67
68pub fn os_random_unit() -> Result<f64, String> {
69    let mut bytes = [0_u8; 8];
70    getrandom::fill(&mut bytes).map_err(|error| format!("Kmap randomness failed: {error}"))?;
71    let value = u64::from_ne_bytes(bytes) >> 11;
72    Ok(value as f64 / (1_u64 << 53) as f64)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::{choose, os_random_unit, score};
78
79    #[test]
80    fn scores_and_selects_deterministically() {
81        let propagated = score(0.8, 0.3);
82        assert!((propagated - 0.24).abs() <= f64::EPSILON);
83        assert_eq!(score(0.8, 1.0), 0.8);
84        let tied = [(0, 0.5, 0), (1, 0.5, 0)];
85        assert_eq!(choose(&tied, 0.0, &mut || Ok(0.75)).unwrap(), 1);
86        let weighted = [(0, 1.0, 0), (1, 2.0, 0)];
87        assert_eq!(choose(&weighted, 1.0, &mut || Ok(0.0)).unwrap(), 0);
88        assert_eq!(choose(&weighted, 1.0, &mut || Ok(0.99)).unwrap(), 1);
89    }
90
91    #[test]
92    fn rejects_invalid_inputs_and_entropy_is_bounded() {
93        let candidates = [(0, 1.0, 0)];
94        assert_eq!(
95            choose(&candidates, 0.0, &mut || Ok(1.0)).unwrap_err(),
96            "Kmap random value must be finite and in [0, 1)"
97        );
98        assert_eq!(
99            choose(&[], 0.0, &mut || Ok(0.0)).unwrap_err(),
100            "Kmap candidate set was empty"
101        );
102        assert!((0.0..1.0).contains(&os_random_unit().unwrap()));
103    }
104}