use super::array::Logits;
use super::features::{FEATURES_LEN, FEATURES_LEN_V2, FEATURES_LEN_V3};
use std::sync::LazyLock;
const HID: usize = 256;
const OUT: usize = 38;
const N_W2: usize = HID * HID;
const N_W3: usize = OUT * HID;
const fn total(in_dim: usize) -> usize {
HID * in_dim + HID + N_W2 + HID + N_W3 + OUT
}
fn decode(raw: &[u8]) -> Vec<f32> {
raw.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect()
}
const IN_V1: usize = FEATURES_LEN;
static RAW_V1: &[u8] = include_bytes!("weights/american_v1.f32");
const _: () = assert!(
RAW_V1.len() == total(IN_V1) * 4,
"v1 weights artifact size mismatch"
);
static WEIGHTS_V1: LazyLock<Vec<f32>> = LazyLock::new(|| decode(RAW_V1));
fn affine(weight: &[f32], bias: &[f32], x: &[f32], out: &mut [f32]) {
let n = x.len();
for (o, slot) in out.iter_mut().enumerate() {
let row = &weight[o * n..o * n + n];
*slot = bias[o] + row.iter().zip(x).map(|(w, xi)| w * xi).sum::<f32>();
}
}
fn relu(v: &mut [f32]) {
for x in v {
*x = x.max(0.0);
}
}
fn forward(weights: &[f32], x: &[f32], in_dim: usize) -> Logits {
let (w1, rest) = weights.split_at(HID * in_dim);
let (b1, rest) = rest.split_at(HID);
let (w2, rest) = rest.split_at(N_W2);
let (b2, rest) = rest.split_at(HID);
let (w3, b3) = rest.split_at(N_W3);
let mut h1 = [0f32; HID];
affine(w1, b1, x, &mut h1);
relu(&mut h1);
let mut h2 = [0f32; HID];
affine(w2, b2, &h1, &mut h2);
relu(&mut h2);
let mut z = [0f32; OUT];
affine(w3, b3, &h2, &mut z);
let mut logits = Logits::new();
for ((_call, slot), &value) in logits.iter_mut().zip(&z) {
*slot = value;
}
logits
}
#[must_use]
pub fn classify(features: &[f32]) -> Logits {
assert_eq!(features.len(), IN_V1, "expected {IN_V1} features");
forward(WEIGHTS_V1.as_slice(), features, IN_V1)
}
const IN_V2: usize = FEATURES_LEN_V2;
static RAW_V2: &[u8] = include_bytes!("weights/american_v2.f32");
const _: () = assert!(
RAW_V2.len() == total(IN_V2) * 4,
"v2 weights artifact size mismatch"
);
static WEIGHTS_V2: LazyLock<Vec<f32>> = LazyLock::new(|| decode(RAW_V2));
#[must_use]
pub fn classify_v2(features: &[f32]) -> Logits {
assert_eq!(features.len(), IN_V2, "expected {IN_V2} features");
forward(WEIGHTS_V2.as_slice(), features, IN_V2)
}
static RAW_SEARCH_V1: &[u8] = include_bytes!("weights/american_v1_search.f32");
const _: () = assert!(
RAW_SEARCH_V1.len() == total(IN_V1) * 4,
"search-target weights artifact size mismatch"
);
static WEIGHTS_SEARCH_V1: LazyLock<Vec<f32>> = LazyLock::new(|| decode(RAW_SEARCH_V1));
#[must_use]
pub fn classify_search(features: &[f32]) -> Logits {
assert_eq!(features.len(), IN_V1, "expected {IN_V1} features");
forward(WEIGHTS_SEARCH_V1.as_slice(), features, IN_V1)
}
const IN_V3: usize = FEATURES_LEN_V3;
static RAW_V3: &[u8] = include_bytes!("weights/american_v3.f32");
const _: () = assert!(
RAW_V3.len() == total(IN_V3) * 4,
"v3 weights artifact size mismatch"
);
static WEIGHTS_V3: LazyLock<Vec<f32>> = LazyLock::new(|| decode(RAW_V3));
#[must_use]
pub fn classify_v3(features: &[f32]) -> Logits {
assert_eq!(features.len(), IN_V3, "expected {IN_V3} features");
forward(WEIGHTS_V3.as_slice(), features, IN_V3)
}
#[cfg(test)]
mod tests {
use super::*;
fn argmax(v: &[f32]) -> usize {
v.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap()
}
fn check_fixture(fixture: &str, classify: impl Fn(&[f32]) -> Vec<f32>) {
let fx: serde_json::Value = serde_json::from_str(fixture).unwrap();
let rows = fx["features"].as_array().unwrap();
let golds = fx["logits"].as_array().unwrap();
assert_eq!(rows.len(), golds.len());
assert!(!rows.is_empty(), "fixture has no rows");
let to_vec = |v: &serde_json::Value| -> Vec<f32> {
v.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap() as f32)
.collect()
};
let mut max_abs = 0f32;
for (frow, grow) in rows.iter().zip(golds) {
let x = to_vec(frow);
let gold = to_vec(grow);
let pred = classify(&x);
assert_eq!(pred.len(), gold.len());
for (p, g) in pred.iter().zip(&gold) {
max_abs = max_abs.max((p - g).abs());
}
assert_eq!(
argmax(&pred),
argmax(&gold),
"arg-max (chosen call) differs"
);
}
assert!(
max_abs < 1.0e-3,
"max abs logit diff {max_abs} exceeds tolerance"
);
}
#[test]
fn matches_candle_fixture() {
check_fixture(include_str!("weights/american_v1.fixture.json"), |x| {
classify(x).iter().map(|(_, l)| *l).collect()
});
}
#[test]
fn matches_candle_fixture_v2() {
check_fixture(include_str!("weights/american_v2.fixture.json"), |x| {
classify_v2(x).iter().map(|(_, l)| *l).collect()
});
}
#[test]
fn matches_candle_fixture_search() {
check_fixture(
include_str!("weights/american_v1_search.fixture.json"),
|x| classify_search(x).iter().map(|(_, l)| *l).collect(),
);
}
#[test]
fn matches_candle_fixture_v3() {
check_fixture(include_str!("weights/american_v3.fixture.json"), |x| {
classify_v3(x).iter().map(|(_, l)| *l).collect()
});
}
}