Skip to main content

ipfrs_tensorlogic/
data_loader.rs

1//! TensorDataLoader — batch data loading with shuffling and epoch tracking.
2//!
3//! Provides a deterministic, reproducible data loading pipeline for training
4//! loops. Supports configurable batch sizes, Fisher-Yates shuffling with
5//! FNV-1a-based PRNG, epoch tracking, and progress reporting.
6
7/// Configuration for [`TensorDataLoader`].
8#[derive(Debug, Clone)]
9pub struct DataLoaderConfig {
10    /// Number of samples per batch.
11    pub batch_size: usize,
12    /// Whether to shuffle indices at the start of each epoch.
13    pub shuffle: bool,
14    /// If `true`, the last batch is dropped when it has fewer samples than
15    /// `batch_size`.
16    pub drop_last: bool,
17}
18
19impl Default for DataLoaderConfig {
20    fn default() -> Self {
21        Self {
22            batch_size: 32,
23            shuffle: true,
24            drop_last: false,
25        }
26    }
27}
28
29/// A single batch of data yielded by [`TensorDataLoader::next_batch`].
30#[derive(Debug, Clone)]
31pub struct DataBatch {
32    /// Zero-based index of this batch within the current epoch.
33    pub batch_index: usize,
34    /// Sample feature vectors in this batch.
35    pub samples: Vec<Vec<f64>>,
36    /// Corresponding labels.
37    pub labels: Vec<String>,
38    /// The epoch during which this batch was yielded.
39    pub epoch: usize,
40}
41
42/// Aggregate statistics about the data loader state.
43#[derive(Debug, Clone)]
44pub struct DataLoaderStats {
45    pub total_samples: usize,
46    pub batch_size: usize,
47    pub current_epoch: usize,
48    pub batches_yielded: u64,
49    pub progress: f64,
50}
51
52/// Batch data loader with deterministic shuffling and epoch tracking.
53///
54/// Samples are stored as `(feature_vector, label)` pairs. Each epoch iterates
55/// through all samples in (optionally shuffled) order, yielding fixed-size
56/// batches.
57pub struct TensorDataLoader {
58    config: DataLoaderConfig,
59    samples: Vec<(Vec<f64>, String)>,
60    current_index: usize,
61    current_epoch: usize,
62    indices: Vec<usize>,
63    batches_yielded: u64,
64    seed: u64,
65    /// Batch index within the current epoch (reset each epoch).
66    batch_in_epoch: usize,
67    /// Whether the initial shuffle for the current epoch has been performed.
68    epoch_started: bool,
69}
70
71impl std::fmt::Debug for TensorDataLoader {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("TensorDataLoader")
74            .field("config", &self.config)
75            .field("total_samples", &self.samples.len())
76            .field("current_epoch", &self.current_epoch)
77            .field("current_index", &self.current_index)
78            .field("batches_yielded", &self.batches_yielded)
79            .finish()
80    }
81}
82
83// ---------------------------------------------------------------------------
84// FNV-1a based PRNG helpers
85// ---------------------------------------------------------------------------
86
87/// FNV-1a hash of a u64 value (used as PRNG seed mixer).
88fn fnv1a_hash(value: u64) -> u64 {
89    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
90    const FNV_PRIME: u64 = 0x00000100000001B3;
91
92    let bytes = value.to_le_bytes();
93    let mut hash = FNV_OFFSET;
94    for &b in &bytes {
95        hash ^= b as u64;
96        hash = hash.wrapping_mul(FNV_PRIME);
97    }
98    hash
99}
100
101/// Simple deterministic PRNG: returns the next state and a value in `[0, bound)`.
102fn prng_next(state: u64, bound: usize) -> (u64, usize) {
103    let next = fnv1a_hash(state);
104    let value = (next % bound as u64) as usize;
105    (next, value)
106}
107
108impl TensorDataLoader {
109    /// Create a new data loader with the given config and PRNG seed.
110    pub fn new(config: DataLoaderConfig, seed: u64) -> Self {
111        Self {
112            config,
113            samples: Vec::new(),
114            current_index: 0,
115            current_epoch: 0,
116            indices: Vec::new(),
117            batches_yielded: 0,
118            seed,
119            batch_in_epoch: 0,
120            epoch_started: false,
121        }
122    }
123
124    /// Add a single training sample.
125    pub fn add_sample(&mut self, data: Vec<f64>, label: &str) {
126        self.samples.push((data, label.to_string()));
127        self.rebuild_indices();
128    }
129
130    /// Bulk-add training samples.
131    pub fn add_samples(&mut self, samples: Vec<(Vec<f64>, String)>) {
132        self.samples.extend(samples);
133        self.rebuild_indices();
134    }
135
136    /// Return the next batch, or `None` if there are no samples.
137    ///
138    /// When the current epoch is exhausted the loader automatically increments
139    /// the epoch counter, reshuffles (if enabled), and begins yielding batches
140    /// from the new epoch.
141    pub fn next_batch(&mut self) -> Option<DataBatch> {
142        if self.samples.is_empty() {
143            return None;
144        }
145
146        let n = self.samples.len();
147        let remaining = n.saturating_sub(self.current_index);
148
149        // If we have exhausted this epoch, advance.
150        if remaining == 0 {
151            self.advance_epoch();
152        } else if self.config.drop_last && remaining < self.config.batch_size {
153            // Drop incomplete final batch; advance to next epoch.
154            self.advance_epoch();
155        }
156
157        // Ensure indices are initialised and shuffled for this epoch.
158        if !self.epoch_started {
159            if self.indices.is_empty() {
160                self.rebuild_indices();
161            }
162            if self.config.shuffle {
163                self.shuffle_indices();
164            }
165            self.epoch_started = true;
166        }
167
168        self.yield_batch()
169    }
170
171    /// Reset to the beginning of the current epoch without incrementing.
172    pub fn reset(&mut self) {
173        self.current_index = 0;
174        self.batch_in_epoch = 0;
175        self.epoch_started = false;
176    }
177
178    /// Perform a Fisher-Yates shuffle on `self.indices` using an FNV-1a-based
179    /// PRNG seeded from `seed + current_epoch`.
180    pub fn shuffle_indices(&mut self) {
181        let n = self.indices.len();
182        if n <= 1 {
183            return;
184        }
185        // Mix seed with epoch for per-epoch determinism.
186        let mut state = fnv1a_hash(self.seed.wrapping_add(self.current_epoch as u64));
187        for i in (1..n).rev() {
188            let (next_state, j) = prng_next(state, i + 1);
189            state = next_state;
190            self.indices.swap(i, j);
191        }
192    }
193
194    /// Total number of loaded samples.
195    pub fn total_samples(&self) -> usize {
196        self.samples.len()
197    }
198
199    /// Total number of batches per epoch.
200    ///
201    /// Returns `ceil(samples / batch_size)` normally, or
202    /// `floor(samples / batch_size)` when `drop_last` is enabled.
203    pub fn total_batches(&self) -> usize {
204        if self.samples.is_empty() || self.config.batch_size == 0 {
205            return 0;
206        }
207        if self.config.drop_last {
208            self.samples.len() / self.config.batch_size
209        } else {
210            self.samples.len().div_ceil(self.config.batch_size)
211        }
212    }
213
214    /// Current epoch (zero-based).
215    pub fn current_epoch(&self) -> usize {
216        self.current_epoch
217    }
218
219    /// Fraction of the current epoch that has been consumed (`0.0..=1.0`).
220    pub fn progress(&self) -> f64 {
221        if self.samples.is_empty() {
222            return 0.0;
223        }
224        self.current_index as f64 / self.samples.len() as f64
225    }
226
227    /// Snapshot of the loader's current statistics.
228    pub fn stats(&self) -> DataLoaderStats {
229        DataLoaderStats {
230            total_samples: self.samples.len(),
231            batch_size: self.config.batch_size,
232            current_epoch: self.current_epoch,
233            batches_yielded: self.batches_yielded,
234            progress: self.progress(),
235        }
236    }
237
238    // -- private helpers ----------------------------------------------------
239
240    fn rebuild_indices(&mut self) {
241        self.indices = (0..self.samples.len()).collect();
242    }
243
244    fn advance_epoch(&mut self) {
245        self.current_epoch += 1;
246        self.current_index = 0;
247        self.batch_in_epoch = 0;
248        self.epoch_started = false;
249        self.rebuild_indices();
250    }
251
252    fn yield_batch(&mut self) -> Option<DataBatch> {
253        let n = self.samples.len();
254        if self.current_index >= n {
255            return None;
256        }
257
258        let end = (self.current_index + self.config.batch_size).min(n);
259        let batch_indices = &self.indices[self.current_index..end];
260
261        let mut samples = Vec::with_capacity(batch_indices.len());
262        let mut labels = Vec::with_capacity(batch_indices.len());
263        for &idx in batch_indices {
264            let (ref data, ref label) = self.samples[idx];
265            samples.push(data.clone());
266            labels.push(label.clone());
267        }
268
269        let batch = DataBatch {
270            batch_index: self.batch_in_epoch,
271            samples,
272            labels,
273            epoch: self.current_epoch,
274        };
275
276        self.current_index = end;
277        self.batch_in_epoch += 1;
278        self.batches_yielded += 1;
279
280        Some(batch)
281    }
282}
283
284// ===========================================================================
285// Tests
286// ===========================================================================
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn make_samples(n: usize) -> Vec<(Vec<f64>, String)> {
293        (0..n)
294            .map(|i| (vec![i as f64], format!("label_{i}")))
295            .collect()
296    }
297
298    // -- basic batch size ---------------------------------------------------
299
300    #[test]
301    fn test_next_batch_correct_size() {
302        let mut loader = TensorDataLoader::new(
303            DataLoaderConfig {
304                batch_size: 3,
305                shuffle: false,
306                drop_last: false,
307            },
308            42,
309        );
310        loader.add_samples(make_samples(10));
311        let batch = loader.next_batch().expect("should yield batch");
312        assert_eq!(batch.samples.len(), 3);
313        assert_eq!(batch.labels.len(), 3);
314    }
315
316    #[test]
317    fn test_last_batch_smaller_when_not_drop_last() {
318        let mut loader = TensorDataLoader::new(
319            DataLoaderConfig {
320                batch_size: 3,
321                shuffle: false,
322                drop_last: false,
323            },
324            42,
325        );
326        loader.add_samples(make_samples(10));
327        // consume 3 full batches
328        for _ in 0..3 {
329            let _ = loader.next_batch();
330        }
331        let last = loader.next_batch().expect("should yield partial batch");
332        assert_eq!(last.samples.len(), 1); // 10 - 9 = 1
333    }
334
335    #[test]
336    fn test_drop_last_skips_partial_batch() {
337        let mut loader = TensorDataLoader::new(
338            DataLoaderConfig {
339                batch_size: 3,
340                shuffle: false,
341                drop_last: true,
342            },
343            42,
344        );
345        loader.add_samples(make_samples(10));
346        // 10 / 3 = 3 full batches, last (1 sample) dropped
347        let mut count = 0;
348        let mut epoch0_batches = Vec::new();
349        loop {
350            let b = loader.next_batch().expect("should yield");
351            if b.epoch > 0 {
352                break;
353            }
354            epoch0_batches.push(b);
355            count += 1;
356            if count > 20 {
357                panic!("infinite loop guard");
358            }
359        }
360        assert_eq!(epoch0_batches.len(), 3);
361        for b in &epoch0_batches {
362            assert_eq!(b.samples.len(), 3);
363        }
364    }
365
366    // -- epoch tracking -----------------------------------------------------
367
368    #[test]
369    fn test_epoch_increments_after_exhaustion() {
370        let mut loader = TensorDataLoader::new(
371            DataLoaderConfig {
372                batch_size: 5,
373                shuffle: false,
374                drop_last: false,
375            },
376            42,
377        );
378        loader.add_samples(make_samples(5));
379        let b1 = loader.next_batch().expect("batch");
380        assert_eq!(b1.epoch, 0);
381        let b2 = loader.next_batch().expect("batch");
382        assert_eq!(b2.epoch, 1);
383    }
384
385    #[test]
386    fn test_epoch_increments_multiple_times() {
387        let mut loader = TensorDataLoader::new(
388            DataLoaderConfig {
389                batch_size: 2,
390                shuffle: false,
391                drop_last: false,
392            },
393            42,
394        );
395        loader.add_samples(make_samples(2));
396        assert_eq!(loader.next_batch().expect("b").epoch, 0);
397        assert_eq!(loader.next_batch().expect("b").epoch, 1);
398        assert_eq!(loader.next_batch().expect("b").epoch, 2);
399    }
400
401    // -- shuffle ------------------------------------------------------------
402
403    #[test]
404    fn test_shuffle_changes_order() {
405        let mut loader = TensorDataLoader::new(
406            DataLoaderConfig {
407                batch_size: 100,
408                shuffle: true,
409                drop_last: false,
410            },
411            12345,
412        );
413        loader.add_samples(make_samples(20));
414
415        let b0 = loader.next_batch().expect("b");
416        let order0: Vec<f64> = b0.samples.iter().map(|s| s[0]).collect();
417
418        let b1 = loader.next_batch().expect("b");
419        let order1: Vec<f64> = b1.samples.iter().map(|s| s[0]).collect();
420
421        // With high probability the two epochs have different orderings.
422        // (The probability of identical order for 20 elements is 1/20! ~ 0.)
423        assert_ne!(
424            order0, order1,
425            "shuffled orders should differ across epochs"
426        );
427    }
428
429    #[test]
430    fn test_deterministic_with_same_seed() {
431        let make_loader = || {
432            let mut l = TensorDataLoader::new(
433                DataLoaderConfig {
434                    batch_size: 100,
435                    shuffle: true,
436                    drop_last: false,
437                },
438                999,
439            );
440            l.add_samples(make_samples(15));
441            l
442        };
443
444        let mut l1 = make_loader();
445        let mut l2 = make_loader();
446
447        for _ in 0..3 {
448            let b1 = l1.next_batch().expect("b");
449            let b2 = l2.next_batch().expect("b");
450            assert_eq!(b1.samples, b2.samples);
451            assert_eq!(b1.labels, b2.labels);
452        }
453    }
454
455    #[test]
456    fn test_different_seeds_different_order() {
457        let make = |seed| {
458            let mut l = TensorDataLoader::new(
459                DataLoaderConfig {
460                    batch_size: 100,
461                    shuffle: true,
462                    drop_last: false,
463                },
464                seed,
465            );
466            l.add_samples(make_samples(20));
467            l.next_batch().expect("b").samples
468        };
469        let a = make(1);
470        let b = make(2);
471        assert_ne!(a, b);
472    }
473
474    #[test]
475    fn test_no_shuffle_preserves_order() {
476        let mut loader = TensorDataLoader::new(
477            DataLoaderConfig {
478                batch_size: 100,
479                shuffle: false,
480                drop_last: false,
481            },
482            42,
483        );
484        loader.add_samples(make_samples(10));
485        let b = loader.next_batch().expect("b");
486        let order: Vec<f64> = b.samples.iter().map(|s| s[0]).collect();
487        let expected: Vec<f64> = (0..10).map(|i| i as f64).collect();
488        assert_eq!(order, expected);
489    }
490
491    // -- reset --------------------------------------------------------------
492
493    #[test]
494    fn test_reset_restarts_epoch() {
495        let mut loader = TensorDataLoader::new(
496            DataLoaderConfig {
497                batch_size: 2,
498                shuffle: false,
499                drop_last: false,
500            },
501            42,
502        );
503        loader.add_samples(make_samples(6));
504
505        let b1 = loader.next_batch().expect("b");
506        assert_eq!(b1.batch_index, 0);
507        let _ = loader.next_batch(); // batch_index 1
508
509        loader.reset();
510
511        let b3 = loader.next_batch().expect("b after reset");
512        assert_eq!(b3.batch_index, 0);
513        assert_eq!(b3.epoch, 0); // still epoch 0
514    }
515
516    // -- progress -----------------------------------------------------------
517
518    #[test]
519    fn test_progress_tracking() {
520        let mut loader = TensorDataLoader::new(
521            DataLoaderConfig {
522                batch_size: 5,
523                shuffle: false,
524                drop_last: false,
525            },
526            42,
527        );
528        loader.add_samples(make_samples(10));
529
530        assert!((loader.progress() - 0.0).abs() < f64::EPSILON);
531        let _ = loader.next_batch();
532        assert!((loader.progress() - 0.5).abs() < f64::EPSILON);
533        let _ = loader.next_batch();
534        assert!((loader.progress() - 1.0).abs() < f64::EPSILON);
535    }
536
537    #[test]
538    fn test_progress_empty() {
539        let loader = TensorDataLoader::new(DataLoaderConfig::default(), 0);
540        assert!((loader.progress() - 0.0).abs() < f64::EPSILON);
541    }
542
543    // -- empty loader -------------------------------------------------------
544
545    #[test]
546    fn test_empty_loader_returns_none() {
547        let mut loader = TensorDataLoader::new(DataLoaderConfig::default(), 0);
548        assert!(loader.next_batch().is_none());
549    }
550
551    #[test]
552    fn test_empty_loader_total_batches_zero() {
553        let loader = TensorDataLoader::new(DataLoaderConfig::default(), 0);
554        assert_eq!(loader.total_batches(), 0);
555    }
556
557    // -- single sample ------------------------------------------------------
558
559    #[test]
560    fn test_single_sample() {
561        let mut loader = TensorDataLoader::new(
562            DataLoaderConfig {
563                batch_size: 1,
564                shuffle: true,
565                drop_last: false,
566            },
567            42,
568        );
569        loader.add_sample(vec![1.0, 2.0], "only");
570
571        let b = loader.next_batch().expect("b");
572        assert_eq!(b.samples.len(), 1);
573        assert_eq!(b.labels[0], "only");
574        assert_eq!(b.epoch, 0);
575
576        let b2 = loader.next_batch().expect("b2");
577        assert_eq!(b2.epoch, 1);
578    }
579
580    #[test]
581    fn test_single_sample_drop_last_batch_size_2() {
582        let mut loader = TensorDataLoader::new(
583            DataLoaderConfig {
584                batch_size: 2,
585                shuffle: false,
586                drop_last: true,
587            },
588            42,
589        );
590        loader.add_sample(vec![1.0], "a");
591        // With drop_last and batch_size=2, 1 sample is always incomplete.
592        assert_eq!(loader.total_batches(), 0);
593    }
594
595    // -- add_samples bulk ---------------------------------------------------
596
597    #[test]
598    fn test_add_samples_bulk() {
599        let mut loader = TensorDataLoader::new(
600            DataLoaderConfig {
601                batch_size: 5,
602                shuffle: false,
603                drop_last: false,
604            },
605            42,
606        );
607        loader.add_samples(make_samples(10));
608        assert_eq!(loader.total_samples(), 10);
609        assert_eq!(loader.total_batches(), 2);
610    }
611
612    #[test]
613    fn test_add_sample_incremental() {
614        let mut loader = TensorDataLoader::new(
615            DataLoaderConfig {
616                batch_size: 2,
617                shuffle: false,
618                drop_last: false,
619            },
620            42,
621        );
622        loader.add_sample(vec![1.0], "a");
623        loader.add_sample(vec![2.0], "b");
624        loader.add_sample(vec![3.0], "c");
625        assert_eq!(loader.total_samples(), 3);
626        assert_eq!(loader.total_batches(), 2); // ceil(3/2)
627    }
628
629    // -- stats --------------------------------------------------------------
630
631    #[test]
632    fn test_stats_accuracy() {
633        let mut loader = TensorDataLoader::new(
634            DataLoaderConfig {
635                batch_size: 4,
636                shuffle: false,
637                drop_last: false,
638            },
639            42,
640        );
641        loader.add_samples(make_samples(10));
642
643        let s0 = loader.stats();
644        assert_eq!(s0.total_samples, 10);
645        assert_eq!(s0.batch_size, 4);
646        assert_eq!(s0.current_epoch, 0);
647        assert_eq!(s0.batches_yielded, 0);
648        assert!((s0.progress - 0.0).abs() < f64::EPSILON);
649
650        let _ = loader.next_batch(); // 4 consumed
651        let s1 = loader.stats();
652        assert_eq!(s1.batches_yielded, 1);
653        assert!((s1.progress - 0.4).abs() < f64::EPSILON);
654    }
655
656    #[test]
657    fn test_stats_after_epoch_change() {
658        let mut loader = TensorDataLoader::new(
659            DataLoaderConfig {
660                batch_size: 5,
661                shuffle: false,
662                drop_last: false,
663            },
664            42,
665        );
666        loader.add_samples(make_samples(5));
667
668        let _ = loader.next_batch(); // epoch 0
669        let _ = loader.next_batch(); // epoch 1
670        let s = loader.stats();
671        assert_eq!(s.current_epoch, 1);
672        assert_eq!(s.batches_yielded, 2);
673    }
674
675    // -- total_batches ------------------------------------------------------
676
677    #[test]
678    fn test_total_batches_ceil() {
679        let mut loader = TensorDataLoader::new(
680            DataLoaderConfig {
681                batch_size: 3,
682                shuffle: false,
683                drop_last: false,
684            },
685            0,
686        );
687        loader.add_samples(make_samples(10));
688        assert_eq!(loader.total_batches(), 4); // ceil(10/3)
689    }
690
691    #[test]
692    fn test_total_batches_floor_drop_last() {
693        let mut loader = TensorDataLoader::new(
694            DataLoaderConfig {
695                batch_size: 3,
696                shuffle: false,
697                drop_last: true,
698            },
699            0,
700        );
701        loader.add_samples(make_samples(10));
702        assert_eq!(loader.total_batches(), 3); // floor(10/3)
703    }
704
705    #[test]
706    fn test_total_batches_exact_division() {
707        let mut loader = TensorDataLoader::new(
708            DataLoaderConfig {
709                batch_size: 5,
710                shuffle: false,
711                drop_last: false,
712            },
713            0,
714        );
715        loader.add_samples(make_samples(10));
716        assert_eq!(loader.total_batches(), 2);
717    }
718
719    // -- batch_index within epoch -------------------------------------------
720
721    #[test]
722    fn test_batch_index_increments_within_epoch() {
723        let mut loader = TensorDataLoader::new(
724            DataLoaderConfig {
725                batch_size: 2,
726                shuffle: false,
727                drop_last: false,
728            },
729            42,
730        );
731        loader.add_samples(make_samples(6));
732
733        for expected in 0..3 {
734            let b = loader.next_batch().expect("batch");
735            assert_eq!(b.batch_index, expected);
736        }
737        // Next call triggers new epoch; batch_index resets.
738        let b_new_epoch = loader.next_batch().expect("batch");
739        assert_eq!(b_new_epoch.batch_index, 0);
740        assert_eq!(b_new_epoch.epoch, 1);
741    }
742
743    // -- multiple epochs consistency ----------------------------------------
744
745    #[test]
746    fn test_multiple_epochs_all_samples_seen() {
747        let n = 7;
748        let mut loader = TensorDataLoader::new(
749            DataLoaderConfig {
750                batch_size: 3,
751                shuffle: true,
752                drop_last: false,
753            },
754            42,
755        );
756        loader.add_samples(make_samples(n));
757
758        // Collect all batches for several epochs and verify each epoch covers
759        // all samples exactly once.
760        let num_epochs = 3;
761        let batches_per_epoch = loader.total_batches(); // ceil(7/3) = 3
762        for epoch in 0..num_epochs {
763            let mut seen = std::collections::HashSet::new();
764            for _ in 0..batches_per_epoch {
765                let b = loader.next_batch().expect("should yield");
766                assert_eq!(b.epoch, epoch, "unexpected epoch");
767                for s in &b.samples {
768                    seen.insert(s[0] as usize);
769                }
770            }
771            assert_eq!(seen.len(), n, "epoch {epoch}: not all samples seen");
772        }
773    }
774
775    // -- debug impl ---------------------------------------------------------
776
777    #[test]
778    fn test_debug_impl() {
779        let loader = TensorDataLoader::new(DataLoaderConfig::default(), 0);
780        let dbg = format!("{loader:?}");
781        assert!(dbg.contains("TensorDataLoader"));
782    }
783
784    // -- default config -----------------------------------------------------
785
786    #[test]
787    fn test_default_config() {
788        let cfg = DataLoaderConfig::default();
789        assert_eq!(cfg.batch_size, 32);
790        assert!(cfg.shuffle);
791        assert!(!cfg.drop_last);
792    }
793
794    // -- current_epoch accessor ---------------------------------------------
795
796    #[test]
797    fn test_current_epoch_accessor() {
798        let mut loader = TensorDataLoader::new(
799            DataLoaderConfig {
800                batch_size: 1,
801                shuffle: false,
802                drop_last: false,
803            },
804            0,
805        );
806        loader.add_sample(vec![1.0], "a");
807        assert_eq!(loader.current_epoch(), 0);
808        let _ = loader.next_batch();
809        assert_eq!(loader.current_epoch(), 0);
810        let _ = loader.next_batch();
811        assert_eq!(loader.current_epoch(), 1);
812    }
813}