pub mod epsilon_greedy;
pub mod linucb;
pub mod stats;
pub mod thompson;
pub mod ucb1;
pub use epsilon_greedy::{EpsilonGreedy, EpsilonGreedyConfig};
pub use linucb::{LinUcb, LinUcbConfig};
pub use stats::ArmStats;
pub use thompson::{ThompsonConfig, ThompsonSampling};
pub use ucb1::{Ucb1, Ucb1Config};
use crate::error::RillError;
use rand::Rng;
pub trait Bandit {
fn arm_count(&self) -> usize;
fn samples_seen(&self) -> u64;
fn select(&self, rng: &mut impl Rng) -> Result<usize, RillError>;
fn update(&mut self, arm: usize, reward: f64) -> Result<(), RillError>;
fn reset(&mut self);
fn arm_stats(&self, arm: usize) -> Result<ArmStats, RillError>;
}
pub trait ContextualBandit {
fn arm_count(&self) -> usize;
fn feature_count(&self) -> usize;
fn samples_seen(&self) -> u64;
fn select(&self, context: &[f64], rng: &mut impl Rng) -> Result<usize, RillError>;
fn update(&mut self, arm: usize, context: &[f64], reward: f64) -> Result<(), RillError>;
fn reset(&mut self);
}
pub(crate) fn validate_arm(arm_count: usize, arm: usize) -> Result<(), RillError> {
if arm >= arm_count {
Err(RillError::InvalidArm {
expected: arm_count,
actual: arm,
})
} else {
Ok(())
}
}
pub(crate) fn validate_reward_finite(reward: f64) -> Result<(), RillError> {
if reward.is_finite() {
Ok(())
} else {
Err(RillError::InvalidReward(reward))
}
}
pub(crate) fn validate_reward_01(reward: f64) -> Result<(), RillError> {
if reward.is_finite() && (0.0..=1.0).contains(&reward) {
Ok(())
} else {
Err(RillError::InvalidReward(reward))
}
}
pub(crate) fn checked_finite_add(
current: f64,
delta: f64,
field: &'static str,
) -> Result<f64, RillError> {
let value = current + delta;
if value.is_finite() {
Ok(value)
} else {
Err(RillError::NonFiniteValue { field, value })
}
}
pub(crate) fn checked_increment(value: u64, field: &'static str) -> Result<u64, RillError> {
value
.checked_add(1)
.ok_or_else(|| RillError::InvalidState(format!("{field} overflow")))
}
pub(crate) fn validate_sample_count(pulls: &[u64], samples_seen: u64) -> Result<(), RillError> {
let total = pulls.iter().try_fold(0u64, |sum, &pulls| {
sum.checked_add(pulls)
.ok_or_else(|| RillError::InvalidState("pull count sum overflow".into()))
})?;
if total == samples_seen {
Ok(())
} else {
Err(RillError::InvalidState(format!(
"samples_seen is {samples_seen}, but per-arm pulls sum to {total}"
)))
}
}