use std::cmp::Ordering;
#[inline]
pub fn puct(score: f32, node_visits: i32, parent_visits: i32, policy: f32, c: f32) -> f32{
let q = if node_visits > 0 { score / node_visits as f32 } else { 0.0 };
q + c * policy * (parent_visits as f32).sqrt() / (1. + node_visits as f32)
}
#[inline]
pub fn negate_score(s: f32) -> f32{
-s
}
#[inline]
pub fn visits_to_probabilities<const N: usize>(visits: [i32; N]) -> [f32; N]{
const EPSILON: f32 = 1e-18;
let sum = visits.iter().sum::<i32>() as f32 + (EPSILON * N as f32);
std::array::from_fn(|i| {
(visits[i] as f32 + EPSILON) / sum
})
}
#[inline]
pub fn visits_to_probabilities_with_temperature<const N: usize>(visits: [i32; N], temperature: f32) -> [f32; N]{
const EPSILON: f32 = 1e-18;
if temperature > 0.{
let exponent = 1. / temperature;
let mut sum = 0.;
let mut visits = std::array::from_fn(|i| {
let v = (visits[i] as f32).powf(exponent) + EPSILON;
sum += v;
v
});
visits.iter_mut().for_each(|v| *v /= sum);
visits
}
else{
let (max, count) = visits.iter().fold((0i32, 0usize), |(max, count), v|{
match (*v).cmp(&max) {
Ordering::Greater => (*v, 1),
Ordering::Equal => (max, count+1),
Ordering::Less => (max, count)
}
});
let value = 1. / (count as f32);
std::array::from_fn(|i| {
if visits[i] == max { value } else { 0. }
})
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => {
assert!(($a - $b).abs() < 1e-5, "left: {}, right: {}", $a, $b);
};
}
#[test]
fn test_puct_c_0() {
let score = puct(1.5, 2, 8, 0.25, 0.);
assert_approx_eq!(score, 0.75);
}
#[test]
fn test_puct_policy_0() {
let score = puct(1.5, 2, 8, 0., std::f32::consts::SQRT_2);
assert_approx_eq!(score, 0.75);
}
#[test]
fn test_puct_score_0() {
let score = puct(0.0, 2, 8, 0.25, std::f32::consts::SQRT_2);
assert_approx_eq!(score, 1./3.);
}
#[test]
fn test_puct_visits_0() {
let score = puct(0.0, 0, 8, 0.25, std::f32::consts::SQRT_2);
assert_approx_eq!(score, 1.);
}
#[test]
fn test_puct_1() {
let score = puct(1.5, 2, 8, 0.25, std::f32::consts::SQRT_2);
assert_approx_eq!(score, 13./12.);
}
#[test]
fn test_puct_2() {
let score = puct(-0.5, 5, 18, 0.75, std::f32::consts::SQRT_2);
assert_approx_eq!(score, 0.65);
}
#[test]
fn test_negate_score_positive_to_negative() {
assert_eq!(negate_score(1.0), -1.0);
assert_eq!(negate_score(42.5), -42.5);
}
#[test]
fn test_negate_score_negative_to_positive() {
assert_eq!(negate_score(-1.0), 1.0);
assert_eq!(negate_score(-0.75), 0.75);
}
#[test]
fn test_negate_score_zero() {
assert_eq!(negate_score(0.0), 0.0);
assert_eq!(negate_score(-0.0), 0.0);
}
fn assert_array_approx_eq<const N: usize>(actual: [f32; N], expected: [f32; N], threshold: f32) {
for i in 0..N {
assert!(
(actual[i] - expected[i]).abs() < threshold,
"Error at index {}: received value {}, expected value {}",
i, actual[i], expected[i]
);
}
}
#[test]
fn test_probabilities_normal_case() {
let visits = [10, 30, 0, 10];
let probas = visits_to_probabilities(visits);
assert_array_approx_eq(probas, [0.2, 0.6, 0.0, 0.2], 1e-6);
}
#[test]
fn test_probabilities_all_zeros() {
let visits = [0, 0, 0, 0, 0];
let probas = visits_to_probabilities(visits);
assert_array_approx_eq(probas, [0.2, 0.2, 0.2, 0.2, 0.2], 1e-6);
}
#[test]
fn test_temperature_normal_tau_1() {
let visits = [10, 30];
let probas = visits_to_probabilities_with_temperature(visits, 1.0);
assert_array_approx_eq(probas, [0.25, 0.75], 1e-6);
}
#[test]
fn test_temperature_high_tau_exploration() {
let visits = [10, 90];
let probas = visits_to_probabilities_with_temperature(visits, 10.0);
assert_array_approx_eq(probas, [0.44528, 0.55471], 1e-4);
}
#[test]
fn test_temperature_greedy_single_max() {
let visits = [5, 100, 10, 0];
let probas = visits_to_probabilities_with_temperature(visits, 0.0);
assert_array_approx_eq(probas, [0.0, 1.0, 0.0, 0.0], 1e-6);
}
#[test]
fn test_temperature_greedy_ties() {
let visits = [10, 50, 50, 5];
let probas = visits_to_probabilities_with_temperature(visits, 0.0);
assert_array_approx_eq(probas, [0.0, 0.5, 0.5, 0.0], 1e-6);
}
#[test]
fn test_temperature_greedy_triple_ties() {
let visits = [100, 100, 100];
let probas = visits_to_probabilities_with_temperature(visits, 0.0);
assert_array_approx_eq(probas, [0.333333, 0.333333, 0.333333], 1e-5);
}
#[test]
fn test_temperature_all_zeros_fallback() {
let visits = [0, 0, 0, 0];
let probas_greedy = visits_to_probabilities_with_temperature(visits, 0.0);
let probas_soft = visits_to_probabilities_with_temperature(visits, 0.5);
assert_array_approx_eq(probas_greedy, [0.25, 0.25, 0.25, 0.25], 1e-6);
assert_array_approx_eq(probas_soft, [0.25, 0.25, 0.25, 0.25], 1e-6);
}
}