kcode-k1-kmap-selection 0.1.0

Weighted frontier selection for K1 Kmap traversal
Documentation
pub const DEPTH_DECAY: f64 = 0.7;

pub fn score(value: f64, depth: usize) -> f64 {
    value * DEPTH_DECAY.powf(depth as f64)
}

pub fn choose<R>(
    candidates: &[(usize, f64, u64)],
    temperature: f64,
    random: &mut R,
) -> Result<usize, String>
where
    R: FnMut() -> Result<f64, String>,
{
    if candidates.is_empty() {
        return Err("Kmap candidate set was empty".to_owned());
    }
    if temperature == 0.0 {
        let maximum = candidates
            .iter()
            .map(|candidate| candidate.1)
            .max_by(f64::total_cmp)
            .ok_or_else(|| "Kmap candidate set was empty".to_owned())?;
        let maxima: Vec<usize> = candidates
            .iter()
            .enumerate()
            .filter(|(_, candidate)| candidate.1.total_cmp(&maximum).is_eq())
            .map(|(index, _)| index)
            .collect();
        let index = (random_unit(random)? * maxima.len() as f64) as usize;
        return maxima
            .get(index.min(maxima.len() - 1))
            .copied()
            .ok_or_else(|| "Kmap maximum candidate set was empty".to_owned());
    }

    let maximum_log = candidates
        .iter()
        .map(|candidate| candidate.1.ln())
        .max_by(f64::total_cmp)
        .ok_or_else(|| "Kmap candidate set was empty".to_owned())?;
    let weights: Vec<f64> = candidates
        .iter()
        .map(|candidate| ((candidate.1.ln() - maximum_log) / temperature).exp())
        .collect();
    let total: f64 = weights.iter().sum();
    let threshold = random_unit(random)? * total;
    let mut cumulative = 0.0;
    for (index, weight) in weights.into_iter().enumerate() {
        cumulative += weight;
        if threshold < cumulative {
            return Ok(index);
        }
    }
    Ok(candidates.len() - 1)
}

fn random_unit<R>(random: &mut R) -> Result<f64, String>
where
    R: FnMut() -> Result<f64, String>,
{
    let value = random()?;
    if value.is_finite() && (0.0..1.0).contains(&value) {
        Ok(value)
    } else {
        Err("Kmap random value must be finite and in [0, 1)".to_owned())
    }
}

pub fn os_random_unit() -> Result<f64, String> {
    let mut bytes = [0_u8; 8];
    getrandom::fill(&mut bytes).map_err(|error| format!("Kmap randomness failed: {error}"))?;
    let value = u64::from_ne_bytes(bytes) >> 11;
    Ok(value as f64 / (1_u64 << 53) as f64)
}

#[cfg(test)]
mod tests {
    use super::{choose, os_random_unit, score};

    #[test]
    fn scores_and_selects_deterministically() {
        assert_eq!(score(1.0, 1), 0.7);
        let tied = [(0, 0.5, 0), (1, 0.5, 0)];
        assert_eq!(choose(&tied, 0.0, &mut || Ok(0.75)).unwrap(), 1);
        let weighted = [(0, 1.0, 0), (1, 2.0, 0)];
        assert_eq!(choose(&weighted, 1.0, &mut || Ok(0.0)).unwrap(), 0);
        assert_eq!(choose(&weighted, 1.0, &mut || Ok(0.99)).unwrap(), 1);
    }

    #[test]
    fn rejects_invalid_inputs_and_entropy_is_bounded() {
        let candidates = [(0, 1.0, 0)];
        assert_eq!(
            choose(&candidates, 0.0, &mut || Ok(1.0)).unwrap_err(),
            "Kmap random value must be finite and in [0, 1)"
        );
        assert_eq!(
            choose(&[], 0.0, &mut || Ok(0.0)).unwrap_err(),
            "Kmap candidate set was empty"
        );
        assert!((0.0..1.0).contains(&os_random_unit().unwrap()));
    }
}