use crate::prelude::SimulatorError;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::parallel_ops::{IndexedParallelIterator, ParallelIterator};
use scirs2_core::Complex64;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::device_noise_models::{DeviceNoiseSimulator, DeviceTopology};
use crate::error::Result;
use crate::scirs2_integration::SciRS2Backend;
#[derive(Debug, Clone)]
pub struct QuantumAnnealingConfig {
pub annealing_time: f64,
pub time_steps: usize,
pub schedule_type: AnnealingScheduleType,
pub problem_type: ProblemType,
pub topology: AnnealingTopology,
pub temperature: f64,
pub enable_noise: bool,
pub enable_thermal_fluctuations: bool,
pub enable_control_errors: bool,
pub enable_gauge_transformations: bool,
pub post_processing: PostProcessingConfig,
}
impl Default for QuantumAnnealingConfig {
fn default() -> Self {
Self {
annealing_time: 20.0, time_steps: 2000,
schedule_type: AnnealingScheduleType::DWave,
problem_type: ProblemType::Ising,
topology: AnnealingTopology::Chimera(16),
temperature: 0.015, enable_noise: true,
enable_thermal_fluctuations: true,
enable_control_errors: true,
enable_gauge_transformations: true,
post_processing: PostProcessingConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnnealingScheduleType {
Linear,
DWave,
Optimized,
CustomPause {
pause_start: f64,
pause_duration: f64,
},
NonMonotonic,
Reverse { reinitialize_point: f64 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProblemType {
Ising,
QUBO,
MaxCut,
GraphColoring,
TSP,
NumberPartitioning,
Custom(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AnnealingTopology {
Chimera(usize), Pegasus(usize),
Zephyr(usize),
Complete(usize),
Custom(DeviceTopology),
}
#[derive(Debug, Clone)]
pub struct PostProcessingConfig {
pub enable_spin_reversal: bool,
pub enable_local_search: bool,
pub max_local_search_iterations: usize,
pub enable_majority_vote: bool,
pub majority_vote_reads: usize,
pub enable_energy_filtering: bool,
}
impl Default for PostProcessingConfig {
fn default() -> Self {
Self {
enable_spin_reversal: true,
enable_local_search: true,
max_local_search_iterations: 100,
enable_majority_vote: true,
majority_vote_reads: 1000,
enable_energy_filtering: true,
}
}
}
#[derive(Debug, Clone)]
pub struct IsingProblem {
pub num_spins: usize,
pub h: Array1<f64>,
pub j: Array2<f64>,
pub offset: f64,
pub metadata: ProblemMetadata,
}
#[derive(Debug, Clone)]
pub struct QUBOProblem {
pub num_variables: usize,
pub q: Array2<f64>,
pub linear: Array1<f64>,
pub offset: f64,
pub metadata: ProblemMetadata,
}
#[derive(Debug, Clone, Default)]
pub struct ProblemMetadata {
pub name: Option<String>,
pub description: Option<String>,
pub optimal_energy: Option<f64>,
pub difficulty_score: Option<f64>,
pub variable_labels: Vec<String>,
}
impl IsingProblem {
#[must_use]
pub fn new(num_spins: usize) -> Self {
Self {
num_spins,
h: Array1::zeros(num_spins),
j: Array2::zeros((num_spins, num_spins)),
offset: 0.0,
metadata: ProblemMetadata::default(),
}
}
pub fn set_h(&mut self, i: usize, value: f64) {
if i < self.num_spins {
self.h[i] = value;
}
}
pub fn set_j(&mut self, i: usize, j: usize, value: f64) {
if i < self.num_spins && j < self.num_spins {
self.j[[i, j]] = value;
self.j[[j, i]] = value; }
}
#[must_use]
pub fn calculate_energy(&self, configuration: &[i8]) -> f64 {
if configuration.len() != self.num_spins {
return f64::INFINITY;
}
let mut energy = self.offset;
for i in 0..self.num_spins {
energy += self.h[i] * f64::from(configuration[i]);
}
for i in 0..self.num_spins {
for j in i + 1..self.num_spins {
energy +=
self.j[[i, j]] * f64::from(configuration[i]) * f64::from(configuration[j]);
}
}
energy
}
#[must_use]
pub fn to_qubo(&self) -> QUBOProblem {
let num_vars = self.num_spins;
let mut q = Array2::zeros((num_vars, num_vars));
let mut linear = Array1::zeros(num_vars);
let mut offset = self.offset;
for i in 0..num_vars {
linear[i] += 2.0 * self.h[i];
offset -= self.h[i];
for j in i + 1..num_vars {
q[[i, j]] += 4.0 * self.j[[i, j]];
linear[i] -= 2.0 * self.j[[i, j]];
linear[j] -= 2.0 * self.j[[i, j]];
offset += self.j[[i, j]];
}
}
QUBOProblem {
num_variables: num_vars,
q,
linear,
offset,
metadata: self.metadata.clone(),
}
}
#[must_use]
pub fn find_ground_state_brute_force(&self) -> (Vec<i8>, f64) {
assert!(
(self.num_spins <= 20),
"Brute force search only supported for <= 20 spins"
);
let mut best_config = vec![-1; self.num_spins];
let mut best_energy = f64::INFINITY;
for state in 0..(1 << self.num_spins) {
let mut config = vec![-1; self.num_spins];
for i in 0..self.num_spins {
if (state >> i) & 1 == 1 {
config[i] = 1;
}
}
let energy = self.calculate_energy(&config);
if energy < best_energy {
best_energy = energy;
best_config = config;
}
}
(best_config, best_energy)
}
}
impl QUBOProblem {
#[must_use]
pub fn new(num_variables: usize) -> Self {
Self {
num_variables,
q: Array2::zeros((num_variables, num_variables)),
linear: Array1::zeros(num_variables),
offset: 0.0,
metadata: ProblemMetadata::default(),
}
}
#[must_use]
pub fn calculate_energy(&self, configuration: &[u8]) -> f64 {
if configuration.len() != self.num_variables {
return f64::INFINITY;
}
let mut energy = self.offset;
for i in 0..self.num_variables {
energy += self.linear[i] * f64::from(configuration[i]);
}
for i in 0..self.num_variables {
for j in 0..self.num_variables {
if i != j {
energy +=
self.q[[i, j]] * f64::from(configuration[i]) * f64::from(configuration[j]);
}
}
}
energy
}
#[must_use]
pub fn to_ising(&self) -> IsingProblem {
let num_spins = self.num_variables;
let mut h = Array1::zeros(num_spins);
let mut j = Array2::zeros((num_spins, num_spins));
let mut offset = self.offset;
for i in 0..num_spins {
h[i] = self.linear[i] / 2.0;
offset += self.linear[i] / 2.0;
for k in 0..num_spins {
if k != i {
h[i] += self.q[[i, k]] / 4.0;
offset += self.q[[i, k]] / 4.0;
}
}
}
for i in 0..num_spins {
for k in i + 1..num_spins {
j[[i, k]] = self.q[[i, k]] / 4.0;
}
}
IsingProblem {
num_spins,
h,
j,
offset,
metadata: self.metadata.clone(),
}
}
}
pub struct QuantumAnnealingSimulator {
config: QuantumAnnealingConfig,
current_problem: Option<IsingProblem>,
noise_simulator: Option<DeviceNoiseSimulator>,
backend: Option<SciRS2Backend>,
annealing_history: Vec<AnnealingSnapshot>,
solutions: Vec<AnnealingSolution>,
stats: AnnealingStats,
}
#[derive(Debug, Clone)]
pub struct AnnealingSnapshot {
pub time: f64,
pub s: f64,
pub transverse_field: f64,
pub longitudinal_field: f64,
pub quantum_state: Option<Array1<Complex64>>,
pub classical_probabilities: Option<Array1<f64>>,
pub energy_expectation: f64,
pub temperature_factor: f64,
}
#[derive(Debug, Clone)]
pub struct AnnealingSolution {
pub configuration: Vec<i8>,
pub energy: f64,
pub probability: f64,
pub num_occurrences: usize,
pub rank: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnnealingStats {
pub total_annealing_time_ms: f64,
pub num_annealing_runs: usize,
pub num_solutions_found: usize,
pub best_energy_found: f64,
pub success_probability: f64,
pub time_to_solution: TimeToSolutionStats,
pub noise_stats: NoiseStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TimeToSolutionStats {
pub median_tts: f64,
pub percentile_99_tts: f64,
pub success_rate: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NoiseStats {
pub thermal_excitations: usize,
pub control_errors: usize,
pub decoherence_events: usize,
}
impl QuantumAnnealingSimulator {
pub fn new(config: QuantumAnnealingConfig) -> Result<Self> {
Ok(Self {
config,
current_problem: None,
noise_simulator: None,
backend: None,
annealing_history: Vec::new(),
solutions: Vec::new(),
stats: AnnealingStats::default(),
})
}
pub fn with_backend(mut self) -> Result<Self> {
self.backend = Some(SciRS2Backend::new());
Ok(self)
}
pub fn set_problem(&mut self, problem: IsingProblem) -> Result<()> {
let max_spins = match &self.config.topology {
AnnealingTopology::Chimera(size) => size * size * 8,
AnnealingTopology::Pegasus(size) => size * (size - 1) * 12,
AnnealingTopology::Zephyr(size) => size * size * 8,
AnnealingTopology::Complete(size) => *size,
AnnealingTopology::Custom(topology) => topology.num_qubits,
};
if problem.num_spins > max_spins {
return Err(SimulatorError::InvalidInput(format!(
"Problem size {} exceeds topology limit {}",
problem.num_spins, max_spins
)));
}
self.current_problem = Some(problem);
Ok(())
}
pub fn anneal(&mut self, num_reads: usize) -> Result<AnnealingResult> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let start_time = std::time::Instant::now();
self.solutions.clear();
for read in 0..num_reads {
let read_start = std::time::Instant::now();
let solution = self.single_anneal(read)?;
self.solutions.push(solution);
if read % 100 == 0 {
println!(
"Completed read {}/{}, time={:.2}ms",
read,
num_reads,
read_start.elapsed().as_secs_f64() * 1000.0
);
}
}
if self.config.post_processing.enable_majority_vote {
self.apply_majority_vote_post_processing()?;
}
if self.config.post_processing.enable_local_search {
self.apply_local_search_post_processing()?;
}
self.solutions.sort_by(|a, b| {
a.energy
.partial_cmp(&b.energy)
.unwrap_or(std::cmp::Ordering::Equal)
});
for (rank, solution) in self.solutions.iter_mut().enumerate() {
solution.rank = rank;
}
self.compute_annealing_statistics()?;
let total_time = start_time.elapsed().as_secs_f64() * 1000.0;
self.stats.total_annealing_time_ms += total_time;
self.stats.num_annealing_runs += num_reads;
Ok(AnnealingResult {
solutions: self.solutions.clone(),
best_energy: self.solutions.first().map_or(f64::INFINITY, |s| s.energy),
annealing_history: self.annealing_history.clone(),
total_time_ms: total_time,
success_probability: self.stats.success_probability,
time_to_solution: self.stats.time_to_solution.clone(),
})
}
fn single_anneal(&mut self, _read_id: usize) -> Result<AnnealingSolution> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let problem_num_spins = problem.num_spins;
let state_size = 1 << problem_num_spins.min(20); let mut quantum_state = if problem_num_spins <= 20 {
let mut state = Array1::zeros(state_size);
let amplitude = (1.0 / state_size as f64).sqrt();
state.fill(Complex64::new(amplitude, 0.0));
Some(state)
} else {
None };
let dt = self.config.annealing_time / self.config.time_steps as f64;
self.annealing_history.clear();
for step in 0..=self.config.time_steps {
let t = step as f64 * dt;
let s = self.schedule_function(t);
let (transverse_field, longitudinal_field) = self.calculate_field_strengths(s);
if let Some(ref mut state) = quantum_state {
self.apply_quantum_evolution(state, transverse_field, longitudinal_field, dt)?;
if self.config.enable_noise {
self.apply_annealing_noise(state, dt)?;
}
}
if step % (self.config.time_steps / 100) == 0 {
let snapshot = self.take_annealing_snapshot(
t,
s,
transverse_field,
longitudinal_field,
quantum_state.as_ref(),
)?;
self.annealing_history.push(snapshot);
}
}
let final_configuration = if let Some(ref state) = quantum_state {
self.measure_final_state(state)?
} else {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
self.classical_sampling(problem)?
};
let energy = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?
.calculate_energy(&final_configuration);
Ok(AnnealingSolution {
configuration: final_configuration,
energy,
probability: 1.0 / (self.config.time_steps as f64), num_occurrences: 1,
rank: 0,
})
}
fn schedule_function(&self, t: f64) -> f64 {
let normalized_t = t / self.config.annealing_time;
match self.config.schedule_type {
AnnealingScheduleType::Linear => normalized_t,
AnnealingScheduleType::DWave => {
if normalized_t < 0.1 {
5.0 * normalized_t * normalized_t
} else if normalized_t < 0.9 {
0.05 + 0.9 * (normalized_t - 0.1) / 0.8
} else {
0.05f64.mul_add(
1.0 - (1.0 - normalized_t) * (1.0 - normalized_t) / 0.01,
0.95,
)
}
}
AnnealingScheduleType::Optimized => {
self.optimized_schedule(normalized_t)
}
AnnealingScheduleType::CustomPause {
pause_start,
pause_duration,
} => {
if normalized_t >= pause_start && normalized_t <= pause_start + pause_duration {
pause_start } else if normalized_t > pause_start + pause_duration {
(normalized_t - pause_duration - pause_start) / (1.0 - pause_duration)
} else {
normalized_t / pause_start
}
}
AnnealingScheduleType::NonMonotonic => {
(0.1 * (10.0 * std::f64::consts::PI * normalized_t).sin())
.mul_add(1.0 - normalized_t, normalized_t)
}
AnnealingScheduleType::Reverse { reinitialize_point } => {
if normalized_t < reinitialize_point {
1.0 } else {
1.0 - (normalized_t - reinitialize_point) / (1.0 - reinitialize_point)
}
}
}
}
fn optimized_schedule(&self, t: f64) -> f64 {
if t < 0.3 {
t * t / 0.09 * 0.3
} else if t < 0.7 {
0.3 + (t - 0.3) * 0.4 / 0.4
} else {
((t - 0.7) * (t - 0.7) / 0.09).mul_add(0.3, 0.7)
}
}
fn calculate_field_strengths(&self, s: f64) -> (f64, f64) {
let a_s = (1.0 - s) * 1.0; let b_s = s * 1.0; (a_s, b_s)
}
fn apply_quantum_evolution(
&self,
state: &mut Array1<Complex64>,
transverse_field: f64,
longitudinal_field: f64,
dt: f64,
) -> Result<()> {
let _problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let hamiltonian = self.build_annealing_hamiltonian(transverse_field, longitudinal_field)?;
let evolution_operator = self.compute_evolution_operator(&hamiltonian, dt)?;
*state = evolution_operator.dot(state);
let norm: f64 = state
.iter()
.map(scirs2_core::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
if norm > 1e-15 {
state.mapv_inplace(|x| x / norm);
}
Ok(())
}
fn build_annealing_hamiltonian(
&self,
transverse_field: f64,
longitudinal_field: f64,
) -> Result<Array2<Complex64>> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let num_spins = problem.num_spins;
let dim = 1 << num_spins;
let mut hamiltonian = Array2::zeros((dim, dim));
for spin in 0..num_spins {
let sigma_x = self.build_sigma_x(spin, num_spins);
hamiltonian = hamiltonian - sigma_x.mapv(|x| x * transverse_field);
}
let problem_hamiltonian = self.build_problem_hamiltonian()?;
hamiltonian = hamiltonian + problem_hamiltonian.mapv(|x| x * longitudinal_field);
Ok(hamiltonian)
}
fn build_sigma_x(&self, target_spin: usize, num_spins: usize) -> Array2<Complex64> {
let dim = 1 << num_spins;
let mut sigma_x = Array2::zeros((dim, dim));
for i in 0..dim {
let j = i ^ (1 << target_spin); sigma_x[[i, j]] = Complex64::new(1.0, 0.0);
}
sigma_x
}
fn build_problem_hamiltonian(&self) -> Result<Array2<Complex64>> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let num_spins = problem.num_spins;
let dim = 1 << num_spins;
let mut hamiltonian = Array2::zeros((dim, dim));
for i in 0..num_spins {
let sigma_z = self.build_sigma_z(i, num_spins);
hamiltonian = hamiltonian + sigma_z.mapv(|x| x * problem.h[i]);
}
for i in 0..num_spins {
for j in i + 1..num_spins {
if problem.j[[i, j]] != 0.0 {
let sigma_z_i = self.build_sigma_z(i, num_spins);
let sigma_z_j = self.build_sigma_z(j, num_spins);
let sigma_z_ij = sigma_z_i.dot(&sigma_z_j);
hamiltonian = hamiltonian + sigma_z_ij.mapv(|x| x * problem.j[[i, j]]);
}
}
}
for i in 0..dim {
hamiltonian[[i, i]] += Complex64::new(problem.offset, 0.0);
}
Ok(hamiltonian)
}
fn build_sigma_z(&self, target_spin: usize, num_spins: usize) -> Array2<Complex64> {
let dim = 1 << num_spins;
let mut sigma_z = Array2::zeros((dim, dim));
for i in 0..dim {
let sign = if (i >> target_spin) & 1 == 0 {
1.0
} else {
-1.0
};
sigma_z[[i, i]] = Complex64::new(sign, 0.0);
}
sigma_z
}
fn compute_evolution_operator(
&self,
hamiltonian: &Array2<Complex64>,
dt: f64,
) -> Result<Array2<Complex64>> {
self.matrix_exponential(hamiltonian, -Complex64::new(0.0, dt))
}
fn matrix_exponential(
&self,
matrix: &Array2<Complex64>,
factor: Complex64,
) -> Result<Array2<Complex64>> {
let dim = matrix.dim().0;
let scaled_matrix = matrix.mapv(|x| x * factor);
let mut result = Array2::eye(dim);
let mut term = Array2::eye(dim);
for n in 1..=15 {
term = term.dot(&scaled_matrix) / f64::from(n);
let term_norm: f64 = term
.iter()
.map(scirs2_core::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
result += &term;
if term_norm < 1e-12 {
break;
}
}
Ok(result)
}
fn apply_annealing_noise(&mut self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
if self.config.enable_thermal_fluctuations {
self.apply_thermal_noise(state, dt)?;
self.stats.noise_stats.thermal_excitations += 1;
}
if self.config.enable_control_errors {
self.apply_control_error_noise(state, dt)?;
self.stats.noise_stats.control_errors += 1;
}
self.apply_decoherence_noise(state, dt)?;
self.stats.noise_stats.decoherence_events += 1;
Ok(())
}
fn apply_thermal_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
let kb_t = 1.38e-23 * self.config.temperature; let thermal_energy = kb_t * dt * 1e6;
for amplitude in state.iter_mut() {
let thermal_phase = fastrand::f64() * thermal_energy * 2.0 * std::f64::consts::PI;
*amplitude *= Complex64::new(0.0, thermal_phase).exp();
}
Ok(())
}
fn apply_control_error_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
let error_strength = 0.01;
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
for spin in 0..problem.num_spins.min(10) {
if fastrand::f64() < error_strength * dt {
let error_angle = fastrand::f64() * 0.1; self.apply_single_spin_rotation(state, spin, error_angle)?;
}
}
Ok(())
}
fn apply_decoherence_noise(&self, state: &mut Array1<Complex64>, dt: f64) -> Result<()> {
let decoherence_rate = 1e-3; let decoherence_prob = decoherence_rate * dt;
for amplitude in state.iter_mut() {
if fastrand::f64() < decoherence_prob {
let phase = fastrand::f64() * 2.0 * std::f64::consts::PI;
*amplitude *= Complex64::new(0.0, phase).exp();
}
}
Ok(())
}
fn apply_single_spin_rotation(
&self,
state: &mut Array1<Complex64>,
spin: usize,
angle: f64,
) -> Result<()> {
let _problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let spin_mask = 1 << spin;
let cos_half = (angle / 2.0).cos();
let sin_half = (angle / 2.0).sin();
for i in 0..state.len() {
if i & spin_mask == 0 {
let j = i | spin_mask;
if j < state.len() {
let amp_0 = state[i];
let amp_1 = state[j];
state[i] = cos_half * amp_0 - Complex64::new(0.0, sin_half) * amp_1;
state[j] = cos_half * amp_1 - Complex64::new(0.0, sin_half) * amp_0;
}
}
}
Ok(())
}
fn take_annealing_snapshot(
&self,
time: f64,
s: f64,
transverse_field: f64,
longitudinal_field: f64,
quantum_state: Option<&Array1<Complex64>>,
) -> Result<AnnealingSnapshot> {
let energy_expectation = if let Some(state) = quantum_state {
self.calculate_energy_expectation(state)?
} else {
0.0
};
let temperature_factor = (-1.0 / (1.38e-23 * self.config.temperature)).exp();
Ok(AnnealingSnapshot {
time,
s,
transverse_field,
longitudinal_field,
quantum_state: quantum_state.cloned(),
classical_probabilities: None,
energy_expectation,
temperature_factor,
})
}
fn calculate_energy_expectation(&self, state: &Array1<Complex64>) -> Result<f64> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let mut expectation = 0.0;
for (i, &litude) in state.iter().enumerate() {
let prob = amplitude.norm_sqr();
let mut config = vec![-1; problem.num_spins];
for spin in 0..problem.num_spins {
if (i >> spin) & 1 == 1 {
config[spin] = 1;
}
}
let energy = problem.calculate_energy(&config);
expectation += prob * energy;
}
Ok(expectation)
}
fn measure_final_state(&self, state: &Array1<Complex64>) -> Result<Vec<i8>> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
let probabilities: Vec<f64> = state.iter().map(scirs2_core::Complex::norm_sqr).collect();
let random_val = fastrand::f64();
let mut cumulative_prob = 0.0;
for (i, &prob) in probabilities.iter().enumerate() {
cumulative_prob += prob;
if random_val < cumulative_prob {
let mut config = vec![-1; problem.num_spins];
for spin in 0..problem.num_spins {
if (i >> spin) & 1 == 1 {
config[spin] = 1;
}
}
return Ok(config);
}
}
Ok(vec![-1; problem.num_spins])
}
fn classical_sampling(&self, problem: &IsingProblem) -> Result<Vec<i8>> {
let mut config: Vec<i8> = (0..problem.num_spins)
.map(|_| if fastrand::f64() > 0.5 { 1 } else { -1 })
.collect();
for _ in 0..1000 {
let spin_to_flip = fastrand::usize(0..problem.num_spins);
let old_energy = problem.calculate_energy(&config);
config[spin_to_flip] *= -1;
let new_energy = problem.calculate_energy(&config);
if new_energy > old_energy {
config[spin_to_flip] *= -1; }
}
Ok(config)
}
fn apply_majority_vote_post_processing(&mut self) -> Result<()> {
if self.solutions.is_empty() {
return Ok(());
}
let mut config_groups: HashMap<Vec<i8>, Vec<usize>> = HashMap::new();
for (i, solution) in self.solutions.iter().enumerate() {
config_groups
.entry(solution.configuration.clone())
.or_default()
.push(i);
}
for (config, indices) in config_groups {
let num_occurrences = indices.len();
for &idx in &indices {
self.solutions[idx].num_occurrences = num_occurrences;
}
}
Ok(())
}
fn apply_local_search_post_processing(&mut self) -> Result<()> {
let problem = self
.current_problem
.as_ref()
.ok_or_else(|| SimulatorError::InvalidInput("No problem set".to_string()))?;
for solution in &mut self.solutions {
let mut improved_config = solution.configuration.clone();
let mut improved_energy = solution.energy;
for _ in 0..self.config.post_processing.max_local_search_iterations {
let mut found_improvement = false;
for spin in 0..problem.num_spins {
improved_config[spin] *= -1;
let new_energy = problem.calculate_energy(&improved_config);
if new_energy < improved_energy {
improved_energy = new_energy;
found_improvement = true;
break;
}
improved_config[spin] *= -1; }
if !found_improvement {
break;
}
}
if improved_energy < solution.energy {
solution.configuration = improved_config;
solution.energy = improved_energy;
}
}
Ok(())
}
fn compute_annealing_statistics(&mut self) -> Result<()> {
if self.solutions.is_empty() {
return Ok(());
}
self.stats.num_solutions_found = self.solutions.len();
self.stats.best_energy_found = self
.solutions
.iter()
.map(|s| s.energy)
.fold(f64::INFINITY, f64::min);
if let Some(optimal_energy) = self
.current_problem
.as_ref()
.and_then(|p| p.metadata.optimal_energy)
{
let tolerance = 1e-6;
let successful_solutions = self
.solutions
.iter()
.filter(|s| (s.energy - optimal_energy).abs() < tolerance)
.count();
self.stats.success_probability =
successful_solutions as f64 / self.solutions.len() as f64;
}
Ok(())
}
#[must_use]
pub const fn get_stats(&self) -> &AnnealingStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = AnnealingStats::default();
}
}
#[derive(Debug, Clone)]
pub struct AnnealingResult {
pub solutions: Vec<AnnealingSolution>,
pub best_energy: f64,
pub annealing_history: Vec<AnnealingSnapshot>,
pub total_time_ms: f64,
pub success_probability: f64,
pub time_to_solution: TimeToSolutionStats,
}
pub struct QuantumAnnealingUtils;
impl QuantumAnnealingUtils {
#[must_use]
pub fn create_max_cut_problem(graph_edges: &[(usize, usize)], weights: &[f64]) -> IsingProblem {
let num_vertices = graph_edges
.iter()
.flat_map(|&(u, v)| [u, v])
.max()
.unwrap_or(0)
+ 1;
let mut problem = IsingProblem::new(num_vertices);
problem.metadata.name = Some("Max-Cut".to_string());
for (i, &(u, v)) in graph_edges.iter().enumerate() {
let weight = weights.get(i).copied().unwrap_or(1.0);
problem.set_j(u, v, weight / 2.0);
problem.offset -= weight / 2.0;
}
problem
}
#[must_use]
pub fn create_number_partitioning_problem(numbers: &[f64]) -> IsingProblem {
let n = numbers.len();
let mut problem = IsingProblem::new(n);
problem.metadata.name = Some("Number Partitioning".to_string());
for i in 0..n {
problem.offset += numbers[i] * numbers[i];
for j in i + 1..n {
problem.set_j(i, j, 2.0 * numbers[i] * numbers[j]);
}
}
problem
}
#[must_use]
pub fn create_random_ising_problem(
num_spins: usize,
h_range: f64,
j_range: f64,
) -> IsingProblem {
let mut problem = IsingProblem::new(num_spins);
problem.metadata.name = Some("Random Ising".to_string());
for i in 0..num_spins {
problem.set_h(i, (fastrand::f64() - 0.5) * 2.0 * h_range);
}
for i in 0..num_spins {
for j in i + 1..num_spins {
if fastrand::f64() < 0.5 {
problem.set_j(i, j, (fastrand::f64() - 0.5) * 2.0 * j_range);
}
}
}
problem
}
pub fn benchmark_quantum_annealing() -> Result<AnnealingBenchmarkResults> {
let mut results = AnnealingBenchmarkResults::default();
let problem_sizes = vec![8, 12, 16];
let annealing_times = vec![1.0, 10.0, 100.0];
for &size in &problem_sizes {
for &time in &annealing_times {
let problem = Self::create_random_ising_problem(size, 1.0, 1.0);
let config = QuantumAnnealingConfig {
annealing_time: time,
time_steps: (time * 100.0) as usize,
topology: AnnealingTopology::Complete(size),
..Default::default()
};
let mut simulator = QuantumAnnealingSimulator::new(config)?;
simulator.set_problem(problem)?;
let start = std::time::Instant::now();
let result = simulator.anneal(100)?;
let execution_time = start.elapsed().as_secs_f64() * 1000.0;
results
.execution_times
.push((format!("{size}spins_{time}us"), execution_time));
results
.best_energies
.push((format!("{size}spins_{time}us"), result.best_energy));
}
}
Ok(results)
}
}
#[derive(Debug, Clone, Default)]
pub struct AnnealingBenchmarkResults {
pub execution_times: Vec<(String, f64)>,
pub best_energies: Vec<(String, f64)>,
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_ising_problem_creation() {
let mut problem = IsingProblem::new(3);
problem.set_h(0, 0.5);
problem.set_j(0, 1, -1.0);
assert_eq!(problem.num_spins, 3);
assert_eq!(problem.h[0], 0.5);
assert_eq!(problem.j[[0, 1]], -1.0);
assert_eq!(problem.j[[1, 0]], -1.0);
}
#[test]
fn test_ising_energy_calculation() {
let mut problem = IsingProblem::new(2);
problem.set_h(0, 1.0);
problem.set_h(1, -0.5);
problem.set_j(0, 1, 2.0);
let config = vec![1, -1];
let energy = problem.calculate_energy(&config);
assert_abs_diff_eq!(energy, -0.5, epsilon = 1e-10);
}
#[test]
fn test_ising_to_qubo_conversion() {
let mut ising = IsingProblem::new(2);
ising.set_h(0, 1.0);
ising.set_j(0, 1, -1.0);
let qubo = ising.to_qubo();
assert_eq!(qubo.num_variables, 2);
let ising_config = vec![1, -1];
let qubo_config = vec![1, 0];
let ising_energy = ising.calculate_energy(&ising_config);
let qubo_energy = qubo.calculate_energy(&qubo_config);
assert_abs_diff_eq!(ising_energy, qubo_energy, epsilon = 1e-10);
}
#[test]
fn test_quantum_annealing_simulator_creation() {
let config = QuantumAnnealingConfig::default();
let simulator = QuantumAnnealingSimulator::new(config);
assert!(simulator.is_ok());
}
#[test]
fn test_schedule_functions() {
let config = QuantumAnnealingConfig {
annealing_time: 10.0,
schedule_type: AnnealingScheduleType::Linear,
..Default::default()
};
let simulator = QuantumAnnealingSimulator::new(config)
.expect("should create quantum annealing simulator");
assert_abs_diff_eq!(simulator.schedule_function(0.0), 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(simulator.schedule_function(5.0), 0.5, epsilon = 1e-10);
assert_abs_diff_eq!(simulator.schedule_function(10.0), 1.0, epsilon = 1e-10);
}
#[test]
fn test_max_cut_problem_creation() {
let edges = vec![(0, 1), (1, 2), (2, 0)];
let weights = vec![1.0, 1.0, 1.0];
let problem = QuantumAnnealingUtils::create_max_cut_problem(&edges, &weights);
assert_eq!(problem.num_spins, 3);
assert!(problem
.metadata
.name
.as_ref()
.expect("metadata name should be set")
.contains("Max-Cut"));
}
#[test]
fn test_number_partitioning_problem() {
let numbers = vec![3.0, 1.0, 1.0, 2.0, 2.0, 1.0];
let problem = QuantumAnnealingUtils::create_number_partitioning_problem(&numbers);
assert_eq!(problem.num_spins, 6);
assert!(problem
.metadata
.name
.as_ref()
.expect("metadata name should be set")
.contains("Number Partitioning"));
}
#[test]
fn test_small_problem_annealing() {
let problem = QuantumAnnealingUtils::create_random_ising_problem(3, 1.0, 1.0);
let config = QuantumAnnealingConfig {
annealing_time: 1.0,
time_steps: 100,
topology: AnnealingTopology::Complete(3),
enable_noise: false, ..Default::default()
};
let mut simulator = QuantumAnnealingSimulator::new(config)
.expect("should create quantum annealing simulator");
simulator
.set_problem(problem)
.expect("should set problem successfully");
let result = simulator.anneal(10);
assert!(result.is_ok());
let annealing_result = result.expect("should get annealing result");
assert_eq!(annealing_result.solutions.len(), 10);
assert!(!annealing_result.annealing_history.is_empty());
}
#[test]
fn test_field_strength_calculation() {
let config = QuantumAnnealingConfig::default();
let simulator = QuantumAnnealingSimulator::new(config)
.expect("should create quantum annealing simulator");
let (transverse, longitudinal) = simulator.calculate_field_strengths(0.0);
assert_abs_diff_eq!(transverse, 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(longitudinal, 0.0, epsilon = 1e-10);
let (transverse, longitudinal) = simulator.calculate_field_strengths(1.0);
assert_abs_diff_eq!(transverse, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(longitudinal, 1.0, epsilon = 1e-10);
}
#[test]
fn test_annealing_topologies() {
let topologies = vec![
AnnealingTopology::Chimera(4),
AnnealingTopology::Pegasus(3),
AnnealingTopology::Complete(5),
];
for topology in topologies {
let config = QuantumAnnealingConfig {
topology,
..Default::default()
};
let simulator = QuantumAnnealingSimulator::new(config);
assert!(simulator.is_ok());
}
}
#[test]
fn test_ising_ground_state_brute_force() {
let mut problem = IsingProblem::new(2);
problem.set_j(0, 1, -1.0);
let (ground_state, ground_energy) = problem.find_ground_state_brute_force();
assert!(ground_state == vec![1, 1] || ground_state == vec![-1, -1]);
assert_abs_diff_eq!(ground_energy, -1.0, epsilon = 1e-10);
}
#[test]
fn test_post_processing_config() {
let config = PostProcessingConfig::default();
assert!(config.enable_spin_reversal);
assert!(config.enable_local_search);
assert!(config.enable_majority_vote);
assert_eq!(config.majority_vote_reads, 1000);
}
}