learn 0.1.1

A minimal reinforcement learning library written in Rust from scratch.
Documentation
use crate::core::Agent;
use crate::utils::epsilon_greedy;
use rand::rngs::StdRng;
use rand::RngCore;
use rand::SeedableRng;

pub struct Sarsa {
    q_table: Vec<Vec<f32>>,
    alpha: f32,
    gamma: f32,
    epsilon: f32,
    rng: StdRng,
    prev_obs: Option<usize>,
    prev_action: Option<usize>,
    prev_reward: f32,
}

impl Sarsa {
    pub fn new(n_states: usize, n_actions: usize, alpha: f32, gamma: f32, epsilon: f32, seed: u64) -> Self {
        let mut rng = StdRng::seed_from_u64(seed);
        let mut q_table = vec![vec![0.0; n_actions]; n_states];
        for s in 0..n_states {
            for a in 0..n_actions {
                q_table[s][a] = (rng.next_u32() as f32 / u32::MAX as f32) * 0.01;
            }
        }
        Self {
            q_table,
            alpha,
            gamma,
            epsilon,
            rng,
            prev_obs: None,
            prev_action: None,
            prev_reward: 0.0,
        }
    }

    pub fn q_values(&self, state: usize) -> &[f32] {
        &self.q_table[state]
    }
}

impl Agent for Sarsa {
    type Observation = usize;
    type Action = usize;

    fn act(&mut self, obs: &Self::Observation, training: bool) -> Self::Action {
        let q = &self.q_table[*obs];
        let action = if training {
            epsilon_greedy(q, self.epsilon, &mut self.rng)
        } else {
            q.iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap_or(0)
        };

        if let (Some(prev_o), Some(prev_a)) = (self.prev_obs, self.prev_action) {
            let target = self.q_table[*obs][action];
            let td_error = self.prev_reward + self.gamma * target - self.q_table[prev_o][prev_a];
            self.q_table[prev_o][prev_a] += self.alpha * td_error;
        }

        self.prev_obs = Some(*obs);
        self.prev_action = Some(action);
        action
    }

    fn handle_step(
        &mut self,
        _obs: &Self::Observation,
        _action: &Self::Action,
        reward: f32,
        _next_obs: &Self::Observation,
        done: bool,
    ) {
        if done {
            if let (Some(prev_o), Some(prev_a)) = (self.prev_obs, self.prev_action) {
                let td_error = reward - self.q_table[prev_o][prev_a];
                self.q_table[prev_o][prev_a] += self.alpha * td_error;
            }
            self.prev_obs = None;
            self.prev_action = None;
        } else {
            self.prev_reward = reward;
        }
    }

    fn episode_end(&mut self) {
        self.prev_obs = None;
        self.prev_action = None;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sarsa_update() {
        let mut agent = Sarsa::new(2, 2, 0.1, 0.9, 0.0, 0);
        let a = agent.act(&0, true);
        let initial = agent.q_values(0)[a];
        agent.handle_step(&0, &a, 1.0, &1, false);
        let _a2 = agent.act(&1, true);
        let updated = agent.q_values(0)[a];
        assert!(updated > initial);
    }
}