use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub enum PolicyMode {
Stub,
Linear,
Table,
}
pub struct RlPolicyBridge<
S: ControlScalar,
const OBS: usize,
const ACT: usize,
const TABLE_SIZE: usize,
> {
pub mode: PolicyMode,
pub weights: [[S; OBS]; ACT],
pub table: [[S; ACT]; TABLE_SIZE],
pub obs_min: S,
pub obs_max: S,
pub action_min: [S; ACT],
pub action_max: [S; ACT],
pub episode_reward: S,
pub gamma: S,
}
impl<S: ControlScalar, const OBS: usize, const ACT: usize, const TABLE_SIZE: usize>
RlPolicyBridge<S, OBS, ACT, TABLE_SIZE>
{
pub fn new_stub() -> Self {
Self {
mode: PolicyMode::Stub,
weights: [[S::ZERO; OBS]; ACT],
table: [[S::ZERO; ACT]; TABLE_SIZE],
obs_min: -S::ONE,
obs_max: S::ONE,
action_min: [-S::ONE; ACT],
action_max: [S::ONE; ACT],
episode_reward: S::ZERO,
gamma: S::from_f64(0.99),
}
}
pub fn load_linear(weights: [[S; OBS]; ACT]) -> Self {
let mut s = Self::new_stub();
s.mode = PolicyMode::Linear;
s.weights = weights;
s
}
pub fn load_table(table: [[S; ACT]; TABLE_SIZE], obs_min: S, obs_max: S) -> Self {
let mut s = Self::new_stub();
s.mode = PolicyMode::Table;
s.table = table;
s.obs_min = obs_min;
s.obs_max = obs_max;
s
}
pub fn act(&self, obs: &[S; OBS]) -> [S; ACT] {
let raw = match self.mode {
PolicyMode::Stub => [S::ZERO; ACT],
PolicyMode::Linear => core::array::from_fn(|i| {
obs.iter()
.zip(self.weights[i].iter())
.fold(S::ZERO, |acc, (&o, &w)| acc + w * o)
}),
PolicyMode::Table => {
if TABLE_SIZE == 0 {
return [S::ZERO; ACT];
}
let o = obs[0];
let range = self.obs_max - self.obs_min;
let idx = if range.abs() < S::from_f64(1e-15) {
0
} else {
let t = (o - self.obs_min) / range;
let t_clamped = t.clamp_val(S::ZERO, S::ONE);
let idx_f = t_clamped * S::from_f64((TABLE_SIZE - 1) as f64);
let idx_rounded = idx_f + S::from_f64(0.5);
let idx_u = (idx_rounded * S::ONE).abs();
let mut best = 0usize;
let mut best_diff = (idx_u - S::ZERO).abs();
for k in 0..TABLE_SIZE {
let diff = (idx_u - S::from_f64(k as f64)).abs();
if diff < best_diff {
best_diff = diff;
best = k;
}
}
best
};
self.table[idx]
}
};
core::array::from_fn(|i| raw[i].clamp_val(self.action_min[i], self.action_max[i]))
}
pub fn update_reward(&mut self, reward: S) {
self.episode_reward = self.episode_reward * self.gamma + reward;
}
pub fn reset_episode(&mut self) {
self.episode_reward = S::ZERO;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stub_returns_zeros() {
let policy = RlPolicyBridge::<f64, 3, 2, 10>::new_stub();
let obs = [1.0, -0.5, 0.3];
let act = policy.act(&obs);
assert_eq!(act, [0.0, 0.0]);
}
#[test]
fn linear_policy_correct() {
let weights = [[-1.0_f64, 0.0], [0.0, 0.5]];
let mut policy = RlPolicyBridge::<f64, 2, 2, 1>::load_linear(weights);
policy.action_min = [-10.0, -10.0];
policy.action_max = [10.0, 10.0];
let obs = [3.0, 4.0];
let act = policy.act(&obs);
assert!((act[0] - (-3.0)).abs() < 1e-10, "act[0]={:.4}", act[0]);
assert!((act[1] - 2.0).abs() < 1e-10, "act[1]={:.4}", act[1]);
}
#[test]
fn linear_policy_action_clipped() {
let weights = [[-10.0_f64, 0.0]]; let mut policy = RlPolicyBridge::<f64, 2, 1, 1>::load_linear(weights);
policy.action_min = [-2.0];
policy.action_max = [2.0];
let obs = [5.0, 0.0]; let act = policy.act(&obs);
assert_eq!(act[0], -2.0);
}
#[test]
fn table_policy_lookup() {
let table = [[-2.0_f64], [-1.0], [0.0], [1.0], [2.0]];
let mut policy = RlPolicyBridge::<f64, 1, 1, 5>::load_table(table, -1.0, 1.0);
policy.action_min = [-5.0];
policy.action_max = [5.0];
let act = policy.act(&[-1.0]);
assert!((act[0] - (-2.0)).abs() < 0.01, "act={:.4}", act[0]);
let act2 = policy.act(&[0.0]);
assert!((act2[0] - 0.0).abs() < 0.01, "act={:.4}", act2[0]);
let act3 = policy.act(&[1.0]);
assert!((act3[0] - 2.0).abs() < 0.01, "act={:.4}", act3[0]);
}
#[test]
fn reward_accumulation() {
let mut policy = RlPolicyBridge::<f64, 1, 1, 1>::new_stub();
policy.update_reward(1.0);
policy.update_reward(1.0);
assert!((policy.episode_reward - 1.99).abs() < 1e-6);
policy.reset_episode();
assert_eq!(policy.episode_reward, 0.0);
}
}