1use crate::{Nystroem, RBFSampler};
7use scirs2_core::ndarray::Array2;
8use scirs2_linalg::compat::{Norm, SVD};
9use sklears_core::{
10 error::{Result, SklearsError},
11 traits::{Fit, Transform},
12};
13use std::collections::HashMap;
14use std::time::Instant;
15
16#[derive(Debug, Clone)]
18pub enum BudgetConstraint {
20 Time { max_seconds: f64 },
22 Memory { max_bytes: usize },
24 Operations { max_operations: usize },
26 Combined {
28 max_seconds: Option<f64>,
29
30 max_bytes: Option<usize>,
31 max_operations: Option<usize>,
32 },
33}
34
35#[derive(Debug, Clone)]
37pub struct BudgetUsage {
39 pub time_used: f64,
41 pub memory_used: usize,
43 pub operations_used: usize,
45}
46
47impl Default for BudgetUsage {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl BudgetUsage {
54 pub fn new() -> Self {
56 Self {
57 time_used: 0.0,
58 memory_used: 0,
59 operations_used: 0,
60 }
61 }
62
63 pub fn is_within_budget(&self, constraint: &BudgetConstraint) -> bool {
65 match constraint {
66 BudgetConstraint::Time { max_seconds } => self.time_used <= *max_seconds,
67 BudgetConstraint::Memory { max_bytes } => self.memory_used <= *max_bytes,
68 BudgetConstraint::Operations { max_operations } => {
69 self.operations_used <= *max_operations
70 }
71 BudgetConstraint::Combined {
72 max_seconds,
73 max_bytes,
74 max_operations,
75 } => {
76 let time_ok = max_seconds.map_or(true, |max| self.time_used <= max);
77 let memory_ok = max_bytes.map_or(true, |max| self.memory_used <= max);
78 let ops_ok = max_operations.map_or(true, |max| self.operations_used <= max);
79 time_ok && memory_ok && ops_ok
80 }
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub enum OptimizationStrategy {
89 MaxQuality,
91 MinCost { target_quality: f64 },
93 Balanced {
95 quality_weight: f64,
96
97 cost_weight: f64,
98 },
99 Greedy,
101}
102
103#[derive(Debug, Clone)]
105pub struct BudgetConstrainedConfig {
107 pub budget: BudgetConstraint,
109 pub strategy: OptimizationStrategy,
111 pub min_components: usize,
113 pub max_components: usize,
115 pub step_size: usize,
117 pub n_trials: usize,
119 pub random_seed: Option<u64>,
121 pub early_stopping_tolerance: f64,
123}
124
125impl Default for BudgetConstrainedConfig {
126 fn default() -> Self {
127 Self {
128 budget: BudgetConstraint::Time { max_seconds: 10.0 },
129 strategy: OptimizationStrategy::MaxQuality,
130 min_components: 10,
131 max_components: 1000,
132 step_size: 10,
133 n_trials: 3,
134 random_seed: None,
135 early_stopping_tolerance: 0.01,
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct BudgetOptimizationResult {
144 pub optimal_components: usize,
146 pub quality_score: f64,
148 pub budget_usage: BudgetUsage,
150 pub tested_configs: HashMap<usize, (f64, BudgetUsage)>,
152 pub completed: bool,
154}
155
156#[derive(Debug, Clone)]
158pub struct BudgetConstrainedRBFSampler {
160 gamma: f64,
161 config: BudgetConstrainedConfig,
162}
163
164impl Default for BudgetConstrainedRBFSampler {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl BudgetConstrainedRBFSampler {
171 pub fn new() -> Self {
173 Self {
174 gamma: 1.0,
175 config: BudgetConstrainedConfig::default(),
176 }
177 }
178
179 pub fn gamma(mut self, gamma: f64) -> Self {
181 self.gamma = gamma;
182 self
183 }
184
185 pub fn config(mut self, config: BudgetConstrainedConfig) -> Self {
187 self.config = config;
188 self
189 }
190
191 pub fn budget(mut self, budget: BudgetConstraint) -> Self {
193 self.config.budget = budget;
194 self
195 }
196
197 pub fn strategy(mut self, strategy: OptimizationStrategy) -> Self {
199 self.config.strategy = strategy;
200 self
201 }
202
203 pub fn find_optimal_config(&self, x: &Array2<f64>) -> Result<BudgetOptimizationResult> {
205 let _start_time = Instant::now();
206 let mut total_usage = BudgetUsage::new();
207 let mut tested_configs = HashMap::new();
208 let mut best_quality = f64::NEG_INFINITY;
209 let mut best_components = self.config.min_components;
210 let mut completed = true;
211
212 let n_samples = x.nrows();
214 let split_idx = (n_samples as f64 * 0.8) as usize;
215 let x_train = x
216 .slice(scirs2_core::ndarray::s![..split_idx, ..])
217 .to_owned();
218 let x_val = x
219 .slice(scirs2_core::ndarray::s![split_idx.., ..])
220 .to_owned();
221
222 for n_components in
224 (self.config.min_components..=self.config.max_components).step_by(self.config.step_size)
225 {
226 let _config_start = Instant::now();
227 let mut config_usage = BudgetUsage::new();
228 let mut trial_qualities = Vec::new();
229
230 if !total_usage.is_within_budget(&self.config.budget) {
232 completed = false;
233 break;
234 }
235
236 for trial in 0..self.config.n_trials {
238 let trial_start = Instant::now();
239
240 let seed = self.config.random_seed.map(|s| s + trial as u64);
242 let sampler = if let Some(s) = seed {
243 RBFSampler::new(n_components)
244 .gamma(self.gamma)
245 .random_state(s)
246 } else {
247 RBFSampler::new(n_components).gamma(self.gamma)
248 };
249
250 let fitted = sampler.fit(&x_train, &())?;
252 let _x_train_transformed = fitted.transform(&x_train)?;
253 let x_val_transformed = fitted.transform(&x_val)?;
254
255 let quality = self.compute_quality(&x_val, &x_val_transformed)?;
257 trial_qualities.push(quality);
258
259 let trial_time = trial_start.elapsed().as_secs_f64();
261 config_usage.time_used += trial_time;
262 config_usage.memory_used += self.estimate_memory_usage(n_components, x.ncols());
263 config_usage.operations_used += self.estimate_operations(n_components, x.nrows());
264
265 if !config_usage.is_within_budget(&self.config.budget) {
267 completed = false;
268 break;
269 }
270 }
271
272 let avg_quality = trial_qualities.iter().sum::<f64>() / trial_qualities.len() as f64;
274
275 total_usage.time_used += config_usage.time_used;
277 total_usage.memory_used = total_usage.memory_used.max(config_usage.memory_used);
278 total_usage.operations_used += config_usage.operations_used;
279
280 tested_configs.insert(n_components, (avg_quality, config_usage));
282
283 if self.is_better_config(avg_quality, n_components, best_quality, best_components) {
285 best_quality = avg_quality;
286 best_components = n_components;
287 }
288
289 if self.should_stop_early(avg_quality, &tested_configs) {
291 break;
292 }
293
294 if !total_usage.is_within_budget(&self.config.budget) {
296 completed = false;
297 break;
298 }
299 }
300
301 Ok(BudgetOptimizationResult {
302 optimal_components: best_components,
303 quality_score: best_quality,
304 budget_usage: total_usage,
305 tested_configs,
306 completed,
307 })
308 }
309
310 fn compute_quality(&self, x: &Array2<f64>, x_transformed: &Array2<f64>) -> Result<f64> {
312 let n_samples = x.nrows().min(100); let x_subset = x.slice(scirs2_core::ndarray::s![..n_samples, ..]);
315 let x_transformed_subset = x_transformed.slice(scirs2_core::ndarray::s![..n_samples, ..]);
316
317 let k_approx = x_transformed_subset.dot(&x_transformed_subset.t());
319
320 let mut k_exact = Array2::zeros((n_samples, n_samples));
322 for i in 0..n_samples {
323 for j in 0..n_samples {
324 let diff = &x_subset.row(i) - &x_subset.row(j);
325 let squared_norm = diff.dot(&diff);
326 k_exact[[i, j]] = (-self.gamma * squared_norm).exp();
327 }
328 }
329
330 let k_exact_norm = k_exact.norm_l2();
332 let k_approx_norm = k_approx.norm_l2();
333 let alignment = if k_exact_norm > 1e-12 && k_approx_norm > 1e-12 {
334 (&k_exact * &k_approx).sum() / (k_exact_norm * k_approx_norm)
335 } else {
336 0.0
337 };
338
339 Ok(alignment)
340 }
341
342 fn estimate_memory_usage(&self, n_components: usize, n_features: usize) -> usize {
344 n_components * n_features * 8 + 1024
346 }
347
348 fn estimate_operations(&self, n_components: usize, n_samples: usize) -> usize {
350 n_components * n_samples * 10
352 }
353
354 fn is_better_config(
356 &self,
357 quality: f64,
358 components: usize,
359 best_quality: f64,
360 best_components: usize,
361 ) -> bool {
362 match &self.config.strategy {
363 OptimizationStrategy::MaxQuality => quality > best_quality,
364 OptimizationStrategy::MinCost { target_quality } => {
365 if quality >= *target_quality {
366 components < best_components || best_quality < *target_quality
367 } else {
368 quality > best_quality
369 }
370 }
371 OptimizationStrategy::Balanced {
372 quality_weight,
373 cost_weight,
374 } => {
375 let score = quality * quality_weight - (components as f64) * cost_weight;
376 let best_score =
377 best_quality * quality_weight - (best_components as f64) * cost_weight;
378 score > best_score
379 }
380 OptimizationStrategy::Greedy => quality > best_quality,
381 }
382 }
383
384 fn should_stop_early(
386 &self,
387 _current_quality: f64,
388 tested_configs: &HashMap<usize, (f64, BudgetUsage)>,
389 ) -> bool {
390 if tested_configs.len() < 3 {
391 return false;
392 }
393
394 let mut recent_qualities: Vec<f64> = tested_configs
396 .values()
397 .map(|(quality, _)| *quality)
398 .collect();
399 recent_qualities.sort_by(|a, b| a.partial_cmp(b).unwrap());
400
401 let n = recent_qualities.len();
402 if n >= 3 {
403 let improvement = recent_qualities[n - 1] - recent_qualities[n - 3];
404 improvement < self.config.early_stopping_tolerance
405 } else {
406 false
407 }
408 }
409}
410
411pub struct FittedBudgetConstrainedRBFSampler {
413 fitted_rbf: crate::rbf_sampler::RBFSampler<sklears_core::traits::Trained>,
414 optimization_result: BudgetOptimizationResult,
415}
416
417impl Fit<Array2<f64>, ()> for BudgetConstrainedRBFSampler {
418 type Fitted = FittedBudgetConstrainedRBFSampler;
419
420 fn fit(self, x: &Array2<f64>, _y: &()) -> Result<Self::Fitted> {
421 let optimization_result = self.find_optimal_config(x)?;
423
424 let rbf_sampler = RBFSampler::new(optimization_result.optimal_components).gamma(self.gamma);
426 let fitted_rbf = rbf_sampler.fit(x, &())?;
427
428 Ok(FittedBudgetConstrainedRBFSampler {
429 fitted_rbf,
430 optimization_result,
431 })
432 }
433}
434
435impl Transform<Array2<f64>, Array2<f64>> for FittedBudgetConstrainedRBFSampler {
436 fn transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
437 self.fitted_rbf.transform(x)
438 }
439}
440
441impl FittedBudgetConstrainedRBFSampler {
442 pub fn optimization_result(&self) -> &BudgetOptimizationResult {
444 &self.optimization_result
445 }
446
447 pub fn optimal_components(&self) -> usize {
449 self.optimization_result.optimal_components
450 }
451
452 pub fn quality_score(&self) -> f64 {
454 self.optimization_result.quality_score
455 }
456
457 pub fn budget_usage(&self) -> &BudgetUsage {
459 &self.optimization_result.budget_usage
460 }
461
462 pub fn completed(&self) -> bool {
464 self.optimization_result.completed
465 }
466}
467
468#[derive(Debug, Clone)]
470pub struct BudgetConstrainedNystroem {
472 kernel: crate::nystroem::Kernel,
473 config: BudgetConstrainedConfig,
474}
475
476impl Default for BudgetConstrainedNystroem {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482impl BudgetConstrainedNystroem {
483 pub fn new() -> Self {
485 Self {
486 kernel: crate::nystroem::Kernel::Rbf { gamma: 1.0 },
487 config: BudgetConstrainedConfig::default(),
488 }
489 }
490
491 pub fn kernel(mut self, kernel: crate::nystroem::Kernel) -> Self {
493 self.kernel = kernel;
494 self
495 }
496
497 pub fn gamma(mut self, gamma: f64) -> Self {
499 self.kernel = crate::nystroem::Kernel::Rbf { gamma };
500 self
501 }
502
503 pub fn config(mut self, config: BudgetConstrainedConfig) -> Self {
505 self.config = config;
506 self
507 }
508
509 pub fn find_optimal_config(&self, x: &Array2<f64>) -> Result<BudgetOptimizationResult> {
511 let _start_time = Instant::now();
512 let mut total_usage = BudgetUsage::new();
513 let mut tested_configs = HashMap::new();
514 let mut best_quality = f64::NEG_INFINITY;
515 let mut best_components = self.config.min_components;
516 let mut completed = true;
517
518 for n_components in
520 (self.config.min_components..=self.config.max_components).step_by(self.config.step_size)
521 {
522 let _config_start = Instant::now();
523 let mut config_usage = BudgetUsage::new();
524 let mut trial_qualities = Vec::new();
525
526 if !total_usage.is_within_budget(&self.config.budget) {
528 completed = false;
529 break;
530 }
531
532 for trial in 0..self.config.n_trials {
534 let trial_start = Instant::now();
535
536 let seed = self.config.random_seed.map(|s| s + trial as u64);
538 let nystroem = if let Some(s) = seed {
539 Nystroem::new(self.kernel.clone(), n_components).random_state(s)
540 } else {
541 Nystroem::new(self.kernel.clone(), n_components)
542 };
543
544 let fitted = nystroem.fit(x, &())?;
546 let x_transformed = fitted.transform(x)?;
547
548 let quality = self.compute_nystroem_quality(&x_transformed)?;
550 trial_qualities.push(quality);
551
552 let trial_time = trial_start.elapsed().as_secs_f64();
554 config_usage.time_used += trial_time;
555 config_usage.memory_used += n_components * x.ncols() * 8;
556 config_usage.operations_used += n_components * x.nrows() * 5;
557
558 if !config_usage.is_within_budget(&self.config.budget) {
560 completed = false;
561 break;
562 }
563 }
564
565 let avg_quality = trial_qualities.iter().sum::<f64>() / trial_qualities.len() as f64;
567
568 total_usage.time_used += config_usage.time_used;
570 total_usage.memory_used = total_usage.memory_used.max(config_usage.memory_used);
571 total_usage.operations_used += config_usage.operations_used;
572
573 tested_configs.insert(n_components, (avg_quality, config_usage));
575
576 if avg_quality > best_quality {
578 best_quality = avg_quality;
579 best_components = n_components;
580 }
581
582 if !total_usage.is_within_budget(&self.config.budget) {
584 completed = false;
585 break;
586 }
587 }
588
589 Ok(BudgetOptimizationResult {
590 optimal_components: best_components,
591 quality_score: best_quality,
592 budget_usage: total_usage,
593 tested_configs,
594 completed,
595 })
596 }
597
598 fn compute_nystroem_quality(&self, x_transformed: &Array2<f64>) -> Result<f64> {
600 let (_, s, _) = x_transformed
602 .svd(true)
603 .map_err(|_| SklearsError::InvalidInput("SVD computation failed".to_string()))?;
604
605 let s_sum = s.sum();
606 if s_sum == 0.0 {
607 return Ok(0.0);
608 }
609
610 let s_normalized = &s / s_sum;
611 let entropy = -s_normalized
612 .iter()
613 .filter(|&&x| x > 1e-12)
614 .map(|&x| x * x.ln())
615 .sum::<f64>();
616
617 Ok(entropy.exp())
618 }
619}
620
621pub struct FittedBudgetConstrainedNystroem {
623 fitted_nystroem: crate::nystroem::Nystroem<sklears_core::traits::Trained>,
624 optimization_result: BudgetOptimizationResult,
625}
626
627impl Fit<Array2<f64>, ()> for BudgetConstrainedNystroem {
628 type Fitted = FittedBudgetConstrainedNystroem;
629
630 fn fit(self, x: &Array2<f64>, _y: &()) -> Result<Self::Fitted> {
631 let optimization_result = self.find_optimal_config(x)?;
633
634 let nystroem = Nystroem::new(self.kernel, optimization_result.optimal_components);
636 let fitted_nystroem = nystroem.fit(x, &())?;
637
638 Ok(FittedBudgetConstrainedNystroem {
639 fitted_nystroem,
640 optimization_result,
641 })
642 }
643}
644
645impl Transform<Array2<f64>, Array2<f64>> for FittedBudgetConstrainedNystroem {
646 fn transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
647 self.fitted_nystroem.transform(x)
648 }
649}
650
651impl FittedBudgetConstrainedNystroem {
652 pub fn optimization_result(&self) -> &BudgetOptimizationResult {
654 &self.optimization_result
655 }
656
657 pub fn optimal_components(&self) -> usize {
659 self.optimization_result.optimal_components
660 }
661
662 pub fn quality_score(&self) -> f64 {
664 self.optimization_result.quality_score
665 }
666
667 pub fn budget_usage(&self) -> &BudgetUsage {
669 &self.optimization_result.budget_usage
670 }
671
672 pub fn completed(&self) -> bool {
674 self.optimization_result.completed
675 }
676}
677
678#[allow(non_snake_case)]
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use approx::assert_abs_diff_eq;
683
684 #[test]
685 fn test_budget_constrained_rbf_sampler() {
686 let x = Array2::from_shape_vec((100, 4), (0..400).map(|i| (i as f64) * 0.01).collect())
687 .unwrap();
688
689 let config = BudgetConstrainedConfig {
690 budget: BudgetConstraint::Time { max_seconds: 5.0 },
691 min_components: 10,
692 max_components: 50,
693 step_size: 10,
694 n_trials: 2,
695 ..Default::default()
696 };
697
698 let sampler = BudgetConstrainedRBFSampler::new().gamma(0.5).config(config);
699
700 let fitted = sampler.fit(&x, &()).unwrap();
701 let transformed = fitted.transform(&x).unwrap();
702
703 assert_eq!(transformed.nrows(), 100);
704 assert!(fitted.optimal_components() >= 10);
705 assert!(fitted.optimal_components() <= 50);
706 assert!(fitted.quality_score() >= 0.0);
707 assert!(fitted.budget_usage().time_used <= 5.0);
708 }
709
710 #[test]
711 fn test_budget_constrained_nystroem() {
712 let x =
713 Array2::from_shape_vec((80, 3), (0..240).map(|i| (i as f64) * 0.02).collect()).unwrap();
714
715 let config = BudgetConstrainedConfig {
716 budget: BudgetConstraint::Memory { max_bytes: 100000 },
717 min_components: 5,
718 max_components: 30,
719 step_size: 5,
720 n_trials: 2,
721 ..Default::default()
722 };
723
724 let nystroem = BudgetConstrainedNystroem::new().gamma(1.0).config(config);
725
726 let fitted = nystroem.fit(&x, &()).unwrap();
727 let transformed = fitted.transform(&x).unwrap();
728
729 assert_eq!(transformed.nrows(), 80);
730 assert!(fitted.optimal_components() >= 5);
731 assert!(fitted.optimal_components() <= 30);
732 assert!(fitted.budget_usage().memory_used <= 100000);
733 }
734
735 #[test]
736 fn test_budget_constraint_types() {
737 let x =
738 Array2::from_shape_vec((50, 2), (0..100).map(|i| (i as f64) * 0.05).collect()).unwrap();
739
740 let constraints = vec![
741 BudgetConstraint::Time { max_seconds: 10.0 },
742 BudgetConstraint::Memory { max_bytes: 100000 },
743 BudgetConstraint::Operations {
744 max_operations: 50000,
745 },
746 BudgetConstraint::Combined {
747 max_seconds: Some(10.0),
748 max_bytes: Some(100000),
749 max_operations: Some(50000),
750 },
751 ];
752
753 for constraint in constraints {
754 let config = BudgetConstrainedConfig {
755 budget: constraint,
756 min_components: 5,
757 max_components: 20,
758 step_size: 5,
759 n_trials: 1,
760 ..Default::default()
761 };
762
763 let sampler = BudgetConstrainedRBFSampler::new()
764 .gamma(0.8)
765 .config(config.clone());
766
767 let fitted = sampler.fit(&x, &()).unwrap();
768 let usage = fitted.budget_usage();
769
770 assert!(
772 usage.is_within_budget(&config.budget),
773 "Budget constraint violated"
774 );
775 }
776 }
777
778 #[test]
779 fn test_optimization_strategies() {
780 let x =
781 Array2::from_shape_vec((60, 3), (0..180).map(|i| (i as f64) * 0.03).collect()).unwrap();
782
783 let strategies = vec![
784 OptimizationStrategy::MaxQuality,
785 OptimizationStrategy::MinCost {
786 target_quality: 0.7,
787 },
788 OptimizationStrategy::Balanced {
789 quality_weight: 1.0,
790 cost_weight: 0.01,
791 },
792 OptimizationStrategy::Greedy,
793 ];
794
795 for strategy in strategies {
796 let config = BudgetConstrainedConfig {
797 budget: BudgetConstraint::Time { max_seconds: 4.0 },
798 strategy,
799 min_components: 10,
800 max_components: 30,
801 step_size: 10,
802 n_trials: 1,
803 ..Default::default()
804 };
805
806 let sampler = BudgetConstrainedRBFSampler::new().gamma(0.5).config(config);
807
808 let fitted = sampler.fit(&x, &()).unwrap();
809
810 assert!(fitted.optimal_components() >= 10);
811 assert!(fitted.optimal_components() <= 30);
812 assert!(fitted.quality_score() >= 0.0);
813 }
814 }
815
816 #[test]
817 fn test_budget_usage_tracking() {
818 let mut usage = BudgetUsage::new();
819 usage.time_used = 2.5;
820 usage.memory_used = 1000;
821 usage.operations_used = 500;
822
823 let constraint1 = BudgetConstraint::Time { max_seconds: 3.0 };
824 let constraint2 = BudgetConstraint::Memory { max_bytes: 800 };
825 let constraint3 = BudgetConstraint::Operations {
826 max_operations: 600,
827 };
828 let constraint4 = BudgetConstraint::Combined {
829 max_seconds: Some(3.0),
830 max_bytes: Some(1200),
831 max_operations: Some(600),
832 };
833
834 assert!(usage.is_within_budget(&constraint1));
835 assert!(!usage.is_within_budget(&constraint2));
836 assert!(usage.is_within_budget(&constraint3));
837 assert!(usage.is_within_budget(&constraint4));
838 }
839
840 #[test]
841 fn test_early_stopping() {
842 let x =
843 Array2::from_shape_vec((40, 2), (0..80).map(|i| (i as f64) * 0.05).collect()).unwrap();
844
845 let config = BudgetConstrainedConfig {
846 budget: BudgetConstraint::Time { max_seconds: 10.0 },
847 min_components: 5,
848 max_components: 50,
849 step_size: 5,
850 n_trials: 1,
851 early_stopping_tolerance: 0.001,
852 ..Default::default()
853 };
854
855 let sampler = BudgetConstrainedRBFSampler::new().gamma(0.3).config(config);
856
857 let result = sampler.find_optimal_config(&x).unwrap();
858
859 assert!(result.optimal_components >= 5);
861 assert!(result.optimal_components <= 50);
862 assert!(result.quality_score >= 0.0);
863 }
864
865 #[test]
866 fn test_reproducibility() {
867 let x =
868 Array2::from_shape_vec((50, 3), (0..150).map(|i| (i as f64) * 0.02).collect()).unwrap();
869
870 let config = BudgetConstrainedConfig {
871 budget: BudgetConstraint::Time { max_seconds: 3.0 },
872 min_components: 10,
873 max_components: 30,
874 step_size: 10,
875 n_trials: 2,
876 random_seed: Some(42),
877 ..Default::default()
878 };
879
880 let sampler1 = BudgetConstrainedRBFSampler::new()
881 .gamma(0.7)
882 .config(config.clone());
883
884 let sampler2 = BudgetConstrainedRBFSampler::new().gamma(0.7).config(config);
885
886 let fitted1 = sampler1.fit(&x, &()).unwrap();
887 let fitted2 = sampler2.fit(&x, &()).unwrap();
888
889 assert_eq!(fitted1.optimal_components(), fitted2.optimal_components());
890 assert_abs_diff_eq!(
891 fitted1.quality_score(),
892 fitted2.quality_score(),
893 epsilon = 1e-10
894 );
895 }
896}