1use crate::sampler::{SampleResult, Sampler, SamplerError, SamplerResult};
22use quantrs2_anneal::QuboModel;
23use scirs2_core::ndarray::{Array1, Array2};
24use scirs2_core::random::prelude::*;
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
30pub enum PathInterpolation {
31 Linear,
33 Polynomial { exponent: f64 },
35 GapOptimized,
37 Custom,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AdiabaticPathConfig {
44 pub total_time: f64,
46 pub time_steps: usize,
48 pub interpolation: PathInterpolation,
50 pub dynamic_adjustment: bool,
52 pub gap_threshold: f64,
54 pub max_diabatic_probability: f64,
56 pub temperature: f64,
58 pub num_samples: usize,
60}
61
62impl Default for AdiabaticPathConfig {
63 fn default() -> Self {
64 Self {
65 total_time: 100.0,
66 time_steps: 1000,
67 interpolation: PathInterpolation::GapOptimized,
68 dynamic_adjustment: true,
69 gap_threshold: 0.1,
70 max_diabatic_probability: 0.01,
71 temperature: 0.0,
72 num_samples: 100,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79struct InstantaneousHamiltonian {
80 s: f64,
82 eigenvalues: Vec<f64>,
84 ground_energy: f64,
86 excited_energy: f64,
88 gap: f64,
90 diabatic_probability: f64,
92}
93
94#[derive(Debug, Clone)]
96pub struct QuantumAdiabaticPathOptimizer {
97 config: AdiabaticPathConfig,
98 time_schedule: Vec<f64>,
100 s_schedule: Vec<f64>,
102 gap_schedule: Vec<f64>,
104}
105
106impl QuantumAdiabaticPathOptimizer {
107 pub fn new(config: AdiabaticPathConfig) -> Self {
109 let mut optimizer = Self {
110 config,
111 time_schedule: Vec::new(),
112 s_schedule: Vec::new(),
113 gap_schedule: Vec::new(),
114 };
115 optimizer.initialize_schedule();
116 optimizer
117 }
118
119 fn initialize_schedule(&mut self) {
121 let n = self.config.time_steps;
122 self.time_schedule = (0..=n)
123 .map(|i| self.config.total_time * (i as f64) / (n as f64))
124 .collect();
125
126 match self.config.interpolation {
127 PathInterpolation::Linear => {
128 self.s_schedule = (0..=n).map(|i| (i as f64) / (n as f64)).collect();
129 }
130 PathInterpolation::Polynomial { exponent } => {
131 self.s_schedule = (0..=n)
132 .map(|i| ((i as f64) / (n as f64)).powf(exponent))
133 .collect();
134 }
135 PathInterpolation::GapOptimized | PathInterpolation::Custom => {
136 self.s_schedule = (0..=n).map(|i| (i as f64) / (n as f64)).collect();
138 }
139 }
140
141 self.gap_schedule = vec![0.0; n + 1];
142 }
143
144 fn compute_instantaneous_hamiltonian(
146 &self,
147 qubo: &Array2<f64>,
148 s: f64,
149 ) -> InstantaneousHamiltonian {
150 let n = qubo.nrows();
151
152 if n <= 10 {
162 self.exact_gap_computation(qubo, s)
164 } else {
165 self.estimated_gap_computation(qubo, s)
167 }
168 }
169
170 fn exact_gap_computation(&self, qubo: &Array2<f64>, s: f64) -> InstantaneousHamiltonian {
172 let n = qubo.nrows();
173 let dim = 1 << n; let mut hamiltonian = Array2::<f64>::zeros((dim, dim));
178
179 let transverse_strength = 1.0 - s;
181 for i in 0..dim {
182 for j in 0..dim {
183 if (i ^ j).is_power_of_two() {
185 hamiltonian[[i, j]] = -transverse_strength;
186 }
187 }
188 }
189
190 for i in 0..dim {
192 let mut energy = 0.0;
193 for bit_i in 0..n {
194 let val_i = if (i >> bit_i) & 1 == 1 { 1.0 } else { 0.0 };
195 energy += qubo[[bit_i, bit_i]] * val_i;
196 for bit_j in (bit_i + 1)..n {
197 let val_j = if (i >> bit_j) & 1 == 1 { 1.0 } else { 0.0 };
198 energy += qubo[[bit_i, bit_j]] * val_i * val_j;
199 }
200 }
201 hamiltonian[[i, i]] += s * energy;
202 }
203
204 let mut eigenvalues: Vec<f64> = (0..dim).map(|i| hamiltonian[[i, i]]).collect();
206 eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
207
208 let ground = eigenvalues[0];
209 let excited = if eigenvalues.len() > 1 {
210 eigenvalues[1]
211 } else {
212 ground
213 };
214 let gap = excited - ground;
215
216 let diabatic_prob = if gap > 1e-10 {
218 let velocity = 1.0 / self.config.total_time; (-std::f64::consts::PI * gap.powi(2) / (2.0 * velocity)).exp()
220 } else {
221 1.0
222 };
223
224 InstantaneousHamiltonian {
225 s,
226 eigenvalues,
227 ground_energy: ground,
228 excited_energy: excited,
229 gap,
230 diabatic_probability: diabatic_prob,
231 }
232 }
233
234 fn estimated_gap_computation(&self, qubo: &Array2<f64>, s: f64) -> InstantaneousHamiltonian {
236 let n = qubo.nrows();
237
238 let num_samples = 1000.min(1 << n.min(20));
244 let mut rng = thread_rng();
245 let mut energies = Vec::with_capacity(num_samples);
246
247 for _ in 0..num_samples {
248 let state: Vec<f64> = (0..n)
249 .map(|_| if rng.gen_bool(0.5) { 1.0 } else { 0.0 })
250 .collect();
251 let energy = self.compute_state_energy(qubo, &state);
252 energies.push(energy);
253 }
254
255 energies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
256
257 let ground = energies[0];
258 let excited = if energies.len() > 1 {
259 energies[1]
260 } else {
261 ground
262 };
263
264 let transverse_contribution = (1.0 - s) * (n as f64).sqrt();
266 let problem_gap = excited - ground;
267 let gap = (problem_gap * s + transverse_contribution * (1.0 - s)).max(0.01);
268
269 let velocity = 1.0 / self.config.total_time;
270 let diabatic_prob = (-std::f64::consts::PI * gap.powi(2) / (2.0 * velocity)).exp();
271
272 InstantaneousHamiltonian {
273 s,
274 eigenvalues: energies,
275 ground_energy: ground,
276 excited_energy: excited,
277 gap,
278 diabatic_probability: diabatic_prob,
279 }
280 }
281
282 fn compute_state_energy(&self, qubo: &Array2<f64>, state: &[f64]) -> f64 {
284 let n = state.len();
285 let mut energy = 0.0;
286
287 for i in 0..n {
288 energy += qubo[[i, i]] * state[i];
289 for j in (i + 1)..n {
290 energy += qubo[[i, j]] * state[i] * state[j];
291 }
292 }
293
294 energy
295 }
296
297 pub fn optimize_path(&mut self, qubo: &Array2<f64>) -> Result<(), String> {
299 if !self.config.dynamic_adjustment {
300 return Ok(());
301 }
302
303 println!("Optimizing adiabatic path...");
304
305 let mut gaps = Vec::new();
307 for &s in &self.s_schedule {
308 let h_info = self.compute_instantaneous_hamiltonian(qubo, s);
309 gaps.push(h_info.gap);
310 }
311
312 let avg_gap = gaps.iter().sum::<f64>() / gaps.len() as f64;
314 let min_gap = gaps.iter().copied().fold(f64::INFINITY, f64::min);
315
316 println!("Average gap: {avg_gap:.4}, Min gap: {min_gap:.4}");
317
318 let mut new_schedule = Vec::new();
320 let mut cumulative_time = 0.0;
321
322 for i in 0..gaps.len() - 1 {
323 let gap = gaps[i];
324 let weight = if gap < self.config.gap_threshold {
326 1.0 / gap.max(0.001)
327 } else {
328 1.0
329 };
330
331 cumulative_time += weight;
332 new_schedule.push((self.s_schedule[i], cumulative_time));
333 }
334
335 let total_weight = cumulative_time;
337 for (_, time) in &mut new_schedule {
338 *time = *time * self.config.total_time / total_weight;
339 }
340
341 self.time_schedule = new_schedule.iter().map(|(_, t)| *t).collect();
343 self.s_schedule = new_schedule.iter().map(|(s, _)| *s).collect();
344 self.gap_schedule = gaps;
345
346 println!("Path optimization complete");
347 Ok(())
348 }
349
350 pub fn run_adiabatic_evolution(
352 &self,
353 qubo: &Array2<f64>,
354 ) -> Result<Vec<HashMap<String, i32>>, String> {
355 let n = qubo.nrows();
356 let mut rng = thread_rng();
357 let mut samples = Vec::new();
358
359 println!("Running adiabatic evolution...");
360
361 for sample_idx in 0..self.config.num_samples {
362 let mut state: Vec<f64> = (0..n)
364 .map(|_| if rng.gen_bool(0.5) { 1.0 } else { 0.0 })
365 .collect();
366
367 for step_idx in 0..self.s_schedule.len() - 1 {
369 let s = self.s_schedule[step_idx];
370 let gap = self.gap_schedule[step_idx];
371
372 let diabatic_prob = if gap > 1e-10 {
374 let ds = self.s_schedule[step_idx + 1] - s;
375 (-std::f64::consts::PI * gap.powi(2) / (2.0 * ds)).exp()
376 } else {
377 0.5
378 };
379
380 if rng.gen_bool(diabatic_prob) {
382 let flip_idx = rng.gen_range(0..n);
384 state[flip_idx] = 1.0 - state[flip_idx];
385 }
386
387 if rng.gen_bool(0.1) {
389 state = self.local_optimization(qubo, &state, s);
390 }
391 }
392
393 let mut result = HashMap::new();
395 for (i, &val) in state.iter().enumerate() {
396 result.insert(format!("x{i}"), i32::from(val > 0.5));
397 }
398 samples.push(result);
399
400 if (sample_idx + 1) % 10 == 0 {
401 println!(
402 "Generated {}/{} samples",
403 sample_idx + 1,
404 self.config.num_samples
405 );
406 }
407 }
408
409 Ok(samples)
410 }
411
412 fn local_optimization(&self, qubo: &Array2<f64>, state: &[f64], s: f64) -> Vec<f64> {
414 let n = state.len();
415 let mut best_state = state.to_vec();
416 let mut best_energy = self.compute_state_energy(qubo, state);
417 let mut rng = thread_rng();
418
419 let temp = self.config.temperature.max(0.1) * (1.0 - s);
421
422 for _ in 0..n {
423 let flip_idx = rng.gen_range(0..n);
424 let mut new_state = best_state.clone();
425 new_state[flip_idx] = 1.0 - new_state[flip_idx];
426
427 let new_energy = self.compute_state_energy(qubo, &new_state);
428 let delta = new_energy - best_energy;
429
430 if delta < 0.0 || rng.gen_bool((-delta / temp).exp()) {
432 best_state = new_state;
433 best_energy = new_energy;
434 }
435 }
436
437 best_state
438 }
439
440 pub fn get_path_diagnostics(&self) -> PathDiagnostics {
442 PathDiagnostics {
443 total_time: self.config.total_time,
444 num_steps: self.config.time_steps,
445 min_gap: self
446 .gap_schedule
447 .iter()
448 .copied()
449 .fold(f64::INFINITY, f64::min),
450 max_gap: self
451 .gap_schedule
452 .iter()
453 .copied()
454 .fold(f64::NEG_INFINITY, f64::max),
455 avg_gap: self.gap_schedule.iter().sum::<f64>() / self.gap_schedule.len() as f64,
456 gap_variance: {
457 let mean = self.gap_schedule.iter().sum::<f64>() / self.gap_schedule.len() as f64;
458 self.gap_schedule
459 .iter()
460 .map(|g| (g - mean).powi(2))
461 .sum::<f64>()
462 / self.gap_schedule.len() as f64
463 },
464 }
465 }
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct PathDiagnostics {
471 pub total_time: f64,
472 pub num_steps: usize,
473 pub min_gap: f64,
474 pub max_gap: f64,
475 pub avg_gap: f64,
476 pub gap_variance: f64,
477}
478
479#[derive(Debug, Clone)]
481pub struct QuantumAdiabaticSampler {
482 config: AdiabaticPathConfig,
483}
484
485impl QuantumAdiabaticSampler {
486 pub const fn new(config: AdiabaticPathConfig) -> Self {
488 Self { config }
489 }
490}
491
492impl Sampler for QuantumAdiabaticSampler {
493 fn run_qubo(
494 &self,
495 problem: &(Array2<f64>, HashMap<String, usize>),
496 shots: usize,
497 ) -> SamplerResult<Vec<SampleResult>> {
498 let (matrix, _var_map) = problem;
499
500 let mut optimizer = QuantumAdiabaticPathOptimizer::new(self.config.clone());
502
503 optimizer
505 .optimize_path(matrix)
506 .map_err(SamplerError::InvalidParameter)?;
507
508 let samples = optimizer
510 .run_adiabatic_evolution(matrix)
511 .map_err(SamplerError::InvalidParameter)?;
512
513 let results: Vec<SampleResult> = samples
515 .into_iter()
516 .take(shots)
517 .map(|sample| {
518 let energy = self.compute_sample_energy(&sample, matrix);
519 let assignments = sample.into_iter().map(|(k, v)| (k, v != 0)).collect();
520 SampleResult {
521 assignments,
522 energy,
523 occurrences: 1,
524 }
525 })
526 .collect();
527
528 Ok(results)
529 }
530
531 fn run_hobo(
532 &self,
533 _problem: &(scirs2_core::ndarray::ArrayD<f64>, HashMap<String, usize>),
534 _shots: usize,
535 ) -> SamplerResult<Vec<SampleResult>> {
536 Err(SamplerError::UnsupportedOperation(
537 "HOBO sampling not yet implemented for adiabatic sampler".to_string(),
538 ))
539 }
540}
541
542impl QuantumAdiabaticSampler {
543 fn compute_sample_energy(&self, sample: &HashMap<String, i32>, qubo: &Array2<f64>) -> f64 {
545 let n = qubo.nrows();
546 let mut energy = 0.0;
547
548 for i in 0..n {
549 let x_i = sample.get(&format!("x{i}")).copied().unwrap_or(0) as f64;
550 energy += qubo[[i, i]] * x_i;
551 for j in (i + 1)..n {
552 let x_j = sample.get(&format!("x{j}")).copied().unwrap_or(0) as f64;
553 energy += qubo[[i, j]] * x_i * x_j;
554 }
555 }
556
557 energy
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn test_adiabatic_path_creation() {
567 let config = AdiabaticPathConfig::default();
568 let optimizer = QuantumAdiabaticPathOptimizer::new(config);
569 assert_eq!(optimizer.s_schedule.len(), 1001); }
571
572 #[test]
573 fn test_linear_interpolation() {
574 let config = AdiabaticPathConfig {
575 interpolation: PathInterpolation::Linear,
576 time_steps: 10,
577 ..Default::default()
578 };
579 let optimizer = QuantumAdiabaticPathOptimizer::new(config);
580 assert!((optimizer.s_schedule[0] - 0.0).abs() < 1e-10);
581 assert!((optimizer.s_schedule[10] - 1.0).abs() < 1e-10);
582 assert!((optimizer.s_schedule[5] - 0.5).abs() < 1e-10);
583 }
584
585 #[test]
586 fn test_polynomial_interpolation() {
587 let config = AdiabaticPathConfig {
588 interpolation: PathInterpolation::Polynomial { exponent: 2.0 },
589 time_steps: 10,
590 ..Default::default()
591 };
592 let optimizer = QuantumAdiabaticPathOptimizer::new(config);
593 assert!((optimizer.s_schedule[5] - 0.25).abs() < 1e-10); }
595
596 #[test]
597 fn test_small_qubo_evolution() {
598 let config = AdiabaticPathConfig {
599 time_steps: 100,
600 num_samples: 10,
601 ..Default::default()
602 };
603
604 let mut qubo = Array2::zeros((2, 2));
606 qubo[[0, 0]] = -1.0;
607 qubo[[1, 1]] = -1.0;
608 qubo[[0, 1]] = 2.0;
609
610 let optimizer = QuantumAdiabaticPathOptimizer::new(config);
611 let samples = optimizer
612 .run_adiabatic_evolution(&qubo)
613 .expect("Failed to run adiabatic evolution");
614 assert_eq!(samples.len(), 10);
615 }
616
617 #[test]
618 fn test_gap_computation() {
619 let config = AdiabaticPathConfig::default();
620 let optimizer = QuantumAdiabaticPathOptimizer::new(config);
621
622 let mut qubo = Array2::zeros((3, 3));
623 qubo[[0, 0]] = -1.0;
624 qubo[[1, 1]] = -1.0;
625 qubo[[2, 2]] = -1.0;
626
627 let h_info = optimizer.compute_instantaneous_hamiltonian(&qubo, 0.5);
628 assert!(h_info.gap > 0.0);
629 assert!(h_info.diabatic_probability >= 0.0 && h_info.diabatic_probability <= 1.0);
630 }
631}