1#[derive(Debug, Clone, PartialEq)]
49pub enum PruningStrategy {
50 Magnitude(f64),
52 PercentileMagnitude(f64),
55 StructuredL1(f64),
57 RandomPruning(f64),
60 GradualPruning {
63 initial_sparsity: f64,
65 final_sparsity: f64,
67 begin_step: usize,
69 end_step: usize,
71 },
72}
73
74#[derive(Debug, Clone)]
78pub struct PrunerConfig {
79 pub strategy: PruningStrategy,
81 pub seed: u64,
84 pub update_mask: bool,
87}
88
89#[derive(Debug, Clone)]
93pub struct LayerWeights {
94 pub name: String,
96 pub weights: Vec<f64>,
99 pub mask: Option<Vec<bool>>,
103}
104
105#[derive(Debug, Clone)]
107pub struct PruningResult {
108 pub layer_name: String,
110 pub weights_before: usize,
112 pub weights_pruned: usize,
114 pub sparsity: f64,
116 pub step: usize,
118}
119
120#[derive(Debug, Clone, Default)]
125pub struct PrunerStats {
126 pub total_pruning_steps: u64,
128 pub total_weights_pruned: u64,
130 pub avg_sparsity: f64,
132}
133
134pub struct ModelPruner {
139 config: PrunerConfig,
140 step: usize,
142 rng_state: u64,
144 stats: PrunerStats,
145}
146
147impl ModelPruner {
148 pub fn new(config: PrunerConfig) -> Self {
155 let rng_state = if config.seed == 0 { 1 } else { config.seed };
156 Self {
157 config,
158 step: 0,
159 rng_state,
160 stats: PrunerStats::default(),
161 }
162 }
163
164 pub fn prune_layer(&mut self, layer: &mut LayerWeights) -> PruningResult {
171 let n = layer.weights.len();
172 let zeros_before = layer.weights.iter().filter(|&&w| w == 0.0).count();
173
174 match self.config.strategy.clone() {
175 PruningStrategy::Magnitude(threshold) => {
176 self.apply_magnitude(layer, threshold);
177 }
178 PruningStrategy::PercentileMagnitude(pct) => {
179 let threshold = Self::compute_threshold(&layer.weights, pct);
180 self.apply_magnitude(layer, threshold);
181 }
182 PruningStrategy::StructuredL1(threshold) => {
183 self.apply_structured_l1(layer, threshold);
184 }
185 PruningStrategy::RandomPruning(fraction) => {
186 self.apply_random(layer, fraction);
187 }
188 PruningStrategy::GradualPruning { .. } => {
189 let target = self.current_sparsity_target();
190 let pct = target * 100.0;
192 let threshold = Self::compute_threshold(&layer.weights, pct);
193 self.apply_magnitude(layer, threshold);
194 }
195 }
196
197 if self.config.update_mask {
198 Self::rebuild_mask(layer);
199 }
200
201 let zeros_after = layer.weights.iter().filter(|&&w| w == 0.0).count();
202 let newly_pruned = zeros_after.saturating_sub(zeros_before);
203 let sparsity = Self::compute_sparsity(&layer.weights);
204
205 self.stats.total_pruning_steps += 1;
207 self.stats.total_weights_pruned += newly_pruned as u64;
208 let n_steps = self.stats.total_pruning_steps as f64;
209 self.stats.avg_sparsity =
210 self.stats.avg_sparsity * (n_steps - 1.0) / n_steps + sparsity / n_steps;
211
212 PruningResult {
213 layer_name: layer.name.clone(),
214 weights_before: n,
215 weights_pruned: newly_pruned,
216 sparsity,
217 step: self.step,
218 }
219 }
220
221 pub fn prune_all(&mut self, layers: &mut [LayerWeights]) -> Vec<PruningResult> {
223 layers.iter_mut().map(|l| self.prune_layer(l)).collect()
224 }
225
226 pub fn current_sparsity_target(&self) -> f64 {
232 match &self.config.strategy {
233 PruningStrategy::Magnitude(t) => *t,
234 PruningStrategy::PercentileMagnitude(pct) => pct / 100.0,
235 PruningStrategy::StructuredL1(t) => *t,
236 PruningStrategy::RandomPruning(frac) => *frac,
237 PruningStrategy::GradualPruning {
238 initial_sparsity,
239 final_sparsity,
240 begin_step,
241 end_step,
242 } => {
243 let s = self.step;
244 if s <= *begin_step {
245 *initial_sparsity
246 } else if s >= *end_step {
247 *final_sparsity
248 } else {
249 let progress = (s - begin_step) as f64 / (end_step - begin_step).max(1) as f64;
250 initial_sparsity + progress * (final_sparsity - initial_sparsity)
251 }
252 }
253 }
254 }
255
256 pub fn compute_threshold(weights: &[f64], percentile: f64) -> f64 {
262 if weights.is_empty() {
263 return 0.0;
264 }
265 let pct = percentile.clamp(0.0, 100.0);
266 let mut magnitudes: Vec<f64> = weights.iter().map(|w| w.abs()).collect();
267 magnitudes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
268 let idx = ((pct / 100.0) * magnitudes.len() as f64) as usize;
269 let idx = idx.min(magnitudes.len().saturating_sub(1));
270 magnitudes[idx]
271 }
272
273 pub fn compute_sparsity(weights: &[f64]) -> f64 {
275 if weights.is_empty() {
276 return 0.0;
277 }
278 let zeros = weights.iter().filter(|&&w| w == 0.0).count();
279 zeros as f64 / weights.len() as f64
280 }
281
282 pub fn compute_l1_norm(weights: &[f64]) -> f64 {
284 weights.iter().map(|w| w.abs()).sum()
285 }
286
287 pub fn advance_step(&mut self) {
289 self.step += 1;
290 }
291
292 pub fn next_uniform_prng(&mut self) -> f64 {
297 let mut x = self.rng_state;
299 x ^= x << 13;
300 x ^= x >> 7;
301 x ^= x << 17;
302 self.rng_state = x;
303 (x as f64) / (u64::MAX as f64 + 1.0)
305 }
306
307 pub fn apply_mask(layer: &mut LayerWeights) {
310 if let Some(mask) = &layer.mask {
311 let mask_clone: Vec<bool> = mask.clone();
312 for (w, &keep) in layer.weights.iter_mut().zip(mask_clone.iter()) {
313 if !keep {
314 *w = 0.0;
315 }
316 }
317 }
318 }
319
320 pub fn stats(&self) -> &PrunerStats {
322 &self.stats
323 }
324
325 fn apply_magnitude(&self, layer: &mut LayerWeights, threshold: f64) {
329 for w in layer.weights.iter_mut() {
330 if w.abs() < threshold {
331 *w = 0.0;
332 }
333 }
334 }
335
336 fn apply_structured_l1(&self, layer: &mut LayerWeights, threshold: f64) {
342 let n = layer.weights.len();
343 if n < 2 {
344 self.apply_magnitude(layer, threshold);
345 return;
346 }
347 let row_len = Self::choose_row_len(n);
351 let num_rows = n / row_len;
352
353 for row_idx in 0..num_rows {
354 let start = row_idx * row_len;
355 let end = start + row_len;
356 let row = &layer.weights[start..end];
357 let l1_mean = Self::compute_l1_norm(row) / row_len as f64;
358 if l1_mean < threshold {
359 for w in layer.weights[start..end].iter_mut() {
360 *w = 0.0;
361 }
362 }
363 }
364 }
365
366 fn apply_random(&mut self, layer: &mut LayerWeights, fraction: f64) {
368 let frac = fraction.clamp(0.0, 1.0);
369 for w in layer.weights.iter_mut() {
370 if self.next_uniform_prng() < frac {
371 *w = 0.0;
372 }
373 }
374 }
375
376 fn rebuild_mask(layer: &mut LayerWeights) {
378 let mask: Vec<bool> = layer.weights.iter().map(|&w| w != 0.0).collect();
379 layer.mask = Some(mask);
380 }
381
382 fn choose_row_len(n: usize) -> usize {
385 let sqrt_n = (n as f64).sqrt() as usize;
386 for d in (1..=sqrt_n).rev() {
387 if n.is_multiple_of(d) {
388 return n / d; }
390 }
391 1
392 }
393}
394
395#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn make_layer(name: &str, weights: Vec<f64>) -> LayerWeights {
404 LayerWeights {
405 name: name.to_string(),
406 weights,
407 mask: None,
408 }
409 }
410
411 fn pruner(strategy: PruningStrategy) -> ModelPruner {
412 ModelPruner::new(PrunerConfig {
413 strategy,
414 seed: 42,
415 update_mask: true,
416 })
417 }
418
419 #[test]
422 fn magnitude_removes_below_threshold() {
423 let mut p = pruner(PruningStrategy::Magnitude(0.1));
424 let mut layer = make_layer("l", vec![0.05, -0.2, 0.0, 0.3, -0.08]);
425 p.prune_layer(&mut layer);
426 assert_eq!(layer.weights[0], 0.0);
428 assert_ne!(layer.weights[1], 0.0); assert_eq!(layer.weights[2], 0.0);
430 assert_ne!(layer.weights[3], 0.0); assert_eq!(layer.weights[4], 0.0);
432 }
433
434 #[test]
435 fn magnitude_threshold_zero_prunes_nothing() {
436 let mut p = pruner(PruningStrategy::Magnitude(0.0));
437 let weights = vec![0.1, -0.2, 0.3];
438 let mut layer = make_layer("l", weights.clone());
439 p.prune_layer(&mut layer);
440 assert_eq!(layer.weights, weights);
441 }
442
443 #[test]
444 fn magnitude_threshold_high_prunes_all() {
445 let mut p = pruner(PruningStrategy::Magnitude(1e9));
446 let mut layer = make_layer("l", vec![1.0, -2.0, 3.0]);
447 p.prune_layer(&mut layer);
448 assert!(layer.weights.iter().all(|&w| w == 0.0));
449 }
450
451 #[test]
452 fn magnitude_result_fields_correct() {
453 let mut p = pruner(PruningStrategy::Magnitude(0.1));
454 let mut layer = make_layer("fc1", vec![0.05, -0.2, 0.3]);
455 let res = p.prune_layer(&mut layer);
456 assert_eq!(res.layer_name, "fc1");
457 assert_eq!(res.weights_before, 3);
458 assert_eq!(res.weights_pruned, 1);
459 assert!(res.sparsity > 0.0 && res.sparsity <= 1.0);
460 assert_eq!(res.step, 0);
461 }
462
463 #[test]
466 fn percentile_prunes_bottom_fraction() {
467 let weights: Vec<f64> = (1..=10).map(|i| i as f64 * 0.1).collect();
468 let mut p = pruner(PruningStrategy::PercentileMagnitude(50.0));
469 let mut layer = make_layer("l", weights);
470 p.prune_layer(&mut layer);
471 let sparsity = ModelPruner::compute_sparsity(&layer.weights);
472 assert!((0.4..=0.6).contains(&sparsity));
474 }
475
476 #[test]
477 fn percentile_zero_prunes_nothing() {
478 let weights = vec![0.1, 0.2, 0.3];
479 let mut p = pruner(PruningStrategy::PercentileMagnitude(0.0));
480 let mut layer = make_layer("l", weights.clone());
481 p.prune_layer(&mut layer);
482 assert_eq!(layer.weights, weights);
485 }
486
487 #[test]
488 fn percentile_hundred_prunes_all_nonzero() {
489 let mut p = pruner(PruningStrategy::PercentileMagnitude(100.0));
490 let mut layer = make_layer("l", vec![1.0, 2.0, 3.0]);
491 p.prune_layer(&mut layer);
492 assert_eq!(layer.weights[0], 0.0);
496 assert_eq!(layer.weights[1], 0.0);
497 assert_eq!(layer.weights[2], 3.0);
499 }
500
501 #[test]
504 fn structured_l1_prunes_weak_neurons() {
505 let mut weights = vec![0.01f64, 0.01, 0.01]; weights.extend_from_slice(&[1.0, 2.0, 3.0]); weights.extend_from_slice(&[0.5, 0.6, 0.7]); let mut p = pruner(PruningStrategy::StructuredL1(0.5));
511 let mut layer = make_layer("l", weights);
512 p.prune_layer(&mut layer);
513 assert_eq!(layer.weights[0], 0.0);
515 assert_eq!(layer.weights[1], 0.0);
516 assert_eq!(layer.weights[2], 0.0);
517 assert_ne!(layer.weights[3], 0.0);
519 }
520
521 #[test]
522 fn structured_l1_single_element_falls_back_to_magnitude() {
523 let mut p = pruner(PruningStrategy::StructuredL1(0.5));
524 let mut layer = make_layer("l", vec![0.1]);
525 p.prune_layer(&mut layer);
526 assert_eq!(layer.weights[0], 0.0);
528 }
529
530 #[test]
531 fn structured_l1_no_pruning_when_all_strong() {
532 let weights = vec![10.0f64; 9];
533 let mut p = pruner(PruningStrategy::StructuredL1(0.1));
534 let mut layer = make_layer("l", weights);
535 p.prune_layer(&mut layer);
536 assert!(layer.weights.iter().all(|&w| w != 0.0));
537 }
538
539 #[test]
542 fn random_pruning_deterministic_with_seed() {
543 let cfg1 = PrunerConfig {
544 strategy: PruningStrategy::RandomPruning(0.5),
545 seed: 12345,
546 update_mask: false,
547 };
548 let cfg2 = PrunerConfig {
549 strategy: PruningStrategy::RandomPruning(0.5),
550 seed: 12345,
551 update_mask: false,
552 };
553 let mut p1 = ModelPruner::new(cfg1);
554 let mut p2 = ModelPruner::new(cfg2);
555 let weights: Vec<f64> = (1..=20).map(|i| i as f64).collect();
556 let mut l1 = make_layer("a", weights.clone());
557 let mut l2 = make_layer("a", weights);
558 p1.prune_layer(&mut l1);
559 p2.prune_layer(&mut l2);
560 assert_eq!(l1.weights, l2.weights);
561 }
562
563 #[test]
564 fn random_pruning_different_seeds_differ() {
565 let mut p1 = ModelPruner::new(PrunerConfig {
566 strategy: PruningStrategy::RandomPruning(0.5),
567 seed: 1,
568 update_mask: false,
569 });
570 let mut p2 = ModelPruner::new(PrunerConfig {
571 strategy: PruningStrategy::RandomPruning(0.5),
572 seed: 999999,
573 update_mask: false,
574 });
575 let weights: Vec<f64> = (1..=100).map(|i| i as f64).collect();
576 let mut l1 = make_layer("a", weights.clone());
577 let mut l2 = make_layer("a", weights);
578 p1.prune_layer(&mut l1);
579 p2.prune_layer(&mut l2);
580 assert_ne!(l1.weights, l2.weights);
581 }
582
583 #[test]
584 fn random_pruning_zero_fraction_prunes_nothing() {
585 let weights: Vec<f64> = vec![1.0, 2.0, 3.0];
586 let mut p = pruner(PruningStrategy::RandomPruning(0.0));
587 let mut layer = make_layer("l", weights.clone());
588 p.prune_layer(&mut layer);
589 assert_eq!(layer.weights, weights);
590 }
591
592 #[test]
595 fn gradual_pruning_interpolates_between_steps() {
596 let strategy = PruningStrategy::GradualPruning {
597 initial_sparsity: 0.0,
598 final_sparsity: 1.0,
599 begin_step: 0,
600 end_step: 10,
601 };
602 let mut p = pruner(strategy);
603 assert!((p.current_sparsity_target() - 0.0).abs() < 1e-9);
605 p.advance_step(); let t1 = p.current_sparsity_target();
607 assert!(t1 > 0.0 && t1 < 1.0);
608 }
609
610 #[test]
611 fn gradual_pruning_clamps_to_final_after_end_step() {
612 let strategy = PruningStrategy::GradualPruning {
613 initial_sparsity: 0.0,
614 final_sparsity: 0.9,
615 begin_step: 2,
616 end_step: 5,
617 };
618 let mut p = pruner(strategy);
619 for _ in 0..10 {
620 p.advance_step();
621 }
622 assert!((p.current_sparsity_target() - 0.9).abs() < 1e-9);
623 }
624
625 #[test]
626 fn gradual_pruning_holds_initial_before_begin_step() {
627 let strategy = PruningStrategy::GradualPruning {
628 initial_sparsity: 0.1,
629 final_sparsity: 0.8,
630 begin_step: 5,
631 end_step: 10,
632 };
633 let p = pruner(strategy);
634 assert!((p.current_sparsity_target() - 0.1).abs() < 1e-9);
636 }
637
638 #[test]
639 fn gradual_pruning_midpoint_is_correct() {
640 let strategy = PruningStrategy::GradualPruning {
641 initial_sparsity: 0.0,
642 final_sparsity: 1.0,
643 begin_step: 0,
644 end_step: 10,
645 };
646 let mut p = pruner(strategy);
647 for _ in 0..5 {
648 p.advance_step();
649 }
650 let target = p.current_sparsity_target();
651 assert!((target - 0.5).abs() < 1e-9);
652 }
653
654 #[test]
657 fn advance_step_increments_counter() {
658 let strategy = PruningStrategy::GradualPruning {
659 initial_sparsity: 0.0,
660 final_sparsity: 1.0,
661 begin_step: 0,
662 end_step: 100,
663 };
664 let mut p = pruner(strategy);
665 let t0 = p.current_sparsity_target();
666 p.advance_step();
667 let t1 = p.current_sparsity_target();
668 assert!(t1 > t0);
669 }
670
671 #[test]
674 fn compute_threshold_median() {
675 let weights = vec![-3.0, -2.0, -1.0, 1.0, 2.0, 3.0];
676 let t = ModelPruner::compute_threshold(&weights, 50.0);
677 assert!((t - 2.0).abs() < 1e-9);
679 }
680
681 #[test]
682 fn compute_threshold_zero_percentile() {
683 let weights = vec![1.0, 2.0, 3.0];
684 let t = ModelPruner::compute_threshold(&weights, 0.0);
685 assert!((t - 1.0).abs() < 1e-9);
686 }
687
688 #[test]
689 fn compute_threshold_hundred_percentile() {
690 let weights = vec![1.0, 2.0, 3.0];
691 let t = ModelPruner::compute_threshold(&weights, 100.0);
692 assert!((t - 3.0).abs() < 1e-9);
693 }
694
695 #[test]
696 fn compute_threshold_empty() {
697 assert_eq!(ModelPruner::compute_threshold(&[], 50.0), 0.0);
698 }
699
700 #[test]
703 fn compute_sparsity_all_nonzero() {
704 assert_eq!(ModelPruner::compute_sparsity(&[1.0, 2.0, 3.0]), 0.0);
705 }
706
707 #[test]
708 fn compute_sparsity_all_zero() {
709 assert_eq!(ModelPruner::compute_sparsity(&[0.0, 0.0, 0.0]), 1.0);
710 }
711
712 #[test]
713 fn compute_sparsity_half() {
714 assert!((ModelPruner::compute_sparsity(&[0.0, 1.0]) - 0.5).abs() < 1e-9);
715 }
716
717 #[test]
718 fn compute_sparsity_empty() {
719 assert_eq!(ModelPruner::compute_sparsity(&[]), 0.0);
720 }
721
722 #[test]
725 fn apply_mask_zeros_false_entries() {
726 let mut layer = LayerWeights {
727 name: "l".to_string(),
728 weights: vec![1.0, 2.0, 3.0],
729 mask: Some(vec![true, false, true]),
730 };
731 ModelPruner::apply_mask(&mut layer);
732 assert_eq!(layer.weights, vec![1.0, 0.0, 3.0]);
733 }
734
735 #[test]
736 fn apply_mask_no_mask_noop() {
737 let mut layer = LayerWeights {
738 name: "l".to_string(),
739 weights: vec![1.0, 2.0, 3.0],
740 mask: None,
741 };
742 ModelPruner::apply_mask(&mut layer);
743 assert_eq!(layer.weights, vec![1.0, 2.0, 3.0]);
744 }
745
746 #[test]
747 fn apply_mask_all_false_zeroes_all() {
748 let mut layer = LayerWeights {
749 name: "l".to_string(),
750 weights: vec![5.0, 6.0, 7.0],
751 mask: Some(vec![false, false, false]),
752 };
753 ModelPruner::apply_mask(&mut layer);
754 assert!(layer.weights.iter().all(|&w| w == 0.0));
755 }
756
757 #[test]
760 fn prune_all_returns_one_result_per_layer() {
761 let mut p = pruner(PruningStrategy::Magnitude(0.1));
762 let mut layers = vec![
763 make_layer("a", vec![0.05, 0.5]),
764 make_layer("b", vec![0.05, 0.5, -0.5]),
765 make_layer("c", vec![1.0, 2.0]),
766 ];
767 let results = p.prune_all(&mut layers);
768 assert_eq!(results.len(), 3);
769 assert_eq!(results[0].layer_name, "a");
770 assert_eq!(results[1].layer_name, "b");
771 assert_eq!(results[2].layer_name, "c");
772 }
773
774 #[test]
775 fn prune_all_mutates_all_layers() {
776 let mut p = pruner(PruningStrategy::Magnitude(1e9));
777 let mut layers = vec![
778 make_layer("a", vec![0.1, 0.2]),
779 make_layer("b", vec![0.3, 0.4]),
780 ];
781 p.prune_all(&mut layers);
782 for layer in &layers {
783 assert!(layer.weights.iter().all(|&w| w == 0.0));
784 }
785 }
786
787 #[test]
790 fn mask_updated_after_pruning() {
791 let mut p = pruner(PruningStrategy::Magnitude(0.5));
792 let mut layer = make_layer("l", vec![0.1, 1.0, 0.2, 2.0]);
793 p.prune_layer(&mut layer);
794 let mask = layer.mask.expect("test: should succeed");
795 assert!(!mask[0]);
797 assert!(mask[1]);
798 assert!(!mask[2]);
799 assert!(mask[3]);
800 }
801
802 #[test]
803 fn no_mask_update_when_disabled() {
804 let cfg = PrunerConfig {
805 strategy: PruningStrategy::Magnitude(0.5),
806 seed: 0,
807 update_mask: false,
808 };
809 let mut p = ModelPruner::new(cfg);
810 let mut layer = make_layer("l", vec![0.1, 1.0]);
811 p.prune_layer(&mut layer);
812 assert!(layer.mask.is_none());
813 }
814
815 #[test]
818 fn stats_total_pruning_steps_increments() {
819 let mut p = pruner(PruningStrategy::Magnitude(0.5));
820 assert_eq!(p.stats().total_pruning_steps, 0);
821 p.prune_layer(&mut make_layer("a", vec![0.1, 1.0]));
822 assert_eq!(p.stats().total_pruning_steps, 1);
823 p.prune_layer(&mut make_layer("b", vec![0.1, 1.0]));
824 assert_eq!(p.stats().total_pruning_steps, 2);
825 }
826
827 #[test]
828 fn stats_total_weights_pruned_accumulates() {
829 let mut p = pruner(PruningStrategy::Magnitude(0.5));
830 p.prune_layer(&mut make_layer("a", vec![0.1, 0.2, 1.0])); p.prune_layer(&mut make_layer("b", vec![0.3, 0.4, 2.0])); assert_eq!(p.stats().total_weights_pruned, 4);
833 }
834
835 #[test]
836 fn stats_avg_sparsity_is_non_negative() {
837 let mut p = pruner(PruningStrategy::Magnitude(0.5));
838 p.prune_layer(&mut make_layer("a", vec![0.1, 1.0]));
839 assert!(p.stats().avg_sparsity >= 0.0);
840 assert!(p.stats().avg_sparsity <= 1.0);
841 }
842
843 #[test]
846 fn full_zero_weights_remain_zero() {
847 let mut p = pruner(PruningStrategy::Magnitude(0.1));
848 let mut layer = make_layer("l", vec![0.0, 0.0, 0.0]);
849 let result = p.prune_layer(&mut layer);
850 assert_eq!(result.sparsity, 1.0);
851 assert_eq!(result.weights_pruned, 0); }
853
854 #[test]
855 fn empty_layer_produces_valid_result() {
856 let mut p = pruner(PruningStrategy::Magnitude(0.1));
857 let mut layer = make_layer("empty", vec![]);
858 let result = p.prune_layer(&mut layer);
859 assert_eq!(result.weights_before, 0);
860 assert_eq!(result.weights_pruned, 0);
861 assert_eq!(result.sparsity, 0.0);
862 }
863
864 #[test]
865 fn compute_l1_norm_sum_of_abs() {
866 let weights = vec![-1.0, 2.0, -3.0, 4.0];
867 assert!((ModelPruner::compute_l1_norm(&weights) - 10.0).abs() < 1e-9);
868 }
869
870 #[test]
871 fn next_uniform_prng_in_range() {
872 let cfg = PrunerConfig {
873 strategy: PruningStrategy::Magnitude(0.0),
874 seed: 7,
875 update_mask: false,
876 };
877 let mut p = ModelPruner::new(cfg);
878 for _ in 0..1000 {
879 let v = p.next_uniform_prng();
880 assert!((0.0..1.0).contains(&v), "PRNG out of range: {}", v);
881 }
882 }
883}