1use std::time::Instant;
14
15use scirs2_core::ndarray::{Array1, Array2};
16use scirs2_core::random::rngs::StdRng;
17use scirs2_core::random::{seeded_rng, CoreRandom};
18
19use crate::kernels::{Kernel, KernelType};
20use crate::svc::SVC;
21use sklears_core::error::Result;
22use sklears_core::traits::{Fit, Predict};
23
24#[derive(Debug, Clone)]
26pub struct PropertyTestConfig {
27 pub test_cases: usize,
29 pub max_samples: usize,
31 pub max_features: usize,
33 pub numerical_tolerance: f64,
35 pub max_training_time: f64,
37 pub min_accuracy: f64,
39 pub random_seed: Option<u64>,
41}
42
43impl Default for PropertyTestConfig {
44 fn default() -> Self {
45 Self {
46 test_cases: 100,
47 max_samples: 1000,
48 max_features: 50,
49 numerical_tolerance: 1e-10,
50 max_training_time: 60.0,
51 min_accuracy: 0.7,
52 random_seed: Some(42),
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct PropertyTestResult {
60 pub test_name: String,
62 pub passed: usize,
64 pub failed: usize,
66 pub total: usize,
68 pub avg_time: f64,
70 pub failures: Vec<String>,
72}
73
74impl PropertyTestResult {
75 fn new(test_name: String) -> Self {
76 Self {
77 test_name,
78 passed: 0,
79 failed: 0,
80 total: 0,
81 avg_time: 0.0,
82 failures: Vec::new(),
83 }
84 }
85
86 fn success_rate(&self) -> f64 {
87 if self.total == 0 {
88 0.0
89 } else {
90 self.passed as f64 / self.total as f64
91 }
92 }
93}
94
95pub struct SVMPropertyTester {
97 config: PropertyTestConfig,
98 rng: CoreRandom<StdRng>,
99}
100
101impl SVMPropertyTester {
102 pub fn new(config: PropertyTestConfig) -> Self {
104 let rng = seeded_rng(config.random_seed.unwrap_or(42));
105
106 Self { config, rng }
107 }
108
109 pub fn run_all_tests(&mut self) -> Vec<PropertyTestResult> {
111 vec![
112 self.test_convexity_property(),
114 self.test_kkt_conditions(),
115 self.test_dual_gap(),
116 self.test_kernel_properties(),
117 self.test_numerical_stability(),
119 self.test_convergence_properties(),
120 self.test_scale_invariance(),
121 self.test_outlier_robustness(),
123 self.test_noise_robustness(),
124 self.test_edge_cases(),
125 self.test_training_time_bounds(),
127 self.test_memory_usage(),
128 self.test_prediction_consistency(),
129 ]
130 }
131
132 pub fn test_convexity_property(&mut self) -> PropertyTestResult {
134 let mut result = PropertyTestResult::new("Convexity Property".to_string());
135 let start_time = Instant::now();
136
137 for _ in 0..self.config.test_cases {
138 result.total += 1;
139
140 let n_samples = self.rng.gen_range(10..100 + 1);
142 let n_features = self.rng.gen_range(2..10 + 1);
143 let (x, y) = self.generate_linearly_separable_dataset(n_samples, n_features);
144
145 match self.test_convexity_single_case(&x, &y) {
148 Ok(true) => result.passed += 1,
149 Ok(false) => {
150 result.failed += 1;
151 result
152 .failures
153 .push("Convexity property violated".to_string());
154 }
155 Err(e) => {
156 result.failed += 1;
157 result.failures.push(format!("Error: {e}"));
158 }
159 }
160 }
161
162 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
163 result
164 }
165
166 pub fn test_kkt_conditions(&mut self) -> PropertyTestResult {
168 let mut result = PropertyTestResult::new("KKT Conditions".to_string());
169 let start_time = Instant::now();
170
171 for _ in 0..self.config.test_cases {
172 result.total += 1;
173
174 let n_samples = self.rng.gen_range(20..200 + 1);
175 let n_features = self.rng.gen_range(2..20 + 1);
176 let (x, y) = self.generate_random_dataset(n_samples, n_features);
177
178 match self.test_kkt_conditions_single_case(&x, &y) {
179 Ok(true) => result.passed += 1,
180 Ok(false) => {
181 result.failed += 1;
182 result.failures.push("KKT conditions violated".to_string());
183 }
184 Err(e) => {
185 result.failed += 1;
186 result.failures.push(format!("Error: {e}"));
187 }
188 }
189 }
190
191 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
192 result
193 }
194
195 pub fn test_dual_gap(&mut self) -> PropertyTestResult {
197 let mut result = PropertyTestResult::new("Dual Gap Property".to_string());
198 let start_time = Instant::now();
199
200 for _ in 0..self.config.test_cases {
201 result.total += 1;
202
203 let n_samples = self.rng.gen_range(20..100 + 1);
204 let n_features = self.rng.gen_range(2..10 + 1);
205 let (x, y) = self.generate_random_dataset(n_samples, n_features);
206
207 match self.test_dual_gap_single_case(&x, &y) {
208 Ok(true) => result.passed += 1,
209 Ok(false) => {
210 result.failed += 1;
211 result
212 .failures
213 .push("Dual gap property violated".to_string());
214 }
215 Err(e) => {
216 result.failed += 1;
217 result.failures.push(format!("Error: {e}"));
218 }
219 }
220 }
221
222 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
223 result
224 }
225
226 pub fn test_kernel_properties(&mut self) -> PropertyTestResult {
228 let mut result = PropertyTestResult::new("Kernel Properties".to_string());
229 let start_time = Instant::now();
230
231 let kernels = vec![
232 KernelType::Linear,
233 KernelType::Rbf { gamma: 1.0 },
234 KernelType::Polynomial {
235 gamma: 1.0,
236 degree: 2.0,
237 coef0: 1.0,
238 },
239 KernelType::Polynomial {
240 gamma: 1.0,
241 degree: 3.0,
242 coef0: 0.0,
243 },
244 ];
245
246 for kernel_type in kernels {
247 for _ in 0..self.config.test_cases / 4 {
248 result.total += 1;
249
250 let n_samples = self.rng.gen_range(10..50 + 1);
251 let n_features = self.rng.gen_range(2..10 + 1);
252 let x = self.generate_random_matrix(n_samples, n_features);
253
254 match self.test_kernel_properties_single_case(&x, &kernel_type) {
255 Ok(true) => result.passed += 1,
256 Ok(false) => {
257 result.failed += 1;
258 result
259 .failures
260 .push(format!("Kernel property violated: {:?}", kernel_type));
261 }
262 Err(e) => {
263 result.failed += 1;
264 result
265 .failures
266 .push(format!("Error with {:?}: {}", kernel_type, e));
267 }
268 }
269 }
270 }
271
272 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
273 result
274 }
275
276 pub fn test_numerical_stability(&mut self) -> PropertyTestResult {
278 let mut result = PropertyTestResult::new("Numerical Stability".to_string());
279 let start_time = Instant::now();
280
281 for _ in 0..self.config.test_cases {
282 result.total += 1;
283
284 let (x, y) = match self.rng.gen_range(0..4) {
286 0 => self.generate_ill_conditioned_dataset(50, 5),
287 1 => self.generate_very_small_values_dataset(50, 5),
288 2 => self.generate_very_large_values_dataset(50, 5),
289 _ => self.generate_near_duplicate_samples_dataset(50, 5),
290 };
291
292 match self.test_numerical_stability_single_case(&x, &y) {
293 Ok(true) => result.passed += 1,
294 Ok(false) => {
295 result.failed += 1;
296 result
297 .failures
298 .push("Numerical instability detected".to_string());
299 }
300 Err(e) => {
301 result.failed += 1;
302 result.failures.push(format!("Error: {e}"));
303 }
304 }
305 }
306
307 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
308 result
309 }
310
311 pub fn test_convergence_properties(&mut self) -> PropertyTestResult {
313 let mut result = PropertyTestResult::new("Convergence Properties".to_string());
314 let start_time = Instant::now();
315
316 for _ in 0..self.config.test_cases {
317 result.total += 1;
318
319 let n_samples = self.rng.gen_range(50..200 + 1);
320 let n_features = self.rng.gen_range(2..20 + 1);
321 let (x, y) = self.generate_random_dataset(n_samples, n_features);
322
323 match self.test_convergence_single_case(&x, &y) {
324 Ok(true) => result.passed += 1,
325 Ok(false) => {
326 result.failed += 1;
327 result
328 .failures
329 .push("Convergence property violated".to_string());
330 }
331 Err(e) => {
332 result.failed += 1;
333 result.failures.push(format!("Error: {e}"));
334 }
335 }
336 }
337
338 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
339 result
340 }
341
342 pub fn test_scale_invariance(&mut self) -> PropertyTestResult {
344 let mut result = PropertyTestResult::new("Scale Invariance".to_string());
345 let start_time = Instant::now();
346
347 for _ in 0..self.config.test_cases {
348 result.total += 1;
349
350 let n_samples = self.rng.gen_range(20..100 + 1);
351 let n_features = self.rng.gen_range(2..10 + 1);
352 let (x, y) = self.generate_random_dataset(n_samples, n_features);
353
354 match self.test_scale_invariance_single_case(&x, &y) {
355 Ok(true) => result.passed += 1,
356 Ok(false) => {
357 result.failed += 1;
358 result
359 .failures
360 .push("Scale invariance violated".to_string());
361 }
362 Err(e) => {
363 result.failed += 1;
364 result.failures.push(format!("Error: {e}"));
365 }
366 }
367 }
368
369 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
370 result
371 }
372
373 pub fn test_outlier_robustness(&mut self) -> PropertyTestResult {
375 let mut result = PropertyTestResult::new("Outlier Robustness".to_string());
376 let start_time = Instant::now();
377
378 for _ in 0..self.config.test_cases {
379 result.total += 1;
380
381 let n_samples = self.rng.gen_range(50..200 + 1);
382 let n_features = self.rng.gen_range(2..10 + 1);
383 let (x, y) = self.generate_dataset_with_outliers(
384 n_samples, n_features, 0.1, );
386
387 match self.test_outlier_robustness_single_case(&x, &y) {
388 Ok(true) => result.passed += 1,
389 Ok(false) => {
390 result.failed += 1;
391 result
392 .failures
393 .push("Outlier robustness failed".to_string());
394 }
395 Err(e) => {
396 result.failed += 1;
397 result.failures.push(format!("Error: {e}"));
398 }
399 }
400 }
401
402 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
403 result
404 }
405
406 pub fn test_noise_robustness(&mut self) -> PropertyTestResult {
408 let mut result = PropertyTestResult::new("Noise Robustness".to_string());
409 let start_time = Instant::now();
410
411 for _ in 0..self.config.test_cases {
412 result.total += 1;
413
414 let n_samples = self.rng.gen_range(50..200 + 1);
415 let n_features = self.rng.gen_range(2..10 + 1);
416 let (x, y) = self.generate_noisy_dataset(
417 n_samples, n_features, 0.1, );
419
420 match self.test_noise_robustness_single_case(&x, &y) {
421 Ok(true) => result.passed += 1,
422 Ok(false) => {
423 result.failed += 1;
424 result.failures.push("Noise robustness failed".to_string());
425 }
426 Err(e) => {
427 result.failed += 1;
428 result.failures.push(format!("Error: {e}"));
429 }
430 }
431 }
432
433 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
434 result
435 }
436
437 pub fn test_edge_cases(&mut self) -> PropertyTestResult {
439 let mut result = PropertyTestResult::new("Edge Cases".to_string());
440 let start_time = Instant::now();
441
442 let edge_cases = vec![
444 ("Single sample", 1, 2),
445 ("Two samples", 2, 2),
446 ("Single feature", 10, 1),
447 ("More features than samples", 5, 10),
448 ("Square dataset", 10, 10),
449 ];
450
451 for (case_name, n_samples, n_features) in edge_cases {
452 result.total += 1;
453
454 let (x, y) = self.generate_random_dataset(n_samples, n_features);
455
456 match self.test_edge_case_single(&x, &y, case_name) {
457 Ok(true) => result.passed += 1,
458 Ok(false) => {
459 result.failed += 1;
460 result
461 .failures
462 .push(format!("Edge case failed: {case_name}"));
463 }
464 Err(e) => {
465 result.failed += 1;
466 result
467 .failures
468 .push(format!("Error in {}: {}", case_name, e));
469 }
470 }
471 }
472
473 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
474 result
475 }
476
477 pub fn test_training_time_bounds(&mut self) -> PropertyTestResult {
479 let mut result = PropertyTestResult::new("Training Time Bounds".to_string());
480 let start_time = Instant::now();
481
482 for _ in 0..self.config.test_cases {
483 result.total += 1;
484
485 let n_samples = self.rng.gen_range(100..500 + 1);
486 let n_features = self.rng.gen_range(5..20 + 1);
487 let (x, y) = self.generate_random_dataset(n_samples, n_features);
488
489 match self.test_training_time_single_case(&x, &y) {
490 Ok(true) => result.passed += 1,
491 Ok(false) => {
492 result.failed += 1;
493 result
494 .failures
495 .push("Training time exceeded bounds".to_string());
496 }
497 Err(e) => {
498 result.failed += 1;
499 result.failures.push(format!("Error: {e}"));
500 }
501 }
502 }
503
504 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
505 result
506 }
507
508 pub fn test_memory_usage(&mut self) -> PropertyTestResult {
510 let mut result = PropertyTestResult::new("Memory Usage".to_string());
511 let start_time = Instant::now();
512
513 for _ in 0..self.config.test_cases {
514 result.total += 1;
515
516 let n_samples = self.rng.gen_range(100..1000 + 1);
517 let n_features = self.rng.gen_range(5..50 + 1);
518 let (x, y) = self.generate_random_dataset(n_samples, n_features);
519
520 match self.test_memory_usage_single_case(&x, &y) {
521 Ok(true) => result.passed += 1,
522 Ok(false) => {
523 result.failed += 1;
524 result
525 .failures
526 .push("Memory usage exceeded bounds".to_string());
527 }
528 Err(e) => {
529 result.failed += 1;
530 result.failures.push(format!("Error: {e}"));
531 }
532 }
533 }
534
535 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
536 result
537 }
538
539 pub fn test_prediction_consistency(&mut self) -> PropertyTestResult {
541 let mut result = PropertyTestResult::new("Prediction Consistency".to_string());
542 let start_time = Instant::now();
543
544 for _ in 0..self.config.test_cases {
545 result.total += 1;
546
547 let n_samples = self.rng.gen_range(50..200 + 1);
548 let n_features = self.rng.gen_range(2..10 + 1);
549 let (x, y) = self.generate_random_dataset(n_samples, n_features);
550
551 match self.test_prediction_consistency_single_case(&x, &y) {
552 Ok(true) => result.passed += 1,
553 Ok(false) => {
554 result.failed += 1;
555 result
556 .failures
557 .push("Prediction consistency violated".to_string());
558 }
559 Err(e) => {
560 result.failed += 1;
561 result.failures.push(format!("Error: {e}"));
562 }
563 }
564 }
565
566 result.avg_time = start_time.elapsed().as_secs_f64() / result.total as f64;
567 result
568 }
569
570 fn test_convexity_single_case(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<bool> {
572 let svm1 = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
574 let svm2 = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
575
576 let fitted_svm1 = svm1.fit(x, y)?;
578 let fitted_svm2 = svm2.fit(x, y)?;
579
580 let decision1 = fitted_svm1.decision_function(x)?;
582 let decision2 = fitted_svm2.decision_function(x)?;
583
584 let diff_norm = (&decision1 - &decision2)
585 .iter()
586 .map(|v| v * v)
587 .sum::<f64>()
588 .sqrt();
589 Ok(diff_norm < self.config.numerical_tolerance * 10.0)
590 }
591
592 fn test_kkt_conditions_single_case(
593 &mut self,
594 x: &Array2<f64>,
595 y: &Array1<f64>,
596 ) -> Result<bool> {
597 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
598
599 let fitted_svm = svm.fit(x, y)?;
600
601 let decision_values = fitted_svm.decision_function(x)?;
603 let margins: Vec<f64> = decision_values
604 .iter()
605 .zip(y.iter())
606 .map(|(&d, &label)| label * d)
607 .collect();
608
609 let dual_coef = fitted_svm.dual_coef();
611 let mut kkt_satisfied = true;
612 for (i, &margin) in margins.iter().enumerate() {
613 let alpha = if i < dual_coef.len() {
614 dual_coef[i]
615 } else {
616 0.0
617 };
618
619 if alpha > self.config.numerical_tolerance
621 && (margin - 1.0).abs() > self.config.numerical_tolerance
622 {
623 kkt_satisfied = false;
624 break;
625 }
626
627 if alpha < -self.config.numerical_tolerance
629 || alpha > 1.0 + self.config.numerical_tolerance
630 {
631 kkt_satisfied = false;
632 break;
633 }
634 }
635
636 Ok(kkt_satisfied)
637 }
638
639 fn test_dual_gap_single_case(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<bool> {
640 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
641
642 let fitted_svm = svm.fit(x, y)?;
643
644 let decision_values = fitted_svm.decision_function(x)?;
646 let margins: Vec<f64> = decision_values
647 .iter()
648 .zip(y.iter())
649 .map(|(&d, &label)| label * d)
650 .collect();
651
652 let hinge_loss: f64 = margins.iter().map(|&m| (1.0 - m).max(0.0)).sum();
654 let primal_obj =
655 0.5 * fitted_svm.dual_coef().iter().map(|v| v * v).sum::<f64>() + hinge_loss;
656
657 let dual_obj = fitted_svm.dual_coef().iter().sum::<f64>()
659 - 0.5 * fitted_svm.dual_coef().iter().map(|v| v * v).sum::<f64>();
660
661 let dual_gap = primal_obj - dual_obj;
663 Ok(dual_gap >= -self.config.numerical_tolerance && dual_gap < 1.0)
664 }
665
666 fn test_kernel_properties_single_case(
667 &mut self,
668 x: &Array2<f64>,
669 kernel_type: &KernelType,
670 ) -> Result<bool> {
671 let kernel = kernel_type.clone();
672 let n = x.nrows();
673
674 for i in 0..n.min(10) {
676 for j in (i + 1)..n.min(10) {
677 let k_ij = kernel.compute(x.row(i), x.row(j));
678 let k_ji = kernel.compute(x.row(j), x.row(i));
679
680 if (k_ij - k_ji).abs() > self.config.numerical_tolerance {
681 return Ok(false);
682 }
683 }
684 }
685
686 let m = n.min(20);
688 let mut gram_matrix = Array2::zeros((m, m));
689 for i in 0..m {
690 for j in 0..m {
691 gram_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
692 }
693 }
694
695 let trace = (0..m).map(|i| gram_matrix[[i, i]]).sum::<f64>();
698 Ok(trace >= 0.0)
699 }
700
701 fn test_numerical_stability_single_case(
702 &mut self,
703 x: &Array2<f64>,
704 y: &Array1<f64>,
705 ) -> Result<bool> {
706 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
707
708 match svm.fit(x, y) {
710 Ok(fitted_svm) => {
711 let predictions = fitted_svm.predict(x)?;
713 let has_nan = predictions.iter().any(|&p| p.is_nan());
714 let has_inf = predictions.iter().any(|&p| p.is_infinite());
715
716 Ok(!has_nan && !has_inf)
717 }
718 Err(_) => Ok(false), }
720 }
721
722 fn test_convergence_single_case(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<bool> {
723 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
724
725 let fitted_svm = svm.fit(x, y)?;
727
728 let decision_values = fitted_svm.decision_function(x)?;
730 let margins: Vec<f64> = decision_values
731 .iter()
732 .zip(y.iter())
733 .map(|(&d, &label)| label * d)
734 .collect();
735
736 let avg_margin = margins.iter().sum::<f64>() / margins.len() as f64;
738 Ok(avg_margin.is_finite() && avg_margin.abs() < 1000.0)
739 }
740
741 fn test_scale_invariance_single_case(
742 &mut self,
743 x: &Array2<f64>,
744 y: &Array1<f64>,
745 ) -> Result<bool> {
746 let svm1 = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
748
749 let fitted_svm1 = svm1.fit(x, y)?;
750 let pred1 = fitted_svm1.predict(x)?;
751
752 let scale_factor = 10.0;
754 let x_scaled = x * scale_factor;
755 let svm2 = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
756 let fitted_svm2 = svm2.fit(&x_scaled, y)?;
757 let pred2 = fitted_svm2.predict(&x_scaled)?;
758
759 let diff = (&pred1 - &pred2).iter().map(|v| v * v).sum::<f64>().sqrt();
761 Ok(diff < self.config.numerical_tolerance * 10.0)
762 }
763
764 fn test_outlier_robustness_single_case(
765 &mut self,
766 x: &Array2<f64>,
767 y: &Array1<f64>,
768 ) -> Result<bool> {
769 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
770
771 let result = svm.fit(x, y);
773
774 match result {
776 Ok(fitted_svm) => {
777 let predictions = fitted_svm.predict(x)?;
778 let accuracy = predictions
779 .iter()
780 .zip(y.iter())
781 .map(|(&p, &t)| if (p - t).abs() < 0.5 { 1.0 } else { 0.0 })
782 .sum::<f64>()
783 / predictions.len() as f64;
784
785 Ok(accuracy > 0.5) }
787 Err(_) => Ok(false),
788 }
789 }
790
791 fn test_noise_robustness_single_case(
792 &mut self,
793 x: &Array2<f64>,
794 y: &Array1<f64>,
795 ) -> Result<bool> {
796 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
797
798 let result = svm.fit(x, y);
800
801 match result {
802 Ok(fitted_svm) => {
803 let predictions = fitted_svm.predict(x)?;
804 let accuracy = predictions
805 .iter()
806 .zip(y.iter())
807 .map(|(&p, &t)| if (p - t).abs() < 0.5 { 1.0 } else { 0.0 })
808 .sum::<f64>()
809 / predictions.len() as f64;
810
811 Ok(accuracy > self.config.min_accuracy)
812 }
813 Err(_) => Ok(false),
814 }
815 }
816
817 fn test_edge_case_single(
818 &mut self,
819 x: &Array2<f64>,
820 y: &Array1<f64>,
821 case_name: &str,
822 ) -> Result<bool> {
823 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
824
825 match svm.fit(x, y) {
827 Ok(fitted_svm) => {
828 let predictions = fitted_svm.predict(x)?;
829 let has_valid_predictions = predictions.iter().all(|&p| p.is_finite());
830 Ok(has_valid_predictions)
831 }
832 Err(_) => {
833 match case_name {
835 "Single sample" => Ok(true), _ => Ok(false),
837 }
838 }
839 }
840 }
841
842 fn test_training_time_single_case(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<bool> {
843 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
844
845 let start_time = Instant::now();
846 let result = svm.fit(x, y);
847 let training_time = start_time.elapsed().as_secs_f64();
848
849 match result {
850 Ok(_) => Ok(training_time < self.config.max_training_time),
851 Err(_) => Ok(false),
852 }
853 }
854
855 fn test_memory_usage_single_case(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<bool> {
856 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
857
858 let result = svm.fit(x, y);
861
862 match result {
863 Ok(fitted_svm) => {
864 let model_size = std::mem::size_of_val(&fitted_svm);
867 Ok(model_size < 1024 * 1024) }
869 Err(_) => Ok(false),
870 }
871 }
872
873 fn test_prediction_consistency_single_case(
874 &mut self,
875 x: &Array2<f64>,
876 y: &Array1<f64>,
877 ) -> Result<bool> {
878 let svm = SVC::new().c(1.0).linear().tol(1e-6).max_iter(1000);
879
880 let fitted_svm = svm.fit(x, y)?;
881
882 let pred1 = fitted_svm.predict(x)?;
884 let pred2 = fitted_svm.predict(x)?;
885
886 let diff = (&pred1 - &pred2).iter().map(|v| v * v).sum::<f64>().sqrt();
887 Ok(diff < self.config.numerical_tolerance)
888 }
889
890 fn generate_random_dataset(
892 &mut self,
893 n_samples: usize,
894 n_features: usize,
895 ) -> (Array2<f64>, Array1<f64>) {
896 let x = Array2::from_shape_fn((n_samples, n_features), |_| self.rng.gen_range(-1.0..2.0));
897 let y = Array1::from_shape_fn(n_samples, |_| {
898 if self.rng.random::<f64>() > 0.5 {
899 1.0
900 } else {
901 -1.0
902 }
903 });
904 (x, y)
905 }
906
907 fn generate_linearly_separable_dataset(
908 &mut self,
909 n_samples: usize,
910 n_features: usize,
911 ) -> (Array2<f64>, Array1<f64>) {
912 let mut x =
913 Array2::from_shape_fn((n_samples, n_features), |_| self.rng.gen_range(-1.0..2.0));
914 let w = Array1::from_shape_fn(n_features, |_| self.rng.gen_range(-1.0..2.0));
915
916 let mut y = Array1::zeros(n_samples);
918 for i in 0..n_samples {
919 let decision = x.row(i).dot(&w);
920 y[i] = if decision > 0.0 { 1.0 } else { -1.0 };
921 }
922
923 for i in 0..n_samples {
925 let margin = 0.1 * y[i];
926 for k in 0..n_features {
927 x[[i, k]] += w[k] * margin;
928 }
929 }
930
931 (x, y)
932 }
933
934 fn generate_random_matrix(&mut self, n_rows: usize, n_cols: usize) -> Array2<f64> {
935 Array2::from_shape_fn((n_rows, n_cols), |_| self.rng.gen_range(-1.0..2.0))
936 }
937
938 fn generate_ill_conditioned_dataset(
939 &mut self,
940 n_samples: usize,
941 n_features: usize,
942 ) -> (Array2<f64>, Array1<f64>) {
943 let mut x = self.generate_random_matrix(n_samples, n_features);
944
945 if n_features > 1 {
947 let noise_scale = 1e-10;
948 for j in 1..n_features {
949 for i in 0..n_samples {
950 x[[i, j]] = x[[i, 0]] + self.rng.gen_range(-noise_scale..noise_scale + 1.0);
951 }
952 }
953 }
954
955 let y = Array1::from_shape_fn(n_samples, |_| {
956 if self.rng.random::<f64>() > 0.5 {
957 1.0
958 } else {
959 -1.0
960 }
961 });
962 (x, y)
963 }
964
965 fn generate_very_small_values_dataset(
966 &mut self,
967 n_samples: usize,
968 n_features: usize,
969 ) -> (Array2<f64>, Array1<f64>) {
970 let scale = 1e-10;
971 let x = Array2::from_shape_fn((n_samples, n_features), |_| {
972 self.rng.gen_range(-scale..scale + 1.0)
973 });
974 let y = Array1::from_shape_fn(n_samples, |_| {
975 if self.rng.random::<f64>() > 0.5 {
976 1.0
977 } else {
978 -1.0
979 }
980 });
981 (x, y)
982 }
983
984 fn generate_very_large_values_dataset(
985 &mut self,
986 n_samples: usize,
987 n_features: usize,
988 ) -> (Array2<f64>, Array1<f64>) {
989 let scale = 1e10;
990 let x = Array2::from_shape_fn((n_samples, n_features), |_| {
991 self.rng.gen_range(-scale..scale + 1.0)
992 });
993 let y = Array1::from_shape_fn(n_samples, |_| {
994 if self.rng.random::<f64>() > 0.5 {
995 1.0
996 } else {
997 -1.0
998 }
999 });
1000 (x, y)
1001 }
1002
1003 fn generate_near_duplicate_samples_dataset(
1004 &mut self,
1005 n_samples: usize,
1006 n_features: usize,
1007 ) -> (Array2<f64>, Array1<f64>) {
1008 let base_sample = Array1::from_shape_fn(n_features, |_| self.rng.gen_range(-1.0..2.0));
1009 let noise_scale = 1e-8;
1010
1011 let mut x = Array2::zeros((n_samples, n_features));
1012 for i in 0..n_samples {
1013 for j in 0..n_features {
1014 x[[i, j]] = base_sample[j] + self.rng.gen_range(-noise_scale..noise_scale + 1.0);
1015 }
1016 }
1017
1018 let y = Array1::from_shape_fn(n_samples, |_| {
1019 if self.rng.random::<f64>() > 0.5 {
1020 1.0
1021 } else {
1022 -1.0
1023 }
1024 });
1025 (x, y)
1026 }
1027
1028 fn generate_dataset_with_outliers(
1029 &mut self,
1030 n_samples: usize,
1031 n_features: usize,
1032 outlier_fraction: f64,
1033 ) -> (Array2<f64>, Array1<f64>) {
1034 let n_outliers = (n_samples as f64 * outlier_fraction) as usize;
1035 let n_normal = n_samples - n_outliers;
1036
1037 let mut x = Array2::zeros((n_samples, n_features));
1039 let mut y = Array1::zeros(n_samples);
1040
1041 for i in 0..n_normal {
1043 for j in 0..n_features {
1044 x[[i, j]] = self.rng.gen_range(-1.0..2.0);
1045 }
1046 y[i] = if self.rng.random::<f64>() > 0.5 {
1047 1.0
1048 } else {
1049 -1.0
1050 };
1051 }
1052
1053 let outlier_scale = 10.0;
1055 for i in n_normal..n_samples {
1056 for j in 0..n_features {
1057 x[[i, j]] = self.rng.gen_range(-outlier_scale..outlier_scale + 1.0);
1058 }
1059 y[i] = if self.rng.random::<f64>() > 0.5 {
1060 1.0
1061 } else {
1062 -1.0
1063 };
1064 }
1065
1066 (x, y)
1067 }
1068
1069 fn generate_noisy_dataset(
1070 &mut self,
1071 n_samples: usize,
1072 n_features: usize,
1073 noise_level: f64,
1074 ) -> (Array2<f64>, Array1<f64>) {
1075 let (mut x, mut y) = self.generate_linearly_separable_dataset(n_samples, n_features);
1076
1077 for i in 0..n_samples {
1079 for j in 0..n_features {
1080 x[[i, j]] += self.rng.gen_range(-noise_level..noise_level + 1.0);
1081 }
1082 }
1083
1084 let n_label_flips = (n_samples as f64 * noise_level) as usize;
1086 for _ in 0..n_label_flips {
1087 let idx = self.rng.gen_range(0..n_samples);
1088 y[idx] = -y[idx];
1089 }
1090
1091 (x, y)
1092 }
1093
1094 pub fn print_results_summary(&self, results: &[PropertyTestResult]) {
1096 println!(
1097 "
1098=== SVM Property Test Results ==="
1099 );
1100 println!(
1101 "{:<25} {:<8} {:<8} {:<8} {:<12} {:<10}",
1102 "Test Name", "Passed", "Failed", "Total", "Success Rate", "Avg Time"
1103 );
1104 println!("{}", "-".repeat(75));
1105
1106 for result in results {
1107 println!(
1108 "{:<25} {:<8} {:<8} {:<8} {:<12.2}% {:<10.3}s",
1109 result.test_name,
1110 result.passed,
1111 result.failed,
1112 result.total,
1113 result.success_rate() * 100.0,
1114 result.avg_time
1115 );
1116 }
1117
1118 let total_passed: usize = results.iter().map(|r| r.passed).sum();
1119 let total_failed: usize = results.iter().map(|r| r.failed).sum();
1120 let total_tests: usize = results.iter().map(|r| r.total).sum();
1121 let overall_success_rate = if total_tests > 0 {
1122 total_passed as f64 / total_tests as f64
1123 } else {
1124 0.0
1125 };
1126
1127 println!("{}", "-".repeat(75));
1128 println!(
1129 "{:<25} {:<8} {:<8} {:<8} {:<12.2}%",
1130 "OVERALL",
1131 total_passed,
1132 total_failed,
1133 total_tests,
1134 overall_success_rate * 100.0
1135 );
1136
1137 if total_failed > 0 {
1139 println!(
1140 "
1141=== Failure Summary ==="
1142 );
1143 for result in results {
1144 if !result.failures.is_empty() {
1145 println!(
1146 "
1147{}: {} failures",
1148 result.test_name,
1149 result.failures.len()
1150 );
1151 for (i, failure) in result.failures.iter().enumerate() {
1152 if i < 3 {
1153 println!(" - {failure}");
1155 } else if i == 3 {
1156 println!(" - ... and {} more", result.failures.len() - 3);
1157 break;
1158 }
1159 }
1160 }
1161 }
1162 }
1163 }
1164}
1165
1166impl Default for SVMPropertyTester {
1167 fn default() -> Self {
1169 Self::new(PropertyTestConfig::default())
1170 }
1171}
1172
1173#[allow(non_snake_case)]
1174#[cfg(test)]
1175mod tests {
1176 use super::*;
1177
1178 #[test]
1179 fn test_property_tester_creation() {
1180 let config = PropertyTestConfig::default();
1181 let tester = SVMPropertyTester::new(config);
1182
1183 assert!(tester.config.test_cases > 0);
1185 assert!(tester.config.max_samples > 0);
1186 assert!(tester.config.max_features > 0);
1187 }
1188
1189 #[test]
1190 fn test_data_generation() {
1191 let config = PropertyTestConfig::default();
1192 let mut tester = SVMPropertyTester::new(config);
1193
1194 let (x, y) = tester.generate_random_dataset(10, 5);
1195 assert_eq!(x.nrows(), 10);
1196 assert_eq!(x.ncols(), 5);
1197 assert_eq!(y.len(), 10);
1198
1199 let (x, y) = tester.generate_linearly_separable_dataset(20, 3);
1200 assert_eq!(x.nrows(), 20);
1201 assert_eq!(x.ncols(), 3);
1202 assert_eq!(y.len(), 20);
1203 }
1204
1205 #[test]
1206 fn test_single_property_test() {
1207 let config = PropertyTestConfig {
1208 test_cases: 5,
1209 max_samples: 50,
1210 max_features: 5,
1211 ..Default::default()
1212 };
1213 let mut tester = SVMPropertyTester::new(config);
1214
1215 let result = tester.test_prediction_consistency();
1216 assert!(result.total > 0);
1217 println!(
1218 "Prediction consistency test: {}/{} passed",
1219 result.passed, result.total
1220 );
1221 }
1222
1223 #[test]
1224 fn test_results_summary() {
1225 let config = PropertyTestConfig {
1226 test_cases: 2,
1227 max_samples: 20,
1228 max_features: 3,
1229 ..Default::default()
1230 };
1231 let mut tester = SVMPropertyTester::new(config);
1232
1233 let results = vec![
1234 tester.test_prediction_consistency(),
1235 tester.test_numerical_stability(),
1236 ];
1237
1238 tester.print_results_summary(&results);
1239
1240 assert_eq!(results.len(), 2);
1241 assert!(results.iter().all(|r| r.total > 0));
1242 }
1243}