#[derive(Debug, Clone)]
pub struct ContinuousReplayBuffer {
obs_dim: usize,
action_dim: usize,
capacity: usize,
len: usize,
write_idx: usize,
observations: Vec<f32>,
actions: Vec<f32>,
rewards: Vec<f32>,
next_observations: Vec<f32>,
dones: Vec<bool>,
}
impl ContinuousReplayBuffer {
pub fn new(capacity: usize, obs_dim: usize, action_dim: usize) -> Self {
assert!(capacity > 0, "ContinuousReplayBuffer capacity must be > 0");
assert!(obs_dim > 0, "ContinuousReplayBuffer obs_dim must be > 0");
assert!(action_dim > 0, "ContinuousReplayBuffer action_dim must be > 0");
Self {
obs_dim,
action_dim,
capacity,
len: 0,
write_idx: 0,
observations: vec![0.0; capacity * obs_dim],
actions: vec![0.0; capacity * action_dim],
rewards: vec![0.0; capacity],
next_observations: vec![0.0; capacity * obs_dim],
dones: vec![false; capacity],
}
}
pub fn push(&mut self, obs: &[f32], action: &[f32], reward: f32, next_obs: &[f32], done: bool) {
debug_assert_eq!(
obs.len(),
self.obs_dim,
"ContinuousReplayBuffer::push: obs.len() ({}) != obs_dim ({})",
obs.len(),
self.obs_dim
);
debug_assert_eq!(
next_obs.len(),
self.obs_dim,
"ContinuousReplayBuffer::push: next_obs.len() ({}) != obs_dim ({})",
next_obs.len(),
self.obs_dim
);
debug_assert_eq!(
action.len(),
self.action_dim,
"ContinuousReplayBuffer::push: action.len() ({}) != action_dim ({})",
action.len(),
self.action_dim
);
let obs_start = self.write_idx * self.obs_dim;
let obs_end = obs_start + self.obs_dim;
self.observations[obs_start..obs_end].copy_from_slice(obs);
self.next_observations[obs_start..obs_end].copy_from_slice(next_obs);
let act_start = self.write_idx * self.action_dim;
let act_end = act_start + self.action_dim;
self.actions[act_start..act_end].copy_from_slice(action);
self.rewards[self.write_idx] = reward;
self.dones[self.write_idx] = done;
self.write_idx = (self.write_idx + 1) % self.capacity;
if self.len < self.capacity {
self.len += 1;
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn obs_dim(&self) -> usize {
self.obs_dim
}
pub fn action_dim(&self) -> usize {
self.action_dim
}
pub fn is_ready(&self, min_size: usize) -> bool {
self.len >= min_size
}
pub(super) fn read_into(
&self,
i: usize,
obs_out: &mut [f32],
action_out: &mut [f32],
next_obs_out: &mut [f32],
) -> (f32, bool) {
assert!(
i < self.len,
"ContinuousReplayBuffer::read_into: i ({}) >= len ({})",
i,
self.len
);
assert_eq!(obs_out.len(), self.obs_dim);
assert_eq!(next_obs_out.len(), self.obs_dim);
assert_eq!(action_out.len(), self.action_dim);
let obs_start = i * self.obs_dim;
let obs_end = obs_start + self.obs_dim;
obs_out.copy_from_slice(&self.observations[obs_start..obs_end]);
next_obs_out.copy_from_slice(&self.next_observations[obs_start..obs_end]);
let act_start = i * self.action_dim;
let act_end = act_start + self.action_dim;
action_out.copy_from_slice(&self.actions[act_start..act_end]);
(self.rewards[i], self.dones[i])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_push_and_len() {
let mut buf = ContinuousReplayBuffer::new(8, 3, 2);
assert!(buf.is_empty());
assert_eq!(buf.capacity(), 8);
assert_eq!(buf.obs_dim(), 3);
assert_eq!(buf.action_dim(), 2);
buf.push(&[1.0, 2.0, 3.0], &[0.1, 0.2], 0.5, &[4.0, 5.0, 6.0], false);
assert_eq!(buf.len(), 1);
assert!(!buf.is_empty());
for i in 0..5 {
buf.push(
&[i as f32; 3],
&[i as f32, i as f32 + 0.5],
i as f32,
&[(i + 1) as f32; 3],
false,
);
}
assert_eq!(buf.len(), 6);
}
#[test]
fn test_roundtrip_values() {
let mut buf = ContinuousReplayBuffer::new(4, 2, 3);
buf.push(&[1.0, 2.0], &[0.1, 0.2, 0.3], 10.0, &[3.0, 4.0], false);
buf.push(&[5.0, 6.0], &[0.4, 0.5, 0.6], 20.0, &[7.0, 8.0], true);
let mut obs = [0.0f32; 2];
let mut action = [0.0f32; 3];
let mut next_obs = [0.0f32; 2];
let (r, d) = buf.read_into(0, &mut obs, &mut action, &mut next_obs);
assert_eq!(obs, [1.0, 2.0]);
assert_eq!(action, [0.1, 0.2, 0.3]);
assert_eq!(next_obs, [3.0, 4.0]);
assert_eq!(r, 10.0);
assert!(!d);
let (r, d) = buf.read_into(1, &mut obs, &mut action, &mut next_obs);
assert_eq!(obs, [5.0, 6.0]);
assert_eq!(action, [0.4, 0.5, 0.6]);
assert_eq!(next_obs, [7.0, 8.0]);
assert_eq!(r, 20.0);
assert!(d);
}
#[test]
fn test_ring_wraparound_evicts_oldest() {
let mut buf = ContinuousReplayBuffer::new(4, 1, 2);
for i in 0..6 {
buf.push(&[i as f32], &[i as f32, -(i as f32)], i as f32, &[(i + 100) as f32], false);
}
assert_eq!(buf.len(), 4, "len must saturate at capacity");
let mut found_rewards = std::collections::HashSet::new();
let mut obs_scratch = [0.0f32; 1];
let mut action_scratch = [0.0f32; 2];
let mut next_scratch = [0.0f32; 1];
for i in 0..buf.len() {
let (r, _) = buf.read_into(i, &mut obs_scratch, &mut action_scratch, &mut next_scratch);
assert_eq!(action_scratch, [r, -r]);
found_rewards.insert(r as i64);
}
assert!(!found_rewards.contains(&0), "oldest transition 0 not evicted");
assert!(!found_rewards.contains(&1), "oldest transition 1 not evicted");
assert!(found_rewards.contains(&2));
assert!(found_rewards.contains(&3));
assert!(found_rewards.contains(&4));
assert!(found_rewards.contains(&5));
}
#[test]
fn test_is_ready() {
let mut buf = ContinuousReplayBuffer::new(8, 2, 1);
assert!(!buf.is_ready(1));
for _ in 0..4 {
buf.push(&[0.0, 0.0], &[0.0], 0.0, &[0.0, 0.0], false);
}
assert!(buf.is_ready(4));
assert!(!buf.is_ready(5));
}
#[test]
#[should_panic(expected = "capacity must be > 0")]
fn test_zero_capacity_panics() {
let _ = ContinuousReplayBuffer::new(0, 4, 2);
}
#[test]
#[should_panic(expected = "obs_dim must be > 0")]
fn test_zero_obs_dim_panics() {
let _ = ContinuousReplayBuffer::new(4, 0, 2);
}
#[test]
#[should_panic(expected = "action_dim must be > 0")]
fn test_zero_action_dim_panics() {
let _ = ContinuousReplayBuffer::new(4, 2, 0);
}
}