1use std::collections::HashMap;
28
29struct Xorshift64 {
33 state: u64,
34}
35
36impl Xorshift64 {
37 fn new(seed: u64) -> Self {
39 let state = if seed == 0 { 0x853c49e6748fea9b } else { seed };
40 Self { state }
41 }
42
43 fn next_u64(&mut self) -> u64 {
45 let mut x = self.state;
46 x ^= x << 13;
47 x ^= x >> 7;
48 x ^= x << 17;
49 self.state = x;
50 x
51 }
52
53 fn next_usize(&mut self, n: usize) -> usize {
55 if n == 0 {
56 return 0;
57 }
58 (self.next_u64() as usize) % n
59 }
60}
61
62fn fisher_yates_shuffle(indices: &mut [usize], rng: &mut Xorshift64) {
64 let n = indices.len();
65 for i in (1..n).rev() {
66 let j = rng.next_usize(i + 1);
67 indices.swap(i, j);
68 }
69}
70
71#[derive(Debug, Clone)]
75pub struct BatchSamplerConfig {
76 pub batch_size: usize,
78 pub shuffle: bool,
80 pub seed: Option<u64>,
82 pub drop_last: bool,
84 pub stratified: bool,
87}
88
89impl Default for BatchSamplerConfig {
90 fn default() -> Self {
91 Self {
92 batch_size: 32,
93 shuffle: false,
94 seed: None,
95 drop_last: false,
96 stratified: false,
97 }
98 }
99}
100
101pub struct BatchSampler {
106 indices: Vec<usize>,
108 config: BatchSamplerConfig,
110 current_pos: usize,
112 epoch: usize,
114 rng: Xorshift64,
117}
118
119impl BatchSampler {
120 pub fn new(n_samples: usize, config: BatchSamplerConfig) -> Self {
122 let seed = config.seed.unwrap_or(0xa1b2c3d4e5f6_u64);
123 let mut rng = Xorshift64::new(seed);
124 let mut indices: Vec<usize> = (0..n_samples).collect();
125 if config.shuffle {
126 fisher_yates_shuffle(&mut indices, &mut rng);
127 }
128 Self {
129 indices,
130 config,
131 current_pos: 0,
132 epoch: 0,
133 rng,
134 }
135 }
136
137 pub fn from_labels(labels: &[usize], config: BatchSamplerConfig) -> Self {
144 let n_samples = labels.len();
145 let seed = config.seed.unwrap_or(0xa1b2c3d4e5f6_u64);
146 let mut rng = Xorshift64::new(seed);
147
148 let indices = if config.stratified {
149 let mut class_buckets: HashMap<usize, Vec<usize>> = HashMap::new();
151 for (idx, &label) in labels.iter().enumerate() {
152 class_buckets.entry(label).or_default().push(idx);
153 }
154
155 let mut sorted_classes: Vec<usize> = class_buckets.keys().copied().collect();
157 sorted_classes.sort_unstable();
158
159 let mut buckets: Vec<Vec<usize>> = sorted_classes
160 .into_iter()
161 .map(|cls| {
162 let mut bucket = class_buckets.remove(&cls).unwrap_or_default();
163 if config.shuffle {
164 fisher_yates_shuffle(&mut bucket, &mut rng);
165 }
166 bucket
167 })
168 .collect();
169
170 let mut interleaved = Vec::with_capacity(n_samples);
172 loop {
173 let mut any_pushed = false;
174 for bucket in &mut buckets {
175 if let Some(idx) = bucket.first().copied() {
176 *bucket = bucket[1..].to_vec();
177 interleaved.push(idx);
178 any_pushed = true;
179 }
180 }
181 if !any_pushed {
182 break;
183 }
184 }
185 interleaved
186 } else {
187 let mut idxs: Vec<usize> = (0..n_samples).collect();
188 if config.shuffle {
189 fisher_yates_shuffle(&mut idxs, &mut rng);
190 }
191 idxs
192 };
193
194 Self {
195 indices,
196 config,
197 current_pos: 0,
198 epoch: 0,
199 rng,
200 }
201 }
202
203 pub fn next_batch(&mut self) -> Option<Vec<usize>> {
205 let remaining = self.indices.len().saturating_sub(self.current_pos);
206 if remaining == 0 {
207 return None;
208 }
209
210 if self.config.drop_last && remaining < self.config.batch_size {
211 return None;
212 }
213
214 let end = (self.current_pos + self.config.batch_size).min(self.indices.len());
215 let batch = self.indices[self.current_pos..end].to_vec();
216 self.current_pos = end;
217 Some(batch)
218 }
219
220 pub fn reset(&mut self) {
224 self.epoch += 1;
225 self.current_pos = 0;
226 if self.config.shuffle {
227 let epoch_mix = self.epoch as u64 * 0x9e3779b97f4a7c15;
230 let mut epoch_rng = Xorshift64::new(self.rng.state ^ epoch_mix);
231 fisher_yates_shuffle(&mut self.indices, &mut epoch_rng);
232 self.rng.state = epoch_rng.state;
234 }
235 }
236
237 pub fn n_batches(&self) -> usize {
239 let n = self.indices.len();
240 let bs = self.config.batch_size;
241 if bs == 0 {
242 return 0;
243 }
244 if self.config.drop_last {
245 n / bs
246 } else {
247 n.div_ceil(bs)
248 }
249 }
250
251 pub fn epoch(&self) -> usize {
253 self.epoch
254 }
255}
256
257pub fn train_val_test_split(
270 n_samples: usize,
271 labels: Option<&[usize]>,
272 train_frac: f64,
273 val_frac: f64,
274 seed: Option<u64>,
275) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
276 let rng_seed = seed.unwrap_or(0xdeadbeef_cafebabe);
277 let mut rng = Xorshift64::new(rng_seed);
278
279 let do_stratify = labels.is_some();
280
281 if do_stratify {
282 let labels = labels.expect("checked above");
283 let mut class_buckets: HashMap<usize, Vec<usize>> = HashMap::new();
285 for (idx, &label) in labels.iter().enumerate() {
286 class_buckets.entry(label).or_default().push(idx);
287 }
288
289 let mut train_set = Vec::new();
290 let mut val_set = Vec::new();
291 let mut test_set = Vec::new();
292
293 let mut sorted_classes: Vec<usize> = class_buckets.keys().copied().collect();
294 sorted_classes.sort_unstable();
295
296 for cls in sorted_classes {
297 let mut bucket = class_buckets.remove(&cls).unwrap_or_default();
298 fisher_yates_shuffle(&mut bucket, &mut rng);
299 let n = bucket.len();
300 let n_train = ((n as f64 * train_frac).round() as usize).min(n);
301 let n_val = ((n as f64 * val_frac).round() as usize).min(n.saturating_sub(n_train));
302 train_set.extend_from_slice(&bucket[..n_train]);
303 val_set.extend_from_slice(&bucket[n_train..n_train + n_val]);
304 test_set.extend_from_slice(&bucket[n_train + n_val..]);
305 }
306
307 (train_set, val_set, test_set)
308 } else {
309 let mut all: Vec<usize> = (0..n_samples).collect();
310 fisher_yates_shuffle(&mut all, &mut rng);
311
312 let n_train = ((n_samples as f64 * train_frac).round() as usize).min(n_samples);
313 let n_val =
314 ((n_samples as f64 * val_frac).round() as usize).min(n_samples.saturating_sub(n_train));
315
316 let train_set = all[..n_train].to_vec();
317 let val_set = all[n_train..n_train + n_val].to_vec();
318 let test_set = all[n_train + n_val..].to_vec();
319
320 (train_set, val_set, test_set)
321 }
322}
323
324#[cfg(test)]
327mod tests {
328 use super::*;
329 use std::collections::HashSet;
330
331 #[test]
332 fn test_batch_sampler_basic() {
333 let config = BatchSamplerConfig {
334 batch_size: 10,
335 shuffle: false,
336 seed: None,
337 drop_last: false,
338 stratified: false,
339 };
340 let mut sampler = BatchSampler::new(100, config);
341 let n = sampler.n_batches();
342 assert_eq!(n, 10, "expected 10 batches for 100 samples / batch_size 10");
343
344 let mut count = 0usize;
345 let mut all_indices: Vec<usize> = Vec::new();
346 while let Some(batch) = sampler.next_batch() {
347 assert_eq!(batch.len(), 10, "each batch should have 10 elements");
348 all_indices.extend_from_slice(&batch);
349 count += 1;
350 }
351 assert_eq!(count, 10);
352 let unique: HashSet<usize> = all_indices.into_iter().collect();
354 assert_eq!(unique.len(), 100);
355 }
356
357 #[test]
358 fn test_batch_sampler_drop_last() {
359 let config = BatchSamplerConfig {
360 batch_size: 10,
361 shuffle: false,
362 seed: None,
363 drop_last: true,
364 stratified: false,
365 };
366 let mut sampler = BatchSampler::new(105, config);
367 assert_eq!(sampler.n_batches(), 10, "drop_last should yield 10 batches");
368
369 let mut count = 0usize;
370 while let Some(_batch) = sampler.next_batch() {
371 count += 1;
372 }
373 assert_eq!(
374 count, 10,
375 "should get exactly 10 batches with drop_last=true"
376 );
377 }
378
379 #[test]
380 fn test_batch_sampler_no_drop_last() {
381 let config = BatchSamplerConfig {
383 batch_size: 10,
384 shuffle: false,
385 seed: None,
386 drop_last: false,
387 stratified: false,
388 };
389 let mut sampler = BatchSampler::new(105, config);
390 assert_eq!(sampler.n_batches(), 11);
391
392 let mut count = 0usize;
393 let mut total_items = 0usize;
394 while let Some(batch) = sampler.next_batch() {
395 total_items += batch.len();
396 count += 1;
397 }
398 assert_eq!(count, 11);
399 assert_eq!(total_items, 105);
400 }
401
402 #[test]
403 fn test_batch_sampler_shuffle() {
404 let config = BatchSamplerConfig {
405 batch_size: 10,
406 shuffle: true,
407 seed: Some(12345),
408 drop_last: false,
409 stratified: false,
410 };
411 let mut sampler = BatchSampler::new(50, config);
412
413 let mut epoch0: Vec<usize> = Vec::new();
415 while let Some(batch) = sampler.next_batch() {
416 epoch0.extend_from_slice(&batch);
417 }
418
419 sampler.reset();
421 let mut epoch1: Vec<usize> = Vec::new();
422 while let Some(batch) = sampler.next_batch() {
423 epoch1.extend_from_slice(&batch);
424 }
425
426 let set0: HashSet<usize> = epoch0.iter().copied().collect();
428 let set1: HashSet<usize> = epoch1.iter().copied().collect();
429 assert_eq!(set0.len(), 50);
430 assert_eq!(set1.len(), 50);
431
432 assert_ne!(epoch0, epoch1, "two shuffled epochs should differ");
435 }
436
437 #[test]
438 fn test_batch_sampler_epoch_counter() {
439 let config = BatchSamplerConfig {
440 batch_size: 5,
441 shuffle: false,
442 seed: None,
443 drop_last: false,
444 stratified: false,
445 };
446 let mut sampler = BatchSampler::new(20, config);
447 assert_eq!(sampler.epoch(), 0);
448 while sampler.next_batch().is_some() {}
450 sampler.reset();
451 assert_eq!(sampler.epoch(), 1);
452 while sampler.next_batch().is_some() {}
453 sampler.reset();
454 assert_eq!(sampler.epoch(), 2);
455 }
456
457 #[test]
458 fn test_batch_sampler_empty() {
459 let config = BatchSamplerConfig {
460 batch_size: 10,
461 shuffle: false,
462 seed: None,
463 drop_last: false,
464 stratified: false,
465 };
466 let mut sampler = BatchSampler::new(0, config);
467 assert_eq!(sampler.n_batches(), 0);
468 assert!(sampler.next_batch().is_none());
469 }
470
471 #[test]
472 fn test_stratified_sampler_balance() {
473 let mut labels = Vec::new();
475 for cls in 0..3usize {
476 for _ in 0..30 {
477 labels.push(cls);
478 }
479 }
480 let config = BatchSamplerConfig {
481 batch_size: 9,
482 shuffle: false,
483 seed: Some(99),
484 drop_last: false,
485 stratified: true,
486 };
487 let mut sampler = BatchSampler::from_labels(&labels, config);
488
489 while let Some(batch) = sampler.next_batch() {
491 if batch.len() < 9 {
492 continue; }
494 let mut counts = [0usize; 3];
495 for &idx in &batch {
496 counts[labels[idx]] += 1;
497 }
498 assert_eq!(counts[0], 3, "class 0 count in batch");
500 assert_eq!(counts[1], 3, "class 1 count in batch");
501 assert_eq!(counts[2], 3, "class 2 count in batch");
502 }
503 }
504
505 #[test]
506 fn test_train_val_test_split() {
507 let n = 1000;
508 let (train, val, test) = train_val_test_split(n, None, 0.7, 0.15, Some(42));
509
510 let train_set: HashSet<usize> = train.iter().copied().collect();
512 let val_set: HashSet<usize> = val.iter().copied().collect();
513 let test_set: HashSet<usize> = test.iter().copied().collect();
514
515 assert!(
516 train_set.is_disjoint(&val_set),
517 "train and val should not overlap"
518 );
519 assert!(
520 train_set.is_disjoint(&test_set),
521 "train and test should not overlap"
522 );
523 assert!(
524 val_set.is_disjoint(&test_set),
525 "val and test should not overlap"
526 );
527
528 let total = train.len() + val.len() + test.len();
530 assert_eq!(total, n, "all samples must be assigned to a split");
531
532 let train_frac = train.len() as f64 / n as f64;
534 let val_frac = val.len() as f64 / n as f64;
535 assert!(
536 (train_frac - 0.7).abs() < 0.05,
537 "train fraction {train_frac:.3} too far from 0.7"
538 );
539 assert!(
540 (val_frac - 0.15).abs() < 0.05,
541 "val fraction {val_frac:.3} too far from 0.15"
542 );
543 }
544
545 #[test]
546 fn test_train_val_test_split_stratified() {
547 let n = 600;
548 let labels: Vec<usize> = (0..n).map(|i| i % 3).collect();
550 let (train, val, test) = train_val_test_split(n, Some(&labels), 0.6, 0.2, Some(7));
551
552 let train_set: HashSet<usize> = train.iter().copied().collect();
554 let val_set: HashSet<usize> = val.iter().copied().collect();
555 let test_set: HashSet<usize> = test.iter().copied().collect();
556
557 assert!(train_set.is_disjoint(&val_set));
558 assert!(train_set.is_disjoint(&test_set));
559 assert!(val_set.is_disjoint(&test_set));
560
561 let total = train.len() + val.len() + test.len();
562 assert_eq!(total, n);
563
564 for split in [&train, &val, &test] {
566 let mut counts = [0usize; 3];
567 for &idx in split {
568 counts[labels[idx]] += 1;
569 }
570 let min_count = *counts.iter().min().expect("non-empty split");
571 let max_count = *counts.iter().max().expect("non-empty split");
572 if split.len() >= 10 {
574 let balance = min_count as f64 / max_count as f64;
575 assert!(
576 balance >= 0.8,
577 "split imbalanced: counts {counts:?}, balance {balance:.2}"
578 );
579 }
580 }
581 }
582
583 #[test]
584 fn test_xorshift64_coverage() {
585 let mut rng = Xorshift64::new(0xfedcba9876543210);
587 let vals: HashSet<u64> = (0..1000).map(|_| rng.next_u64()).collect();
588 assert_eq!(vals.len(), 1000, "PRNG produced duplicate values");
590
591 for n in [1usize, 2, 5, 100] {
593 for _ in 0..200 {
594 let v = rng.next_usize(n);
595 assert!(v < n, "next_usize({n}) returned {v}");
596 }
597 }
598 }
599}