quantrs2_anneal/
problem_schedules.rs

1//! Problem-specific annealing schedules
2//!
3//! This module provides optimized annealing schedules for specific problem types.
4//! Different optimization problems benefit from different temperature and field
5//! schedules based on their energy landscape characteristics.
6
7use crate::ising::{IsingModel, IsingResult};
8use crate::simulator::{AnnealingParams, TemperatureSchedule, TransverseFieldSchedule};
9use std::collections::HashMap;
10
11/// Problem types with specialized annealing schedules
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum ProblemType {
14    /// Traveling Salesman Problem
15    TSP,
16    /// Maximum Cut problem
17    MaxCut,
18    /// Graph Coloring
19    GraphColoring,
20    /// Quadratic Assignment Problem
21    QAP,
22    /// Number Partitioning
23    NumberPartitioning,
24    /// Satisfiability (SAT)
25    SAT,
26    /// Portfolio Optimization
27    Portfolio,
28    /// Job Shop Scheduling
29    JobShop,
30    /// Spin Glass
31    SpinGlass,
32    /// Custom problem type
33    Custom(String),
34}
35
36/// Schedule optimizer that creates problem-specific annealing parameters
37pub struct ProblemSpecificScheduler {
38    /// Problem characteristics analyzer
39    analyzer: ProblemAnalyzer,
40    /// Schedule templates for different problem types
41    templates: HashMap<ProblemType, ScheduleTemplate>,
42}
43
44/// Template for annealing schedules
45#[derive(Debug, Clone)]
46pub struct ScheduleTemplate {
47    /// Initial temperature factor (multiplied by problem scale)
48    pub initial_temp_factor: f64,
49    /// Final temperature factor
50    pub final_temp_factor: f64,
51    /// Temperature schedule type
52    pub temp_schedule: TemperatureSchedule,
53    /// Initial transverse field strength
54    pub initial_field: f64,
55    /// Transverse field schedule
56    pub field_schedule: TransverseFieldSchedule,
57    /// Number of sweeps factor (multiplied by problem size)
58    pub sweeps_factor: f64,
59    /// Number of repetitions
60    pub num_repetitions: usize,
61    /// Use parallel tempering
62    pub use_parallel_tempering: bool,
63    /// Custom parameters
64    pub custom_params: HashMap<String, f64>,
65}
66
67impl Default for ScheduleTemplate {
68    fn default() -> Self {
69        Self {
70            initial_temp_factor: 2.0,
71            final_temp_factor: 0.01,
72            temp_schedule: TemperatureSchedule::Exponential(3.0),
73            initial_field: 2.0,
74            field_schedule: TransverseFieldSchedule::Linear,
75            sweeps_factor: 100.0,
76            num_repetitions: 10,
77            use_parallel_tempering: false,
78            custom_params: HashMap::new(),
79        }
80    }
81}
82
83/// Analyzes problem characteristics to inform schedule optimization
84pub struct ProblemAnalyzer {
85    /// Coupling strength statistics
86    coupling_stats: CouplingStatistics,
87    /// Graph properties
88    graph_properties: GraphProperties,
89}
90
91#[derive(Debug, Clone, Default)]
92struct CouplingStatistics {
93    mean_coupling: f64,
94    std_coupling: f64,
95    max_coupling: f64,
96    min_coupling: f64,
97    sparsity: f64,
98}
99
100#[derive(Debug, Clone, Default)]
101struct GraphProperties {
102    num_variables: usize,
103    num_couplings: usize,
104    avg_degree: f64,
105    max_degree: usize,
106    clustering_coefficient: f64,
107    is_bipartite: bool,
108}
109
110impl ProblemSpecificScheduler {
111    /// Create a new problem-specific scheduler
112    #[must_use]
113    pub fn new() -> Self {
114        let mut scheduler = Self {
115            analyzer: ProblemAnalyzer::new(),
116            templates: HashMap::new(),
117        };
118
119        // Initialize with default templates
120        scheduler.init_default_templates();
121        scheduler
122    }
123
124    /// Initialize default schedule templates for known problem types
125    fn init_default_templates(&mut self) {
126        // TSP: Needs careful temperature control for constraint satisfaction
127        self.templates.insert(
128            ProblemType::TSP,
129            ScheduleTemplate {
130                initial_temp_factor: 5.0, // Higher initial temperature
131                final_temp_factor: 0.001,
132                temp_schedule: TemperatureSchedule::Geometric(0.95, 10.0),
133                initial_field: 3.0,
134                field_schedule: TransverseFieldSchedule::Exponential(2.0),
135                sweeps_factor: 200.0, // More sweeps needed
136                num_repetitions: 20,
137                use_parallel_tempering: true,
138                custom_params: [("constraint_weight".to_string(), 10.0)].into(),
139            },
140        );
141
142        // MaxCut: Benefits from aggressive cooling
143        self.templates.insert(
144            ProblemType::MaxCut,
145            ScheduleTemplate {
146                initial_temp_factor: 1.5,
147                final_temp_factor: 0.01,
148                temp_schedule: TemperatureSchedule::Exponential(4.0),
149                initial_field: 2.0,
150                field_schedule: TransverseFieldSchedule::Linear,
151                sweeps_factor: 50.0,
152                num_repetitions: 10,
153                use_parallel_tempering: false,
154                custom_params: HashMap::new(),
155            },
156        );
157
158        // Graph Coloring: Needs exploration of discrete states
159        self.templates.insert(
160            ProblemType::GraphColoring,
161            ScheduleTemplate {
162                initial_temp_factor: 3.0,
163                final_temp_factor: 0.1, // Higher final temperature
164                temp_schedule: TemperatureSchedule::Geometric(0.98, 5.0),
165                initial_field: 2.5,
166                field_schedule: TransverseFieldSchedule::Custom(|t, t_f| {
167                    // Step-like decrease to maintain quantum fluctuations
168                    if t < 0.7 * t_f {
169                        2.5
170                    } else {
171                        0.5
172                    }
173                }),
174                sweeps_factor: 150.0,
175                num_repetitions: 15,
176                use_parallel_tempering: true,
177                custom_params: HashMap::new(),
178            },
179        );
180
181        // Number Partitioning: Highly frustrated, needs careful approach
182        self.templates.insert(
183            ProblemType::NumberPartitioning,
184            ScheduleTemplate {
185                initial_temp_factor: 10.0, // Very high initial temperature
186                final_temp_factor: 0.001,
187                temp_schedule: TemperatureSchedule::Custom(|t, t_f| {
188                    // Two-stage cooling
189                    let progress = t / t_f;
190                    if progress < 0.5 {
191                        10.0 * (2.0 * progress).mul_add(-0.8, 1.0)
192                    } else {
193                        2.0 * 2.0f64.mul_add(-progress, 2.0).powi(2)
194                    }
195                }),
196                initial_field: 4.0,
197                field_schedule: TransverseFieldSchedule::Exponential(3.0),
198                sweeps_factor: 300.0,
199                num_repetitions: 30,
200                use_parallel_tempering: true,
201                custom_params: HashMap::new(),
202            },
203        );
204
205        // Portfolio Optimization: Continuous-like problem
206        self.templates.insert(
207            ProblemType::Portfolio,
208            ScheduleTemplate {
209                initial_temp_factor: 1.0,
210                final_temp_factor: 0.01,
211                temp_schedule: TemperatureSchedule::Linear,
212                initial_field: 1.5,
213                field_schedule: TransverseFieldSchedule::Linear,
214                sweeps_factor: 75.0,
215                num_repetitions: 10,
216                use_parallel_tempering: false,
217                custom_params: [("risk_weight".to_string(), 1.0)].into(),
218            },
219        );
220
221        // Spin Glass: Complex energy landscape
222        self.templates.insert(
223            ProblemType::SpinGlass,
224            ScheduleTemplate {
225                initial_temp_factor: 2.0,
226                final_temp_factor: 0.001,
227                temp_schedule: TemperatureSchedule::Exponential(2.5),
228                initial_field: 3.0,
229                field_schedule: TransverseFieldSchedule::Custom(|t, t_f| {
230                    // Non-monotonic schedule for spin glasses
231                    let progress = t / t_f;
232                    3.0 * (1.0 - progress) * 0.3f64.mul_add((10.0 * progress).sin(), 1.0)
233                }),
234                sweeps_factor: 200.0,
235                num_repetitions: 25,
236                use_parallel_tempering: true,
237                custom_params: HashMap::new(),
238            },
239        );
240    }
241
242    /// Create optimized annealing parameters for a specific problem type
243    pub fn create_schedule(
244        &mut self,
245        model: &IsingModel,
246        problem_type: ProblemType,
247    ) -> IsingResult<AnnealingParams> {
248        // Analyze the problem
249        self.analyzer.analyze(model)?;
250
251        // Get the template or use default
252        let template = self
253            .templates
254            .get(&problem_type)
255            .cloned()
256            .unwrap_or_else(ScheduleTemplate::default);
257
258        // Adapt template based on problem analysis
259        let adapted = self.adapt_template(&template, &self.analyzer);
260
261        // Create annealing parameters
262        Ok(self.template_to_params(&adapted, model.num_qubits))
263    }
264
265    /// Automatically detect problem type from model structure
266    pub fn detect_problem_type(&mut self, model: &IsingModel) -> IsingResult<ProblemType> {
267        self.analyzer.analyze(model)?;
268
269        // Simple heuristics for problem detection
270        let props = &self.analyzer.graph_properties;
271        let stats = &self.analyzer.coupling_stats;
272
273        // Check for specific patterns
274        if props.is_bipartite && stats.min_coupling < 0.0 && stats.max_coupling < 0.0 {
275            return Ok(ProblemType::MaxCut);
276        }
277
278        if props.avg_degree as f64 > 0.8 * props.num_variables as f64 {
279            // Dense problem, might be QAP or TSP
280            if stats.std_coupling > stats.mean_coupling.abs() * 2.0 {
281                return Ok(ProblemType::QAP);
282            }
283            return Ok(ProblemType::TSP);
284        }
285
286        if stats.sparsity < 0.1 && props.clustering_coefficient < 0.1 {
287            return Ok(ProblemType::NumberPartitioning);
288        }
289
290        // Default to spin glass for unknown patterns
291        Ok(ProblemType::SpinGlass)
292    }
293
294    /// Adapt template based on problem analysis
295    fn adapt_template(
296        &self,
297        template: &ScheduleTemplate,
298        analyzer: &ProblemAnalyzer,
299    ) -> ScheduleTemplate {
300        let mut adapted = template.clone();
301        let props = &analyzer.graph_properties;
302        let stats = &analyzer.coupling_stats;
303
304        // Scale initial temperature based on coupling strengths
305        let energy_scale = stats
306            .max_coupling
307            .abs()
308            .max(stats.mean_coupling.abs() * 3.0);
309        if energy_scale > 0.0 {
310            adapted.initial_temp_factor *= energy_scale;
311        }
312        // If no couplings, keep the original factor
313
314        // Adjust sweeps based on problem size and connectivity
315        let size_factor = (props.num_variables as f64).sqrt();
316        let connectivity_factor = (props.avg_degree / 4.0).max(1.0);
317        adapted.sweeps_factor *= size_factor * connectivity_factor;
318
319        // Enable parallel tempering for highly frustrated problems
320        if stats.std_coupling > stats.mean_coupling.abs() {
321            adapted.use_parallel_tempering = true;
322        }
323
324        adapted
325    }
326
327    /// Convert template to annealing parameters
328    fn template_to_params(
329        &self,
330        template: &ScheduleTemplate,
331        num_qubits: usize,
332    ) -> AnnealingParams {
333        let mut params = AnnealingParams::new();
334
335        // Calculate base temperature from problem characteristics
336        let coupling_scale = self.analyzer.coupling_stats.std_coupling.abs();
337        let base_temp = if coupling_scale > 0.0 {
338            coupling_scale
339        } else {
340            1.0
341        };
342
343        params.initial_temperature = template.initial_temp_factor * base_temp;
344        params.final_temperature = template.final_temp_factor * base_temp;
345        params.temperature_schedule = template.temp_schedule.clone();
346        params.initial_transverse_field = template.initial_field;
347        params.transverse_field_schedule = template.field_schedule.clone();
348        params.num_sweeps = (template.sweeps_factor * num_qubits as f64) as usize;
349        params.num_repetitions = template.num_repetitions;
350
351        // Set updates per sweep based on problem size
352        params.updates_per_sweep = Some(num_qubits * 10);
353
354        params
355    }
356
357    /// Add custom schedule template
358    pub fn add_custom_template(&mut self, name: String, template: ScheduleTemplate) {
359        self.templates.insert(ProblemType::Custom(name), template);
360    }
361}
362
363impl ProblemAnalyzer {
364    /// Create a new problem analyzer
365    fn new() -> Self {
366        Self {
367            coupling_stats: CouplingStatistics::default(),
368            graph_properties: GraphProperties::default(),
369        }
370    }
371
372    /// Analyze problem characteristics
373    fn analyze(&mut self, model: &IsingModel) -> IsingResult<()> {
374        // Reset statistics
375        self.coupling_stats = CouplingStatistics::default();
376        self.graph_properties = GraphProperties::default();
377
378        // Basic properties
379        self.graph_properties.num_variables = model.num_qubits;
380
381        // Analyze couplings
382        let couplings = model.couplings();
383        self.graph_properties.num_couplings = couplings.len();
384
385        if !couplings.is_empty() {
386            let strengths: Vec<f64> = couplings.iter().map(|c| c.strength).collect();
387
388            self.coupling_stats.mean_coupling =
389                strengths.iter().sum::<f64>() / strengths.len() as f64;
390            self.coupling_stats.max_coupling =
391                strengths.iter().copied().fold(f64::NEG_INFINITY, f64::max);
392            self.coupling_stats.min_coupling =
393                strengths.iter().copied().fold(f64::INFINITY, f64::min);
394
395            // Calculate standard deviation
396            let variance = strengths
397                .iter()
398                .map(|&x| (x - self.coupling_stats.mean_coupling).powi(2))
399                .sum::<f64>()
400                / strengths.len() as f64;
401            self.coupling_stats.std_coupling = variance.sqrt();
402        }
403
404        // Calculate graph properties
405        let mut degrees = vec![0; model.num_qubits];
406        for coupling in &couplings {
407            degrees[coupling.i] += 1;
408            degrees[coupling.j] += 1;
409        }
410
411        self.graph_properties.avg_degree =
412            degrees.iter().sum::<usize>() as f64 / model.num_qubits as f64;
413        self.graph_properties.max_degree = degrees.iter().copied().max().unwrap_or(0);
414
415        // Sparsity
416        let max_edges = model.num_qubits * (model.num_qubits - 1) / 2;
417        self.coupling_stats.sparsity = if max_edges > 0 {
418            couplings.len() as f64 / max_edges as f64
419        } else {
420            0.0
421        };
422
423        // Simple bipartite check (can be improved)
424        self.graph_properties.is_bipartite = self.check_bipartite(model, &couplings);
425
426        Ok(())
427    }
428
429    /// Check if the coupling graph is bipartite
430    fn check_bipartite(&self, model: &IsingModel, couplings: &[crate::ising::Coupling]) -> bool {
431        let mut colors = vec![None; model.num_qubits];
432        let mut queue = Vec::new();
433
434        // Try to 2-color the graph
435        for start in 0..model.num_qubits {
436            if colors[start].is_some() {
437                continue;
438            }
439
440            colors[start] = Some(0);
441            queue.push(start);
442
443            while let Some(node) = queue.pop() {
444                let node_color = colors[node].expect("node should have a color assigned");
445
446                for coupling in couplings {
447                    let neighbor = if coupling.i == node {
448                        coupling.j
449                    } else if coupling.j == node {
450                        coupling.i
451                    } else {
452                        continue;
453                    };
454
455                    match colors[neighbor] {
456                        None => {
457                            colors[neighbor] = Some(1 - node_color);
458                            queue.push(neighbor);
459                        }
460                        Some(color) if color == node_color => return false,
461                        _ => {}
462                    }
463                }
464            }
465        }
466
467        true
468    }
469}
470
471/// Schedule optimization based on runtime performance
472pub struct AdaptiveScheduleOptimizer {
473    /// History of schedule performance
474    performance_history: Vec<SchedulePerformance>,
475    /// Learning rate for schedule adaptation
476    learning_rate: f64,
477}
478
479#[derive(Debug, Clone)]
480struct SchedulePerformance {
481    problem_type: ProblemType,
482    schedule_params: HashMap<String, f64>,
483    final_energy: f64,
484    convergence_time: f64,
485    success_rate: f64,
486}
487
488impl AdaptiveScheduleOptimizer {
489    /// Create a new adaptive optimizer
490    #[must_use]
491    pub const fn new(learning_rate: f64) -> Self {
492        Self {
493            performance_history: Vec::new(),
494            learning_rate,
495        }
496    }
497
498    /// Record performance of a schedule
499    pub fn record_performance(
500        &mut self,
501        problem_type: ProblemType,
502        params: &AnnealingParams,
503        energy: f64,
504        time: f64,
505        success: bool,
506    ) {
507        let schedule_params = HashMap::from([
508            ("initial_temp".to_string(), params.initial_temperature),
509            ("initial_field".to_string(), params.initial_transverse_field),
510            ("num_sweeps".to_string(), params.num_sweeps as f64),
511        ]);
512
513        self.performance_history.push(SchedulePerformance {
514            problem_type,
515            schedule_params,
516            final_energy: energy,
517            convergence_time: time,
518            success_rate: if success { 1.0 } else { 0.0 },
519        });
520    }
521
522    /// Suggest improved schedule based on history
523    #[must_use]
524    pub fn suggest_schedule(
525        &self,
526        problem_type: ProblemType,
527        base_params: &AnnealingParams,
528    ) -> AnnealingParams {
529        let mut params = base_params.clone();
530
531        // Find similar successful runs
532        let similar_runs: Vec<_> = self
533            .performance_history
534            .iter()
535            .filter(|p| p.problem_type == problem_type && p.success_rate > 0.8)
536            .collect();
537
538        if !similar_runs.is_empty() {
539            // Average successful parameters
540            let avg_temp = similar_runs
541                .iter()
542                .filter_map(|p| p.schedule_params.get("initial_temp"))
543                .sum::<f64>()
544                / similar_runs.len() as f64;
545
546            let avg_field = similar_runs
547                .iter()
548                .filter_map(|p| p.schedule_params.get("initial_field"))
549                .sum::<f64>()
550                / similar_runs.len() as f64;
551
552            // Blend with current parameters
553            params.initial_temperature = params
554                .initial_temperature
555                .mul_add(1.0 - self.learning_rate, avg_temp * self.learning_rate);
556            params.initial_transverse_field = params
557                .initial_transverse_field
558                .mul_add(1.0 - self.learning_rate, avg_field * self.learning_rate);
559        }
560
561        params
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568
569    #[test]
570    fn test_schedule_creation() {
571        let mut scheduler = ProblemSpecificScheduler::new();
572        let model = IsingModel::new(10);
573
574        let params = scheduler
575            .create_schedule(&model, ProblemType::MaxCut)
576            .expect("failed to create schedule in test");
577        assert!(params.initial_temperature > 0.0);
578        assert!(params.num_sweeps > 0);
579    }
580
581    #[test]
582    fn test_problem_detection() {
583        let mut scheduler = ProblemSpecificScheduler::new();
584        let mut model = IsingModel::new(4);
585
586        // Create a simple bipartite structure
587        model
588            .set_coupling(0, 2, -1.0)
589            .expect("failed to set coupling in test");
590        model
591            .set_coupling(0, 3, -1.0)
592            .expect("failed to set coupling in test");
593        model
594            .set_coupling(1, 2, -1.0)
595            .expect("failed to set coupling in test");
596        model
597            .set_coupling(1, 3, -1.0)
598            .expect("failed to set coupling in test");
599
600        let detected = scheduler
601            .detect_problem_type(&model)
602            .expect("failed to detect problem type in test");
603        assert_eq!(detected, ProblemType::MaxCut);
604    }
605
606    #[test]
607    fn test_adaptive_optimizer() {
608        let mut optimizer = AdaptiveScheduleOptimizer::new(0.3);
609        let params = AnnealingParams::new();
610
611        // Record some performance data
612        optimizer.record_performance(ProblemType::TSP, &params, -100.0, 1.5, true);
613
614        // Get suggested parameters
615        let suggested = optimizer.suggest_schedule(ProblemType::TSP, &params);
616        assert!(suggested.initial_temperature > 0.0);
617    }
618}