1#![allow(dead_code)]
7
8#[cfg(feature = "dwave")]
9use crate::compile::{Compile, CompiledModel};
10use crate::sampler::Sampler;
11use scirs2_core::ndarray::Array2;
12use scirs2_core::random::prelude::*;
13use scirs2_core::random::SeedableRng;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::Write;
18use std::time::{Duration, Instant};
19
20pub struct TestingFramework {
22 config: TestConfig,
24 suite: TestSuite,
26 results: TestResults,
28 validators: Vec<Box<dyn Validator>>,
30 generators: Vec<Box<dyn TestGenerator>>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TestConfig {
36 pub seed: Option<u64>,
38 pub cases_per_category: usize,
40 pub problem_sizes: Vec<usize>,
42 pub samplers: Vec<SamplerConfig>,
44 pub timeout: Duration,
46 pub validation: ValidationConfig,
48 pub output: OutputConfig,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SamplerConfig {
54 pub name: String,
56 pub num_samples: usize,
58 pub parameters: HashMap<String, f64>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ValidationConfig {
64 pub check_constraints: bool,
66 pub check_objective: bool,
68 pub statistical_tests: bool,
70 pub tolerance: f64,
72 pub min_quality: f64,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct OutputConfig {
78 pub generate_report: bool,
80 pub format: ReportFormat,
82 pub output_dir: String,
84 pub verbosity: VerbosityLevel,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub enum ReportFormat {
90 Text,
92 Json,
94 Html,
96 Markdown,
98 Csv,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum VerbosityLevel {
104 Error,
106 Warning,
108 Info,
110 Debug,
112}
113
114#[derive(Debug, Clone)]
116pub struct TestSuite {
117 pub categories: Vec<TestCategory>,
119 pub test_cases: Vec<TestCase>,
121 pub benchmarks: Vec<Benchmark>,
123}
124
125#[derive(Debug, Clone)]
126pub struct TestCategory {
127 pub name: String,
129 pub description: String,
131 pub problem_types: Vec<ProblemType>,
133 pub difficulties: Vec<Difficulty>,
135 pub tags: Vec<String>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub enum ProblemType {
141 MaxCut,
143 TSP,
145 GraphColoring,
147 NumberPartitioning,
149 Knapsack,
151 SetCover,
153 VRP,
155 JobScheduling,
157 Portfolio,
159 Ising,
161 Custom { name: String },
163}
164
165#[derive(Debug, Clone)]
166pub enum Difficulty {
167 Easy,
169 Medium,
171 Hard,
173 VeryHard,
175 Extreme,
177}
178
179#[derive(Debug, Clone)]
180pub struct TestCase {
181 pub id: String,
183 pub problem_type: ProblemType,
185 pub size: usize,
187 pub qubo: Array2<f64>,
189 pub var_map: HashMap<String, usize>,
191 pub optimal_solution: Option<HashMap<String, bool>>,
193 pub optimal_value: Option<f64>,
195 pub constraints: Vec<Constraint>,
197 pub metadata: TestMetadata,
199}
200
201#[derive(Debug, Clone)]
202pub struct TestMetadata {
203 pub generation_method: String,
205 pub difficulty: Difficulty,
207 pub expected_runtime: Duration,
209 pub notes: String,
211 pub tags: Vec<String>,
213}
214
215#[derive(Debug, Clone)]
216pub struct Constraint {
217 pub constraint_type: ConstraintType,
219 pub variables: Vec<String>,
221 pub parameters: HashMap<String, f64>,
223 pub penalty: f64,
225}
226
227#[derive(Debug, Clone)]
228pub enum ConstraintType {
229 LinearEquality { target: f64 },
231 LinearInequality { bound: f64, is_upper: bool },
233 OneHot,
235 AtMostK { k: usize },
237 AtLeastK { k: usize },
239 ExactlyK { k: usize },
241 Custom { name: String },
243}
244
245#[derive(Debug, Clone)]
246pub struct Benchmark {
247 pub name: String,
249 pub test_cases: Vec<String>,
251 pub metrics: Vec<PerformanceMetric>,
253 pub baseline: Option<BenchmarkResults>,
255}
256
257#[derive(Debug, Clone)]
258pub enum PerformanceMetric {
259 SolveTime,
261 SolutionQuality,
263 ConstraintViolations,
265 MemoryUsage,
267 ConvergenceRate,
269 SampleEfficiency,
271}
272
273#[derive(Debug, Clone)]
275pub struct TestResults {
276 pub test_results: Vec<TestResult>,
278 pub summary: TestSummary,
280 pub failures: Vec<TestFailure>,
282 pub performance: PerformanceData,
284}
285
286#[derive(Debug, Clone)]
287pub struct TestResult {
288 pub test_id: String,
290 pub sampler: String,
292 pub solution: HashMap<String, bool>,
294 pub objective_value: f64,
296 pub constraints_satisfied: bool,
298 pub validation: ValidationResult,
300 pub runtime: Duration,
302 pub metrics: HashMap<String, f64>,
304}
305
306#[derive(Debug, Clone)]
307pub struct ValidationResult {
308 pub is_valid: bool,
310 pub checks: Vec<ValidationCheck>,
312 pub warnings: Vec<String>,
314}
315
316#[derive(Debug, Clone)]
317pub struct ValidationCheck {
318 pub name: String,
320 pub passed: bool,
322 pub message: String,
324 pub details: Option<String>,
326}
327
328#[derive(Debug, Clone)]
329pub struct TestFailure {
330 pub test_id: String,
332 pub failure_type: FailureType,
334 pub message: String,
336 pub stack_trace: Option<String>,
338 pub context: HashMap<String, String>,
340}
341
342#[derive(Debug, Clone)]
343pub enum FailureType {
344 Timeout,
346 ConstraintViolation,
348 InvalidSolution,
350 SamplerError,
352 ValidationError,
354 UnexpectedError,
356}
357
358#[derive(Debug, Clone)]
359pub struct TestSummary {
360 pub total_tests: usize,
362 pub passed: usize,
364 pub failed: usize,
366 pub skipped: usize,
368 pub avg_runtime: Duration,
370 pub success_rate: f64,
372 pub quality_metrics: QualityMetrics,
374}
375
376#[derive(Debug, Clone)]
377pub struct QualityMetrics {
378 pub avg_quality: f64,
380 pub best_quality: f64,
382 pub worst_quality: f64,
384 pub std_dev: f64,
386 pub constraint_satisfaction_rate: f64,
388}
389
390#[derive(Debug, Clone)]
391pub struct PerformanceData {
392 pub runtime_stats: RuntimeStats,
394 pub memory_stats: MemoryStats,
396 pub convergence_data: ConvergenceData,
398}
399
400#[derive(Debug, Clone)]
401pub struct RuntimeStats {
402 pub total_time: Duration,
404 pub qubo_generation_time: Duration,
406 pub solving_time: Duration,
408 pub validation_time: Duration,
410 pub time_per_test: Vec<(String, Duration)>,
412}
413
414#[derive(Debug, Clone)]
415pub struct MemoryStats {
416 pub peak_memory: usize,
418 pub avg_memory: usize,
420 pub memory_per_test: Vec<(String, usize)>,
422}
423
424#[derive(Debug, Clone)]
425pub struct ConvergenceData {
426 pub curves: Vec<ConvergenceCurve>,
428 pub avg_iterations: f64,
430 pub convergence_rate: f64,
432}
433
434#[derive(Debug, Clone)]
435pub struct ConvergenceCurve {
436 pub test_id: String,
438 pub iterations: Vec<IterationData>,
440 pub converged: bool,
442}
443
444#[derive(Debug, Clone)]
445pub struct IterationData {
446 pub iteration: usize,
448 pub best_value: f64,
450 pub current_value: f64,
452 pub temperature: Option<f64>,
454}
455
456#[derive(Debug, Clone)]
457pub struct BenchmarkResults {
458 pub name: String,
460 pub metrics: HashMap<String, f64>,
462 pub timestamp: std::time::SystemTime,
464}
465
466#[derive(Debug, Clone)]
468pub struct RegressionReport {
469 pub regressions: Vec<RegressionIssue>,
471 pub improvements: Vec<RegressionIssue>,
473 pub baseline_tests: usize,
475 pub current_tests: usize,
477}
478
479#[derive(Debug, Clone)]
481pub struct RegressionIssue {
482 pub test_id: String,
484 pub metric: String,
486 pub baseline_value: f64,
488 pub current_value: f64,
490 pub change_percent: f64,
492}
493
494#[derive(Debug, Clone)]
496pub struct CIReport {
497 pub status: CIStatus,
499 pub passed_rate: f64,
501 pub total_tests: usize,
503 pub failed_tests: usize,
505 pub critical_failures: usize,
507 pub avg_runtime: Duration,
509 pub quality_score: f64,
511}
512
513#[derive(Debug, Clone)]
515pub enum CIStatus {
516 Pass,
518 Warning,
520 Fail,
522}
523
524#[derive(Debug, Clone)]
526pub struct ExtendedPerformanceMetrics {
527 pub cpu_utilization: f64,
529 pub memory_usage: f64,
531 pub gpu_utilization: Option<f64>,
533 pub energy_consumption: f64,
535 pub iterations_per_second: f64,
537 pub quality_trend: QualityTrend,
539}
540
541#[derive(Debug, Clone)]
543pub enum QualityTrend {
544 Improving,
546 Stable,
548 Degrading,
550 Unknown,
552}
553
554#[derive(Debug, Clone)]
556pub struct TestEnvironment {
557 pub os: String,
559 pub cpu_model: String,
561 pub memory_gb: f64,
563 pub gpu_info: Option<String>,
565 pub rust_version: String,
567 pub compile_flags: Vec<String>,
569}
570
571#[derive(Debug, Clone)]
573pub struct SamplerComparison {
574 pub sampler1_name: String,
576 pub sampler2_name: String,
578 pub test_comparisons: Vec<TestComparison>,
580 pub avg_quality_improvement: f64,
582 pub avg_runtime_ratio: f64,
584 pub winner: String,
586}
587
588#[derive(Debug, Clone)]
590pub struct TestComparison {
591 pub test_id: String,
593 pub sampler1_quality: f64,
595 pub sampler2_quality: f64,
597 pub quality_improvement: f64,
599 pub sampler1_runtime: Duration,
601 pub sampler2_runtime: Duration,
603 pub runtime_ratio: f64,
605}
606
607pub trait TestGenerator: Send + Sync {
609 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String>;
611
612 fn name(&self) -> &str;
614
615 fn supported_types(&self) -> Vec<ProblemType>;
617}
618
619#[derive(Debug, Clone)]
620pub struct GeneratorConfig {
621 pub problem_type: ProblemType,
623 pub size: usize,
625 pub difficulty: Difficulty,
627 pub seed: Option<u64>,
629 pub parameters: HashMap<String, f64>,
631}
632
633pub trait Validator: Send + Sync {
635 fn validate(&self, test_case: &TestCase, result: &TestResult) -> ValidationResult;
637
638 fn name(&self) -> &str;
640}
641
642impl TestingFramework {
643 pub fn run_regression_tests<S: Sampler>(
645 &mut self,
646 sampler: &S,
647 baseline_file: &str,
648 ) -> Result<RegressionReport, String> {
649 let baseline = self.load_baseline(baseline_file)?;
651
652 self.run_suite(sampler)?;
654
655 let mut regressions = Vec::new();
657 let mut improvements = Vec::new();
658
659 for current_result in &self.results.test_results {
660 if let Some(baseline_result) = baseline
661 .iter()
662 .find(|b| b.test_id == current_result.test_id)
663 {
664 let quality_change = (current_result.objective_value
665 - baseline_result.objective_value)
666 / baseline_result.objective_value.abs();
667 let runtime_change = (current_result.runtime.as_secs_f64()
668 - baseline_result.runtime.as_secs_f64())
669 / baseline_result.runtime.as_secs_f64();
670
671 if quality_change > 0.05 || runtime_change > 0.2 {
672 regressions.push(RegressionIssue {
673 test_id: current_result.test_id.clone(),
674 metric: if quality_change > 0.05 {
675 "quality".to_string()
676 } else {
677 "runtime".to_string()
678 },
679 baseline_value: if quality_change > 0.05 {
680 baseline_result.objective_value
681 } else {
682 baseline_result.runtime.as_secs_f64()
683 },
684 current_value: if quality_change > 0.05 {
685 current_result.objective_value
686 } else {
687 current_result.runtime.as_secs_f64()
688 },
689 change_percent: if quality_change > 0.05 {
690 quality_change * 100.0
691 } else {
692 runtime_change * 100.0
693 },
694 });
695 } else if quality_change < -0.05 || runtime_change < -0.2 {
696 improvements.push(RegressionIssue {
697 test_id: current_result.test_id.clone(),
698 metric: if quality_change < -0.05 {
699 "quality".to_string()
700 } else {
701 "runtime".to_string()
702 },
703 baseline_value: if quality_change < -0.05 {
704 baseline_result.objective_value
705 } else {
706 baseline_result.runtime.as_secs_f64()
707 },
708 current_value: if quality_change < -0.05 {
709 current_result.objective_value
710 } else {
711 current_result.runtime.as_secs_f64()
712 },
713 change_percent: if quality_change < -0.05 {
714 quality_change * 100.0
715 } else {
716 runtime_change * 100.0
717 },
718 });
719 }
720 }
721 }
722
723 Ok(RegressionReport {
724 regressions,
725 improvements,
726 baseline_tests: baseline.len(),
727 current_tests: self.results.test_results.len(),
728 })
729 }
730
731 const fn load_baseline(&self, _filename: &str) -> Result<Vec<TestResult>, String> {
733 Ok(Vec::new())
735 }
736
737 pub fn run_suite_parallel<S: Sampler + Clone + Send + Sync + 'static>(
739 &mut self,
740 sampler: &S,
741 num_threads: usize,
742 ) -> Result<(), String> {
743 use std::sync::{Arc, Mutex};
744 use std::thread;
745
746 let test_cases = Arc::new(self.suite.test_cases.clone());
747 let results = Arc::new(Mutex::new(Vec::new()));
748 let failures = Arc::new(Mutex::new(Vec::new()));
749
750 let total_start = Instant::now();
751 let chunk_size = test_cases.len().div_ceil(num_threads);
752
753 let mut handles = Vec::new();
754
755 for thread_id in 0..num_threads {
756 let start_idx = thread_id * chunk_size;
757 let end_idx = ((thread_id + 1) * chunk_size).min(test_cases.len());
758
759 if start_idx >= test_cases.len() {
760 break;
761 }
762
763 let test_cases_clone = Arc::clone(&test_cases);
764 let results_clone = Arc::clone(&results);
765 let failures_clone = Arc::clone(&failures);
766 let sampler_clone = sampler.clone();
767
768 let handle = thread::spawn(move || {
769 for idx in start_idx..end_idx {
770 let test_case = &test_cases_clone[idx];
771
772 match Self::run_single_test_static(test_case, &sampler_clone) {
773 Ok(result) => {
774 if let Ok(mut guard) = results_clone.lock() {
775 guard.push(result);
776 }
777 }
778 Err(e) => {
779 if let Ok(mut guard) = failures_clone.lock() {
780 guard.push(TestFailure {
781 test_id: test_case.id.clone(),
782 failure_type: FailureType::SamplerError,
783 message: e,
784 stack_trace: None,
785 context: HashMap::new(),
786 });
787 }
788 }
789 }
790 }
791 });
792
793 handles.push(handle);
794 }
795
796 for handle in handles {
798 handle.join().map_err(|_| "Thread panic")?;
799 }
800
801 self.results.test_results = results
803 .lock()
804 .map(|guard| guard.clone())
805 .unwrap_or_default();
806 self.results.failures = failures
807 .lock()
808 .map(|guard| guard.clone())
809 .unwrap_or_default();
810
811 self.results.performance.runtime_stats.total_time = total_start.elapsed();
812 self.results.summary.passed = self.results.test_results.len();
813 self.results.summary.failed = self.results.failures.len();
814 self.results.summary.total_tests =
815 self.results.summary.passed + self.results.summary.failed;
816
817 self.calculate_summary();
818
819 Ok(())
820 }
821
822 fn run_single_test_static<S: Sampler>(
824 test_case: &TestCase,
825 sampler: &S,
826 ) -> Result<TestResult, String> {
827 let solve_start = Instant::now();
828
829 let sample_result = sampler
831 .run_qubo(&(test_case.qubo.clone(), test_case.var_map.clone()), 100)
832 .map_err(|e| format!("Sampler error: {e:?}"))?;
833
834 let solve_time = solve_start.elapsed();
835
836 let best_sample = sample_result
838 .iter()
839 .min_by(|a, b| {
840 a.energy
841 .partial_cmp(&b.energy)
842 .unwrap_or(std::cmp::Ordering::Equal)
843 })
844 .ok_or("No samples returned")?;
845
846 let solution = best_sample.assignments.clone();
847
848 Ok(TestResult {
849 test_id: test_case.id.clone(),
850 sampler: "parallel".to_string(),
851 solution,
852 objective_value: best_sample.energy,
853 constraints_satisfied: true,
854 validation: ValidationResult {
855 is_valid: true,
856 checks: Vec::new(),
857 warnings: Vec::new(),
858 },
859 runtime: solve_time,
860 metrics: HashMap::new(),
861 })
862 }
863
864 pub fn generate_ci_report(&self) -> Result<CIReport, String> {
866 let passed_rate = if self.results.summary.total_tests > 0 {
867 self.results.summary.passed as f64 / self.results.summary.total_tests as f64
868 } else {
869 0.0
870 };
871
872 let status = if passed_rate >= 0.95 {
873 CIStatus::Pass
874 } else if passed_rate >= 0.8 {
875 CIStatus::Warning
876 } else {
877 CIStatus::Fail
878 };
879
880 Ok(CIReport {
881 status,
882 passed_rate,
883 total_tests: self.results.summary.total_tests,
884 failed_tests: self.results.summary.failed,
885 critical_failures: self
886 .results
887 .failures
888 .iter()
889 .filter(|f| {
890 matches!(
891 f.failure_type,
892 FailureType::Timeout | FailureType::SamplerError
893 )
894 })
895 .count(),
896 avg_runtime: self.results.summary.avg_runtime,
897 quality_score: self.calculate_quality_score(),
898 })
899 }
900
901 fn calculate_quality_score(&self) -> f64 {
903 if self.results.test_results.is_empty() {
904 return 0.0;
905 }
906
907 let constraint_score = self
908 .results
909 .summary
910 .quality_metrics
911 .constraint_satisfaction_rate;
912 let success_score = self.results.summary.success_rate;
913 let quality_score = if self
914 .results
915 .summary
916 .quality_metrics
917 .best_quality
918 .is_finite()
919 {
920 0.8 } else {
922 0.0
923 };
924
925 (constraint_score.mul_add(0.4, success_score * 0.4) + quality_score * 0.2) * 100.0
926 }
927
928 pub fn add_stress_tests(&mut self) {
930 let stress_categories = vec![
931 TestCategory {
932 name: "Large Scale Tests".to_string(),
933 description: "Tests with large problem sizes".to_string(),
934 problem_types: vec![ProblemType::MaxCut, ProblemType::TSP],
935 difficulties: vec![Difficulty::Extreme],
936 tags: vec!["stress".to_string(), "large".to_string()],
937 },
938 TestCategory {
939 name: "Memory Stress Tests".to_string(),
940 description: "Tests designed to stress memory usage".to_string(),
941 problem_types: vec![ProblemType::Knapsack],
942 difficulties: vec![Difficulty::VeryHard, Difficulty::Extreme],
943 tags: vec!["stress".to_string(), "memory".to_string()],
944 },
945 TestCategory {
946 name: "Runtime Stress Tests".to_string(),
947 description: "Tests with challenging runtime requirements".to_string(),
948 problem_types: vec![ProblemType::GraphColoring],
949 difficulties: vec![Difficulty::Extreme],
950 tags: vec!["stress".to_string(), "runtime".to_string()],
951 },
952 ];
953
954 for category in stress_categories {
955 self.suite.categories.push(category);
956 }
957 }
958
959 pub fn detect_environment(&self) -> TestEnvironment {
961 TestEnvironment {
962 os: std::env::consts::OS.to_string(),
963 cpu_model: "Unknown".to_string(), memory_gb: 8.0, gpu_info: None,
966 rust_version: std::env::var("RUSTC_VERSION").unwrap_or_else(|_| "unknown".to_string()),
967 compile_flags: vec!["--release".to_string()],
968 }
969 }
970
971 pub fn export_results(&self, format: &str) -> Result<String, String> {
973 match format {
974 "csv" => self.export_csv(),
975 "json" => self.generate_json_report(),
976 "xml" => self.export_xml(),
977 _ => Err(format!("Unsupported export format: {format}")),
978 }
979 }
980
981 fn export_csv(&self) -> Result<String, String> {
983 let mut csv = String::new();
984 csv.push_str("test_id,problem_type,size,sampler,objective_value,runtime_ms,constraints_satisfied,valid\n");
985
986 for result in &self.results.test_results {
987 if let Some(test_case) = self
989 .suite
990 .test_cases
991 .iter()
992 .find(|tc| tc.id == result.test_id)
993 {
994 csv.push_str(&format!(
995 "{},{:?},{},{},{},{},{},{}\n",
996 result.test_id,
997 test_case.problem_type,
998 test_case.size,
999 result.sampler,
1000 result.objective_value,
1001 result.runtime.as_millis(),
1002 result.constraints_satisfied,
1003 result.validation.is_valid
1004 ));
1005 }
1006 }
1007
1008 Ok(csv)
1009 }
1010
1011 fn export_xml(&self) -> Result<String, String> {
1013 let mut xml = String::new();
1014 xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
1015 xml.push_str("<test_results>\n");
1016 xml.push_str(&format!(
1017 " <summary total=\"{}\" passed=\"{}\" failed=\"{}\" success_rate=\"{:.2}\"/>\n",
1018 self.results.summary.total_tests,
1019 self.results.summary.passed,
1020 self.results.summary.failed,
1021 self.results.summary.success_rate
1022 ));
1023
1024 xml.push_str(" <tests>\n");
1025 for result in &self.results.test_results {
1026 xml.push_str(&format!(
1027 " <test id=\"{}\" sampler=\"{}\" objective=\"{}\" runtime_ms=\"{}\" valid=\"{}\"/>\n",
1028 result.test_id,
1029 result.sampler,
1030 result.objective_value,
1031 result.runtime.as_millis(),
1032 result.validation.is_valid
1033 ));
1034 }
1035 xml.push_str(" </tests>\n");
1036 xml.push_str("</test_results>\n");
1037
1038 Ok(xml)
1039 }
1040
1041 pub fn add_industry_generators(&mut self) {
1043 self.generators.push(Box::new(FinanceTestGenerator));
1045
1046 self.generators.push(Box::new(LogisticsTestGenerator));
1048
1049 self.generators.push(Box::new(ManufacturingTestGenerator));
1051 }
1052
1053 pub fn compare_samplers<S1: Sampler, S2: Sampler>(
1055 &mut self,
1056 sampler1: &S1,
1057 sampler2: &S2,
1058 sampler1_name: &str,
1059 sampler2_name: &str,
1060 ) -> Result<SamplerComparison, String> {
1061 self.run_suite(sampler1)?;
1063 let results1 = self.results.test_results.clone();
1064
1065 self.results.test_results.clear();
1067 self.run_suite(sampler2)?;
1068 let results2 = self.results.test_results.clone();
1069
1070 let mut comparisons = Vec::new();
1072
1073 for r1 in &results1 {
1074 if let Some(r2) = results2.iter().find(|r| r.test_id == r1.test_id) {
1075 let quality_diff = r2.objective_value - r1.objective_value;
1076 let runtime_ratio = r2.runtime.as_secs_f64() / r1.runtime.as_secs_f64();
1077
1078 comparisons.push(TestComparison {
1079 test_id: r1.test_id.clone(),
1080 sampler1_quality: r1.objective_value,
1081 sampler2_quality: r2.objective_value,
1082 quality_improvement: -quality_diff, sampler1_runtime: r1.runtime,
1084 sampler2_runtime: r2.runtime,
1085 runtime_ratio,
1086 });
1087 }
1088 }
1089
1090 let avg_quality_improvement = comparisons
1091 .iter()
1092 .map(|c| c.quality_improvement)
1093 .sum::<f64>()
1094 / comparisons.len() as f64;
1095 let avg_runtime_ratio =
1096 comparisons.iter().map(|c| c.runtime_ratio).sum::<f64>() / comparisons.len() as f64;
1097
1098 Ok(SamplerComparison {
1099 sampler1_name: sampler1_name.to_string(),
1100 sampler2_name: sampler2_name.to_string(),
1101 test_comparisons: comparisons,
1102 avg_quality_improvement,
1103 avg_runtime_ratio,
1104 winner: if avg_quality_improvement > 0.0 {
1105 sampler2_name.to_string()
1106 } else {
1107 sampler1_name.to_string()
1108 },
1109 })
1110 }
1111 pub fn new(config: TestConfig) -> Self {
1113 Self {
1114 config,
1115 suite: TestSuite {
1116 categories: Vec::new(),
1117 test_cases: Vec::new(),
1118 benchmarks: Vec::new(),
1119 },
1120 results: TestResults {
1121 test_results: Vec::new(),
1122 summary: TestSummary {
1123 total_tests: 0,
1124 passed: 0,
1125 failed: 0,
1126 skipped: 0,
1127 avg_runtime: Duration::from_secs(0),
1128 success_rate: 0.0,
1129 quality_metrics: QualityMetrics {
1130 avg_quality: 0.0,
1131 best_quality: f64::NEG_INFINITY,
1132 worst_quality: f64::INFINITY,
1133 std_dev: 0.0,
1134 constraint_satisfaction_rate: 0.0,
1135 },
1136 },
1137 failures: Vec::new(),
1138 performance: PerformanceData {
1139 runtime_stats: RuntimeStats {
1140 total_time: Duration::from_secs(0),
1141 qubo_generation_time: Duration::from_secs(0),
1142 solving_time: Duration::from_secs(0),
1143 validation_time: Duration::from_secs(0),
1144 time_per_test: Vec::new(),
1145 },
1146 memory_stats: MemoryStats {
1147 peak_memory: 0,
1148 avg_memory: 0,
1149 memory_per_test: Vec::new(),
1150 },
1151 convergence_data: ConvergenceData {
1152 curves: Vec::new(),
1153 avg_iterations: 0.0,
1154 convergence_rate: 0.0,
1155 },
1156 },
1157 },
1158 validators: Self::default_validators(),
1159 generators: Self::default_generators(),
1160 }
1161 }
1162
1163 fn default_validators() -> Vec<Box<dyn Validator>> {
1165 vec![
1166 Box::new(ConstraintValidator),
1167 Box::new(ObjectiveValidator),
1168 Box::new(BoundsValidator),
1169 Box::new(SymmetryValidator),
1170 ]
1171 }
1172
1173 fn default_generators() -> Vec<Box<dyn TestGenerator>> {
1175 vec![
1176 Box::new(MaxCutGenerator),
1177 Box::new(TSPGenerator),
1178 Box::new(GraphColoringGenerator),
1179 Box::new(KnapsackGenerator),
1180 Box::new(RandomQuboGenerator),
1181 ]
1182 }
1183
1184 pub fn add_category(&mut self, category: TestCategory) {
1186 self.suite.categories.push(category);
1187 }
1188
1189 pub fn add_generator(&mut self, generator: Box<dyn TestGenerator>) {
1191 self.generators.push(generator);
1192 }
1193
1194 pub fn add_validator(&mut self, validator: Box<dyn Validator>) {
1196 self.validators.push(validator);
1197 }
1198
1199 pub fn generate_suite(&mut self) -> Result<(), String> {
1201 let start_time = Instant::now();
1202
1203 for category in &self.suite.categories {
1205 for problem_type in &category.problem_types {
1206 for difficulty in &category.difficulties {
1207 for size in &self.config.problem_sizes {
1208 let config = GeneratorConfig {
1209 problem_type: problem_type.clone(),
1210 size: *size,
1211 difficulty: difficulty.clone(),
1212 seed: self.config.seed,
1213 parameters: HashMap::new(),
1214 };
1215
1216 for generator in &self.generators {
1218 if generator.supported_types().contains(problem_type) {
1219 let test_cases = generator.generate(&config)?;
1220 self.suite.test_cases.extend(test_cases);
1221 break;
1222 }
1223 }
1224 }
1225 }
1226 }
1227 }
1228
1229 self.results.performance.runtime_stats.qubo_generation_time = start_time.elapsed();
1230
1231 Ok(())
1232 }
1233
1234 pub fn run_suite<S: Sampler>(&mut self, sampler: &S) -> Result<(), String> {
1236 let total_start = Instant::now();
1237
1238 let test_cases = self.suite.test_cases.clone();
1239 for test_case in &test_cases {
1240 let test_start = Instant::now();
1241
1242 match self.run_single_test(test_case, sampler) {
1244 Ok(result) => {
1245 self.results.test_results.push(result);
1246 self.results.summary.passed += 1;
1247 }
1248 Err(e) => {
1249 self.results.failures.push(TestFailure {
1250 test_id: test_case.id.clone(),
1251 failure_type: FailureType::SamplerError,
1252 message: e,
1253 stack_trace: None,
1254 context: HashMap::new(),
1255 });
1256 self.results.summary.failed += 1;
1257 }
1258 }
1259
1260 let test_time = test_start.elapsed();
1261 self.results
1262 .performance
1263 .runtime_stats
1264 .time_per_test
1265 .push((test_case.id.clone(), test_time));
1266
1267 self.results.summary.total_tests += 1;
1268 }
1269
1270 self.results.performance.runtime_stats.total_time = total_start.elapsed();
1271 self.calculate_summary();
1272
1273 Ok(())
1274 }
1275
1276 fn run_single_test<S: Sampler>(
1278 &mut self,
1279 test_case: &TestCase,
1280 sampler: &S,
1281 ) -> Result<TestResult, String> {
1282 let solve_start = Instant::now();
1283
1284 let sample_result = sampler
1286 .run_qubo(
1287 &(test_case.qubo.clone(), test_case.var_map.clone()),
1288 self.config.samplers[0].num_samples,
1289 )
1290 .map_err(|e| format!("Sampler error: {e:?}"))?;
1291
1292 let solve_time = solve_start.elapsed();
1293
1294 let best_sample = sample_result
1296 .iter()
1297 .min_by(|a, b| {
1298 a.energy
1299 .partial_cmp(&b.energy)
1300 .unwrap_or(std::cmp::Ordering::Equal)
1301 })
1302 .ok_or("No samples returned")?;
1303
1304 let solution = best_sample.assignments.clone();
1306
1307 let validation_start = Instant::now();
1309 let mut validation = ValidationResult {
1310 is_valid: true,
1311 checks: Vec::new(),
1312 warnings: Vec::new(),
1313 };
1314
1315 for validator in &self.validators {
1316 let result = validator.validate(
1317 test_case,
1318 &TestResult {
1319 test_id: test_case.id.clone(),
1320 sampler: "test".to_string(),
1321 solution: solution.clone(),
1322 objective_value: best_sample.energy,
1323 constraints_satisfied: true,
1324 validation: validation.clone(),
1325 runtime: solve_time,
1326 metrics: HashMap::new(),
1327 },
1328 );
1329
1330 validation.checks.extend(result.checks);
1331 validation.warnings.extend(result.warnings);
1332 validation.is_valid &= result.is_valid;
1333 }
1334
1335 let validation_time = validation_start.elapsed();
1336 self.results.performance.runtime_stats.solving_time += solve_time;
1337 self.results.performance.runtime_stats.validation_time += validation_time;
1338
1339 Ok(TestResult {
1340 test_id: test_case.id.clone(),
1341 sampler: self.config.samplers[0].name.clone(),
1342 solution,
1343 objective_value: best_sample.energy,
1344 constraints_satisfied: validation.is_valid,
1345 validation,
1346 runtime: solve_time + validation_time,
1347 metrics: HashMap::new(),
1348 })
1349 }
1350
1351 fn decode_solution(
1353 &self,
1354 var_map: &HashMap<String, usize>,
1355 sample: &[i8],
1356 ) -> HashMap<String, bool> {
1357 let mut solution = HashMap::new();
1358
1359 for (var_name, &idx) in var_map {
1360 if idx < sample.len() {
1361 solution.insert(var_name.clone(), sample[idx] == 1);
1362 }
1363 }
1364
1365 solution
1366 }
1367
1368 fn calculate_summary(&mut self) {
1370 if self.results.test_results.is_empty() {
1371 return;
1372 }
1373
1374 self.results.summary.success_rate =
1376 self.results.summary.passed as f64 / self.results.summary.total_tests as f64;
1377
1378 let total_runtime: Duration = self.results.test_results.iter().map(|r| r.runtime).sum();
1380 self.results.summary.avg_runtime = total_runtime / self.results.test_results.len() as u32;
1381
1382 let qualities: Vec<f64> = self
1384 .results
1385 .test_results
1386 .iter()
1387 .map(|r| r.objective_value)
1388 .collect();
1389
1390 self.results.summary.quality_metrics.avg_quality =
1391 qualities.iter().sum::<f64>() / qualities.len() as f64;
1392
1393 self.results.summary.quality_metrics.best_quality = *qualities
1394 .iter()
1395 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1396 .unwrap_or(&0.0);
1397
1398 self.results.summary.quality_metrics.worst_quality = *qualities
1399 .iter()
1400 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1401 .unwrap_or(&0.0);
1402
1403 let mean = self.results.summary.quality_metrics.avg_quality;
1405 let variance =
1406 qualities.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / qualities.len() as f64;
1407 self.results.summary.quality_metrics.std_dev = variance.sqrt();
1408
1409 let satisfied = self
1411 .results
1412 .test_results
1413 .iter()
1414 .filter(|r| r.constraints_satisfied)
1415 .count();
1416 self.results
1417 .summary
1418 .quality_metrics
1419 .constraint_satisfaction_rate =
1420 satisfied as f64 / self.results.test_results.len() as f64;
1421 }
1422
1423 pub fn generate_report(&self) -> Result<String, String> {
1425 match self.config.output.format {
1426 ReportFormat::Text => self.generate_text_report(),
1427 ReportFormat::Json => self.generate_json_report(),
1428 ReportFormat::Html => self.generate_html_report(),
1429 ReportFormat::Markdown => self.generate_markdown_report(),
1430 ReportFormat::Csv => self.generate_csv_report(),
1431 }
1432 }
1433
1434 fn generate_text_report(&self) -> Result<String, String> {
1436 let mut report = String::new();
1437
1438 report.push_str("=== Quantum Optimization Test Report ===\n\n");
1439
1440 report.push_str(&format!(
1441 "Total Tests: {}\n",
1442 self.results.summary.total_tests
1443 ));
1444 report.push_str(&format!("Passed: {}\n", self.results.summary.passed));
1445 report.push_str(&format!("Failed: {}\n", self.results.summary.failed));
1446 report.push_str(&format!(
1447 "Success Rate: {:.2}%\n",
1448 self.results.summary.success_rate * 100.0
1449 ));
1450 report.push_str(&format!(
1451 "Average Runtime: {:?}\n\n",
1452 self.results.summary.avg_runtime
1453 ));
1454
1455 report.push_str("Quality Metrics:\n");
1456 report.push_str(&format!(
1457 " Average Quality: {:.4}\n",
1458 self.results.summary.quality_metrics.avg_quality
1459 ));
1460 report.push_str(&format!(
1461 " Best Quality: {:.4}\n",
1462 self.results.summary.quality_metrics.best_quality
1463 ));
1464 report.push_str(&format!(
1465 " Worst Quality: {:.4}\n",
1466 self.results.summary.quality_metrics.worst_quality
1467 ));
1468 report.push_str(&format!(
1469 " Std Dev: {:.4}\n",
1470 self.results.summary.quality_metrics.std_dev
1471 ));
1472 report.push_str(&format!(
1473 " Constraint Satisfaction: {:.2}%\n\n",
1474 self.results
1475 .summary
1476 .quality_metrics
1477 .constraint_satisfaction_rate
1478 * 100.0
1479 ));
1480
1481 if !self.results.failures.is_empty() {
1482 report.push_str("Failures:\n");
1483 for failure in &self.results.failures {
1484 report.push_str(&format!(
1485 " - {} ({}): {}\n",
1486 failure.test_id,
1487 format!("{:?}", failure.failure_type),
1488 failure.message
1489 ));
1490 }
1491 }
1492
1493 Ok(report)
1494 }
1495
1496 fn generate_json_report(&self) -> Result<String, String> {
1498 use std::fmt::Write;
1499
1500 fn write_err(e: std::fmt::Error) -> String {
1502 format!("JSON write error: {e}")
1503 }
1504
1505 let mut json = String::new();
1506
1507 json.push_str("{\n");
1509
1510 json.push_str(" \"summary\": {\n");
1512 writeln!(
1513 &mut json,
1514 " \"total_tests\": {},",
1515 self.results.summary.total_tests
1516 )
1517 .map_err(write_err)?;
1518 writeln!(
1519 &mut json,
1520 " \"passed\": {},",
1521 self.results.summary.passed
1522 )
1523 .map_err(write_err)?;
1524 writeln!(
1525 &mut json,
1526 " \"failed\": {},",
1527 self.results.summary.failed
1528 )
1529 .map_err(write_err)?;
1530 writeln!(
1531 &mut json,
1532 " \"skipped\": {},",
1533 self.results.summary.skipped
1534 )
1535 .map_err(write_err)?;
1536 writeln!(
1537 &mut json,
1538 " \"success_rate\": {},",
1539 self.results.summary.success_rate
1540 )
1541 .map_err(write_err)?;
1542 writeln!(
1543 &mut json,
1544 " \"avg_runtime_ms\": {}",
1545 self.results.summary.avg_runtime.as_millis()
1546 )
1547 .map_err(write_err)?;
1548 json.push_str(" },\n");
1549
1550 json.push_str(" \"quality_metrics\": {\n");
1552 writeln!(
1553 &mut json,
1554 " \"avg_quality\": {},",
1555 self.results.summary.quality_metrics.avg_quality
1556 )
1557 .map_err(write_err)?;
1558 writeln!(
1559 &mut json,
1560 " \"best_quality\": {},",
1561 self.results.summary.quality_metrics.best_quality
1562 )
1563 .map_err(write_err)?;
1564 writeln!(
1565 &mut json,
1566 " \"worst_quality\": {},",
1567 self.results.summary.quality_metrics.worst_quality
1568 )
1569 .map_err(write_err)?;
1570 writeln!(
1571 &mut json,
1572 " \"std_dev\": {},",
1573 self.results.summary.quality_metrics.std_dev
1574 )
1575 .map_err(write_err)?;
1576 writeln!(
1577 &mut json,
1578 " \"constraint_satisfaction_rate\": {}",
1579 self.results
1580 .summary
1581 .quality_metrics
1582 .constraint_satisfaction_rate
1583 )
1584 .map_err(write_err)?;
1585 json.push_str(" },\n");
1586
1587 json.push_str(" \"performance\": {\n");
1589 writeln!(
1590 &mut json,
1591 " \"total_time_ms\": {},",
1592 self.results
1593 .performance
1594 .runtime_stats
1595 .total_time
1596 .as_millis()
1597 )
1598 .map_err(write_err)?;
1599 writeln!(
1600 &mut json,
1601 " \"solving_time_ms\": {},",
1602 self.results
1603 .performance
1604 .runtime_stats
1605 .solving_time
1606 .as_millis()
1607 )
1608 .map_err(write_err)?;
1609 writeln!(
1610 &mut json,
1611 " \"validation_time_ms\": {}",
1612 self.results
1613 .performance
1614 .runtime_stats
1615 .validation_time
1616 .as_millis()
1617 )
1618 .map_err(write_err)?;
1619 json.push_str(" },\n");
1620
1621 json.push_str(" \"test_results\": [\n");
1623 for (i, result) in self.results.test_results.iter().enumerate() {
1624 json.push_str(" {\n");
1625 writeln!(&mut json, " \"test_id\": \"{}\",", result.test_id).map_err(write_err)?;
1626 writeln!(&mut json, " \"sampler\": \"{}\",", result.sampler).map_err(write_err)?;
1627 writeln!(
1628 &mut json,
1629 " \"objective_value\": {},",
1630 result.objective_value
1631 )
1632 .map_err(write_err)?;
1633 writeln!(
1634 &mut json,
1635 " \"constraints_satisfied\": {},",
1636 result.constraints_satisfied
1637 )
1638 .map_err(write_err)?;
1639 writeln!(
1640 &mut json,
1641 " \"runtime_ms\": {},",
1642 result.runtime.as_millis()
1643 )
1644 .map_err(write_err)?;
1645 writeln!(
1646 &mut json,
1647 " \"is_valid\": {}",
1648 result.validation.is_valid
1649 )
1650 .map_err(write_err)?;
1651 json.push_str(" }");
1652 if i < self.results.test_results.len() - 1 {
1653 json.push(',');
1654 }
1655 json.push('\n');
1656 }
1657 json.push_str(" ],\n");
1658
1659 json.push_str(" \"failures\": [\n");
1661 for (i, failure) in self.results.failures.iter().enumerate() {
1662 json.push_str(" {\n");
1663 writeln!(&mut json, " \"test_id\": \"{}\",", failure.test_id)
1664 .map_err(write_err)?;
1665 writeln!(
1666 &mut json,
1667 " \"failure_type\": \"{:?}\",",
1668 failure.failure_type
1669 )
1670 .map_err(write_err)?;
1671 writeln!(
1672 &mut json,
1673 " \"message\": \"{}\"",
1674 failure.message.replace('"', "\\\"")
1675 )
1676 .map_err(write_err)?;
1677 json.push_str(" }");
1678 if i < self.results.failures.len() - 1 {
1679 json.push(',');
1680 }
1681 json.push('\n');
1682 }
1683 json.push_str(" ]\n");
1684
1685 json.push_str("}\n");
1686
1687 Ok(json)
1688 }
1689
1690 fn generate_html_report(&self) -> Result<String, String> {
1692 let mut html = String::new();
1693
1694 html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
1695 html.push_str("<title>Quantum Optimization Test Report</title>\n");
1696 html.push_str(
1697 "<style>
1698 body { font-family: Arial, sans-serif; margin: 20px; }
1699 table { border-collapse: collapse; width: 100%; }
1700 th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
1701 th { background-color: #f2f2f2; }
1702 .passed { color: green; }
1703 .failed { color: red; }
1704 </style>\n",
1705 );
1706 html.push_str("</head>\n<body>\n");
1707
1708 html.push_str("<h1>Quantum Optimization Test Report</h1>\n");
1709
1710 html.push_str("<h2>Summary</h2>\n");
1712 html.push_str("<table>\n");
1713 html.push_str(&format!(
1714 "<tr><td>Total Tests</td><td>{}</td></tr>\n",
1715 self.results.summary.total_tests
1716 ));
1717 html.push_str(&format!(
1718 "<tr><td>Passed</td><td class='passed'>{}</td></tr>\n",
1719 self.results.summary.passed
1720 ));
1721 html.push_str(&format!(
1722 "<tr><td>Failed</td><td class='failed'>{}</td></tr>\n",
1723 self.results.summary.failed
1724 ));
1725 html.push_str(&format!(
1726 "<tr><td>Success Rate</td><td>{:.2}%</td></tr>\n",
1727 self.results.summary.success_rate * 100.0
1728 ));
1729 html.push_str("</table>\n");
1730
1731 html.push_str("</body>\n</html>");
1732
1733 Ok(html)
1734 }
1735
1736 fn generate_markdown_report(&self) -> Result<String, String> {
1738 let mut md = String::new();
1739
1740 md.push_str("# Quantum Optimization Test Report\n\n");
1741
1742 md.push_str("## Summary\n\n");
1743 md.push_str("| Metric | Value |\n");
1744 md.push_str("|--------|-------|\n");
1745 md.push_str(&format!(
1746 "| Total Tests | {} |\n",
1747 self.results.summary.total_tests
1748 ));
1749 md.push_str(&format!("| Passed | {} |\n", self.results.summary.passed));
1750 md.push_str(&format!("| Failed | {} |\n", self.results.summary.failed));
1751 md.push_str(&format!(
1752 "| Success Rate | {:.2}% |\n",
1753 self.results.summary.success_rate * 100.0
1754 ));
1755 md.push_str(&format!(
1756 "| Average Runtime | {:?} |\n\n",
1757 self.results.summary.avg_runtime
1758 ));
1759
1760 md.push_str("## Quality Metrics\n\n");
1761 md.push_str(&format!(
1762 "- **Average Quality**: {:.4}\n",
1763 self.results.summary.quality_metrics.avg_quality
1764 ));
1765 md.push_str(&format!(
1766 "- **Best Quality**: {:.4}\n",
1767 self.results.summary.quality_metrics.best_quality
1768 ));
1769 md.push_str(&format!(
1770 "- **Worst Quality**: {:.4}\n",
1771 self.results.summary.quality_metrics.worst_quality
1772 ));
1773 md.push_str(&format!(
1774 "- **Standard Deviation**: {:.4}\n",
1775 self.results.summary.quality_metrics.std_dev
1776 ));
1777 md.push_str(&format!(
1778 "- **Constraint Satisfaction Rate**: {:.2}%\n\n",
1779 self.results
1780 .summary
1781 .quality_metrics
1782 .constraint_satisfaction_rate
1783 * 100.0
1784 ));
1785
1786 Ok(md)
1787 }
1788
1789 fn generate_csv_report(&self) -> Result<String, String> {
1791 let mut csv = String::new();
1792
1793 csv.push_str("test_id,sampler,objective_value,constraints_satisfied,runtime_ms,valid\n");
1794
1795 for result in &self.results.test_results {
1796 csv.push_str(&format!(
1797 "{},{},{},{},{},{}\n",
1798 result.test_id,
1799 result.sampler,
1800 result.objective_value,
1801 result.constraints_satisfied,
1802 result.runtime.as_millis(),
1803 result.validation.is_valid
1804 ));
1805 }
1806
1807 Ok(csv)
1808 }
1809
1810 pub fn save_report(&self, filename: &str) -> Result<(), String> {
1812 let report = self.generate_report()?;
1813 let mut file = File::create(filename).map_err(|e| format!("Failed to create file: {e}"))?;
1814 file.write_all(report.as_bytes())
1815 .map_err(|e| format!("Failed to write file: {e}"))?;
1816 Ok(())
1817 }
1818}
1819
1820struct ConstraintValidator;
1822
1823impl Validator for ConstraintValidator {
1824 fn validate(&self, test_case: &TestCase, result: &TestResult) -> ValidationResult {
1825 let mut checks = Vec::new();
1826 let mut is_valid = true;
1827
1828 for constraint in &test_case.constraints {
1829 let satisfied = self.check_constraint(constraint, &result.solution);
1830
1831 checks.push(ValidationCheck {
1832 name: format!("Constraint {:?}", constraint.constraint_type),
1833 passed: satisfied,
1834 message: if satisfied {
1835 "Constraint satisfied".to_string()
1836 } else {
1837 "Constraint violated".to_string()
1838 },
1839 details: None,
1840 });
1841
1842 is_valid &= satisfied;
1843 }
1844
1845 ValidationResult {
1846 is_valid,
1847 checks,
1848 warnings: Vec::new(),
1849 }
1850 }
1851
1852 fn name(&self) -> &'static str {
1853 "ConstraintValidator"
1854 }
1855}
1856
1857impl ConstraintValidator {
1858 fn check_constraint(&self, constraint: &Constraint, solution: &HashMap<String, bool>) -> bool {
1859 match &constraint.constraint_type {
1860 ConstraintType::OneHot => {
1861 let active = constraint
1862 .variables
1863 .iter()
1864 .filter(|v| *solution.get(*v).unwrap_or(&false))
1865 .count();
1866 active == 1
1867 }
1868 ConstraintType::AtMostK { k } => {
1869 let active = constraint
1870 .variables
1871 .iter()
1872 .filter(|v| *solution.get(*v).unwrap_or(&false))
1873 .count();
1874 active <= *k
1875 }
1876 ConstraintType::AtLeastK { k } => {
1877 let active = constraint
1878 .variables
1879 .iter()
1880 .filter(|v| *solution.get(*v).unwrap_or(&false))
1881 .count();
1882 active >= *k
1883 }
1884 ConstraintType::ExactlyK { k } => {
1885 let active = constraint
1886 .variables
1887 .iter()
1888 .filter(|v| *solution.get(*v).unwrap_or(&false))
1889 .count();
1890 active == *k
1891 }
1892 _ => true, }
1894 }
1895}
1896
1897struct ObjectiveValidator;
1899
1900impl Validator for ObjectiveValidator {
1901 fn validate(&self, test_case: &TestCase, result: &TestResult) -> ValidationResult {
1902 let mut checks = Vec::new();
1903
1904 let random_value = self.estimate_random_objective(&test_case.qubo);
1906 let improvement = (random_value - result.objective_value) / random_value.abs();
1907
1908 checks.push(ValidationCheck {
1909 name: "Objective improvement".to_string(),
1910 passed: improvement > 0.0,
1911 message: format!("Improvement over random: {:.2}%", improvement * 100.0),
1912 details: Some(format!(
1913 "Random: {:.4}, Found: {:.4}",
1914 random_value, result.objective_value
1915 )),
1916 });
1917
1918 if let Some(optimal_value) = test_case.optimal_value {
1920 let gap = (result.objective_value - optimal_value).abs() / optimal_value.abs();
1921 let acceptable_gap = 0.05; checks.push(ValidationCheck {
1924 name: "Optimality gap".to_string(),
1925 passed: gap <= acceptable_gap,
1926 message: format!("Gap to optimal: {:.2}%", gap * 100.0),
1927 details: Some(format!(
1928 "Optimal: {:.4}, Found: {:.4}",
1929 optimal_value, result.objective_value
1930 )),
1931 });
1932 }
1933
1934 ValidationResult {
1935 is_valid: checks.iter().all(|c| c.passed),
1936 checks,
1937 warnings: Vec::new(),
1938 }
1939 }
1940
1941 fn name(&self) -> &'static str {
1942 "ObjectiveValidator"
1943 }
1944}
1945
1946impl ObjectiveValidator {
1947 fn estimate_random_objective(&self, qubo: &Array2<f64>) -> f64 {
1948 let n = qubo.shape()[0];
1949 let mut rng = thread_rng();
1950 let mut total = 0.0;
1951 let samples = 100;
1952
1953 for _ in 0..samples {
1954 let mut x = vec![0.0; n];
1955 for x_item in x.iter_mut().take(n) {
1956 *x_item = if rng.gen::<bool>() { 1.0 } else { 0.0 };
1957 }
1958
1959 let mut value = 0.0;
1960 for i in 0..n {
1961 for j in 0..n {
1962 value += qubo[[i, j]] * x[i] * x[j];
1963 }
1964 }
1965
1966 total += value;
1967 }
1968
1969 total / samples as f64
1970 }
1971}
1972
1973struct BoundsValidator;
1975
1976impl Validator for BoundsValidator {
1977 fn validate(&self, test_case: &TestCase, result: &TestResult) -> ValidationResult {
1978 let mut checks = Vec::new();
1979
1980 let all_binary = true;
1982
1983 checks.push(ValidationCheck {
1984 name: "Binary variables".to_string(),
1985 passed: all_binary,
1986 message: if all_binary {
1987 "All variables are binary".to_string()
1988 } else {
1989 "Non-binary values found".to_string()
1990 },
1991 details: None,
1992 });
1993
1994 let expected_vars = test_case.var_map.len();
1996 let actual_vars = result.solution.len();
1997
1998 checks.push(ValidationCheck {
1999 name: "Variable count".to_string(),
2000 passed: expected_vars == actual_vars,
2001 message: format!("Expected {expected_vars} variables, found {actual_vars}"),
2002 details: None,
2003 });
2004
2005 ValidationResult {
2006 is_valid: checks.iter().all(|c| c.passed),
2007 checks,
2008 warnings: Vec::new(),
2009 }
2010 }
2011
2012 fn name(&self) -> &'static str {
2013 "BoundsValidator"
2014 }
2015}
2016
2017struct SymmetryValidator;
2019
2020impl Validator for SymmetryValidator {
2021 fn validate(&self, test_case: &TestCase, _result: &TestResult) -> ValidationResult {
2022 let mut warnings = Vec::new();
2023
2024 if self.is_symmetric(&test_case.qubo) {
2026 warnings.push("QUBO matrix has symmetries that might not be broken".to_string());
2027 }
2028
2029 ValidationResult {
2030 is_valid: true,
2031 checks: Vec::new(),
2032 warnings,
2033 }
2034 }
2035
2036 fn name(&self) -> &'static str {
2037 "SymmetryValidator"
2038 }
2039}
2040
2041impl SymmetryValidator {
2042 fn is_symmetric(&self, qubo: &Array2<f64>) -> bool {
2043 let n = qubo.shape()[0];
2044
2045 for i in 0..n {
2046 for j in i + 1..n {
2047 if (qubo[[i, j]] - qubo[[j, i]]).abs() > 1e-10 {
2048 return false;
2049 }
2050 }
2051 }
2052
2053 true
2054 }
2055}
2056
2057struct MaxCutGenerator;
2059
2060impl TestGenerator for MaxCutGenerator {
2061 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2062 let mut rng = if let Some(seed) = config.seed {
2063 StdRng::seed_from_u64(seed)
2064 } else {
2065 let mut thread_rng = thread_rng();
2066 StdRng::from_rng(&mut thread_rng)
2067 };
2068
2069 let mut test_cases = Vec::new();
2070
2071 let n = config.size;
2073 let edge_probability = match config.difficulty {
2074 Difficulty::Easy => 0.3,
2075 Difficulty::Medium => 0.5,
2076 Difficulty::Hard => 0.7,
2077 Difficulty::VeryHard => 0.9,
2078 Difficulty::Extreme => 0.95,
2079 };
2080
2081 let mut qubo = Array2::zeros((n, n));
2082 let mut var_map = HashMap::new();
2083
2084 for i in 0..n {
2085 var_map.insert(format!("x_{i}"), i);
2086 }
2087
2088 for i in 0..n {
2090 for j in i + 1..n {
2091 if rng.gen::<f64>() < edge_probability {
2092 let weight = rng.gen_range(1.0..10.0);
2093 qubo[[i, i]] -= weight;
2095 qubo[[j, j]] -= weight;
2096 qubo[[i, j]] += 2.0 * weight;
2097 qubo[[j, i]] += 2.0 * weight;
2098 }
2099 }
2100 }
2101
2102 test_cases.push(TestCase {
2103 id: format!("maxcut_{}_{:?}", n, config.difficulty),
2104 problem_type: ProblemType::MaxCut,
2105 size: n,
2106 qubo,
2107 var_map,
2108 optimal_solution: None,
2109 optimal_value: None,
2110 constraints: Vec::new(),
2111 metadata: TestMetadata {
2112 generation_method: "Random graph".to_string(),
2113 difficulty: config.difficulty.clone(),
2114 expected_runtime: Duration::from_millis(100),
2115 notes: format!("Edge probability: {edge_probability}"),
2116 tags: vec!["graph".to_string(), "maxcut".to_string()],
2117 },
2118 });
2119
2120 Ok(test_cases)
2121 }
2122
2123 fn name(&self) -> &'static str {
2124 "MaxCutGenerator"
2125 }
2126
2127 fn supported_types(&self) -> Vec<ProblemType> {
2128 vec![ProblemType::MaxCut]
2129 }
2130}
2131
2132struct TSPGenerator;
2134
2135impl TestGenerator for TSPGenerator {
2136 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2137 let mut rng = if let Some(seed) = config.seed {
2138 StdRng::seed_from_u64(seed)
2139 } else {
2140 let mut thread_rng = thread_rng();
2141 StdRng::from_rng(&mut thread_rng)
2142 };
2143
2144 let n_cities = config.size;
2145 let mut test_cases = Vec::new();
2146
2147 let mut cities = Vec::new();
2149 for _ in 0..n_cities {
2150 cities.push((rng.gen_range(0.0..100.0), rng.gen_range(0.0..100.0)));
2151 }
2152
2153 let mut distances = Array2::zeros((n_cities, n_cities));
2155 for i in 0..n_cities {
2156 for j in 0..n_cities {
2157 if i != j {
2158 let dx: f64 = cities[i].0 - cities[j].0;
2159 let dy: f64 = cities[i].1 - cities[j].1;
2160 distances[[i, j]] = dx.hypot(dy);
2161 }
2162 }
2163 }
2164
2165 let n_vars = n_cities * n_cities;
2167 let mut qubo = Array2::zeros((n_vars, n_vars));
2168 let mut var_map = HashMap::new();
2169
2170 for i in 0..n_cities {
2172 for j in 0..n_cities {
2173 let idx = i * n_cities + j;
2174 var_map.insert(format!("x_{i}_{j}"), idx);
2175 }
2176 }
2177
2178 for i in 0..n_cities {
2180 for j in 0..n_cities {
2181 for k in 0..n_cities {
2182 let next_j = (j + 1) % n_cities;
2183 let idx1 = i * n_cities + j;
2184 let idx2 = k * n_cities + next_j;
2185 qubo[[idx1, idx2]] += distances[[i, k]];
2186 }
2187 }
2188 }
2189
2190 let mut constraints = Vec::new();
2192 let penalty = 1000.0;
2193
2194 for i in 0..n_cities {
2196 let vars: Vec<_> = (0..n_cities).map(|j| format!("x_{i}_{j}")).collect();
2197
2198 constraints.push(Constraint {
2199 constraint_type: ConstraintType::ExactlyK { k: 1 },
2200 variables: vars,
2201 parameters: HashMap::new(),
2202 penalty,
2203 });
2204 }
2205
2206 for j in 0..n_cities {
2208 let vars: Vec<_> = (0..n_cities).map(|i| format!("x_{i}_{j}")).collect();
2209
2210 constraints.push(Constraint {
2211 constraint_type: ConstraintType::ExactlyK { k: 1 },
2212 variables: vars,
2213 parameters: HashMap::new(),
2214 penalty,
2215 });
2216 }
2217
2218 self.add_constraint_penalties(&mut qubo, &var_map, &constraints)?;
2220
2221 test_cases.push(TestCase {
2222 id: format!("tsp_{}_{:?}", n_cities, config.difficulty),
2223 problem_type: ProblemType::TSP,
2224 size: n_cities,
2225 qubo,
2226 var_map,
2227 optimal_solution: None,
2228 optimal_value: None,
2229 constraints,
2230 metadata: TestMetadata {
2231 generation_method: "Random cities".to_string(),
2232 difficulty: config.difficulty.clone(),
2233 expected_runtime: Duration::from_millis(500),
2234 notes: format!("{n_cities} cities"),
2235 tags: vec!["routing".to_string(), "tsp".to_string()],
2236 },
2237 });
2238
2239 Ok(test_cases)
2240 }
2241
2242 fn name(&self) -> &'static str {
2243 "TSPGenerator"
2244 }
2245
2246 fn supported_types(&self) -> Vec<ProblemType> {
2247 vec![ProblemType::TSP]
2248 }
2249}
2250
2251impl TSPGenerator {
2252 fn add_constraint_penalties(
2253 &self,
2254 qubo: &mut Array2<f64>,
2255 var_map: &HashMap<String, usize>,
2256 constraints: &[Constraint],
2257 ) -> Result<(), String> {
2258 for constraint in constraints {
2259 if let ConstraintType::ExactlyK { k } = &constraint.constraint_type {
2260 for v1 in &constraint.variables {
2262 if let Some(&idx1) = var_map.get(v1) {
2263 qubo[[idx1, idx1]] +=
2265 constraint.penalty * 2.0f64.mul_add(-(*k as f64), 1.0);
2266
2267 for v2 in &constraint.variables {
2269 if v1 != v2 {
2270 if let Some(&idx2) = var_map.get(v2) {
2271 qubo[[idx1, idx2]] += constraint.penalty * 2.0;
2272 }
2273 }
2274 }
2275 }
2276 }
2277 }
2278 }
2279
2280 Ok(())
2281 }
2282}
2283
2284struct GraphColoringGenerator;
2286
2287impl TestGenerator for GraphColoringGenerator {
2288 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2289 let mut rng = if let Some(seed) = config.seed {
2290 StdRng::seed_from_u64(seed)
2291 } else {
2292 let mut thread_rng = thread_rng();
2293 StdRng::from_rng(&mut thread_rng)
2294 };
2295
2296 let n_vertices = config.size;
2297 let n_colors = match config.difficulty {
2298 Difficulty::Easy => 4,
2299 Difficulty::Medium => 3,
2300 _ => 3,
2301 };
2302
2303 let mut test_cases = Vec::new();
2304
2305 let edge_prob = 0.3;
2307 let mut edges = Vec::new();
2308
2309 for i in 0..n_vertices {
2310 for j in i + 1..n_vertices {
2311 if rng.gen::<f64>() < edge_prob {
2312 edges.push((i, j));
2313 }
2314 }
2315 }
2316
2317 let n_vars = n_vertices * n_colors;
2319 let mut qubo = Array2::zeros((n_vars, n_vars));
2320 let mut var_map = HashMap::new();
2321
2322 for v in 0..n_vertices {
2324 for c in 0..n_colors {
2325 let idx = v * n_colors + c;
2326 var_map.insert(format!("x_{v}_{c}"), idx);
2327 }
2328 }
2329
2330 for v in 0..n_vertices {
2332 for c in 0..n_colors {
2333 let idx = v * n_colors + c;
2334 qubo[[idx, idx]] -= c as f64; }
2336 }
2337
2338 let mut constraints = Vec::new();
2340 let penalty = 100.0;
2341
2342 for v in 0..n_vertices {
2344 let vars: Vec<_> = (0..n_colors).map(|c| format!("x_{v}_{c}")).collect();
2345
2346 constraints.push(Constraint {
2347 constraint_type: ConstraintType::ExactlyK { k: 1 },
2348 variables: vars,
2349 parameters: HashMap::new(),
2350 penalty,
2351 });
2352 }
2353
2354 for (u, v) in &edges {
2356 for c in 0..n_colors {
2357 let idx_u = u * n_colors + c;
2358 let idx_v = v * n_colors + c;
2359 qubo[[idx_u, idx_v]] += penalty;
2360 qubo[[idx_v, idx_u]] += penalty;
2361 }
2362 }
2363
2364 test_cases.push(TestCase {
2365 id: format!(
2366 "coloring_{}_{}_{}_{:?}",
2367 n_vertices,
2368 n_colors,
2369 edges.len(),
2370 config.difficulty
2371 ),
2372 problem_type: ProblemType::GraphColoring,
2373 size: n_vertices,
2374 qubo,
2375 var_map,
2376 optimal_solution: None,
2377 optimal_value: None,
2378 constraints,
2379 metadata: TestMetadata {
2380 generation_method: "Random graph".to_string(),
2381 difficulty: config.difficulty.clone(),
2382 expected_runtime: Duration::from_millis(200),
2383 notes: format!(
2384 "{} vertices, {} colors, {} edges",
2385 n_vertices,
2386 n_colors,
2387 edges.len()
2388 ),
2389 tags: vec!["graph".to_string(), "coloring".to_string()],
2390 },
2391 });
2392
2393 Ok(test_cases)
2394 }
2395
2396 fn name(&self) -> &'static str {
2397 "GraphColoringGenerator"
2398 }
2399
2400 fn supported_types(&self) -> Vec<ProblemType> {
2401 vec![ProblemType::GraphColoring]
2402 }
2403}
2404
2405struct KnapsackGenerator;
2407
2408impl TestGenerator for KnapsackGenerator {
2409 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2410 let mut rng = if let Some(seed) = config.seed {
2411 StdRng::seed_from_u64(seed)
2412 } else {
2413 let mut thread_rng = thread_rng();
2414 StdRng::from_rng(&mut thread_rng)
2415 };
2416
2417 let n_items = config.size;
2418 let mut test_cases = Vec::new();
2419
2420 let mut values = Vec::new();
2422 let mut weights = Vec::new();
2423
2424 for _ in 0..n_items {
2425 values.push(rng.gen_range(1.0..100.0));
2426 weights.push(rng.gen_range(1.0..50.0));
2427 }
2428
2429 let capacity = weights.iter().sum::<f64>() * 0.5; let mut qubo = Array2::zeros((n_items, n_items));
2433 let mut var_map = HashMap::new();
2434
2435 for i in 0..n_items {
2436 var_map.insert(format!("x_{i}"), i);
2437 qubo[[i, i]] -= values[i];
2439 }
2440
2441 let _penalty = values.iter().sum::<f64>() * 2.0;
2443
2444 test_cases.push(TestCase {
2449 id: format!("knapsack_{}_{:?}", n_items, config.difficulty),
2450 problem_type: ProblemType::Knapsack,
2451 size: n_items,
2452 qubo,
2453 var_map,
2454 optimal_solution: None,
2455 optimal_value: None,
2456 constraints: vec![],
2457 metadata: TestMetadata {
2458 generation_method: "Random items".to_string(),
2459 difficulty: config.difficulty.clone(),
2460 expected_runtime: Duration::from_millis(100),
2461 notes: format!("{n_items} items, capacity: {capacity:.1}"),
2462 tags: vec!["optimization".to_string(), "knapsack".to_string()],
2463 },
2464 });
2465
2466 Ok(test_cases)
2467 }
2468
2469 fn name(&self) -> &'static str {
2470 "KnapsackGenerator"
2471 }
2472
2473 fn supported_types(&self) -> Vec<ProblemType> {
2474 vec![ProblemType::Knapsack]
2475 }
2476}
2477
2478struct RandomQuboGenerator;
2480
2481impl TestGenerator for RandomQuboGenerator {
2482 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2483 let mut rng = if let Some(seed) = config.seed {
2484 StdRng::seed_from_u64(seed)
2485 } else {
2486 let mut thread_rng = thread_rng();
2487 StdRng::from_rng(&mut thread_rng)
2488 };
2489
2490 let n = config.size;
2491 let mut test_cases = Vec::new();
2492
2493 let mut qubo = Array2::zeros((n, n));
2495 let density = match config.difficulty {
2496 Difficulty::Easy => 0.3,
2497 Difficulty::Medium => 0.5,
2498 Difficulty::Hard => 0.7,
2499 Difficulty::VeryHard => 0.9,
2500 Difficulty::Extreme => 1.0,
2501 };
2502
2503 for i in 0..n {
2504 for j in i..n {
2505 if rng.gen::<f64>() < density {
2506 let value = rng.gen_range(-10.0..10.0);
2507 qubo[[i, j]] = value;
2508 if i != j {
2509 qubo[[j, i]] = value;
2510 }
2511 }
2512 }
2513 }
2514
2515 let mut var_map = HashMap::new();
2516 for i in 0..n {
2517 var_map.insert(format!("x_{i}"), i);
2518 }
2519
2520 test_cases.push(TestCase {
2521 id: format!("random_{}_{:?}", n, config.difficulty),
2522 problem_type: ProblemType::Custom {
2523 name: "Random QUBO".to_string(),
2524 },
2525 size: n,
2526 qubo,
2527 var_map,
2528 optimal_solution: None,
2529 optimal_value: None,
2530 constraints: vec![],
2531 metadata: TestMetadata {
2532 generation_method: "Random generation".to_string(),
2533 difficulty: config.difficulty.clone(),
2534 expected_runtime: Duration::from_millis(50),
2535 notes: format!("Density: {density}"),
2536 tags: vec!["random".to_string(), "qubo".to_string()],
2537 },
2538 });
2539
2540 Ok(test_cases)
2541 }
2542
2543 fn name(&self) -> &'static str {
2544 "RandomQuboGenerator"
2545 }
2546
2547 fn supported_types(&self) -> Vec<ProblemType> {
2548 vec![
2549 ProblemType::Custom {
2550 name: "Random".to_string(),
2551 },
2552 ProblemType::Ising,
2553 ]
2554 }
2555}
2556
2557struct FinanceTestGenerator;
2559
2560impl TestGenerator for FinanceTestGenerator {
2561 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2562 let mut rng = if let Some(seed) = config.seed {
2563 StdRng::seed_from_u64(seed)
2564 } else {
2565 let mut thread_rng = thread_rng();
2566 StdRng::from_rng(&mut thread_rng)
2567 };
2568
2569 let n_assets = config.size;
2570 let mut test_cases = Vec::new();
2571
2572 let mut qubo = Array2::zeros((n_assets, n_assets));
2574 let mut var_map = HashMap::new();
2575
2576 for i in 0..n_assets {
2577 var_map.insert(format!("asset_{i}"), i);
2578
2579 let expected_return = rng.gen_range(0.05..0.15);
2581 qubo[[i, i]] -= expected_return;
2582 }
2583
2584 for i in 0..n_assets {
2586 for j in 0..n_assets {
2587 let covariance = if i == j {
2588 rng.gen_range(0.01..0.04) } else {
2590 rng.gen_range(-0.01..0.01) };
2592 qubo[[i, j]] += covariance;
2593 }
2594 }
2595
2596 test_cases.push(TestCase {
2597 id: format!("portfolio_{}_{:?}", n_assets, config.difficulty),
2598 problem_type: ProblemType::Portfolio,
2599 size: n_assets,
2600 qubo,
2601 var_map,
2602 optimal_solution: None,
2603 optimal_value: None,
2604 constraints: vec![Constraint {
2605 constraint_type: ConstraintType::LinearEquality { target: 1.0 },
2606 variables: (0..n_assets).map(|i| format!("asset_{i}")).collect(),
2607 parameters: HashMap::new(),
2608 penalty: 1000.0,
2609 }],
2610 metadata: TestMetadata {
2611 generation_method: "Random portfolio".to_string(),
2612 difficulty: config.difficulty.clone(),
2613 expected_runtime: Duration::from_millis(200),
2614 notes: format!("{n_assets} assets"),
2615 tags: vec!["finance".to_string(), "portfolio".to_string()],
2616 },
2617 });
2618
2619 Ok(test_cases)
2620 }
2621
2622 fn name(&self) -> &'static str {
2623 "FinanceTestGenerator"
2624 }
2625
2626 fn supported_types(&self) -> Vec<ProblemType> {
2627 vec![ProblemType::Portfolio]
2628 }
2629}
2630
2631struct LogisticsTestGenerator;
2633
2634impl TestGenerator for LogisticsTestGenerator {
2635 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2636 let mut rng = if let Some(seed) = config.seed {
2637 StdRng::seed_from_u64(seed)
2638 } else {
2639 let mut thread_rng = thread_rng();
2640 StdRng::from_rng(&mut thread_rng)
2641 };
2642
2643 let n_vehicles = 2;
2644 let n_locations = config.size;
2645 let mut test_cases = Vec::new();
2646
2647 let n_vars = n_vehicles * n_locations * n_locations;
2649 let mut qubo = Array2::zeros((n_vars, n_vars));
2650 let mut var_map = HashMap::new();
2651
2652 for v in 0..n_vehicles {
2654 for i in 0..n_locations {
2655 for j in 0..n_locations {
2656 let idx = v * n_locations * n_locations + i * n_locations + j;
2657 var_map.insert(format!("x_{v}_{i}_{j}"), idx);
2658 }
2659 }
2660 }
2661
2662 for v in 0..n_vehicles {
2664 for i in 0..n_locations {
2665 for j in 0..n_locations {
2666 if i != j {
2667 let idx = v * n_locations * n_locations + i * n_locations + j;
2668 let distance = rng.gen_range(1.0..20.0);
2669 qubo[[idx, idx]] += distance;
2670 }
2671 }
2672 }
2673 }
2674
2675 test_cases.push(TestCase {
2676 id: format!("vrp_{}_{}_{:?}", n_vehicles, n_locations, config.difficulty),
2677 problem_type: ProblemType::VRP,
2678 size: n_locations,
2679 qubo,
2680 var_map,
2681 optimal_solution: None,
2682 optimal_value: None,
2683 constraints: vec![],
2684 metadata: TestMetadata {
2685 generation_method: "Random VRP".to_string(),
2686 difficulty: config.difficulty.clone(),
2687 expected_runtime: Duration::from_millis(500),
2688 notes: format!("{n_vehicles} vehicles, {n_locations} locations"),
2689 tags: vec!["logistics".to_string(), "vrp".to_string()],
2690 },
2691 });
2692
2693 Ok(test_cases)
2694 }
2695
2696 fn name(&self) -> &'static str {
2697 "LogisticsTestGenerator"
2698 }
2699
2700 fn supported_types(&self) -> Vec<ProblemType> {
2701 vec![ProblemType::VRP]
2702 }
2703}
2704
2705struct ManufacturingTestGenerator;
2707
2708impl TestGenerator for ManufacturingTestGenerator {
2709 fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
2710 let mut rng = if let Some(seed) = config.seed {
2711 StdRng::seed_from_u64(seed)
2712 } else {
2713 let mut thread_rng = thread_rng();
2714 StdRng::from_rng(&mut thread_rng)
2715 };
2716
2717 let n_jobs = config.size;
2718 let n_machines = 3;
2719 let mut test_cases = Vec::new();
2720
2721 let n_vars = n_jobs * n_machines;
2723 let mut qubo = Array2::zeros((n_vars, n_vars));
2724 let mut var_map = HashMap::new();
2725
2726 for j in 0..n_jobs {
2728 for m in 0..n_machines {
2729 let idx = j * n_machines + m;
2730 var_map.insert(format!("job_{j}_machine_{m}"), idx);
2731 }
2732 }
2733
2734 for j in 0..n_jobs {
2736 for m in 0..n_machines {
2737 let idx = j * n_machines + m;
2738 let processing_time = rng.gen_range(1.0..10.0);
2739 qubo[[idx, idx]] += processing_time;
2740 }
2741 }
2742
2743 let mut constraints = Vec::new();
2745 for j in 0..n_jobs {
2746 let vars: Vec<_> = (0..n_machines)
2747 .map(|m| format!("job_{j}_machine_{m}"))
2748 .collect();
2749 constraints.push(Constraint {
2750 constraint_type: ConstraintType::ExactlyK { k: 1 },
2751 variables: vars,
2752 parameters: HashMap::new(),
2753 penalty: 100.0,
2754 });
2755 }
2756
2757 test_cases.push(TestCase {
2758 id: format!(
2759 "scheduling_{}_{}_{:?}",
2760 n_jobs, n_machines, config.difficulty
2761 ),
2762 problem_type: ProblemType::JobScheduling,
2763 size: n_jobs,
2764 qubo,
2765 var_map,
2766 optimal_solution: None,
2767 optimal_value: None,
2768 constraints,
2769 metadata: TestMetadata {
2770 generation_method: "Random job scheduling".to_string(),
2771 difficulty: config.difficulty.clone(),
2772 expected_runtime: Duration::from_millis(300),
2773 notes: format!("{n_jobs} jobs, {n_machines} machines"),
2774 tags: vec!["manufacturing".to_string(), "scheduling".to_string()],
2775 },
2776 });
2777
2778 Ok(test_cases)
2779 }
2780
2781 fn name(&self) -> &'static str {
2782 "ManufacturingTestGenerator"
2783 }
2784
2785 fn supported_types(&self) -> Vec<ProblemType> {
2786 vec![ProblemType::JobScheduling]
2787 }
2788}
2789
2790#[cfg(test)]
2791mod tests {
2792 use super::*;
2793 use crate::sampler::SASampler;
2794
2795 #[test]
2796 #[ignore]
2797 fn test_testing_framework() {
2798 let mut config = TestConfig {
2799 seed: Some(42),
2800 cases_per_category: 5,
2801 problem_sizes: vec![5, 10],
2802 samplers: vec![SamplerConfig {
2803 name: "SA".to_string(),
2804 num_samples: 100,
2805 parameters: HashMap::new(),
2806 }],
2807 timeout: Duration::from_secs(10),
2808 validation: ValidationConfig {
2809 check_constraints: true,
2810 check_objective: true,
2811 statistical_tests: false,
2812 tolerance: 1e-6,
2813 min_quality: 0.0,
2814 },
2815 output: OutputConfig {
2816 generate_report: true,
2817 format: ReportFormat::Text,
2818 output_dir: "/tmp".to_string(),
2819 verbosity: VerbosityLevel::Info,
2820 },
2821 };
2822
2823 let mut framework = TestingFramework::new(config);
2824
2825 framework.add_category(TestCategory {
2827 name: "Graph Problems".to_string(),
2828 description: "Graph-based optimization problems".to_string(),
2829 problem_types: vec![ProblemType::MaxCut, ProblemType::GraphColoring],
2830 difficulties: vec![Difficulty::Easy, Difficulty::Medium],
2831 tags: vec!["graph".to_string()],
2832 });
2833
2834 let mut result = framework.generate_suite();
2836 assert!(result.is_ok());
2837 assert!(!framework.suite.test_cases.is_empty());
2838
2839 let sampler = SASampler::new(Some(42));
2841 let mut result = framework.run_suite(&sampler);
2842 assert!(result.is_ok());
2843
2844 assert!(framework.results.summary.total_tests > 0);
2846 assert!(framework.results.summary.success_rate >= 0.0);
2847
2848 let mut report = framework.generate_report();
2850 assert!(report.is_ok());
2851 }
2852}