1use crate::ising::{IsingModel, IsingResult};
8use crate::simulator::{AnnealingParams, TemperatureSchedule, TransverseFieldSchedule};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum ProblemType {
14 TSP,
16 MaxCut,
18 GraphColoring,
20 QAP,
22 NumberPartitioning,
24 SAT,
26 Portfolio,
28 JobShop,
30 SpinGlass,
32 Custom(String),
34}
35
36pub struct ProblemSpecificScheduler {
38 analyzer: ProblemAnalyzer,
40 templates: HashMap<ProblemType, ScheduleTemplate>,
42}
43
44#[derive(Debug, Clone)]
46pub struct ScheduleTemplate {
47 pub initial_temp_factor: f64,
49 pub final_temp_factor: f64,
51 pub temp_schedule: TemperatureSchedule,
53 pub initial_field: f64,
55 pub field_schedule: TransverseFieldSchedule,
57 pub sweeps_factor: f64,
59 pub num_repetitions: usize,
61 pub use_parallel_tempering: bool,
63 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
83pub struct ProblemAnalyzer {
85 coupling_stats: CouplingStatistics,
87 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 #[must_use]
113 pub fn new() -> Self {
114 let mut scheduler = Self {
115 analyzer: ProblemAnalyzer::new(),
116 templates: HashMap::new(),
117 };
118
119 scheduler.init_default_templates();
121 scheduler
122 }
123
124 fn init_default_templates(&mut self) {
126 self.templates.insert(
128 ProblemType::TSP,
129 ScheduleTemplate {
130 initial_temp_factor: 5.0, 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, num_repetitions: 20,
137 use_parallel_tempering: true,
138 custom_params: [("constraint_weight".to_string(), 10.0)].into(),
139 },
140 );
141
142 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 self.templates.insert(
160 ProblemType::GraphColoring,
161 ScheduleTemplate {
162 initial_temp_factor: 3.0,
163 final_temp_factor: 0.1, temp_schedule: TemperatureSchedule::Geometric(0.98, 5.0),
165 initial_field: 2.5,
166 field_schedule: TransverseFieldSchedule::Custom(|t, t_f| {
167 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 self.templates.insert(
183 ProblemType::NumberPartitioning,
184 ScheduleTemplate {
185 initial_temp_factor: 10.0, final_temp_factor: 0.001,
187 temp_schedule: TemperatureSchedule::Custom(|t, t_f| {
188 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 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 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 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 pub fn create_schedule(
244 &mut self,
245 model: &IsingModel,
246 problem_type: ProblemType,
247 ) -> IsingResult<AnnealingParams> {
248 self.analyzer.analyze(model)?;
250
251 let template = self
253 .templates
254 .get(&problem_type)
255 .cloned()
256 .unwrap_or_else(ScheduleTemplate::default);
257
258 let adapted = self.adapt_template(&template, &self.analyzer);
260
261 Ok(self.template_to_params(&adapted, model.num_qubits))
263 }
264
265 pub fn detect_problem_type(&mut self, model: &IsingModel) -> IsingResult<ProblemType> {
267 self.analyzer.analyze(model)?;
268
269 let props = &self.analyzer.graph_properties;
271 let stats = &self.analyzer.coupling_stats;
272
273 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 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 Ok(ProblemType::SpinGlass)
292 }
293
294 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 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 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 if stats.std_coupling > stats.mean_coupling.abs() {
321 adapted.use_parallel_tempering = true;
322 }
323
324 adapted
325 }
326
327 fn template_to_params(
329 &self,
330 template: &ScheduleTemplate,
331 num_qubits: usize,
332 ) -> AnnealingParams {
333 let mut params = AnnealingParams::new();
334
335 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 params.updates_per_sweep = Some(num_qubits * 10);
353
354 params
355 }
356
357 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 fn new() -> Self {
366 Self {
367 coupling_stats: CouplingStatistics::default(),
368 graph_properties: GraphProperties::default(),
369 }
370 }
371
372 fn analyze(&mut self, model: &IsingModel) -> IsingResult<()> {
374 self.coupling_stats = CouplingStatistics::default();
376 self.graph_properties = GraphProperties::default();
377
378 self.graph_properties.num_variables = model.num_qubits;
380
381 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 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 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 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 self.graph_properties.is_bipartite = self.check_bipartite(model, &couplings);
425
426 Ok(())
427 }
428
429 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 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
471pub struct AdaptiveScheduleOptimizer {
473 performance_history: Vec<SchedulePerformance>,
475 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 #[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 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 #[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 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 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 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 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 optimizer.record_performance(ProblemType::TSP, ¶ms, -100.0, 1.5, true);
613
614 let suggested = optimizer.suggest_schedule(ProblemType::TSP, ¶ms);
616 assert!(suggested.initial_temperature > 0.0);
617 }
618}