use burn::tensor::{Int, Tensor as BurnTensor, TensorData, backend::Backend};
use rand::Rng;
use super::storage::ReplayBuffer;
#[derive(Debug, Clone)]
pub struct ReplayBatch {
pub observations: Vec<f32>,
pub actions: Vec<i64>,
pub rewards: Vec<f32>,
pub next_observations: Vec<f32>,
pub dones: Vec<bool>,
pub obs_dim: usize,
}
impl ReplayBatch {
pub fn len(&self) -> usize {
self.actions.len()
}
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
pub fn to_burn_tensors<B: Backend>(&self, device: &B::Device) -> ReplayBurnTensors<B> {
let batch = self.len();
let obs_dim = self.obs_dim;
let observations = BurnTensor::<B, 2>::from_data(
TensorData::new(self.observations.clone(), [batch, obs_dim]),
device,
);
let next_observations = BurnTensor::<B, 2>::from_data(
TensorData::new(self.next_observations.clone(), [batch, obs_dim]),
device,
);
let actions = BurnTensor::<B, 1, Int>::from_data(
TensorData::new(self.actions.clone(), [batch]),
device,
);
let rewards =
BurnTensor::<B, 1>::from_data(TensorData::new(self.rewards.clone(), [batch]), device);
let dones_f: Vec<f32> = self.dones.iter().map(|&d| if d { 1.0 } else { 0.0 }).collect();
let dones = BurnTensor::<B, 1>::from_data(TensorData::new(dones_f, [batch]), device);
ReplayBurnTensors { observations, actions, rewards, next_observations, dones }
}
}
#[derive(Debug)]
pub struct ReplayBurnTensors<B: Backend> {
pub observations: BurnTensor<B, 2>,
pub actions: BurnTensor<B, 1, Int>,
pub rewards: BurnTensor<B, 1>,
pub next_observations: BurnTensor<B, 2>,
pub dones: BurnTensor<B, 1>,
}
pub fn sample<R: Rng>(buffer: &ReplayBuffer, batch_size: usize, rng: &mut R) -> ReplayBatch {
assert!(!buffer.is_empty(), "ReplayBuffer is empty; cannot sample");
assert!(batch_size > 0, "batch_size must be > 0");
let obs_dim = buffer.obs_dim();
let len = buffer.len();
let mut observations = vec![0.0f32; batch_size * obs_dim];
let mut next_observations = vec![0.0f32; batch_size * obs_dim];
let mut actions = Vec::with_capacity(batch_size);
let mut rewards = Vec::with_capacity(batch_size);
let mut dones = Vec::with_capacity(batch_size);
for k in 0..batch_size {
let idx = rng.random_range(0..len);
let obs_slice = &mut observations[k * obs_dim..(k + 1) * obs_dim];
let next_slice = &mut next_observations[k * obs_dim..(k + 1) * obs_dim];
let (a, r, d) = buffer.read_into(idx, obs_slice, next_slice);
actions.push(a);
rewards.push(r);
dones.push(d);
}
ReplayBatch { observations, actions, rewards, next_observations, dones, obs_dim }
}
#[cfg(test)]
mod tests {
use rand::{SeedableRng, rngs::StdRng};
use super::*;
#[test]
fn test_sample_returns_correct_count() {
let mut buf = ReplayBuffer::new(16, 3);
for i in 0..10 {
buf.push(
&[i as f32, i as f32 + 0.1, i as f32 + 0.2],
(i % 2) as i64,
i as f32,
&[(i + 1) as f32, (i + 1) as f32 + 0.1, (i + 1) as f32 + 0.2],
false,
);
}
let mut rng = StdRng::seed_from_u64(42);
let batch = sample(&buf, 5, &mut rng);
assert_eq!(batch.len(), 5);
assert_eq!(batch.actions.len(), 5);
assert_eq!(batch.rewards.len(), 5);
assert_eq!(batch.dones.len(), 5);
assert_eq!(batch.observations.len(), 5 * 3);
assert_eq!(batch.next_observations.len(), 5 * 3);
assert_eq!(batch.obs_dim, 3);
}
#[test]
fn test_sampled_values_match_pushed_values() {
let mut buf = ReplayBuffer::new(8, 2);
buf.push(&[7.0, 8.0], 1, 42.0, &[9.0, 10.0], true);
let mut rng = StdRng::seed_from_u64(0);
let batch = sample(&buf, 4, &mut rng);
for k in 0..4 {
assert_eq!(batch.actions[k], 1);
assert_eq!(batch.rewards[k], 42.0);
assert!(batch.dones[k]);
assert_eq!(&batch.observations[k * 2..(k + 1) * 2], &[7.0, 8.0]);
assert_eq!(&batch.next_observations[k * 2..(k + 1) * 2], &[9.0, 10.0]);
}
}
mod burn_tests {
use burn::backend::NdArray;
use super::*;
type B = NdArray<f32>;
#[test]
fn test_to_burn_tensors_shapes_and_roundtrip() {
let mut buf = ReplayBuffer::new(8, 4);
for i in 0..6 {
buf.push(&[i as f32; 4], (i % 2) as i64, i as f32, &[i as f32 + 1.0; 4], i == 5);
}
let mut rng = StdRng::seed_from_u64(1);
let batch = sample(&buf, 3, &mut rng);
let device = crate::utils::cuda::default_burn_device::<B>();
let t = batch.to_burn_tensors::<B>(&device);
assert_eq!(t.observations.dims(), [3, 4]);
assert_eq!(t.next_observations.dims(), [3, 4]);
assert_eq!(t.actions.dims(), [3]);
assert_eq!(t.rewards.dims(), [3]);
assert_eq!(t.dones.dims(), [3]);
let obs_flat: Vec<f32> = t.observations.into_data().to_vec().unwrap();
assert_eq!(obs_flat, batch.observations);
let next_flat: Vec<f32> = t.next_observations.into_data().to_vec().unwrap();
assert_eq!(next_flat, batch.next_observations);
let acts: Vec<i64> = t.actions.into_data().to_vec().unwrap();
assert_eq!(acts, batch.actions);
let rews: Vec<f32> = t.rewards.into_data().to_vec().unwrap();
assert_eq!(rews, batch.rewards);
let dones_f: Vec<f32> = t.dones.into_data().to_vec().unwrap();
let expected_dones: Vec<f32> =
batch.dones.iter().map(|&d| if d { 1.0 } else { 0.0 }).collect();
assert_eq!(dones_f, expected_dones);
}
#[test]
fn test_to_burn_tensors_empty_batch_does_not_panic() {
let batch = ReplayBatch {
observations: vec![],
actions: vec![],
rewards: vec![],
next_observations: vec![],
dones: vec![],
obs_dim: 4,
};
let device = crate::utils::cuda::default_burn_device::<B>();
let t = batch.to_burn_tensors::<B>(&device);
assert_eq!(t.observations.dims(), [0, 4]);
assert_eq!(t.next_observations.dims(), [0, 4]);
assert_eq!(t.actions.dims(), [0]);
assert_eq!(t.rewards.dims(), [0]);
assert_eq!(t.dones.dims(), [0]);
}
}
#[test]
#[should_panic(expected = "ReplayBuffer is empty")]
fn test_sample_empty_panics() {
let buf = ReplayBuffer::new(4, 2);
let mut rng = StdRng::seed_from_u64(0);
let _ = sample(&buf, 2, &mut rng);
}
#[test]
#[should_panic(expected = "batch_size must be > 0")]
fn test_zero_batch_size_panics() {
let mut buf = ReplayBuffer::new(4, 2);
buf.push(&[0.0, 0.0], 0, 0.0, &[0.0, 0.0], false);
let mut rng = StdRng::seed_from_u64(0);
let _ = sample(&buf, 0, &mut rng);
}
}