Skip to main content

quantrs2_anneal/advanced_testing_framework/
utils.rs

1//! Utility functions and test helpers for advanced testing framework
2
3use super::{
4    ApplicationError, ApplicationResult, Duration, IsingModel, ProblemType, TestExecutionResult,
5    TestSuiteResults, TestingConfig,
6};
7use scirs2_core::random::prelude::*;
8
9use std::fmt::Write;
10/// Create standard test configuration
11pub fn create_standard_test_config(test_type: &str) -> ApplicationResult<TestingConfig> {
12    match test_type {
13        "performance" => Ok(TestingConfig {
14            enable_parallel: true,
15            max_concurrent_tests: 4,
16            test_timeout: Duration::from_secs(600),
17            performance_tolerance: 0.05,
18            significance_level: 0.05,
19            data_retention: Duration::from_secs(14 * 24 * 3600),
20            detailed_logging: true,
21            stress_test_sizes: vec![50, 100, 200, 500],
22        }),
23        "regression" => Ok(TestingConfig {
24            enable_parallel: false,
25            max_concurrent_tests: 1,
26            test_timeout: Duration::from_secs(300),
27            performance_tolerance: 0.1,
28            significance_level: 0.01,
29            data_retention: Duration::from_secs(60 * 24 * 3600),
30            detailed_logging: true,
31            stress_test_sizes: vec![10, 50, 100],
32        }),
33        "stress" => Ok(TestingConfig {
34            enable_parallel: true,
35            max_concurrent_tests: 8,
36            test_timeout: Duration::from_secs(1200),
37            performance_tolerance: 0.2,
38            significance_level: 0.05,
39            data_retention: Duration::from_secs(30 * 24 * 3600),
40            detailed_logging: false,
41            stress_test_sizes: vec![100, 500, 1000, 2000, 5000, 10_000],
42        }),
43        "property" => Ok(TestingConfig {
44            enable_parallel: true,
45            max_concurrent_tests: 6,
46            test_timeout: Duration::from_secs(180),
47            performance_tolerance: 0.1,
48            significance_level: 0.05,
49            data_retention: Duration::from_secs(21 * 24 * 3600),
50            detailed_logging: true,
51            stress_test_sizes: vec![10, 25, 50, 100],
52        }),
53        _ => Err(ApplicationError::ConfigurationError(format!(
54            "Unknown test type: {test_type}"
55        ))),
56    }
57}
58
59/// Create test problem with specific characteristics
60pub fn create_test_problem(
61    problem_type: ProblemType,
62    size: usize,
63    density: f64,
64    seed: Option<u64>,
65) -> ApplicationResult<IsingModel> {
66    let mut problem = IsingModel::new(size);
67
68    // Set random seed if provided
69    let mut rng_seed = seed.unwrap_or_else(|| thread_rng().random());
70
71    match problem_type {
72        ProblemType::RandomIsing => create_random_ising_problem(&mut problem, density, rng_seed)?,
73        ProblemType::MaxCut => create_max_cut_problem(&mut problem, density, rng_seed)?,
74        ProblemType::VertexCover => create_vertex_cover_problem(&mut problem, density, rng_seed)?,
75        ProblemType::TSP => create_tsp_problem(&mut problem, density, rng_seed)?,
76        ProblemType::Portfolio => create_portfolio_problem(&mut problem, density, rng_seed)?,
77        ProblemType::Custom(ref name) => {
78            return Err(ApplicationError::ConfigurationError(format!(
79                "Custom problem type not implemented: {name}"
80            )));
81        }
82    }
83
84    Ok(problem)
85}
86
87/// Create random Ising model problem
88fn create_random_ising_problem(
89    problem: &mut IsingModel,
90    density: f64,
91    seed: u64,
92) -> ApplicationResult<()> {
93    let size = problem.num_qubits;
94
95    // Use seed for reproducibility
96    let mut local_seed = seed;
97
98    // Set random biases
99    for i in 0..size {
100        local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
101        let bias = ((local_seed % 2000) as f64 / 1000.0) - 1.0; // Range [-1, 1]
102        problem.set_bias(i, bias)?;
103    }
104
105    // Set random couplings based on density
106    let max_edges = size * (size - 1) / 2;
107    let target_edges = (max_edges as f64 * density) as usize;
108
109    let mut edges_added = 0;
110    for i in 0..size {
111        for j in (i + 1)..size {
112            if edges_added >= target_edges {
113                break;
114            }
115
116            local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
117            if (local_seed % 1000) < (density * 1000.0) as u64 {
118                local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
119                let coupling = ((local_seed % 2000) as f64 / 1000.0) - 1.0; // Range [-1, 1]
120                problem.set_coupling(i, j, coupling)?;
121                edges_added += 1;
122            }
123        }
124        if edges_added >= target_edges {
125            break;
126        }
127    }
128
129    Ok(())
130}
131
132/// Create Max-Cut problem instance
133fn create_max_cut_problem(
134    problem: &mut IsingModel,
135    density: f64,
136    seed: u64,
137) -> ApplicationResult<()> {
138    let size = problem.num_qubits;
139    let mut local_seed = seed;
140
141    // Max-Cut: no biases, only edge weights
142    for i in 0..size {
143        problem.set_bias(i, 0.0)?;
144    }
145
146    // Add edges with weights
147    let max_edges = size * (size - 1) / 2;
148    let target_edges = (max_edges as f64 * density) as usize;
149
150    let mut edges_added = 0;
151    for i in 0..size {
152        for j in (i + 1)..size {
153            if edges_added >= target_edges {
154                break;
155            }
156
157            local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
158            if (local_seed % 1000) < (density * 1000.0) as u64 {
159                local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
160                let weight = (local_seed % 100) as f64 / 100.0; // Range [0, 1]
161                                                                // For Max-Cut, use negative coupling (ferromagnetic)
162                problem.set_coupling(i, j, -weight)?;
163                edges_added += 1;
164            }
165        }
166        if edges_added >= target_edges {
167            break;
168        }
169    }
170
171    Ok(())
172}
173
174/// Create Vertex Cover problem instance
175fn create_vertex_cover_problem(
176    problem: &mut IsingModel,
177    density: f64,
178    seed: u64,
179) -> ApplicationResult<()> {
180    let size = problem.num_qubits;
181    let mut local_seed = seed;
182
183    // Vertex Cover: penalty for not covering edges
184    // Set biases to encourage smaller covers
185    for i in 0..size {
186        problem.set_bias(i, 1.0)?; // Cost of including vertex
187    }
188
189    // Add edge constraints
190    let max_edges = size * (size - 1) / 2;
191    let target_edges = (max_edges as f64 * density) as usize;
192
193    let mut edges_added = 0;
194    for i in 0..size {
195        for j in (i + 1)..size {
196            if edges_added >= target_edges {
197                break;
198            }
199
200            local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
201            if (local_seed % 1000) < (density * 1000.0) as u64 {
202                // Large penalty if neither vertex is in cover
203                let penalty = 10.0;
204                problem.set_coupling(i, j, penalty)?;
205                edges_added += 1;
206            }
207        }
208        if edges_added >= target_edges {
209            break;
210        }
211    }
212
213    Ok(())
214}
215
216/// Create TSP problem instance (simplified)
217fn create_tsp_problem(problem: &mut IsingModel, _density: f64, seed: u64) -> ApplicationResult<()> {
218    let size = problem.num_qubits;
219    let mut local_seed = seed;
220
221    // TSP encoding requires careful mapping - simplified version here
222    // Set biases to encourage valid tours
223    for i in 0..size {
224        problem.set_bias(i, 0.0)?;
225    }
226
227    // Add constraints for TSP (simplified)
228    for i in 0..size {
229        for j in (i + 1)..size {
230            local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
231            let distance = (local_seed % 100) as f64 / 10.0; // Distance between cities
232            problem.set_coupling(i, j, distance)?;
233        }
234    }
235
236    Ok(())
237}
238
239/// Create Portfolio Optimization problem instance
240fn create_portfolio_problem(
241    problem: &mut IsingModel,
242    _density: f64,
243    seed: u64,
244) -> ApplicationResult<()> {
245    let size = problem.num_qubits;
246    let mut local_seed = seed;
247
248    // Portfolio: expected returns (biases) and correlations (couplings)
249    for i in 0..size {
250        local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
251        let expected_return = ((local_seed % 200) as f64 / 1000.0) + 0.05; // 5-25% return
252        problem.set_bias(i, -expected_return)?; // Negative because we want to maximize
253    }
254
255    // Add correlation matrix (risk)
256    for i in 0..size {
257        for j in (i + 1)..size {
258            local_seed = local_seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
259            let correlation = ((local_seed % 100) as f64 / 500.0) - 0.1; // Range [-0.1, 0.1]
260            problem.set_coupling(i, j, correlation)?;
261        }
262    }
263
264    Ok(())
265}
266
267/// Validate test framework configuration
268pub fn validate_framework_config(config: &TestingConfig) -> ApplicationResult<()> {
269    if config.max_concurrent_tests == 0 {
270        return Err(ApplicationError::ConfigurationError(
271            "max_concurrent_tests must be greater than 0".to_string(),
272        ));
273    }
274
275    if config.test_timeout.as_secs() == 0 {
276        return Err(ApplicationError::ConfigurationError(
277            "test_timeout must be greater than 0".to_string(),
278        ));
279    }
280
281    if config.performance_tolerance < 0.0 || config.performance_tolerance > 1.0 {
282        return Err(ApplicationError::ConfigurationError(
283            "performance_tolerance must be between 0.0 and 1.0".to_string(),
284        ));
285    }
286
287    if config.significance_level < 0.0 || config.significance_level > 1.0 {
288        return Err(ApplicationError::ConfigurationError(
289            "significance_level must be between 0.0 and 1.0".to_string(),
290        ));
291    }
292
293    if config.stress_test_sizes.is_empty() {
294        return Err(ApplicationError::ConfigurationError(
295            "stress_test_sizes cannot be empty".to_string(),
296        ));
297    }
298
299    // Check that stress test sizes are in ascending order
300    for i in 1..config.stress_test_sizes.len() {
301        if config.stress_test_sizes[i] <= config.stress_test_sizes[i - 1] {
302            return Err(ApplicationError::ConfigurationError(
303                "stress_test_sizes must be in ascending order".to_string(),
304            ));
305        }
306    }
307
308    Ok(())
309}
310
311/// Calculate test quality metrics
312#[must_use]
313pub fn calculate_test_quality_metrics(results: &[TestExecutionResult]) -> TestQualityMetrics {
314    if results.is_empty() {
315        return TestQualityMetrics {
316            mean_quality: 0.0,
317            std_dev_quality: 0.0,
318            min_quality: 0.0,
319            max_quality: 0.0,
320            median_quality: 0.0,
321            success_rate: 0.0,
322            mean_execution_time: Duration::default(),
323            std_dev_execution_time: Duration::default(),
324        };
325    }
326
327    let qualities: Vec<f64> = results.iter().map(|r| r.solution_quality).collect();
328    let execution_times: Vec<Duration> = results.iter().map(|r| r.execution_time).collect();
329
330    // Quality statistics
331    let mean_quality = qualities.iter().sum::<f64>() / qualities.len() as f64;
332    let variance_quality = qualities
333        .iter()
334        .map(|q| (q - mean_quality).powi(2))
335        .sum::<f64>()
336        / (qualities.len() - 1).max(1) as f64;
337    let std_dev_quality = variance_quality.sqrt();
338
339    let mut sorted_qualities = qualities;
340    sorted_qualities.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
341    let median_quality = if sorted_qualities.len() % 2 == 0 {
342        f64::midpoint(
343            sorted_qualities[sorted_qualities.len() / 2 - 1],
344            sorted_qualities[sorted_qualities.len() / 2],
345        )
346    } else {
347        sorted_qualities[sorted_qualities.len() / 2]
348    };
349
350    let min_quality = sorted_qualities[0];
351    let max_quality = sorted_qualities[sorted_qualities.len() - 1];
352
353    // Success rate (assuming convergence_achieved indicates success)
354    let successful_tests = results.iter().filter(|r| r.convergence_achieved).count();
355    let success_rate = successful_tests as f64 / results.len() as f64;
356
357    // Execution time statistics
358    let total_time: Duration = execution_times.iter().sum();
359    let mean_execution_time = total_time / execution_times.len() as u32;
360
361    let mean_time_secs = mean_execution_time.as_secs_f64();
362    let variance_time = execution_times
363        .iter()
364        .map(|t| (t.as_secs_f64() - mean_time_secs).powi(2))
365        .sum::<f64>()
366        / (execution_times.len() - 1).max(1) as f64;
367    let std_dev_execution_time = Duration::from_secs_f64(variance_time.sqrt());
368
369    TestQualityMetrics {
370        mean_quality,
371        std_dev_quality,
372        min_quality,
373        max_quality,
374        median_quality,
375        success_rate,
376        mean_execution_time,
377        std_dev_execution_time,
378    }
379}
380
381/// Generate test report summary
382#[must_use]
383pub fn generate_test_summary(results: &TestSuiteResults) -> String {
384    let mut summary = String::new();
385
386    writeln!(summary, "# Test Suite Summary\n").expect("writing to String is infallible");
387    write!(
388        summary,
389        "**Execution Time:** {:?}\n",
390        results.execution_time
391    )
392    .expect("writing to String is infallible");
393    write!(
394        summary,
395        "**Overall Success:** {}\n\n",
396        if results.overall_success {
397            "✅"
398        } else {
399            "❌"
400        }
401    )
402    .expect("writing to String is infallible");
403
404    // Scenario tests summary
405    if !results.scenario_results.is_empty() {
406        let scenario_success = results
407            .scenario_results
408            .iter()
409            .filter(|r| r.success)
410            .count();
411        write!(
412            summary,
413            "## Scenario Tests\n- **Results:** {}/{} passed\n- **Success Rate:** {:.1}%\n\n",
414            scenario_success,
415            results.scenario_results.len(),
416            (scenario_success as f64 / results.scenario_results.len() as f64) * 100.0
417        )
418        .expect("writing to String is infallible");
419    }
420
421    // Regression tests summary
422    if !results.regression_results.is_empty() {
423        let regressions_detected = results
424            .regression_results
425            .iter()
426            .filter(|r| r.regression_detected)
427            .count();
428        write!(
429            summary,
430            "## Regression Tests\n- **Regressions Detected:** {}\n- **Tests Analyzed:** {}\n\n",
431            regressions_detected,
432            results.regression_results.len()
433        )
434        .expect("writing to String is infallible");
435    }
436
437    // Platform tests summary
438    if !results.platform_results.is_empty() {
439        let avg_compatibility = results
440            .platform_results
441            .iter()
442            .map(|r| r.compatibility_score)
443            .sum::<f64>()
444            / results.platform_results.len() as f64;
445
446        write!(
447            summary,
448            "## Platform Tests\n- **Platforms Tested:** {}\n- **Average Compatibility:** {:.2}\n\n",
449            results.platform_results.len(),
450            avg_compatibility
451        )
452        .expect("writing to String is infallible");
453    }
454
455    // Stress tests summary
456    if !results.stress_results.is_empty() {
457        let avg_success_rate = results
458            .stress_results
459            .iter()
460            .map(|r| r.success_rate)
461            .sum::<f64>()
462            / results.stress_results.len() as f64;
463
464        write!(
465            summary,
466            "## Stress Tests\n- **Tests Completed:** {}\n- **Average Success Rate:** {:.1}%\n\n",
467            results.stress_results.len(),
468            avg_success_rate * 100.0
469        )
470        .expect("writing to String is infallible");
471    }
472
473    // Property tests summary
474    if !results.property_results.is_empty() {
475        let total_cases = results
476            .property_results
477            .iter()
478            .map(|r| r.cases_tested)
479            .sum::<usize>();
480        let total_passed = results
481            .property_results
482            .iter()
483            .map(|r| r.cases_passed)
484            .sum::<usize>();
485
486        write!(summary, "## Property Tests\n- **Properties Tested:** {}\n- **Test Cases:** {} total, {} passed\n- **Overall Confidence:** {:.1}%\n\n",
487            results.property_results.len(),
488            total_cases,
489            total_passed,
490            if total_cases > 0 { (total_passed as f64 / total_cases as f64) * 100.0 } else { 0.0 })
491            .expect("writing to String is infallible");
492    }
493
494    write!(
495        summary,
496        "---\n*Generated at: {}*\n",
497        std::time::SystemTime::now()
498            .duration_since(std::time::UNIX_EPOCH)
499            .unwrap_or_default()
500            .as_secs()
501    )
502    .expect("writing to String is infallible");
503
504    summary
505}
506
507/// Compare two test results
508#[must_use]
509pub fn compare_test_results(
510    result1: &TestExecutionResult,
511    result2: &TestExecutionResult,
512    tolerance: f64,
513) -> TestComparisonResult {
514    let quality_diff = (result1.solution_quality - result2.solution_quality).abs();
515    let quality_similar = quality_diff <= tolerance;
516
517    let time_diff = if result1.execution_time > result2.execution_time {
518        result1
519            .execution_time
520            .checked_sub(result2.execution_time)
521            .unwrap_or_default()
522    } else {
523        result2
524            .execution_time
525            .checked_sub(result1.execution_time)
526            .unwrap_or_default()
527    };
528
529    let energy_diff = (result1.final_energy - result2.final_energy).abs();
530    let energy_similar = energy_diff <= tolerance * result1.final_energy.abs().max(1.0);
531
532    TestComparisonResult {
533        quality_difference: quality_diff,
534        quality_similar,
535        time_difference: time_diff,
536        energy_difference: energy_diff,
537        energy_similar,
538        overall_similar: quality_similar && energy_similar,
539    }
540}
541
542/// Test quality metrics
543#[derive(Debug, Clone)]
544pub struct TestQualityMetrics {
545    /// Mean solution quality
546    pub mean_quality: f64,
547    /// Standard deviation of quality
548    pub std_dev_quality: f64,
549    /// Minimum quality observed
550    pub min_quality: f64,
551    /// Maximum quality observed
552    pub max_quality: f64,
553    /// Median quality
554    pub median_quality: f64,
555    /// Success rate (convergence achieved)
556    pub success_rate: f64,
557    /// Mean execution time
558    pub mean_execution_time: Duration,
559    /// Standard deviation of execution time
560    pub std_dev_execution_time: Duration,
561}
562
563/// Test comparison result
564#[derive(Debug, Clone)]
565pub struct TestComparisonResult {
566    /// Absolute difference in solution quality
567    pub quality_difference: f64,
568    /// Whether qualities are similar within tolerance
569    pub quality_similar: bool,
570    /// Difference in execution time
571    pub time_difference: Duration,
572    /// Absolute difference in final energy
573    pub energy_difference: f64,
574    /// Whether energies are similar within tolerance
575    pub energy_similar: bool,
576    /// Overall similarity assessment
577    pub overall_similar: bool,
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use crate::advanced_testing_framework::ScenarioTestResult;
584
585    #[test]
586    fn test_standard_config_creation() {
587        let config = create_standard_test_config("performance")
588            .expect("should create performance test config");
589        assert!(config.enable_parallel);
590        assert_eq!(config.max_concurrent_tests, 4);
591        assert_eq!(config.performance_tolerance, 0.05);
592    }
593
594    #[test]
595    fn test_config_validation() {
596        let mut config = TestingConfig::default();
597        assert!(validate_framework_config(&config).is_ok());
598
599        config.max_concurrent_tests = 0;
600        assert!(validate_framework_config(&config).is_err());
601
602        config.max_concurrent_tests = 4;
603        config.performance_tolerance = 1.5;
604        assert!(validate_framework_config(&config).is_err());
605    }
606
607    #[test]
608    fn test_problem_creation() {
609        let problem = create_test_problem(ProblemType::RandomIsing, 10, 0.3, Some(42));
610        assert!(problem.is_ok());
611
612        let ising = problem.expect("should create random Ising problem");
613        assert_eq!(ising.num_qubits, 10);
614    }
615
616    #[test]
617    fn test_quality_metrics_calculation() {
618        let results = vec![
619            TestExecutionResult {
620                solution_quality: 0.9,
621                execution_time: Duration::from_millis(100),
622                final_energy: -0.9,
623                best_solution: vec![1, -1],
624                convergence_achieved: true,
625                memory_used: 1024,
626            },
627            TestExecutionResult {
628                solution_quality: 0.8,
629                execution_time: Duration::from_millis(150),
630                final_energy: -0.8,
631                best_solution: vec![-1, 1],
632                convergence_achieved: true,
633                memory_used: 1024,
634            },
635        ];
636
637        let metrics = calculate_test_quality_metrics(&results);
638        assert!((metrics.mean_quality - 0.85).abs() < 1e-10);
639        assert_eq!(metrics.success_rate, 1.0);
640        assert_eq!(metrics.min_quality, 0.8);
641        assert_eq!(metrics.max_quality, 0.9);
642    }
643
644    #[test]
645    fn test_test_comparison() {
646        let result1 = TestExecutionResult {
647            solution_quality: 0.9,
648            execution_time: Duration::from_millis(100),
649            final_energy: -0.9,
650            best_solution: vec![1],
651            convergence_achieved: true,
652            memory_used: 1024,
653        };
654
655        let result2 = TestExecutionResult {
656            solution_quality: 0.92,
657            execution_time: Duration::from_millis(110),
658            final_energy: -0.91,
659            best_solution: vec![1],
660            convergence_achieved: true,
661            memory_used: 1024,
662        };
663
664        let comparison = compare_test_results(&result1, &result2, 0.05);
665        assert!(comparison.quality_similar);
666        assert!(comparison.energy_similar);
667        assert!(comparison.overall_similar);
668    }
669
670    #[test]
671    fn test_max_cut_problem_creation() {
672        let problem = create_test_problem(ProblemType::MaxCut, 5, 0.5, Some(123));
673        assert!(problem.is_ok());
674
675        let max_cut = problem.expect("should create Max-Cut problem");
676        assert_eq!(max_cut.num_qubits, 5);
677
678        // Check that biases are zero (Max-Cut characteristic)
679        for i in 0..5 {
680            assert_eq!(max_cut.get_bias(i).expect("should get bias for qubit"), 0.0);
681        }
682    }
683
684    #[test]
685    fn test_test_summary_generation() {
686        let results = TestSuiteResults {
687            scenario_results: vec![ScenarioTestResult {
688                scenario_id: "test1".to_string(),
689                execution_time: Duration::from_millis(100),
690                test_result: TestExecutionResult {
691                    solution_quality: 0.9,
692                    execution_time: Duration::from_millis(100),
693                    final_energy: -0.9,
694                    best_solution: vec![1],
695                    convergence_achieved: true,
696                    memory_used: 1024,
697                },
698                validation_results: Vec::new(),
699                success: true,
700            }],
701            regression_results: Vec::new(),
702            platform_results: Vec::new(),
703            stress_results: Vec::new(),
704            property_results: Vec::new(),
705            execution_time: Duration::from_secs(1),
706            overall_success: true,
707        };
708
709        let summary = generate_test_summary(&results);
710        assert!(summary.contains("Test Suite Summary"));
711        assert!(summary.contains("1/1 passed"));
712        assert!(summary.contains("✅"));
713    }
714}