Skip to main content

sklears_svm/
property_tests.rs

1//! Property-based testing framework for SVM algorithms
2//!
3//! This module provides comprehensive property-based testing utilities to ensure
4//! the mathematical correctness and robustness of SVM implementations.
5//!
6//! Properties tested include:
7//! - Mathematical properties (convexity, KKT conditions, etc.)
8//! - Numerical stability and convergence
9//! - Invariance properties
10//! - Robustness to edge cases
11//! - Performance characteristics
12
13use 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/// Configuration for property-based testing
25#[derive(Debug, Clone)]
26pub struct PropertyTestConfig {
27    /// Number of test cases to run
28    pub test_cases: usize,
29    /// Maximum number of samples in generated datasets
30    pub max_samples: usize,
31    /// Maximum number of features in generated datasets
32    pub max_features: usize,
33    /// Tolerance for numerical comparisons
34    pub numerical_tolerance: f64,
35    /// Maximum training time allowed (seconds)
36    pub max_training_time: f64,
37    /// Minimum accuracy threshold for synthetic datasets
38    pub min_accuracy: f64,
39    /// Random seed for reproducibility
40    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/// Property test result
58#[derive(Debug, Clone)]
59pub struct PropertyTestResult {
60    /// Test name
61    pub test_name: String,
62    /// Number of test cases passed
63    pub passed: usize,
64    /// Number of test cases failed
65    pub failed: usize,
66    /// Total test cases
67    pub total: usize,
68    /// Average execution time per test case
69    pub avg_time: f64,
70    /// Failure reasons
71    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
95/// Property-based test runner for SVM algorithms
96pub struct SVMPropertyTester {
97    config: PropertyTestConfig,
98    rng: CoreRandom<StdRng>,
99}
100
101impl SVMPropertyTester {
102    /// Create a new property tester
103    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    /// Run all property tests
110    pub fn run_all_tests(&mut self) -> Vec<PropertyTestResult> {
111        vec![
112            // Mathematical properties
113            self.test_convexity_property(),
114            self.test_kkt_conditions(),
115            self.test_dual_gap(),
116            self.test_kernel_properties(),
117            // Numerical stability
118            self.test_numerical_stability(),
119            self.test_convergence_properties(),
120            self.test_scale_invariance(),
121            // Robustness tests
122            self.test_outlier_robustness(),
123            self.test_noise_robustness(),
124            self.test_edge_cases(),
125            // Performance properties
126            self.test_training_time_bounds(),
127            self.test_memory_usage(),
128            self.test_prediction_consistency(),
129        ]
130    }
131
132    /// Test convexity property of SVM optimization
133    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            // Generate random dataset
141            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            // Test convexity by checking that linear combinations of solutions
146            // have objective value between the individual solutions
147            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    /// Test KKT conditions for optimality
167    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    /// Test dual gap property
196    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    /// Test kernel properties (positive definiteness, symmetry, etc.)
227    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    /// Test numerical stability
277    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            // Generate dataset with various numerical challenges
285            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    /// Test convergence properties
312    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    /// Test scale invariance
343    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    /// Test outlier robustness
374    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, // 10% outliers
385            );
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    /// Test noise robustness
407    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, // 10% noise level
418            );
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    /// Test edge cases
438    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        // Test various edge cases
443        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    /// Test training time bounds
478    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    /// Test memory usage
509    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    /// Test prediction consistency
540    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    // Helper methods for single test cases
571    fn test_convexity_single_case(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<bool> {
572        // Test convexity by checking that the objective function is convex
573        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        // Train two different models with different initializations
577        let fitted_svm1 = svm1.fit(x, y)?;
578        let fitted_svm2 = svm2.fit(x, y)?;
579
580        // Check if both converged to similar solutions (indicating convexity)
581        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        // Check KKT conditions
602        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        // Check complementary slackness and stationarity conditions
610        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            // Check complementary slackness: α_i * (1 - y_i * f(x_i)) = 0
620            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            // Check bounds: 0 ≤ α_i ≤ C
628            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        // Calculate primal and dual objectives
645        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        // Primal objective: (1/2)||w||² + C∑max(0, 1-y_i*f(x_i))
653        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        // Dual objective: ∑α_i - (1/2)∑∑α_i*α_j*y_i*y_j*K(x_i,x_j)
658        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        // Dual gap should be non-negative and small at optimum
662        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        // Test symmetry: K(x_i, x_j) = K(x_j, x_i)
675        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        // Test positive definiteness (at least positive semidefinite)
687        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        // Check that all eigenvalues are non-negative (simplified check):
696        // a positive semidefinite matrix has a non-negative trace.
697        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        // Try to fit and check for numerical issues
709        match svm.fit(x, y) {
710            Ok(fitted_svm) => {
711                // Check if predictions are reasonable
712                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), // Consider numerical failure as instability
719        }
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        // Train with different tolerances and check convergence
726        let fitted_svm = svm.fit(x, y)?;
727
728        // Check if the algorithm converged (basic check)
729        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        // Check if margins are reasonable
737        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        // Test with original data
747        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        // Test with scaled data
753        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        // Predictions should be the same
760        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        // Train on data with outliers
772        let result = svm.fit(x, y);
773
774        // Check that the algorithm can handle outliers without failing
775        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) // Should achieve reasonable accuracy even with outliers
786            }
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        // Train on noisy data
799        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        // Try to handle edge case gracefully
826        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                // For some edge cases, failure might be expected
834                match case_name {
835                    "Single sample" => Ok(true), // Expected to fail
836                    _ => 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        // This is a simplified memory test
859        // In practice, you'd want to use a proper memory profiler
860        let result = svm.fit(x, y);
861
862        match result {
863            Ok(fitted_svm) => {
864                // Check that the model doesn't use excessive memory
865                // This is a basic check - in practice you'd measure actual memory usage
866                let model_size = std::mem::size_of_val(&fitted_svm);
867                Ok(model_size < 1024 * 1024) // Less than 1MB
868            }
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        // Test that predictions are consistent across multiple calls
883        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    // Data generation methods
891    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        // Create linearly separable labels
917        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        // Add some margin to ensure separability
924        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        // Make the matrix ill-conditioned by making some columns nearly identical
946        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        // Combine normal and outlier samples into a single dataset
1038        let mut x = Array2::zeros((n_samples, n_features));
1039        let mut y = Array1::zeros(n_samples);
1040
1041        // Generate normal samples
1042        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        // Generate outliers
1054        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        // Add noise to features
1078        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        // Add label noise
1085        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    /// Print test results summary
1095    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        // Print failures summary
1138        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                            // Show first 3 failures
1154                            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    /// Create a new property tester with default configuration
1168    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        // Test that the tester was created successfully
1184        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}