use anyhow::{Result, anyhow};
use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
#[derive(Debug, Clone)]
pub struct Demonstrations {
obs: Vec<f32>,
actions: Vec<i64>,
obs_dim: usize,
len: usize,
}
impl Demonstrations {
pub fn new(obs: Vec<f32>, actions: Vec<i64>, obs_dim: usize) -> Result<Self> {
if obs_dim == 0 {
return Err(anyhow!("obs_dim must be positive"));
}
let len = actions.len();
if obs.len() != len * obs_dim {
return Err(anyhow!(
"obs length {} does not match actions.len() ({}) * obs_dim ({}) = {}",
obs.len(),
len,
obs_dim,
len * obs_dim
));
}
Ok(Self { obs, actions, obs_dim, len })
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn obs_dim(&self) -> usize {
self.obs_dim
}
pub fn batch<B: Backend>(
&self,
indices: &[usize],
device: &B::Device,
) -> (Tensor<B, 2>, Tensor<B, 1, Int>) {
let k = indices.len();
let mut obs_data = Vec::with_capacity(k * self.obs_dim);
let mut action_data = Vec::with_capacity(k);
for &i in indices {
assert!(i < self.len, "index {} out of bounds for dataset of len {}", i, self.len);
let start = i * self.obs_dim;
obs_data.extend_from_slice(&self.obs[start..start + self.obs_dim]);
action_data.push(self.actions[i]);
}
let obs = Tensor::<B, 2>::from_data(TensorData::new(obs_data, [k, self.obs_dim]), device);
let actions = Tensor::<B, 1, Int>::from_data(TensorData::new(action_data, [k]), device);
(obs, actions)
}
}
#[cfg(test)]
mod tests {
use burn::backend::{Autodiff, NdArray};
use super::*;
use crate::train::ppo::loss::generate_minibatch_indices_with_rng;
type B = Autodiff<NdArray<f32>>;
fn sample_dataset() -> Demonstrations {
let obs = vec![
0.0, 0.1, 1.0, 1.1, 2.0, 2.1, 3.0, 3.1, ];
let actions = vec![0i64, 1, 0, 1];
Demonstrations::new(obs, actions, 2).unwrap()
}
#[test]
fn test_new_validates_dimensions() {
assert!(Demonstrations::new(vec![0.0; 8], vec![0i64; 4], 2).is_ok());
assert!(Demonstrations::new(vec![0.0; 6], vec![0i64; 4], 2).is_err());
assert!(Demonstrations::new(vec![0.0; 10], vec![0i64; 4], 2).is_err());
assert!(Demonstrations::new(vec![], vec![], 0).is_err());
}
#[test]
fn test_len_and_is_empty() {
let ds = sample_dataset();
assert_eq!(ds.len(), 4);
assert_eq!(ds.obs_dim(), 2);
assert!(!ds.is_empty());
let empty = Demonstrations::new(vec![], vec![], 2).unwrap();
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
}
#[test]
fn test_batch_shapes_and_contents() {
let ds = sample_dataset();
let device = Default::default();
let indices = [2usize, 1];
let (obs, actions) = ds.batch::<B>(&indices, &device);
assert_eq!(obs.dims(), [2, 2]);
assert_eq!(actions.dims(), [2]);
let obs_vals: Vec<f32> = obs.into_data().to_vec().unwrap();
assert_eq!(obs_vals, vec![2.0, 2.1, 1.0, 1.1]);
let action_vals: Vec<i64> = actions.into_data().to_vec().unwrap();
assert_eq!(action_vals, vec![0, 1]);
}
#[test]
fn test_seeded_minibatch_shuffle_is_reproducible() {
use rand::{SeedableRng, rngs::StdRng};
let ds = sample_dataset();
let mut rng_a = StdRng::seed_from_u64(7);
let batches_a = generate_minibatch_indices_with_rng(ds.len(), 2, &mut rng_a);
let mut rng_b = StdRng::seed_from_u64(7);
let batches_b = generate_minibatch_indices_with_rng(ds.len(), 2, &mut rng_b);
assert_eq!(batches_a, batches_b);
let mut seen: Vec<usize> = batches_a.iter().flatten().copied().collect();
seen.sort_unstable();
assert_eq!(seen, vec![0, 1, 2, 3]);
}
}