Skip to main content

thrust_rl/buffer/rollout/
sampling.rs

1//! Data sampling and batching for rollout buffers
2//!
3//! This module provides utilities for creating training batches from
4//! rollout buffers, including minibatch sampling and data shuffling.
5
6use super::storage::RolloutBuffer;
7
8/// Generate minibatch indices for PPO training
9///
10/// Creates shuffled minibatches from a buffer of the given size.
11/// Each minibatch contains approximately `batch_size` samples.
12///
13/// # Arguments
14/// * `buffer_size` - Total number of samples in buffer
15/// * `batch_size` - Desired size of each minibatch
16///
17/// # Returns
18/// Vector of vectors, where each inner vector contains indices for one
19/// minibatch
20pub fn generate_minibatch_indices(buffer_size: usize, batch_size: usize) -> Vec<Vec<usize>> {
21    use rand::seq::SliceRandom;
22
23    let mut indices: Vec<usize> = (0..buffer_size).collect();
24    indices.shuffle(&mut rand::rng());
25
26    indices.chunks(batch_size).map(|chunk| chunk.to_vec()).collect()
27}
28
29/// Sample a minibatch from the rollout buffer
30///
31/// # Arguments
32/// * `buffer` - Source rollout buffer
33/// * `indices` - Indices of samples to include in the batch
34///
35/// # Returns
36/// Minibatch data suitable for training
37pub fn sample_minibatch(buffer: &RolloutBuffer, indices: &[usize]) -> Minibatch {
38    let batch_size = indices.len();
39
40    let mut observations = vec![0.0; batch_size * buffer.shape().2];
41    let mut actions = vec![0; batch_size];
42    let mut old_log_probs = vec![0.0; batch_size];
43    let mut old_values = vec![0.0; batch_size];
44    let mut advantages = vec![0.0; batch_size];
45    let mut returns = vec![0.0; batch_size];
46
47    // Flatten buffer data
48    let flat_obs = buffer.observations().iter().flatten().flatten().cloned().collect::<Vec<f32>>();
49
50    let flat_actions = buffer.actions().iter().flatten().cloned().collect::<Vec<i64>>();
51
52    let flat_log_probs = buffer.log_probs().iter().flatten().cloned().collect::<Vec<f32>>();
53
54    let flat_values = buffer.values().iter().flatten().cloned().collect::<Vec<f32>>();
55
56    let flat_advantages = buffer.advantages().iter().flatten().cloned().collect::<Vec<f32>>();
57
58    let flat_returns = buffer.returns().iter().flatten().cloned().collect::<Vec<f32>>();
59
60    // Sample the minibatch
61    for (i, &idx) in indices.iter().enumerate() {
62        // Copy observation (multiple elements per sample)
63        let obs_start = idx * buffer.shape().2;
64        let obs_end = obs_start + buffer.shape().2;
65        observations[i * buffer.shape().2..(i + 1) * buffer.shape().2]
66            .copy_from_slice(&flat_obs[obs_start..obs_end]);
67
68        actions[i] = flat_actions[idx];
69        old_log_probs[i] = flat_log_probs[idx];
70        old_values[i] = flat_values[idx];
71        advantages[i] = flat_advantages[idx];
72        returns[i] = flat_returns[idx];
73    }
74
75    Minibatch {
76        observations,
77        actions,
78        old_log_probs,
79        old_values,
80        advantages,
81        returns,
82        obs_dim: buffer.shape().2,
83    }
84}
85
86/// Minibatch data for training
87///
88/// Contains a subset of rollout data arranged for efficient training.
89#[derive(Debug, Clone)]
90pub struct Minibatch {
91    /// Observations [batch_size * obs_dim]
92    pub observations: Vec<f32>,
93
94    /// Actions `[batch_size]`
95    pub actions: Vec<i64>,
96
97    /// Old log probabilities `[batch_size]`
98    pub old_log_probs: Vec<f32>,
99
100    /// Old value estimates `[batch_size]`
101    pub old_values: Vec<f32>,
102
103    /// Advantages `[batch_size]`
104    pub advantages: Vec<f32>,
105
106    /// Returns `[batch_size]`
107    pub returns: Vec<f32>,
108
109    /// Observation dimension
110    obs_dim: usize,
111}
112
113impl Minibatch {
114    /// Get batch size
115    pub fn size(&self) -> usize {
116        self.actions.len()
117    }
118
119    /// Get observation shape for neural network input
120    pub fn obs_shape(&self) -> (usize, usize) {
121        (self.size(), self.obs_dim)
122    }
123
124    /// Check if batch is empty
125    pub fn is_empty(&self) -> bool {
126        self.size() == 0
127    }
128}
129
130/// Iterator for generating minibatches from a rollout buffer
131///
132/// Provides an iterator interface for processing rollout data in minibatches.
133/// Useful for training loops that process data incrementally.
134pub struct MinibatchIterator<'a> {
135    buffer: &'a RolloutBuffer,
136    indices: Vec<Vec<usize>>,
137    current_batch: usize,
138}
139
140impl<'a> MinibatchIterator<'a> {
141    /// Create a new minibatch iterator
142    ///
143    /// # Arguments
144    /// * `buffer` - Rollout buffer to iterate over
145    /// * `batch_size` - Size of each minibatch
146    /// * `shuffle` - Whether to shuffle the data order
147    pub fn new(buffer: &'a RolloutBuffer, batch_size: usize, shuffle: bool) -> Self {
148        let buffer_size = buffer.len();
149        let indices = if shuffle {
150            generate_minibatch_indices(buffer_size, batch_size)
151        } else {
152            (0..buffer_size)
153                .collect::<Vec<_>>()
154                .chunks(batch_size)
155                .map(|chunk| chunk.to_vec())
156                .collect()
157        };
158
159        Self { buffer, indices, current_batch: 0 }
160    }
161}
162
163impl<'a> Iterator for MinibatchIterator<'a> {
164    type Item = Minibatch;
165
166    fn next(&mut self) -> Option<Self::Item> {
167        if self.current_batch >= self.indices.len() {
168            return None;
169        }
170
171        let batch_indices = &self.indices[self.current_batch];
172        self.current_batch += 1;
173
174        Some(sample_minibatch(self.buffer, batch_indices))
175    }
176}
177
178/// Create a shuffled sequence of indices for experience replay
179///
180/// This can be used to randomize the order of experience samples
181/// for more robust training.
182///
183/// # Arguments
184/// * `size` - Total number of samples
185///
186/// # Returns
187/// Vector of shuffled indices
188pub fn shuffle_indices(size: usize) -> Vec<usize> {
189    use rand::seq::SliceRandom;
190
191    let mut indices: Vec<usize> = (0..size).collect();
192    indices.shuffle(&mut rand::rng());
193    indices
194}
195
196/// Split buffer into train/validation sets
197///
198/// Useful for hyperparameter tuning or model validation.
199///
200/// # Arguments
201/// * `buffer` - Source buffer to split
202/// * `train_ratio` - Fraction of data to use for training (0.0 to 1.0)
203///
204/// # Returns
205/// (train_indices, val_indices)
206pub fn train_val_split(buffer: &RolloutBuffer, train_ratio: f32) -> (Vec<usize>, Vec<usize>) {
207    let total_size = buffer.len();
208    let train_size = ((total_size as f32) * train_ratio) as usize;
209
210    let indices: Vec<usize> = (0..total_size).collect();
211    // Note: In a real implementation, you'd want to shuffle here
212    // but we'll keep it deterministic for reproducibility
213
214    let train_indices = indices[..train_size].to_vec();
215    let val_indices = indices[train_size..].to_vec();
216
217    (train_indices, val_indices)
218}