#![allow(clippy::all, dead_code)]
use wasm4pm::ml::linucb::{LinUCBAgent, N_ACTIONS, N_FEATURES};
fn assert_action_in_range(action: u32, label: &str) {
assert!(
(action as usize) < N_ACTIONS,
"[{label}] action {action} out of range [0, {N_ACTIONS})"
);
}
fn assert_q_values_finite(qs: &[f32; N_ACTIONS], label: &str) {
for (i, &q) in qs.iter().enumerate() {
assert!(q.is_finite(), "[{label}] Q[{i}] = {q} is not finite");
}
}
fn assert_ucb_bonus_positive(agent: &LinUCBAgent, features: &[f32; N_FEATURES], label: &str) {
let variance = agent.compute_ucb_variance(features);
let bonus = agent.alpha * variance.max(0.0).sqrt();
assert!(
bonus > 0.0,
"[{label}] UCB bonus must be > 0 for non-zero features: \
got bonus={bonus} variance={variance} alpha={}",
agent.alpha
);
}
fn assert_deterministic_100_runs(features: &[f32; N_FEATURES], label: &str) -> u32 {
let (ref_action, ref_q) = LinUCBAgent::new().select(features);
for run in 1..100 {
let (action, q) = LinUCBAgent::new().select(features);
assert_eq!(
action, ref_action,
"[{label}] run {run}: action {action} != {ref_action} (non-deterministic)"
);
assert!(
(q - ref_q).abs() < 1e-5,
"[{label}] run {run}: Q {q} != {ref_q} (non-deterministic)"
);
}
ref_action
}
#[test]
fn test_iv_normalize_01() {
let label = "iv-normalize-01";
let features: [f32; N_FEATURES] = [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
let action = assert_deterministic_100_runs(&features, label);
assert_action_in_range(action, label);
let agent = LinUCBAgent::new();
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
assert_ucb_bonus_positive(&agent, &features, label);
let first_q = qs[0];
for (i, &q) in qs.iter().enumerate() {
assert!(
(q - first_q).abs() < 1e-5,
"[{label}] Q[{i}]={q} differs from Q[0]={first_q} on fresh agent"
);
}
let variance = agent.compute_ucb_variance(&features);
assert!(
(variance - 4.0).abs() < 1e-4,
"[{label}] x^T A^{{-1}} x = {variance}, expected 4.0"
);
assert_eq!(
action, 0,
"[{label}] fresh agent tie must resolve to action 0"
);
}
#[test]
fn test_iv_normalize_02() {
let label = "iv-normalize-02";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let action = assert_deterministic_100_runs(&features, label);
assert_action_in_range(action, label);
let agent = LinUCBAgent::new();
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
assert_ucb_bonus_positive(&agent, &features, label);
let variance = agent.compute_ucb_variance(&features);
assert!(
(variance - 2.0).abs() < 1e-5,
"[{label}] x^T A^{{-1}} x = {variance}, expected 2.0"
);
let bonus = agent.alpha * variance.sqrt();
assert!(
(bonus - 2.0).abs() < 1e-4,
"[{label}] UCB bonus = {bonus}, expected ≈ 2.0"
);
assert_eq!(
action, 0,
"[{label}] uniform features: fresh agent selects action 0"
);
}
#[test]
fn test_iv_normalize_03() {
let label = "iv-normalize-03";
let features: [f32; N_FEATURES] = [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0];
let action = assert_deterministic_100_runs(&features, label);
assert_action_in_range(action, label);
let agent = LinUCBAgent::new();
let variance = agent.compute_ucb_variance(&features);
assert!(
(variance - 4.0).abs() < 1e-4,
"[{label}] x^T A^{{-1}} x = {variance}, expected 4.0 (exact f32 representation)"
);
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
assert_eq!(
action, 0,
"[{label}] mixed 0/1 features: fresh agent selects action 0"
);
}
#[test]
fn test_iv_normalize_04() {
let label = "iv-normalize-04";
let features: [f32; N_FEATURES] = [1e-6, 1.0, 1e-6, 1.0, 1e-6, 1.0, 1e-6, 1.0];
let action = assert_deterministic_100_runs(&features, label);
assert_action_in_range(action, label);
let agent = LinUCBAgent::new();
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
assert_ucb_bonus_positive(&agent, &features, label);
let variance = agent.compute_ucb_variance(&features);
assert!(
variance > 3.999 && variance < 4.001,
"[{label}] variance {variance} should be ≈ 4.0 (dominant 1.0 terms)"
);
}
#[test]
fn test_iv_normalize_05() {
let label = "iv-normalize-05";
let features: [f32; N_FEATURES] = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5];
let action = assert_deterministic_100_runs(&features, label);
assert_action_in_range(action, label);
let agent = LinUCBAgent::new();
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
let first_q = qs[0];
for (i, &q) in qs.iter().enumerate() {
assert!(
(q - first_q).abs() < 1e-5,
"[{label}] Q[{i}]={q} differs from Q[0]={first_q} (all features identical)"
);
}
assert_eq!(
action, 0,
"[{label}] all-identical features: fresh agent selects action 0"
);
}
#[test]
fn test_oi_action_range_01() {
let label = "oi-action-range-01";
let mut actions_seen = std::collections::HashSet::new();
for target in 0..N_ACTIONS {
let mut agent = LinUCBAgent::new();
let mut features = [0.0_f32; N_FEATURES];
let primary = target % N_FEATURES;
features[primary] = 1.0;
if target >= N_FEATURES {
let secondary = (target / N_FEATURES) % N_FEATURES;
if secondary != primary {
features[secondary] = 0.5;
} else {
features[(secondary + 1) % N_FEATURES] = 0.5;
}
}
for _ in 0..300 {
agent.update(&features, target as u32, 1.0);
}
let (selected, _) = agent.select(&features);
assert_eq!(
selected as usize, target,
"[{label}] action {target} must be selectable after 300 positive rewards, got {selected}"
);
actions_seen.insert(selected as usize);
}
assert_eq!(
actions_seen.len(),
N_ACTIONS,
"[{label}] all {N_ACTIONS} actions must be selectable, saw {}",
actions_seen.len()
);
}
#[test]
fn test_oi_action_range_02() {
let label = "oi-action-range-02";
let test_cases: &[([f32; N_FEATURES], u32, f32)] = &[
([0.0; N_FEATURES], 0, 0.0),
([1.0; N_FEATURES], 4, 1.0),
([0.5; N_FEATURES], 2, 0.5),
([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8], 2, 0.8),
([0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2], 3, -0.3),
([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 0, 1.0),
([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], 4, 1.0),
([1e-6, 0.5, 1.0, 0.25, 0.75, 0.125, 0.875, 0.333], 0, 0.6),
];
for (i, &(features, action, reward)) in test_cases.iter().enumerate() {
let mut agent = LinUCBAgent::new();
agent.update(&features, action, reward);
let (selected, q) = agent.select(&features);
assert!(
(selected as usize) < N_ACTIONS,
"[{label}] case {i}: action {selected} out of [0, {N_ACTIONS})"
);
assert!(q.is_finite(), "[{label}] case {i}: Q = {q} is not finite");
}
}
#[test]
fn test_oi_determinism_01() {
let label = "oi-determinism-01";
let features: [f32; N_FEATURES] = [0.1, 0.9, 0.3, 0.7, 0.5, 0.5, 0.2, 0.8];
let update_seq: &[([f32; N_FEATURES], u32, f32)] = &[
([0.5; N_FEATURES], 3, 1.0),
([0.1, 0.9, 0.3, 0.7, 0.5, 0.5, 0.2, 0.8], 2, 0.5),
([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 3, 0.8),
];
let mut agent_a = LinUCBAgent::new();
let mut agent_b = LinUCBAgent::new();
for &(f, a, r) in update_seq {
agent_a.update(&f, a, r);
agent_b.update(&f, a, r);
}
let (action_a, q_a) = agent_a.select(&features);
let (action_b, q_b) = agent_b.select(&features);
assert_eq!(
action_a, action_b,
"[{label}] same update history → same action: {action_a} vs {action_b}"
);
assert!(
(q_a - q_b).abs() < 1e-6,
"[{label}] same update history → same Q: {q_a} vs {q_b}"
);
}
#[test]
fn test_oi_determinism_02() {
let label = "oi-determinism-02";
let features: [f32; N_FEATURES] = [0.3, 0.6, 0.1, 0.9, 0.4, 0.7, 0.2, 0.8];
let update_seq: &[([f32; N_FEATURES], u32, f32)] = &[
([0.5; N_FEATURES], 0, 1.0),
([0.2, 0.8, 0.3, 0.7, 0.4, 0.6, 0.1, 0.9], 4, 0.7),
];
let mut ref_agent = LinUCBAgent::new();
for &(f, a, r) in update_seq {
ref_agent.update(&f, a, r);
}
let (ref_action, ref_q) = ref_agent.select(&features);
assert_action_in_range(ref_action, label);
for run in 1..100 {
let mut agent = LinUCBAgent::new();
for &(f, a, r) in update_seq {
agent.update(&f, a, r);
}
let (action, q) = agent.select(&features);
assert_eq!(
action, ref_action,
"[{label}] run {run}: action {action} != reference {ref_action}"
);
assert!(
(q - ref_q).abs() < 1e-5,
"[{label}] run {run}: Q {q} != reference {ref_q}"
);
}
}
#[test]
fn test_oi_q_bounds_01() {
let label = "oi-q-bounds-01";
let mut agent = LinUCBAgent::new();
let extremes: &[([f32; N_FEATURES], u32, f32)] = &[
([1.0; N_FEATURES], 0, 1000.0),
([0.0; N_FEATURES], 1, 0.0),
([0.5; N_FEATURES], 2, -10.0),
([1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0], 4, 500.0),
([0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], 2, -500.0),
];
for &(f, a, r) in extremes {
agent.update(&f, a, r);
}
let test_features: &[[f32; N_FEATURES]] = &[
[0.5; N_FEATURES],
[0.0; N_FEATURES],
[1.0; N_FEATURES],
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
];
for (case_idx, features) in test_features.iter().enumerate() {
let qs = agent.get_q_values(features);
assert_q_values_finite(&qs, label);
let (action, q) = agent.select(features);
assert_action_in_range(action, label);
assert!(
q.is_finite(),
"[{label}] case {case_idx}: best Q = {q} is not finite"
);
}
}
#[test]
fn test_oi_exploration_01() {
let label = "oi-exploration-01";
let non_zero: &[[f32; N_FEATURES]] = &[
[0.5; N_FEATURES],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
[1e-6, 1.0, 1e-6, 1.0, 1e-6, 1.0, 1e-6, 1.0],
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
];
let agent = LinUCBAgent::new();
for (i, features) in non_zero.iter().enumerate() {
let variance = agent.compute_ucb_variance(features);
let bonus = agent.alpha * variance.max(0.0).sqrt();
assert!(
bonus > 0.0,
"[{label}] case {i}: UCB bonus {bonus} must be > 0 for non-zero x \
(variance={variance}, alpha={})",
agent.alpha
);
}
let zero: [f32; N_FEATURES] = [0.0; N_FEATURES];
let var_zero = agent.compute_ucb_variance(&zero);
let bonus_zero = agent.alpha * var_zero.max(0.0).sqrt();
assert!(
bonus_zero.abs() < 1e-7,
"[{label}] zero features must give zero bonus, got {bonus_zero}"
);
}
#[test]
fn test_oi_learning_01() {
let label = "oi-learning-01";
let features_a: [f32; N_FEATURES] = [0.1, 0.9, 0.2, 0.8, 0.3, 0.7, 0.4, 0.6];
let action = 3_u32;
let mut agent_pos = LinUCBAgent::new();
for _ in 0..500 {
agent_pos.update(&features_a, action, 1.0);
}
let (argmax, _q) = agent_pos.select(&features_a);
assert_eq!(
argmax, action,
"[{label}] Part A: after 500 positive updates, action {action} must be argmax, got {argmax}"
);
let w3_sum: f32 = agent_pos.weight_vector(action as usize).iter().sum();
assert!(
w3_sum > 0.0,
"[{label}] Part A: W[3] must have positive sum after positive rewards, got {w3_sum}"
);
let features_b: [f32; N_FEATURES] = [0.5; N_FEATURES];
let var_fresh = LinUCBAgent::new().compute_ucb_variance(&features_b);
let mut agent_zero = LinUCBAgent::new();
for _ in 0..50 {
agent_zero.update(&features_b, 0, 0.0);
}
let var_after_zero = agent_zero.compute_ucb_variance(&features_b);
assert!(
var_after_zero < var_fresh,
"[{label}] Part B: UCB variance after 50 zero-reward updates ({var_after_zero}) \
must be < fresh variance ({var_fresh})"
);
let w0_sum: f32 = agent_zero.weight_vector(0).iter().sum();
assert!(
w0_sum.abs() < 1e-6,
"[{label}] Part B: W[0] must remain 0 after zero-reward updates, got sum={w0_sum}"
);
}
#[test]
fn test_ec_zero_features_01() {
let label = "ec-zero-features-01";
let features = [0.0_f32; N_FEATURES];
let agent = LinUCBAgent::new();
let (action, q) = agent.select(&features);
assert_action_in_range(action, label);
assert!(
q.is_finite(),
"[{label}] Q must be finite for zero features"
);
assert!(
q.abs() < 1e-6,
"[{label}] Q = {q} for zero features (expected 0.0)"
);
let qs = agent.get_q_values(&features);
for (i, &qi) in qs.iter().enumerate() {
assert!(qi.abs() < 1e-6, "[{label}] Q[{i}] = {qi} (expected 0.0)");
}
assert_eq!(
action, 0,
"[{label}] all Q=0 → action 0 via lowest-index tie-break"
);
let det_action = assert_deterministic_100_runs(&features, label);
assert_eq!(
det_action, 0,
"[{label}] determinism: must always select action 0"
);
}
#[test]
fn test_ec_zero_features_02() {
let label = "ec-zero-features-02";
let zero_f = [0.0_f32; N_FEATURES];
let nonzero_f: [f32; N_FEATURES] = [0.5; N_FEATURES];
let mut agent = LinUCBAgent::new();
let (action_from_zero, _) = agent.select(&zero_f);
assert_action_in_range(action_from_zero, label);
agent.update(&nonzero_f, action_from_zero, 1.0);
assert_q_values_finite(&agent.get_q_values(&zero_f), label);
assert_q_values_finite(&agent.get_q_values(&nonzero_f), label);
let a_inv = agent.inverse_covariance();
for i in 0..N_FEATURES {
assert!(
a_inv[i][i] > 0.0,
"[{label}] A_inv[{i}][{i}] = {} must be positive after update",
a_inv[i][i]
);
}
}
#[test]
fn test_ec_action_0_01() {
let label = "ec-action-0-01";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let agent_fresh = LinUCBAgent::new();
let (initial, _) = agent_fresh.select(&features);
assert_eq!(
initial, 0,
"[{label}] fresh agent must select action 0 (tie at Q=uniform)"
);
let mut agent = LinUCBAgent::new();
for _ in 0..100 {
agent.update(&features, 0, 1.0);
}
let w0 = agent.weight_vector(0);
let w0_sum: f32 = w0.iter().sum();
assert!(
w0_sum > 0.0,
"[{label}] W[0] sum must be > 0 after 100 positive updates, got {w0_sum}"
);
for (j, &wj) in w0.iter().enumerate() {
assert!(
wj > 0.0,
"[{label}] W[0][{j}] = {wj} must be > 0 after 100 positive updates"
);
}
let (argmax, _) = agent.select(&features);
assert_eq!(
argmax, 0,
"[{label}] action 0 must be argmax after 100 positive updates, got {argmax}"
);
let qs = agent.get_q_values(&features);
for (i, &q) in qs.iter().enumerate() {
assert!(q.is_finite(), "[{label}] Q[{i}] = {q} must be finite");
}
}
#[test]
fn test_ec_action_39_01() {
let label = "ec-action-39-01";
let features: [f32; N_FEATURES] = [0.9, 0.1, 0.8, 0.2, 0.7, 0.3, 0.6, 0.4];
let mut agent = LinUCBAgent::new();
for _ in 0..500 {
agent.update(&features, 4, 1.0);
}
let (action, q) = agent.select(&features);
assert_action_in_range(action, label);
assert!(q.is_finite(), "[{label}] Q must be finite");
assert_eq!(
action, 4,
"[{label}] after 500 positive rewards, action 4 must be argmax, got {action}"
);
let w4_max = agent
.weight_vector(4)
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
assert!(w4_max > 0.0, "[{label}] W[4] max component must be > 0");
}
#[test]
fn test_ec_reward_negative_01() {
let label = "ec-reward-negative-01";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let action = 1_u32;
let mut agent = LinUCBAgent::new();
let q_before = agent.get_q_values(&features)[action as usize];
for _ in 0..50 {
agent.update(&features, action, -1.0);
}
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
let q_after = qs[action as usize];
assert!(
q_after < q_before,
"[{label}] Q[{action}] must decrease after negative rewards: {q_before} → {q_after}"
);
let (selected, best_q) = agent.select(&features);
assert_action_in_range(selected, label);
assert!(
best_q.is_finite(),
"[{label}] best Q = {best_q} must be finite"
);
assert_ne!(
selected, action,
"[{label}] negatively-reinforced action {action} must not be selected"
);
}
#[test]
fn test_ec_reward_zero_01() {
let label = "ec-reward-zero-01";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let action = 0_u32;
let mut agent = LinUCBAgent::new();
let w_before = agent.weight_vector(action as usize);
let intercept_before = agent.intercept(action as usize);
agent.update(&features, action, 0.0);
let w_after = agent.weight_vector(action as usize);
let intercept_after = agent.intercept(action as usize);
for (j, (&wb, &wa)) in w_before.iter().zip(w_after.iter()).enumerate() {
assert!(
(wa - wb).abs() < 1e-7,
"[{label}] W[{action}][{j}] changed after zero-reward update: {wb} → {wa}"
);
}
assert!(
(intercept_after - intercept_before).abs() < 1e-7,
"[{label}] intercept changed after zero-reward update"
);
let fresh_var = LinUCBAgent::new().compute_ucb_variance(&features);
let after_var = agent.compute_ucb_variance(&features);
assert!(
after_var < fresh_var,
"[{label}] UCB variance must decrease after zero-reward update \
(A grows, A_inv shrinks): {fresh_var} → {after_var}"
);
assert_q_values_finite(&agent.get_q_values(&features), label);
}
#[test]
fn test_ec_reward_large_01() {
let label = "ec-reward-large-01";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let action = 2_u32;
let mut agent = LinUCBAgent::new();
agent.update(&features, action, 1e6);
let qs = agent.get_q_values(&features);
assert_q_values_finite(&qs, label);
let (selected, best_q) = agent.select(&features);
assert_action_in_range(selected, label);
assert!(
best_q.is_finite(),
"[{label}] best Q = {best_q} must be finite after reward=1e6"
);
assert_eq!(
selected, action,
"[{label}] action {action} should be argmax after reward=1e6, got {selected}"
);
for (j, &wj) in agent.weight_vector(action as usize).iter().enumerate() {
assert!(
wj.is_finite(),
"[{label}] W[{action}][{j}] = {wj} is not finite"
);
assert!(
wj > 0.0,
"[{label}] W[{action}][{j}] = {wj} must be > 0 after large positive reward"
);
}
}
#[test]
fn test_ec_sequential_01() {
let label = "ec-sequential-01";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let agent = LinUCBAgent::new();
let (first_action, first_q) = agent.select(&features);
assert_eq!(
first_action, 0,
"[{label}] first select: expected action 0 on fresh agent"
);
for i in 1..10 {
let (action, q) = agent.select(&features);
assert_eq!(
action, first_action,
"[{label}] select #{i}: action {action} != {first_action} (select must be idempotent)"
);
assert!(
(q - first_q).abs() < 1e-6,
"[{label}] select #{i}: Q {q} != {first_q} (must be stable)"
);
}
}
#[test]
fn test_ec_sequential_02() {
let label = "ec-sequential-02";
let features: [f32; N_FEATURES] = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let var_initial = LinUCBAgent::new().compute_ucb_variance(&features);
let mut agent = LinUCBAgent::new();
let mut prev_var = var_initial;
for step in 0..10 {
agent.update(&features, 0, 1.0);
let var_after = agent.compute_ucb_variance(&features);
assert!(
var_after < prev_var,
"[{label}] step {step}: variance {var_after} must be < previous {prev_var} \
(A grows monotonically)"
);
assert_q_values_finite(&agent.get_q_values(&features), label);
prev_var = var_after;
}
let a_inv = agent.inverse_covariance();
assert!(
a_inv[0][0] < 0.5,
"[{label}] A_inv[0][0] = {} should be ~1/11 ≈ 0.0909 after 10 updates",
a_inv[0][0]
);
assert!(
a_inv[0][0] > 0.05 && a_inv[0][0] < 0.15,
"[{label}] A_inv[0][0] = {} (expected near 1/11 ≈ 0.0909)",
a_inv[0][0]
);
}
#[test]
fn test_ec_feature_switch_01() {
let label = "ec-feature-switch-01";
let f1: [f32; N_FEATURES] = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let f2: [f32; N_FEATURES] = [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let mut agent = LinUCBAgent::new();
for _ in 0..300 {
agent.update(&f1, 3, 1.0);
agent.update(&f2, 4, 1.0);
}
let (action1, _) = agent.select(&f1);
let (action2, _) = agent.select(&f2);
assert_action_in_range(action1, label);
assert_action_in_range(action2, label);
assert_eq!(
action1, 3,
"[{label}] f1 → expected action 3, got {action1}"
);
assert_eq!(
action2, 4,
"[{label}] f2 → expected action 4, got {action2}"
);
assert_ne!(
action1, action2,
"[{label}] different features must yield different actions"
);
}
#[test]
fn test_ec_matrix_inversion_01() {
let label = "ec-matrix-inversion-01";
let mut agent = LinUCBAgent::new();
for step in 0_u32..100 {
let t = step as f32 / 100.0;
let features: [f32; N_FEATURES] = [
t,
1.0 - t,
(t * 2.0).min(1.0),
(1.0 - t * 2.0).max(0.0),
t * 0.5,
(1.0 - t) * 0.5,
t * t,
(1.0 - t) * (1.0 - t),
];
let action = (step % N_ACTIONS as u32) as u32;
let reward = if step % 2 == 0 { 1.0 } else { -1.0 };
agent.update(&features, action, reward);
}
let a = agent.covariance_matrix();
let a_inv = agent.inverse_covariance();
let mut product = [[0.0_f32; N_FEATURES]; N_FEATURES];
for i in 0..N_FEATURES {
for k in 0..N_FEATURES {
for j in 0..N_FEATURES {
product[i][j] += a[i][k] * a_inv[k][j];
}
}
}
let tol = 0.05_f32;
for i in 0..N_FEATURES {
for j in 0..N_FEATURES {
let expected = if i == j { 1.0_f32 } else { 0.0_f32 };
assert!(
(product[i][j] - expected).abs() < tol,
"[{label}] A*A_inv[{i}][{j}] = {} (expected {expected}, tol {tol})",
product[i][j]
);
}
}
let test_f: [f32; N_FEATURES] = [0.5; N_FEATURES];
assert_q_values_finite(&agent.get_q_values(&test_f), label);
}
#[test]
fn test_ec_float_precision_01() {
let label = "ec-float-precision-01";
let cases: &[([f32; N_FEATURES], f32)] = &[
([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 1.0),
([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], 1.0),
([0.5; N_FEATURES], 2.0), ([1.0; N_FEATURES], 8.0), ([0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], 4.0), ([0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 0.5), ];
let agent = LinUCBAgent::new();
for (i, (features, expected)) in cases.iter().enumerate() {
let variance = agent.compute_ucb_variance(features);
assert!(
(variance - expected).abs() < 1e-5,
"[{label}] case {i}: x^T A^{{-1}} x = {variance}, expected {expected} \
(diff = {})",
(variance - expected).abs()
);
}
}
#[test]
fn test_ec_batch_homogeneous_01() {
let label = "ec-batch-homogeneous-01";
let features: [f32; N_FEATURES] = [0.5; N_FEATURES];
let agent = LinUCBAgent::new();
let (ref_action, ref_q) = agent.select(&features);
assert_action_in_range(ref_action, label);
for i in 0..2048_usize {
let (action, q) = agent.select(&features);
assert_eq!(
action, ref_action,
"[{label}] batch item {i}: action {action} != {ref_action}"
);
assert!(
(q - ref_q).abs() < 1e-6,
"[{label}] batch item {i}: Q {q} != {ref_q}"
);
}
let mut agent_post = LinUCBAgent::new();
agent_post.update(&features, ref_action, 1.0);
assert_q_values_finite(&agent_post.get_q_values(&features), label);
let (action_post, q_post) = agent_post.select(&features);
assert_action_in_range(action_post, label);
assert!(
q_post.is_finite(),
"[{label}] Q after post-batch update must be finite"
);
}
#[cfg(feature = "gpu")]
mod gpu_parity {
use wasm4pm::ml::linucb::{LinUCBAgent, N_ACTIONS, N_FEATURES};
const VECTORS: &[(&str, [f32; N_FEATURES], Option<(u32, f32)>)] = &[
(
"iv-normalize-01",
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
None,
),
("iv-normalize-02", [0.5; N_FEATURES], None),
(
"iv-normalize-03",
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
None,
),
(
"iv-normalize-04",
[1e-6, 1.0, 1e-6, 1.0, 1e-6, 1.0, 1e-6, 1.0],
None,
),
("iv-normalize-05", [0.5; N_FEATURES], None),
("oi-action-range-01", [0.5; N_FEATURES], Some((4, 1.0))),
(
"oi-action-range-02",
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
Some((3, 0.5)),
),
(
"oi-determinism-01",
[0.1, 0.9, 0.3, 0.7, 0.5, 0.5, 0.2, 0.8],
Some((2, 0.5)),
),
(
"oi-determinism-02",
[0.3, 0.6, 0.1, 0.9, 0.4, 0.7, 0.2, 0.8],
Some((0, 1.0)),
),
("oi-q-bounds-01", [0.5; N_FEATURES], Some((0, 1000.0))),
("oi-exploration-01", [0.5; N_FEATURES], None),
("oi-learning-01", [0.5; N_FEATURES], Some((3, 1.0))),
("ec-zero-features-01", [0.0; N_FEATURES], None),
("ec-zero-features-02", [0.5; N_FEATURES], Some((0, 1.0))),
("ec-action-0-01", [0.5; N_FEATURES], Some((0, 1.0))),
(
"ec-action-39-01",
[0.9, 0.1, 0.8, 0.2, 0.7, 0.3, 0.6, 0.4],
Some((4, 1.0)),
),
("ec-reward-negative-01", [0.5; N_FEATURES], Some((1, -1.0))),
("ec-reward-zero-01", [0.5; N_FEATURES], Some((0, 0.0))),
("ec-reward-large-01", [0.5; N_FEATURES], Some((2, 1e6))),
("ec-sequential-01", [0.5; N_FEATURES], None),
(
"ec-sequential-02",
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
Some((0, 1.0)),
),
(
"ec-feature-switch-01",
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
Some((3, 1.0)),
),
("ec-matrix-inversion-01", [0.5; N_FEATURES], Some((0, 1.0))),
("ec-float-precision-01", [0.5; N_FEATURES], None),
("ec-batch-homogeneous-01", [0.5; N_FEATURES], None),
];
#[test]
fn test_gpu_parity_all_25_vectors() {
assert_eq!(
VECTORS.len(),
25,
"Must have exactly 25 conformance vectors"
);
for (id, features, update) in VECTORS {
let mut cpu_agent = LinUCBAgent::new();
if let Some((action, reward)) = update {
cpu_agent.update(features, *action, *reward);
}
let (cpu_action, _cpu_q) = cpu_agent.select(features);
let cpu_qs = cpu_agent.get_q_values(features);
assert!(
(cpu_action as usize) < N_ACTIONS,
"[GPU stub] {id}: CPU action {cpu_action} out of range"
);
for (a, &q) in cpu_qs.iter().enumerate() {
assert!(
q.is_finite(),
"[GPU stub] {id}: CPU Q[{a}] = {q} not finite"
);
}
}
}
}