Skip to main content

burn_dataset/transform/
selection.rs

1use crate::Dataset;
2use crate::DatasetError;
3use crate::transform::RngSource;
4use rand::prelude::SliceRandom;
5use rand::rngs::StdRng;
6use std::marker::PhantomData;
7use std::sync::Arc;
8
9/// Generates a vector of indices from 0 to size - 1.
10///
11/// # Arguments
12///
13/// * `size` - The size of the dataset.
14///
15/// # Returns
16///
17/// A vector containing indices from 0 to size - 1.
18#[inline(always)]
19pub fn iota(size: usize) -> Vec<usize> {
20    (0..size).collect()
21}
22
23/// Generates a shuffled vector of indices up to a size.
24///
25/// # Arguments
26///
27/// * `size` - The size of the dataset to shuffle.
28///
29/// # Returns
30///
31/// A vector of shuffled indices.
32#[inline(always)]
33pub fn shuffled_indices(size: usize, rng: &mut StdRng) -> Vec<usize> {
34    let mut indices = iota(size);
35    indices.shuffle(rng);
36    indices
37}
38
39/// A dataset that selects a subset of indices from an existing dataset.
40///
41/// Indices may appear multiple times, but they must be within the bounds of the original dataset.
42#[derive(Clone)]
43pub struct SelectionDataset<D, I>
44where
45    D: Dataset<I>,
46    I: Clone + Send + Sync,
47{
48    /// The wrapped dataset from which to select indices.
49    pub wrapped: Arc<D>,
50
51    /// The indices to select from the wrapped dataset.
52    pub indices: Vec<usize>,
53
54    input: PhantomData<I>,
55}
56
57impl<D, I> SelectionDataset<D, I>
58where
59    D: Dataset<I>,
60    I: Clone + Send + Sync,
61{
62    /// Creates a new selection dataset with the given dataset and indices.
63    ///
64    /// Checks that all indices are within the bounds of the dataset.
65    ///
66    /// # Arguments
67    ///
68    /// * `dataset` - The original dataset to select from.
69    /// * `indices` - A slice of indices to select from the dataset.
70    ///   These indices must be within the bounds of the dataset.
71    ///
72    /// # Panics
73    ///
74    /// Panics if any index is out of bounds for the dataset.
75    pub fn from_indices_checked<S>(dataset: S, indices: Vec<usize>) -> Self
76    where
77        S: Into<Arc<D>>,
78    {
79        let dataset = dataset.into();
80
81        let size = dataset.len();
82        if let Some(idx) = indices.iter().find(|&i| *i >= size) {
83            panic!("Index out of bounds for wrapped dataset size: {idx} >= {size}");
84        }
85
86        Self::from_indices_unchecked(dataset, indices)
87    }
88
89    /// Creates a new selection dataset with the given dataset and indices without checking bounds.
90    ///
91    /// # Arguments
92    ///
93    /// * `dataset` - The original dataset to select from.
94    /// * `indices` - A vector of indices to select from the dataset.
95    ///
96    /// # Safety
97    ///
98    /// This function does not check if the indices are within the bounds of the dataset.
99    pub fn from_indices_unchecked<S>(dataset: S, indices: Vec<usize>) -> Self
100    where
101        S: Into<Arc<D>>,
102    {
103        Self {
104            wrapped: dataset.into(),
105            indices,
106            input: PhantomData,
107        }
108    }
109
110    /// Creates a new selection dataset that selects all indices from the dataset.
111    ///
112    /// This allocates a 1-to-1 mapping of indices to the dataset size,
113    /// essentially functioning as a no-op selection. This is only useful
114    /// when the dataset will later be shuffled or transformed in place.
115    ///
116    /// # Arguments
117    ///
118    /// * `dataset` - The original dataset to select from.
119    ///
120    /// # Returns
121    ///
122    /// A new `SelectionDataset` that selects all indices from the dataset.
123    pub fn new_select_all<S>(dataset: S) -> Self
124    where
125        S: Into<Arc<D>>,
126    {
127        let dataset = dataset.into();
128        let size = dataset.len();
129        Self::from_indices_unchecked(dataset, iota(size))
130    }
131
132    /// Creates a new selection dataset with shuffled indices.
133    ///
134    /// Selects every index of the dataset and shuffles them
135    /// with randomness from the provided random number generator.
136    ///
137    /// # Arguments
138    ///
139    /// * `dataset` - The original dataset to select from.
140    /// * `rng` - A mutable reference to a random number generator.
141    ///
142    /// # Returns
143    ///
144    /// A new `SelectionDataset` with shuffled indices.
145    pub fn new_shuffled<S, R>(dataset: S, rng_source: R) -> Self
146    where
147        S: Into<Arc<D>>,
148        R: Into<RngSource>,
149    {
150        let mut this = Self::new_select_all(dataset);
151        this.shuffle(rng_source);
152        this
153    }
154
155    /// Shuffles the indices of the dataset using a mutable random number generator.
156    ///
157    /// This method modifies the dataset in place, shuffling the indices.
158    ///
159    /// # Arguments
160    ///
161    /// * `rng` - A mutable reference to a random number generator.
162    pub fn shuffle<R>(&mut self, rng_source: R)
163    where
164        R: Into<RngSource>,
165    {
166        let mut rng: StdRng = rng_source.into().into();
167        self.indices.shuffle(&mut rng)
168    }
169
170    /// Creates a new dataset that is a slice of the current selection dataset.
171    ///
172    /// Slices the *selection indices* from ``[start..end]``.
173    ///
174    /// Independent of future shuffles on the parent, but shares the same wrapped dataset.
175    ///
176    ///
177    /// # Arguments
178    ///
179    /// * `start` - The start of the range.
180    /// * `end` - The end of the range (exclusive).
181    // TODO: SliceArg in burn-tensor should be lifted to burn-std; this should use SliceArg.
182    pub fn slice(&self, start: usize, end: usize) -> Self {
183        Self::from_indices_unchecked(self.wrapped.clone(), self.indices[start..end].to_vec())
184    }
185
186    /// Split into `num` datasets by slicing the selection indices evenly.
187    ///
188    /// Split is done via `slice`, so the datasets share the same wrapped dataset.
189    ///
190    /// Independent of future shuffles on the parent, but shares the same wrapped dataset.
191    ///
192    /// # Arguments
193    ///
194    /// * `num` - The number of datasets to split into.
195    ///
196    /// # Returns
197    ///
198    /// A vector of `SelectionDataset` instances, each containing a subset of the indices.
199    pub fn split(&self, num: usize) -> Vec<Self> {
200        let n = self.indices.len();
201
202        let mut current = 0;
203        let mut datasets = Vec::with_capacity(num);
204
205        let batch_size = n / num;
206        for i in 0..num {
207            let start = current;
208            let mut end = current + batch_size;
209
210            if i == (num - 1) {
211                end = n;
212            }
213
214            let dataset = self.slice(start, end);
215
216            current += batch_size;
217            datasets.push(dataset);
218        }
219
220        datasets
221    }
222}
223
224impl<D, I> Dataset<I> for SelectionDataset<D, I>
225where
226    D: Dataset<I>,
227    I: Clone + Send + Sync,
228{
229    fn get(&self, index: usize) -> Result<I, DatasetError> {
230        let Some(&index) = self.indices.get(index) else {
231            panic!(
232                "Index out of bounds for SelectionDataset: {index} >= {}",
233                self.indices.len()
234            );
235        };
236        self.wrapped.get(index)
237    }
238
239    fn get_many(&self, indexes: Vec<usize>) -> Result<Vec<I>, DatasetError> {
240        let translated: Vec<usize> = indexes
241            .into_iter()
242            .map(|i| {
243                self.indices.get(i).copied().unwrap_or_else(|| {
244                    panic!(
245                        "Index out of bounds for SelectionDataset: {i} >= {}",
246                        self.indices.len()
247                    )
248                })
249            })
250            .collect();
251        self.wrapped.get_many(translated)
252    }
253
254    fn len(&self) -> usize {
255        self.indices.len()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::FakeDataset;
263    use rand::SeedableRng;
264
265    #[test]
266    fn test_iota() {
267        let size = 10;
268        let indices = iota(size);
269        assert_eq!(indices.len(), size);
270        assert_eq!(indices, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
271    }
272
273    #[test]
274    fn test_shuffled_indices_same_seed_is_deterministic() {
275        let size = 10;
276
277        let mut rng1 = StdRng::seed_from_u64(10);
278        // `StdRng` is no longer `Clone`, so its internal state cannot be duplicated.
279        // To test determinism, we must explicitly create a second RNG from the same seed.
280        let mut rng2 = StdRng::seed_from_u64(10);
281
282        let mut expected = iota(size);
283        expected.shuffle(&mut rng1);
284
285        let indices = shuffled_indices(size, &mut rng2);
286
287        assert_eq!(indices, expected);
288    }
289
290    #[test]
291    fn test_shuffled_indices_forked_rngs_differ() {
292        let size = 10;
293
294        let mut rng1 = StdRng::seed_from_u64(10);
295        let mut rng2 = rng1.fork();
296
297        let mut a = iota(size);
298        let mut b = iota(size);
299
300        a.shuffle(&mut rng1);
301        b.shuffle(&mut rng2);
302
303        assert_ne!(a, b);
304    }
305
306    #[should_panic(expected = "Index out of bounds for wrapped dataset size: 300 >= 27")]
307    #[test]
308    fn test_from_indices_checked_panics() {
309        let source_dataset = FakeDataset::<String>::new(27);
310        let indices: Vec<usize> = vec![15, 1, 12, 300];
311        SelectionDataset::from_indices_checked(source_dataset, indices);
312    }
313
314    #[test]
315    fn test_checked_selection_dataset() {
316        let source_dataset = FakeDataset::<String>::new(27);
317
318        let indices: Vec<usize> = vec![15, 1, 12, 12];
319        let expected: Vec<String> = indices
320            .iter()
321            .map(|i| source_dataset.get(*i).unwrap())
322            .collect();
323
324        let selection = SelectionDataset::from_indices_checked(source_dataset, indices.clone());
325
326        assert_eq!(&selection.indices, &indices);
327
328        let items = selection.iter().map(Result::unwrap).collect::<Vec<_>>();
329
330        assert_eq!(items, expected);
331    }
332
333    #[test]
334    fn test_selection_dataset_get_many() {
335        let source_dataset = FakeDataset::<String>::new(27);
336
337        let indices: Vec<usize> = vec![15, 1, 12, 12];
338        let selection = SelectionDataset::from_indices_checked(source_dataset, indices);
339
340        // Local indices, out of order and with a duplicate, mapping through `selection.indices`.
341        let requested = vec![3, 0, 2, 0];
342        let expected: Vec<String> = requested
343            .iter()
344            .map(|&i| selection.get(i).unwrap())
345            .collect();
346
347        let items = selection.get_many(requested).unwrap();
348
349        assert_eq!(items, expected);
350    }
351
352    #[test]
353    #[should_panic(expected = "Index out of bounds for SelectionDataset: 4 >= 4")]
354    fn test_selection_dataset_get_many_out_of_bounds() {
355        let source_dataset = FakeDataset::<String>::new(27);
356        let indices: Vec<usize> = vec![15, 1, 12, 12];
357        let selection = SelectionDataset::from_indices_checked(source_dataset, indices);
358
359        let _ = selection.get_many(vec![0, 4]);
360    }
361
362    #[test]
363    fn test_shuffled_dataset() {
364        let dataset = FakeDataset::<String>::new(27);
365        let source_items = dataset.iter().map(Result::unwrap).collect::<Vec<_>>();
366
367        let selection = SelectionDataset::new_shuffled(dataset, 42);
368
369        let indices = shuffled_indices(source_items.len(), &mut StdRng::seed_from_u64(42));
370
371        assert_eq!(&selection.indices, &indices);
372        assert_eq!(selection.len(), source_items.len());
373
374        let expected_items: Vec<_> = indices
375            .iter()
376            .map(|&i| source_items[i].to_string())
377            .collect();
378        assert_eq!(
379            &selection.iter().map(Result::unwrap).collect::<Vec<_>>(),
380            &expected_items
381        );
382    }
383
384    #[test]
385    fn test_slice() {
386        let dataset = FakeDataset::<String>::new(27);
387        let source_items = dataset.iter().map(Result::unwrap).collect::<Vec<_>>();
388
389        let selection = SelectionDataset::new_select_all(dataset);
390
391        let start = 5;
392        let end = 15;
393        let sliced_selection = selection.slice(start, end);
394
395        assert_eq!(sliced_selection.len(), end - start);
396
397        #[allow(clippy::needless_range_loop)]
398        for i in start..end {
399            assert_eq!(
400                sliced_selection.get(i - start).unwrap(),
401                source_items[i].to_string()
402            );
403        }
404    }
405
406    #[test]
407    fn test_split() {
408        let dataset = FakeDataset::<String>::new(28);
409        let source_items = dataset.iter().map(Result::unwrap).collect::<Vec<_>>();
410
411        let selection = SelectionDataset::new_select_all(dataset);
412
413        let split_contents: Vec<Vec<_>> = selection
414            .split(3)
415            .iter()
416            .map(|d| d.iter().map(Result::unwrap).collect::<Vec<_>>())
417            .collect();
418        assert_eq!(
419            split_contents,
420            vec![
421                source_items[0..9].to_vec(),
422                source_items[9..18].to_vec(),
423                source_items[18..28].to_vec(),
424            ]
425        );
426    }
427}