quantrs2_tytan/testing_framework/
generators.rs

1//! Test generators for various optimization problems.
2//!
3//! This module provides test case generators for different problem types
4//! including Max-Cut, TSP, Graph Coloring, Knapsack, and industry-specific problems.
5
6use scirs2_core::ndarray::Array2;
7use scirs2_core::random::prelude::*;
8use scirs2_core::random::SeedableRng;
9use std::collections::HashMap;
10use std::time::Duration;
11
12use super::types::{
13    Constraint, ConstraintType, Difficulty, GeneratorConfig, ProblemType, TestCase, TestGenerator,
14    TestMetadata,
15};
16
17/// Max-cut problem generator
18pub struct MaxCutGenerator;
19
20impl TestGenerator for MaxCutGenerator {
21    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
22        let mut rng = if let Some(seed) = config.seed {
23            StdRng::seed_from_u64(seed)
24        } else {
25            let mut thread_rng = thread_rng();
26            StdRng::from_rng(&mut thread_rng)
27        };
28
29        let mut test_cases = Vec::new();
30
31        // Generate random graph
32        let n = config.size;
33        let edge_probability = match config.difficulty {
34            Difficulty::Easy => 0.3,
35            Difficulty::Medium => 0.5,
36            Difficulty::Hard => 0.7,
37            Difficulty::VeryHard => 0.9,
38            Difficulty::Extreme => 0.95,
39        };
40
41        let mut qubo = Array2::zeros((n, n));
42        let mut var_map = HashMap::new();
43
44        for i in 0..n {
45            var_map.insert(format!("x_{i}"), i);
46        }
47
48        // Generate edges
49        for i in 0..n {
50            for j in i + 1..n {
51                if rng.random::<f64>() < edge_probability {
52                    let weight = rng.random_range(1.0..10.0);
53                    // Max-cut: minimize -w_ij * (x_i + x_j - 2*x_i*x_j)
54                    qubo[[i, i]] -= weight;
55                    qubo[[j, j]] -= weight;
56                    qubo[[i, j]] += 2.0 * weight;
57                    qubo[[j, i]] += 2.0 * weight;
58                }
59            }
60        }
61
62        test_cases.push(TestCase {
63            id: format!("maxcut_{}_{:?}", n, config.difficulty),
64            problem_type: ProblemType::MaxCut,
65            size: n,
66            qubo,
67            var_map,
68            optimal_solution: None,
69            optimal_value: None,
70            constraints: Vec::new(),
71            metadata: TestMetadata {
72                generation_method: "Random graph".to_string(),
73                difficulty: config.difficulty.clone(),
74                expected_runtime: Duration::from_millis(100),
75                notes: format!("Edge probability: {edge_probability}"),
76                tags: vec!["graph".to_string(), "maxcut".to_string()],
77            },
78        });
79
80        Ok(test_cases)
81    }
82
83    fn name(&self) -> &'static str {
84        "MaxCutGenerator"
85    }
86
87    fn supported_types(&self) -> Vec<ProblemType> {
88        vec![ProblemType::MaxCut]
89    }
90}
91
92/// TSP generator
93pub struct TSPGenerator;
94
95impl TestGenerator for TSPGenerator {
96    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
97        let mut rng = if let Some(seed) = config.seed {
98            StdRng::seed_from_u64(seed)
99        } else {
100            let mut thread_rng = thread_rng();
101            StdRng::from_rng(&mut thread_rng)
102        };
103
104        let n_cities = config.size;
105        let mut test_cases = Vec::new();
106
107        // Generate random city locations
108        let mut cities = Vec::new();
109        for _ in 0..n_cities {
110            cities.push((rng.random_range(0.0..100.0), rng.random_range(0.0..100.0)));
111        }
112
113        // Calculate distances
114        let mut distances = Array2::zeros((n_cities, n_cities));
115        for i in 0..n_cities {
116            for j in 0..n_cities {
117                if i != j {
118                    let dx: f64 = cities[i].0 - cities[j].0;
119                    let dy: f64 = cities[i].1 - cities[j].1;
120                    distances[[i, j]] = dx.hypot(dy);
121                }
122            }
123        }
124
125        // Create QUBO
126        let n_vars = n_cities * n_cities;
127        let mut qubo = Array2::zeros((n_vars, n_vars));
128        let mut var_map = HashMap::new();
129
130        // Variable mapping: x[i,j] = city i at position j
131        for i in 0..n_cities {
132            for j in 0..n_cities {
133                let idx = i * n_cities + j;
134                var_map.insert(format!("x_{i}_{j}"), idx);
135            }
136        }
137
138        // Objective: minimize total distance
139        for i in 0..n_cities {
140            for j in 0..n_cities {
141                for k in 0..n_cities {
142                    let next_j = (j + 1) % n_cities;
143                    let idx1 = i * n_cities + j;
144                    let idx2 = k * n_cities + next_j;
145                    qubo[[idx1, idx2]] += distances[[i, k]];
146                }
147            }
148        }
149
150        // Constraints
151        let mut constraints = Vec::new();
152        let penalty = 1000.0;
153
154        // Each city visited exactly once
155        for i in 0..n_cities {
156            let vars: Vec<_> = (0..n_cities).map(|j| format!("x_{i}_{j}")).collect();
157
158            constraints.push(Constraint {
159                constraint_type: ConstraintType::ExactlyK { k: 1 },
160                variables: vars,
161                parameters: HashMap::new(),
162                penalty,
163            });
164        }
165
166        // Each position has exactly one city
167        for j in 0..n_cities {
168            let vars: Vec<_> = (0..n_cities).map(|i| format!("x_{i}_{j}")).collect();
169
170            constraints.push(Constraint {
171                constraint_type: ConstraintType::ExactlyK { k: 1 },
172                variables: vars,
173                parameters: HashMap::new(),
174                penalty,
175            });
176        }
177
178        // Add constraint penalties to QUBO
179        self.add_constraint_penalties(&mut qubo, &var_map, &constraints)?;
180
181        test_cases.push(TestCase {
182            id: format!("tsp_{}_{:?}", n_cities, config.difficulty),
183            problem_type: ProblemType::TSP,
184            size: n_cities,
185            qubo,
186            var_map,
187            optimal_solution: None,
188            optimal_value: None,
189            constraints,
190            metadata: TestMetadata {
191                generation_method: "Random cities".to_string(),
192                difficulty: config.difficulty.clone(),
193                expected_runtime: Duration::from_millis(500),
194                notes: format!("{n_cities} cities"),
195                tags: vec!["routing".to_string(), "tsp".to_string()],
196            },
197        });
198
199        Ok(test_cases)
200    }
201
202    fn name(&self) -> &'static str {
203        "TSPGenerator"
204    }
205
206    fn supported_types(&self) -> Vec<ProblemType> {
207        vec![ProblemType::TSP]
208    }
209}
210
211impl TSPGenerator {
212    fn add_constraint_penalties(
213        &self,
214        qubo: &mut Array2<f64>,
215        var_map: &HashMap<String, usize>,
216        constraints: &[Constraint],
217    ) -> Result<(), String> {
218        for constraint in constraints {
219            if let ConstraintType::ExactlyK { k } = &constraint.constraint_type {
220                // (sum x_i - k)^2
221                for v1 in &constraint.variables {
222                    if let Some(&idx1) = var_map.get(v1) {
223                        // Linear term: -2k
224                        qubo[[idx1, idx1]] +=
225                            constraint.penalty * 2.0f64.mul_add(-(*k as f64), 1.0);
226
227                        // Quadratic terms
228                        for v2 in &constraint.variables {
229                            if v1 != v2 {
230                                if let Some(&idx2) = var_map.get(v2) {
231                                    qubo[[idx1, idx2]] += constraint.penalty * 2.0;
232                                }
233                            }
234                        }
235                    }
236                }
237            }
238        }
239
240        Ok(())
241    }
242}
243
244/// Graph coloring generator
245pub struct GraphColoringGenerator;
246
247impl TestGenerator for GraphColoringGenerator {
248    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
249        let mut rng = if let Some(seed) = config.seed {
250            StdRng::seed_from_u64(seed)
251        } else {
252            let mut thread_rng = thread_rng();
253            StdRng::from_rng(&mut thread_rng)
254        };
255
256        let n_vertices = config.size;
257        let n_colors = match config.difficulty {
258            Difficulty::Easy => 4,
259            Difficulty::Medium => 3,
260            _ => 3,
261        };
262
263        let mut test_cases = Vec::new();
264
265        // Generate random graph
266        let edge_prob = 0.3;
267        let mut edges = Vec::new();
268
269        for i in 0..n_vertices {
270            for j in i + 1..n_vertices {
271                if rng.random::<f64>() < edge_prob {
272                    edges.push((i, j));
273                }
274            }
275        }
276
277        // Create QUBO
278        let n_vars = n_vertices * n_colors;
279        let mut qubo = Array2::zeros((n_vars, n_vars));
280        let mut var_map = HashMap::new();
281
282        // Variable mapping: x[v,c] = vertex v has color c
283        for v in 0..n_vertices {
284            for c in 0..n_colors {
285                let idx = v * n_colors + c;
286                var_map.insert(format!("x_{v}_{c}"), idx);
287            }
288        }
289
290        // Objective: minimize number of colors used (simplified)
291        for v in 0..n_vertices {
292            for c in 0..n_colors {
293                let idx = v * n_colors + c;
294                qubo[[idx, idx]] -= c as f64; // Prefer lower colors
295            }
296        }
297
298        // Constraints
299        let mut constraints = Vec::new();
300        let penalty = 100.0;
301
302        // Each vertex has exactly one color
303        for v in 0..n_vertices {
304            let vars: Vec<_> = (0..n_colors).map(|c| format!("x_{v}_{c}")).collect();
305
306            constraints.push(Constraint {
307                constraint_type: ConstraintType::ExactlyK { k: 1 },
308                variables: vars,
309                parameters: HashMap::new(),
310                penalty,
311            });
312        }
313
314        // Adjacent vertices have different colors
315        for (u, v) in &edges {
316            for c in 0..n_colors {
317                let idx_u = u * n_colors + c;
318                let idx_v = v * n_colors + c;
319                qubo[[idx_u, idx_v]] += penalty;
320                qubo[[idx_v, idx_u]] += penalty;
321            }
322        }
323
324        test_cases.push(TestCase {
325            id: format!(
326                "coloring_{}_{}_{}_{:?}",
327                n_vertices,
328                n_colors,
329                edges.len(),
330                config.difficulty
331            ),
332            problem_type: ProblemType::GraphColoring,
333            size: n_vertices,
334            qubo,
335            var_map,
336            optimal_solution: None,
337            optimal_value: None,
338            constraints,
339            metadata: TestMetadata {
340                generation_method: "Random graph".to_string(),
341                difficulty: config.difficulty.clone(),
342                expected_runtime: Duration::from_millis(200),
343                notes: format!(
344                    "{} vertices, {} colors, {} edges",
345                    n_vertices,
346                    n_colors,
347                    edges.len()
348                ),
349                tags: vec!["graph".to_string(), "coloring".to_string()],
350            },
351        });
352
353        Ok(test_cases)
354    }
355
356    fn name(&self) -> &'static str {
357        "GraphColoringGenerator"
358    }
359
360    fn supported_types(&self) -> Vec<ProblemType> {
361        vec![ProblemType::GraphColoring]
362    }
363}
364
365/// Knapsack generator
366pub struct KnapsackGenerator;
367
368impl TestGenerator for KnapsackGenerator {
369    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
370        let mut rng = if let Some(seed) = config.seed {
371            StdRng::seed_from_u64(seed)
372        } else {
373            let mut thread_rng = thread_rng();
374            StdRng::from_rng(&mut thread_rng)
375        };
376
377        let n_items = config.size;
378        let mut test_cases = Vec::new();
379
380        // Generate items
381        let mut values = Vec::new();
382        let mut weights = Vec::new();
383
384        for _ in 0..n_items {
385            values.push(rng.random_range(1.0..100.0));
386            weights.push(rng.random_range(1.0..50.0));
387        }
388
389        let capacity = weights.iter().sum::<f64>() * 0.5; // 50% of total weight
390
391        // Create QUBO
392        let mut qubo = Array2::zeros((n_items, n_items));
393        let mut var_map = HashMap::new();
394
395        for i in 0..n_items {
396            var_map.insert(format!("x_{i}"), i);
397            // Maximize value (negative in minimization)
398            qubo[[i, i]] -= values[i];
399        }
400
401        // Weight constraint penalty
402        let _penalty = values.iter().sum::<f64>() * 2.0;
403
404        // Add soft constraint for capacity
405        // Penalty for exceeding capacity: (sum w_i x_i - W)^2 if sum > W
406        // This is simplified - proper implementation would use slack variables
407
408        test_cases.push(TestCase {
409            id: format!("knapsack_{}_{:?}", n_items, config.difficulty),
410            problem_type: ProblemType::Knapsack,
411            size: n_items,
412            qubo,
413            var_map,
414            optimal_solution: None,
415            optimal_value: None,
416            constraints: vec![],
417            metadata: TestMetadata {
418                generation_method: "Random items".to_string(),
419                difficulty: config.difficulty.clone(),
420                expected_runtime: Duration::from_millis(100),
421                notes: format!("{n_items} items, capacity: {capacity:.1}"),
422                tags: vec!["optimization".to_string(), "knapsack".to_string()],
423            },
424        });
425
426        Ok(test_cases)
427    }
428
429    fn name(&self) -> &'static str {
430        "KnapsackGenerator"
431    }
432
433    fn supported_types(&self) -> Vec<ProblemType> {
434        vec![ProblemType::Knapsack]
435    }
436}
437
438/// Random QUBO generator
439pub struct RandomQuboGenerator;
440
441impl TestGenerator for RandomQuboGenerator {
442    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
443        let mut rng = if let Some(seed) = config.seed {
444            StdRng::seed_from_u64(seed)
445        } else {
446            let mut thread_rng = thread_rng();
447            StdRng::from_rng(&mut thread_rng)
448        };
449
450        let n = config.size;
451        let mut test_cases = Vec::new();
452
453        // Generate random QUBO
454        let mut qubo = Array2::zeros((n, n));
455        let density = match config.difficulty {
456            Difficulty::Easy => 0.3,
457            Difficulty::Medium => 0.5,
458            Difficulty::Hard => 0.7,
459            Difficulty::VeryHard => 0.9,
460            Difficulty::Extreme => 1.0,
461        };
462
463        for i in 0..n {
464            for j in i..n {
465                if rng.random::<f64>() < density {
466                    let value = rng.random_range(-10.0..10.0);
467                    qubo[[i, j]] = value;
468                    if i != j {
469                        qubo[[j, i]] = value;
470                    }
471                }
472            }
473        }
474
475        let mut var_map = HashMap::new();
476        for i in 0..n {
477            var_map.insert(format!("x_{i}"), i);
478        }
479
480        test_cases.push(TestCase {
481            id: format!("random_{}_{:?}", n, config.difficulty),
482            problem_type: ProblemType::Custom {
483                name: "Random QUBO".to_string(),
484            },
485            size: n,
486            qubo,
487            var_map,
488            optimal_solution: None,
489            optimal_value: None,
490            constraints: vec![],
491            metadata: TestMetadata {
492                generation_method: "Random generation".to_string(),
493                difficulty: config.difficulty.clone(),
494                expected_runtime: Duration::from_millis(50),
495                notes: format!("Density: {density}"),
496                tags: vec!["random".to_string(), "qubo".to_string()],
497            },
498        });
499
500        Ok(test_cases)
501    }
502
503    fn name(&self) -> &'static str {
504        "RandomQuboGenerator"
505    }
506
507    fn supported_types(&self) -> Vec<ProblemType> {
508        vec![
509            ProblemType::Custom {
510                name: "Random".to_string(),
511            },
512            ProblemType::Ising,
513        ]
514    }
515}
516
517/// Finance industry test generator
518pub struct FinanceTestGenerator;
519
520impl TestGenerator for FinanceTestGenerator {
521    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
522        let mut rng = if let Some(seed) = config.seed {
523            StdRng::seed_from_u64(seed)
524        } else {
525            let mut thread_rng = thread_rng();
526            StdRng::from_rng(&mut thread_rng)
527        };
528
529        let n_assets = config.size;
530        let mut test_cases = Vec::new();
531
532        // Generate portfolio optimization test case
533        let mut qubo = Array2::zeros((n_assets, n_assets));
534        let mut var_map = HashMap::new();
535
536        for i in 0..n_assets {
537            var_map.insert(format!("asset_{i}"), i);
538
539            // Expected return (negative for minimization)
540            let expected_return = rng.random_range(0.05..0.15);
541            qubo[[i, i]] -= expected_return;
542        }
543
544        // Risk covariance terms
545        for i in 0..n_assets {
546            for j in 0..n_assets {
547                let covariance = if i == j {
548                    rng.random_range(0.01..0.04) // Variance
549                } else {
550                    rng.random_range(-0.01..0.01) // Covariance
551                };
552                qubo[[i, j]] += covariance;
553            }
554        }
555
556        test_cases.push(TestCase {
557            id: format!("portfolio_{}_{:?}", n_assets, config.difficulty),
558            problem_type: ProblemType::Portfolio,
559            size: n_assets,
560            qubo,
561            var_map,
562            optimal_solution: None,
563            optimal_value: None,
564            constraints: vec![Constraint {
565                constraint_type: ConstraintType::LinearEquality { target: 1.0 },
566                variables: (0..n_assets).map(|i| format!("asset_{i}")).collect(),
567                parameters: HashMap::new(),
568                penalty: 1000.0,
569            }],
570            metadata: TestMetadata {
571                generation_method: "Random portfolio".to_string(),
572                difficulty: config.difficulty.clone(),
573                expected_runtime: Duration::from_millis(200),
574                notes: format!("{n_assets} assets"),
575                tags: vec!["finance".to_string(), "portfolio".to_string()],
576            },
577        });
578
579        Ok(test_cases)
580    }
581
582    fn name(&self) -> &'static str {
583        "FinanceTestGenerator"
584    }
585
586    fn supported_types(&self) -> Vec<ProblemType> {
587        vec![ProblemType::Portfolio]
588    }
589}
590
591/// Logistics industry test generator
592pub struct LogisticsTestGenerator;
593
594impl TestGenerator for LogisticsTestGenerator {
595    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
596        let mut rng = if let Some(seed) = config.seed {
597            StdRng::seed_from_u64(seed)
598        } else {
599            let mut thread_rng = thread_rng();
600            StdRng::from_rng(&mut thread_rng)
601        };
602
603        let n_vehicles = 2;
604        let n_locations = config.size;
605        let mut test_cases = Vec::new();
606
607        // Generate vehicle routing problem
608        let n_vars = n_vehicles * n_locations * n_locations;
609        let mut qubo = Array2::zeros((n_vars, n_vars));
610        let mut var_map = HashMap::new();
611
612        // Variable mapping: x[v][i][j] = vehicle v goes from i to j
613        for v in 0..n_vehicles {
614            for i in 0..n_locations {
615                for j in 0..n_locations {
616                    let idx = v * n_locations * n_locations + i * n_locations + j;
617                    var_map.insert(format!("x_{v}_{i}_{j}"), idx);
618                }
619            }
620        }
621
622        // Add distance objective
623        for v in 0..n_vehicles {
624            for i in 0..n_locations {
625                for j in 0..n_locations {
626                    if i != j {
627                        let idx = v * n_locations * n_locations + i * n_locations + j;
628                        let distance = rng.random_range(1.0..20.0);
629                        qubo[[idx, idx]] += distance;
630                    }
631                }
632            }
633        }
634
635        test_cases.push(TestCase {
636            id: format!("vrp_{}_{}_{:?}", n_vehicles, n_locations, config.difficulty),
637            problem_type: ProblemType::VRP,
638            size: n_locations,
639            qubo,
640            var_map,
641            optimal_solution: None,
642            optimal_value: None,
643            constraints: vec![],
644            metadata: TestMetadata {
645                generation_method: "Random VRP".to_string(),
646                difficulty: config.difficulty.clone(),
647                expected_runtime: Duration::from_millis(500),
648                notes: format!("{n_vehicles} vehicles, {n_locations} locations"),
649                tags: vec!["logistics".to_string(), "vrp".to_string()],
650            },
651        });
652
653        Ok(test_cases)
654    }
655
656    fn name(&self) -> &'static str {
657        "LogisticsTestGenerator"
658    }
659
660    fn supported_types(&self) -> Vec<ProblemType> {
661        vec![ProblemType::VRP]
662    }
663}
664
665/// Manufacturing industry test generator
666pub struct ManufacturingTestGenerator;
667
668impl TestGenerator for ManufacturingTestGenerator {
669    fn generate(&self, config: &GeneratorConfig) -> Result<Vec<TestCase>, String> {
670        let mut rng = if let Some(seed) = config.seed {
671            StdRng::seed_from_u64(seed)
672        } else {
673            let mut thread_rng = thread_rng();
674            StdRng::from_rng(&mut thread_rng)
675        };
676
677        let n_jobs = config.size;
678        let n_machines = 3;
679        let mut test_cases = Vec::new();
680
681        // Generate job scheduling problem
682        let n_vars = n_jobs * n_machines;
683        let mut qubo = Array2::zeros((n_vars, n_vars));
684        let mut var_map = HashMap::new();
685
686        // Variable mapping: x[j][m] = job j on machine m
687        for j in 0..n_jobs {
688            for m in 0..n_machines {
689                let idx = j * n_machines + m;
690                var_map.insert(format!("job_{j}_machine_{m}"), idx);
691            }
692        }
693
694        // Add processing time objective
695        for j in 0..n_jobs {
696            for m in 0..n_machines {
697                let idx = j * n_machines + m;
698                let processing_time = rng.random_range(1.0..10.0);
699                qubo[[idx, idx]] += processing_time;
700            }
701        }
702
703        // Add constraints: each job assigned to exactly one machine
704        let mut constraints = Vec::new();
705        for j in 0..n_jobs {
706            let vars: Vec<_> = (0..n_machines)
707                .map(|m| format!("job_{j}_machine_{m}"))
708                .collect();
709            constraints.push(Constraint {
710                constraint_type: ConstraintType::ExactlyK { k: 1 },
711                variables: vars,
712                parameters: HashMap::new(),
713                penalty: 100.0,
714            });
715        }
716
717        test_cases.push(TestCase {
718            id: format!(
719                "scheduling_{}_{}_{:?}",
720                n_jobs, n_machines, config.difficulty
721            ),
722            problem_type: ProblemType::JobScheduling,
723            size: n_jobs,
724            qubo,
725            var_map,
726            optimal_solution: None,
727            optimal_value: None,
728            constraints,
729            metadata: TestMetadata {
730                generation_method: "Random job scheduling".to_string(),
731                difficulty: config.difficulty.clone(),
732                expected_runtime: Duration::from_millis(300),
733                notes: format!("{n_jobs} jobs, {n_machines} machines"),
734                tags: vec!["manufacturing".to_string(), "scheduling".to_string()],
735            },
736        });
737
738        Ok(test_cases)
739    }
740
741    fn name(&self) -> &'static str {
742        "ManufacturingTestGenerator"
743    }
744
745    fn supported_types(&self) -> Vec<ProblemType> {
746        vec![ProblemType::JobScheduling]
747    }
748}
749
750/// Create default generators
751pub fn default_generators() -> Vec<Box<dyn TestGenerator>> {
752    vec![
753        Box::new(MaxCutGenerator),
754        Box::new(TSPGenerator),
755        Box::new(GraphColoringGenerator),
756        Box::new(KnapsackGenerator),
757        Box::new(RandomQuboGenerator),
758    ]
759}