use crate::sampler::{SampleResult, Sampler, SamplerError, SamplerResult};
use quantrs2_anneal::QuboModel;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum PathInterpolation {
Linear,
Polynomial { exponent: f64 },
GapOptimized,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdiabaticPathConfig {
pub total_time: f64,
pub time_steps: usize,
pub interpolation: PathInterpolation,
pub dynamic_adjustment: bool,
pub gap_threshold: f64,
pub max_diabatic_probability: f64,
pub temperature: f64,
pub num_samples: usize,
}
impl Default for AdiabaticPathConfig {
fn default() -> Self {
Self {
total_time: 100.0,
time_steps: 1000,
interpolation: PathInterpolation::GapOptimized,
dynamic_adjustment: true,
gap_threshold: 0.1,
max_diabatic_probability: 0.01,
temperature: 0.0,
num_samples: 100,
}
}
}
#[derive(Debug, Clone)]
struct InstantaneousHamiltonian {
s: f64,
eigenvalues: Vec<f64>,
ground_energy: f64,
excited_energy: f64,
gap: f64,
diabatic_probability: f64,
}
#[derive(Debug, Clone)]
pub struct QuantumAdiabaticPathOptimizer {
config: AdiabaticPathConfig,
time_schedule: Vec<f64>,
s_schedule: Vec<f64>,
gap_schedule: Vec<f64>,
}
impl QuantumAdiabaticPathOptimizer {
pub fn new(config: AdiabaticPathConfig) -> Self {
let mut optimizer = Self {
config,
time_schedule: Vec::new(),
s_schedule: Vec::new(),
gap_schedule: Vec::new(),
};
optimizer.initialize_schedule();
optimizer
}
fn initialize_schedule(&mut self) {
let n = self.config.time_steps;
self.time_schedule = (0..=n)
.map(|i| self.config.total_time * (i as f64) / (n as f64))
.collect();
match self.config.interpolation {
PathInterpolation::Linear => {
self.s_schedule = (0..=n).map(|i| (i as f64) / (n as f64)).collect();
}
PathInterpolation::Polynomial { exponent } => {
self.s_schedule = (0..=n)
.map(|i| ((i as f64) / (n as f64)).powf(exponent))
.collect();
}
PathInterpolation::GapOptimized | PathInterpolation::Custom => {
self.s_schedule = (0..=n).map(|i| (i as f64) / (n as f64)).collect();
}
}
self.gap_schedule = vec![0.0; n + 1];
}
fn compute_instantaneous_hamiltonian(
&self,
qubo: &Array2<f64>,
s: f64,
) -> InstantaneousHamiltonian {
let n = qubo.nrows();
if n <= 10 {
self.exact_gap_computation(qubo, s)
} else {
self.estimated_gap_computation(qubo, s)
}
}
fn exact_gap_computation(&self, qubo: &Array2<f64>, s: f64) -> InstantaneousHamiltonian {
let n = qubo.nrows();
let dim = 1 << n;
let mut hamiltonian = Array2::<f64>::zeros((dim, dim));
let transverse_strength = 1.0 - s;
for i in 0..dim {
for j in 0..dim {
if (i ^ j).is_power_of_two() {
hamiltonian[[i, j]] = -transverse_strength;
}
}
}
for i in 0..dim {
let mut energy = 0.0;
for bit_i in 0..n {
let val_i = if (i >> bit_i) & 1 == 1 { 1.0 } else { 0.0 };
energy += qubo[[bit_i, bit_i]] * val_i;
for bit_j in (bit_i + 1)..n {
let val_j = if (i >> bit_j) & 1 == 1 { 1.0 } else { 0.0 };
energy += qubo[[bit_i, bit_j]] * val_i * val_j;
}
}
hamiltonian[[i, i]] += s * energy;
}
let mut eigenvalues: Vec<f64> = (0..dim).map(|i| hamiltonian[[i, i]]).collect();
eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let ground = eigenvalues[0];
let excited = if eigenvalues.len() > 1 {
eigenvalues[1]
} else {
ground
};
let gap = excited - ground;
let diabatic_prob = if gap > 1e-10 {
let velocity = 1.0 / self.config.total_time; (-std::f64::consts::PI * gap.powi(2) / (2.0 * velocity)).exp()
} else {
1.0
};
InstantaneousHamiltonian {
s,
eigenvalues,
ground_energy: ground,
excited_energy: excited,
gap,
diabatic_probability: diabatic_prob,
}
}
fn estimated_gap_computation(&self, qubo: &Array2<f64>, s: f64) -> InstantaneousHamiltonian {
let n = qubo.nrows();
let num_samples = 1000.min(1 << n.min(20));
let mut rng = thread_rng();
let mut energies = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
let state: Vec<f64> = (0..n)
.map(|_| if rng.random_bool(0.5) { 1.0 } else { 0.0 })
.collect();
let energy = self.compute_state_energy(qubo, &state);
energies.push(energy);
}
energies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let ground = energies[0];
let excited = if energies.len() > 1 {
energies[1]
} else {
ground
};
let transverse_contribution = (1.0 - s) * (n as f64).sqrt();
let problem_gap = excited - ground;
let gap = (problem_gap * s + transverse_contribution * (1.0 - s)).max(0.01);
let velocity = 1.0 / self.config.total_time;
let diabatic_prob = (-std::f64::consts::PI * gap.powi(2) / (2.0 * velocity)).exp();
InstantaneousHamiltonian {
s,
eigenvalues: energies,
ground_energy: ground,
excited_energy: excited,
gap,
diabatic_probability: diabatic_prob,
}
}
fn compute_state_energy(&self, qubo: &Array2<f64>, state: &[f64]) -> f64 {
let n = state.len();
let mut energy = 0.0;
for i in 0..n {
energy += qubo[[i, i]] * state[i];
for j in (i + 1)..n {
energy += qubo[[i, j]] * state[i] * state[j];
}
}
energy
}
pub fn optimize_path(&mut self, qubo: &Array2<f64>) -> Result<(), String> {
if !self.config.dynamic_adjustment {
return Ok(());
}
println!("Optimizing adiabatic path...");
let mut gaps = Vec::new();
for &s in &self.s_schedule {
let h_info = self.compute_instantaneous_hamiltonian(qubo, s);
gaps.push(h_info.gap);
}
let avg_gap = gaps.iter().sum::<f64>() / gaps.len() as f64;
let min_gap = gaps.iter().copied().fold(f64::INFINITY, f64::min);
println!("Average gap: {avg_gap:.4}, Min gap: {min_gap:.4}");
let mut new_schedule = Vec::new();
let mut cumulative_time = 0.0;
for i in 0..gaps.len() - 1 {
let gap = gaps[i];
let weight = if gap < self.config.gap_threshold {
1.0 / gap.max(0.001)
} else {
1.0
};
cumulative_time += weight;
new_schedule.push((self.s_schedule[i], cumulative_time));
}
let total_weight = cumulative_time;
for (_, time) in &mut new_schedule {
*time = *time * self.config.total_time / total_weight;
}
self.time_schedule = new_schedule.iter().map(|(_, t)| *t).collect();
self.s_schedule = new_schedule.iter().map(|(s, _)| *s).collect();
self.gap_schedule = gaps;
println!("Path optimization complete");
Ok(())
}
pub fn run_adiabatic_evolution(
&self,
qubo: &Array2<f64>,
) -> Result<Vec<HashMap<String, i32>>, String> {
let n = qubo.nrows();
let mut rng = thread_rng();
let mut samples = Vec::new();
println!("Running adiabatic evolution...");
for sample_idx in 0..self.config.num_samples {
let mut state: Vec<f64> = (0..n)
.map(|_| if rng.random_bool(0.5) { 1.0 } else { 0.0 })
.collect();
for step_idx in 0..self.s_schedule.len() - 1 {
let s = self.s_schedule[step_idx];
let gap = self.gap_schedule[step_idx];
let diabatic_prob = if gap > 1e-10 {
let ds = self.s_schedule[step_idx + 1] - s;
(-std::f64::consts::PI * gap.powi(2) / (2.0 * ds)).exp()
} else {
0.5
};
if rng.random_bool(diabatic_prob) {
let flip_idx = rng.random_range(0..n);
state[flip_idx] = 1.0 - state[flip_idx];
}
if rng.random_bool(0.1) {
state = self.local_optimization(qubo, &state, s);
}
}
let mut result = HashMap::new();
for (i, &val) in state.iter().enumerate() {
result.insert(format!("x{i}"), i32::from(val > 0.5));
}
samples.push(result);
if (sample_idx + 1) % 10 == 0 {
println!(
"Generated {}/{} samples",
sample_idx + 1,
self.config.num_samples
);
}
}
Ok(samples)
}
fn local_optimization(&self, qubo: &Array2<f64>, state: &[f64], s: f64) -> Vec<f64> {
let n = state.len();
let mut best_state = state.to_vec();
let mut best_energy = self.compute_state_energy(qubo, state);
let mut rng = thread_rng();
let temp = self.config.temperature.max(0.1) * (1.0 - s);
for _ in 0..n {
let flip_idx = rng.random_range(0..n);
let mut new_state = best_state.clone();
new_state[flip_idx] = 1.0 - new_state[flip_idx];
let new_energy = self.compute_state_energy(qubo, &new_state);
let delta = new_energy - best_energy;
if delta < 0.0 || rng.random_bool((-delta / temp).exp()) {
best_state = new_state;
best_energy = new_energy;
}
}
best_state
}
pub fn get_path_diagnostics(&self) -> PathDiagnostics {
PathDiagnostics {
total_time: self.config.total_time,
num_steps: self.config.time_steps,
min_gap: self
.gap_schedule
.iter()
.copied()
.fold(f64::INFINITY, f64::min),
max_gap: self
.gap_schedule
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max),
avg_gap: self.gap_schedule.iter().sum::<f64>() / self.gap_schedule.len() as f64,
gap_variance: {
let mean = self.gap_schedule.iter().sum::<f64>() / self.gap_schedule.len() as f64;
self.gap_schedule
.iter()
.map(|g| (g - mean).powi(2))
.sum::<f64>()
/ self.gap_schedule.len() as f64
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathDiagnostics {
pub total_time: f64,
pub num_steps: usize,
pub min_gap: f64,
pub max_gap: f64,
pub avg_gap: f64,
pub gap_variance: f64,
}
#[derive(Debug, Clone)]
pub struct QuantumAdiabaticSampler {
config: AdiabaticPathConfig,
}
impl QuantumAdiabaticSampler {
pub const fn new(config: AdiabaticPathConfig) -> Self {
Self { config }
}
}
impl Sampler for QuantumAdiabaticSampler {
fn run_qubo(
&self,
problem: &(Array2<f64>, HashMap<String, usize>),
shots: usize,
) -> SamplerResult<Vec<SampleResult>> {
let (matrix, _var_map) = problem;
let mut optimizer = QuantumAdiabaticPathOptimizer::new(self.config.clone());
optimizer
.optimize_path(matrix)
.map_err(SamplerError::InvalidParameter)?;
let samples = optimizer
.run_adiabatic_evolution(matrix)
.map_err(SamplerError::InvalidParameter)?;
let results: Vec<SampleResult> = samples
.into_iter()
.take(shots)
.map(|sample| {
let energy = self.compute_sample_energy(&sample, matrix);
let assignments = sample.into_iter().map(|(k, v)| (k, v != 0)).collect();
SampleResult {
assignments,
energy,
occurrences: 1,
}
})
.collect();
Ok(results)
}
fn run_hobo(
&self,
_problem: &(scirs2_core::ndarray::ArrayD<f64>, HashMap<String, usize>),
_shots: usize,
) -> SamplerResult<Vec<SampleResult>> {
Err(SamplerError::UnsupportedOperation(
"HOBO sampling not yet implemented for adiabatic sampler".to_string(),
))
}
}
impl QuantumAdiabaticSampler {
fn compute_sample_energy(&self, sample: &HashMap<String, i32>, qubo: &Array2<f64>) -> f64 {
let n = qubo.nrows();
let mut energy = 0.0;
for i in 0..n {
let x_i = sample.get(&format!("x{i}")).copied().unwrap_or(0) as f64;
energy += qubo[[i, i]] * x_i;
for j in (i + 1)..n {
let x_j = sample.get(&format!("x{j}")).copied().unwrap_or(0) as f64;
energy += qubo[[i, j]] * x_i * x_j;
}
}
energy
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adiabatic_path_creation() {
let config = AdiabaticPathConfig::default();
let optimizer = QuantumAdiabaticPathOptimizer::new(config);
assert_eq!(optimizer.s_schedule.len(), 1001); }
#[test]
fn test_linear_interpolation() {
let config = AdiabaticPathConfig {
interpolation: PathInterpolation::Linear,
time_steps: 10,
..Default::default()
};
let optimizer = QuantumAdiabaticPathOptimizer::new(config);
assert!((optimizer.s_schedule[0] - 0.0).abs() < 1e-10);
assert!((optimizer.s_schedule[10] - 1.0).abs() < 1e-10);
assert!((optimizer.s_schedule[5] - 0.5).abs() < 1e-10);
}
#[test]
fn test_polynomial_interpolation() {
let config = AdiabaticPathConfig {
interpolation: PathInterpolation::Polynomial { exponent: 2.0 },
time_steps: 10,
..Default::default()
};
let optimizer = QuantumAdiabaticPathOptimizer::new(config);
assert!((optimizer.s_schedule[5] - 0.25).abs() < 1e-10); }
#[test]
fn test_small_qubo_evolution() {
let config = AdiabaticPathConfig {
time_steps: 100,
num_samples: 10,
..Default::default()
};
let mut qubo = Array2::zeros((2, 2));
qubo[[0, 0]] = -1.0;
qubo[[1, 1]] = -1.0;
qubo[[0, 1]] = 2.0;
let optimizer = QuantumAdiabaticPathOptimizer::new(config);
let samples = optimizer
.run_adiabatic_evolution(&qubo)
.expect("Failed to run adiabatic evolution");
assert_eq!(samples.len(), 10);
}
#[test]
fn test_gap_computation() {
let config = AdiabaticPathConfig::default();
let optimizer = QuantumAdiabaticPathOptimizer::new(config);
let mut qubo = Array2::zeros((3, 3));
qubo[[0, 0]] = -1.0;
qubo[[1, 1]] = -1.0;
qubo[[2, 2]] = -1.0;
let h_info = optimizer.compute_instantaneous_hamiltonian(&qubo, 0.5);
assert!(h_info.gap > 0.0);
assert!(h_info.diabatic_probability >= 0.0 && h_info.diabatic_probability <= 1.0);
}
}