pub fn score(edge_weight: f64, inherited_strength: f64) -> f64 {
edge_weight * inherited_strength
}
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() {
let propagated = score(0.8, 0.3);
assert!((propagated - 0.24).abs() <= f64::EPSILON);
assert_eq!(score(0.8, 1.0), 0.8);
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()));
}
}