1#[derive(Debug, Clone)]
9pub struct DataLoaderConfig {
10 pub batch_size: usize,
12 pub shuffle: bool,
14 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#[derive(Debug, Clone)]
31pub struct DataBatch {
32 pub batch_index: usize,
34 pub samples: Vec<Vec<f64>>,
36 pub labels: Vec<String>,
38 pub epoch: usize,
40}
41
42#[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
52pub 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_in_epoch: usize,
67 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
83fn 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
101fn 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 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 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 pub fn add_samples(&mut self, samples: Vec<(Vec<f64>, String)>) {
132 self.samples.extend(samples);
133 self.rebuild_indices();
134 }
135
136 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 remaining == 0 {
151 self.advance_epoch();
152 } else if self.config.drop_last && remaining < self.config.batch_size {
153 self.advance_epoch();
155 }
156
157 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 pub fn reset(&mut self) {
173 self.current_index = 0;
174 self.batch_in_epoch = 0;
175 self.epoch_started = false;
176 }
177
178 pub fn shuffle_indices(&mut self) {
181 let n = self.indices.len();
182 if n <= 1 {
183 return;
184 }
185 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 pub fn total_samples(&self) -> usize {
196 self.samples.len()
197 }
198
199 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 pub fn current_epoch(&self) -> usize {
216 self.current_epoch
217 }
218
219 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 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 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#[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 #[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 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); }
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 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 #[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 #[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 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 #[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(); 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); }
515
516 #[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 #[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 #[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 assert_eq!(loader.total_batches(), 0);
593 }
594
595 #[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); }
628
629 #[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(); 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(); let _ = loader.next_batch(); let s = loader.stats();
671 assert_eq!(s.current_epoch, 1);
672 assert_eq!(s.batches_yielded, 2);
673 }
674
675 #[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); }
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); }
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 #[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 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 #[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 let num_epochs = 3;
761 let batches_per_epoch = loader.total_batches(); 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 #[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 #[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 #[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}