use std::cell::RefCell;
use std::sync::atomic::Ordering;
use rustc_hash::FxHashMap;
use crate::game::{moves::Move, state::GameState};
use crate::search::plugin::registry;
use super::features::PatchKey;
use super::net::FeatureSource;
use super::{armed, is_armed, DEFAULT_SCALE, GENERATION};
pub const DEFAULT_FEAT_ALPHA: f64 = 0.1;
struct FeatState {
generation: u64,
phi: FxHashMap<PatchKey, Vec<f32>>,
theta: Vec<f64>,
alpha: f64,
lambda: f64,
clamp: Option<f64>,
norm: bool,
keys: Vec<PatchKey>,
grad: Vec<f64>,
}
thread_local! {
static FEAT: RefCell<FeatState> = RefCell::new(FeatState {
generation: 0,
phi: FxHashMap::default(),
theta: Vec::new(),
alpha: DEFAULT_FEAT_ALPHA,
lambda: 0.0,
clamp: None,
norm: false,
keys: Vec::new(),
grad: Vec::new(),
});
}
pub fn active() -> bool {
registry().value_bool("feat-adapt", false) && is_armed()
}
pub fn keep_table() -> bool {
registry().value_bool("feat-table", true)
}
#[inline]
fn dot(theta: &[f64], phi: &[f32]) -> f64 {
theta.iter().zip(phi).map(|(&t, &p)| t * p as f64).sum()
}
pub fn restart() {
if !active() {
return;
}
let guard = armed().read().unwrap();
let Some(prior) = guard.as_ref() else {
return;
};
let src: &dyn FeatureSource = prior.as_ref();
let h = src.feat_dim();
let reg = registry();
let scale = reg.value_f64("neural-scale", DEFAULT_SCALE);
let theta = if reg.value_bool("feat-warm", true) {
let w = src.warm_theta(scale);
if w.len() == h {
w
} else {
vec![0.0; h]
}
} else {
vec![0.0; h]
};
let alpha = reg.value_f64("feat-alpha", DEFAULT_FEAT_ALPHA);
let lambda = reg.value_f64("feat-lambda", 0.0).max(0.0);
let clamp = {
let c = reg.value_f64("feat-clamp", 0.0);
(c > 0.0).then_some(c)
};
let norm = reg.value_bool("feat-norm", false);
let gen = GENERATION.load(Ordering::Relaxed);
FEAT.with(|f| {
let mut f = f.borrow_mut();
if f.generation != gen {
f.phi.clear();
f.generation = gen;
}
f.theta = theta;
f.alpha = alpha;
f.lambda = lambda;
f.clamp = clamp;
f.norm = norm;
});
}
pub fn logits(scratch: &GameState, moves: &[Move], out: &mut Vec<f64>) {
out.clear();
let guard = armed().read().unwrap();
let Some(prior) = guard.as_ref() else {
out.resize(moves.len(), 0.0);
return;
};
let src: &dyn FeatureSource = prior.as_ref();
let gen = GENERATION.load(Ordering::Relaxed);
let mut keys: Vec<PatchKey> = FEAT.with(|f| std::mem::take(&mut f.borrow_mut().keys));
keys.clear();
let mut miss_keys: Vec<PatchKey> = Vec::new();
let mut miss_inputs: Vec<Vec<f32>> = Vec::new();
FEAT.with(|f| {
let mut f = f.borrow_mut();
if f.generation != gen {
f.phi.clear();
f.generation = gen;
}
for mv in moves {
let (key, input) = src.key_and_input(scratch, mv);
if !f.phi.contains_key(&key) {
miss_keys.push(key);
miss_inputs.push(input);
}
keys.push(key);
}
});
if !miss_inputs.is_empty() {
let vs = src.compute_features(&miss_inputs); if vs.len() != miss_inputs.len() {
out.resize(moves.len(), 0.0);
FEAT.with(|f| f.borrow_mut().keys = keys); return;
}
FEAT.with(|f| {
let mut f = f.borrow_mut();
let norm = f.norm;
for (k, mut v) in miss_keys.iter().zip(vs) {
if norm {
normalize_phi(&mut v); }
f.phi.insert(*k, v);
}
});
}
FEAT.with(|f| {
let f = f.borrow();
let theta = &f.theta;
out.extend(keys.iter().map(|k| match f.phi.get(k) {
Some(phi) => dot(theta, phi),
None => 0.0,
}));
});
FEAT.with(|f| f.borrow_mut().keys = keys);
}
pub fn adapt(_scratch: &GameState, moves: &[Move], chosen: &Move, probs: &[f64]) {
if moves.is_empty() {
return;
}
FEAT.with(|f| {
let mut f = f.borrow_mut();
let h = f.theta.len();
if h == 0 {
return;
}
if f.keys.len() != moves.len() {
return;
}
let (alpha, lambda, clamp) = (f.alpha, f.lambda, f.clamp);
let chosen_idx = moves.iter().position(|m| m == chosen);
let mut grad = std::mem::take(&mut f.grad);
grad.clear();
grad.resize(h, 0.0);
if let Some(ci) = chosen_idx {
if let Some(phi_c) = f.phi.get(&f.keys[ci]) {
for (g, &p) in grad.iter_mut().zip(phi_c.iter()) {
*g += p as f64;
}
}
}
for (k, &prob) in f.keys.iter().zip(probs) {
if let Some(phi) = f.phi.get(k) {
for (g, &p) in grad.iter_mut().zip(phi.iter()) {
*g -= prob * p as f64;
}
}
}
for (t, &g) in f.theta.iter_mut().zip(grad.iter()) {
let mut v = *t + alpha * g;
if lambda > 0.0 {
v *= 1.0 - lambda;
}
if let Some(c) = clamp {
v = v.clamp(-c, c);
}
*t = v;
}
f.grad = grad; });
}
fn normalize_phi(v: &mut [f32]) {
let n: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 1e-12 {
for x in v.iter_mut() {
*x /= n;
}
}
}