use crate::embedding::Embedding;
use crate::ising::{IsingError, IsingModel, IsingResult, QuboModel};
use crate::partitioning::{Partition, SpectralPartitioner};
use crate::qubo::QuboFormulation;
use crate::simulator::{
AnnealingParams, AnnealingSolution, ClassicalAnnealingSimulator, QuantumAnnealingSimulator,
};
use scirs2_core::random::prelude::*;
use scirs2_core::random::Rng;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HybridSolverConfig {
pub max_quantum_size: usize,
pub num_iterations: usize,
pub classical_threshold: usize,
pub partition_overlap: usize,
pub combination_temperature: f64,
pub adaptive_partitioning: bool,
pub learning_rate: f64,
}
impl Default for HybridSolverConfig {
fn default() -> Self {
Self {
max_quantum_size: 100,
num_iterations: 10,
classical_threshold: 20,
partition_overlap: 5,
combination_temperature: 1.0,
adaptive_partitioning: true,
learning_rate: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub struct HybridSolverResult {
pub best_solution: Vec<i8>,
pub best_energy: f64,
pub num_partitions: usize,
pub quantum_calls: usize,
pub classical_calls: usize,
pub energy_history: Vec<f64>,
}
pub struct HybridQuantumClassicalSolver {
config: HybridSolverConfig,
quantum_solver: QuantumAnnealingSimulator,
classical_solver: ClassicalAnnealingSimulator,
partitioner: SpectralPartitioner,
}
impl HybridQuantumClassicalSolver {
pub fn new(config: HybridSolverConfig) -> IsingResult<Self> {
let quantum_params = AnnealingParams::default();
let classical_params = AnnealingParams::default();
Ok(Self {
config,
quantum_solver: QuantumAnnealingSimulator::new(quantum_params).map_err(|e| {
IsingError::InvalidValue(format!("Failed to create quantum solver: {e}"))
})?,
classical_solver: ClassicalAnnealingSimulator::new(classical_params).map_err(|e| {
IsingError::InvalidValue(format!("Failed to create classical solver: {e}"))
})?,
partitioner: SpectralPartitioner::default(),
})
}
pub fn solve_ising(&mut self, model: &IsingModel) -> IsingResult<HybridSolverResult> {
let mut result = HybridSolverResult {
best_solution: vec![1; model.num_qubits],
best_energy: f64::INFINITY,
num_partitions: 0,
quantum_calls: 0,
classical_calls: 0,
energy_history: Vec::new(),
};
for i in 0..model.num_qubits {
result.best_solution[i] = if thread_rng().random::<bool>() { 1 } else { -1 };
}
result.best_energy = model.energy(&result.best_solution)?;
for iteration in 0..self.config.num_iterations {
let partitions = self.partition_problem(model)?;
result.num_partitions = partitions.len();
let mut partition_solutions = Vec::new();
for partition in &partitions {
let subproblem = self.extract_subproblem(model, partition)?;
let subsolution = if subproblem.num_qubits <= self.config.classical_threshold {
result.classical_calls += 1;
self.classical_solver.solve(&subproblem).map_err(|e| {
IsingError::InvalidValue(format!("Classical solver failed: {e}"))
})?
} else {
result.quantum_calls += 1;
self.quantum_solver.solve(&subproblem).map_err(|e| {
IsingError::InvalidValue(format!("Quantum solver failed: {e}"))
})?
};
partition_solutions.push((partition.clone(), subsolution));
}
let combined_solution =
self.combine_solutions(&partition_solutions, model.num_qubits)?;
let combined_energy = model.energy(&combined_solution)?;
let refined_solution = self.local_refinement(model, &combined_solution)?;
let refined_energy = model.energy(&refined_solution)?;
if refined_energy < result.best_energy {
result.best_solution = refined_solution;
result.best_energy = refined_energy;
}
result.energy_history.push(result.best_energy);
if self.config.adaptive_partitioning && iteration < self.config.num_iterations - 1 {
self.update_partitioning_strategy(&partition_solutions);
}
}
Ok(result)
}
pub fn solve_qubo(&mut self, model: &QuboModel) -> IsingResult<HybridSolverResult> {
let (ising, _offset) = model.to_ising();
let mut result = self.solve_ising(&ising)?;
for val in &mut result.best_solution {
*val = (*val + 1) / 2;
}
Ok(result)
}
fn partition_problem(&self, model: &IsingModel) -> IsingResult<Vec<Partition>> {
let n = model.num_qubits;
let mut edges = Vec::new();
for coupling in model.couplings() {
edges.push((coupling.i, coupling.j, coupling.strength));
}
let num_partitions = (n as f64 / self.config.max_quantum_size as f64).ceil() as usize;
let partition = self
.partitioner
.partition_graph(n, &edges, num_partitions.max(2))?;
let mut partitions = Vec::new();
for p in 0..partition.num_partitions {
let mut part = Partition::new(1);
for var in partition.get_partition(p) {
part.assignment.insert(var, 0);
}
partitions.push(part);
}
Ok(partitions)
}
fn extract_subproblem(
&self,
model: &IsingModel,
partition: &Partition,
) -> IsingResult<IsingModel> {
let indices: Vec<usize> = partition.assignment.keys().copied().collect();
let mut submodel = IsingModel::new(indices.len());
let index_map: HashMap<usize, usize> = indices
.iter()
.enumerate()
.map(|(new_idx, &old_idx)| (old_idx, new_idx))
.collect();
for (i, &orig_idx) in indices.iter().enumerate() {
let bias = model.get_bias(orig_idx).unwrap_or(0.0);
if bias != 0.0 {
submodel.set_bias(i, bias)?;
}
}
for coupling in model.couplings() {
if let (Some(&new_i), Some(&new_j)) =
(index_map.get(&coupling.i), index_map.get(&coupling.j))
{
submodel.set_coupling(new_i, new_j, coupling.strength)?;
}
}
if self.config.partition_overlap > 0 {
self.add_boundary_terms(&mut submodel, model, partition, &index_map)?;
}
Ok(submodel)
}
fn add_boundary_terms(
&self,
submodel: &mut IsingModel,
original_model: &IsingModel,
partition: &Partition,
index_map: &HashMap<usize, usize>,
) -> IsingResult<()> {
let partition_nodes: std::collections::HashSet<usize> =
partition.assignment.keys().copied().collect();
for &node in partition.assignment.keys() {
if let Some(&sub_idx) = index_map.get(&node) {
let mut boundary_field = 0.0;
for coupling in original_model.couplings() {
if coupling.i == node && !partition_nodes.contains(&coupling.j) {
boundary_field += coupling.strength * 0.0;
} else if coupling.j == node && !partition_nodes.contains(&coupling.i) {
boundary_field += coupling.strength * 0.0;
}
}
let current_bias = submodel.get_bias(sub_idx).unwrap_or(0.0);
submodel.set_bias(sub_idx, current_bias + boundary_field)?;
}
}
Ok(())
}
fn combine_solutions(
&self,
partition_solutions: &[(Partition, AnnealingSolution)],
num_qubits: usize,
) -> IsingResult<Vec<i8>> {
let mut combined = vec![0i8; num_qubits];
let mut vote_counts = vec![0i32; num_qubits];
for (partition, result) in partition_solutions {
let indices: Vec<usize> = partition.assignment.keys().copied().collect();
for (sub_idx, &orig_idx) in indices.iter().enumerate() {
if sub_idx < result.best_spins.len() {
combined[orig_idx] += result.best_spins[sub_idx];
vote_counts[orig_idx] += 1;
}
}
}
for i in 0..num_qubits {
if vote_counts[i] > 0 {
combined[i] = if combined[i] >= 0 { 1 } else { -1 };
} else {
combined[i] = if thread_rng().random::<bool>() { 1 } else { -1 };
}
}
Ok(combined)
}
fn local_refinement(&self, model: &IsingModel, solution: &[i8]) -> IsingResult<Vec<i8>> {
let mut refined = solution.to_vec();
let mut current_energy = model.energy(&refined)?;
let mut improved = true;
while improved {
improved = false;
for i in 0..refined.len() {
refined[i] *= -1;
let new_energy = model.energy(&refined)?;
if new_energy < current_energy {
current_energy = new_energy;
improved = true;
} else {
refined[i] *= -1;
}
}
}
Ok(refined)
}
fn update_partitioning_strategy(
&mut self,
partition_solutions: &[(Partition, AnnealingSolution)],
) {
let mut partition_qualities = Vec::new();
for (partition, result) in partition_solutions {
let quality = if result.best_energy.is_finite() {
1.0 / (1.0 + result.best_energy.abs())
} else {
0.0
};
partition_qualities.push(quality);
}
let avg_quality =
partition_qualities.iter().sum::<f64>() / partition_qualities.len() as f64;
if avg_quality < 0.5 {
self.config.max_quantum_size = (self.config.max_quantum_size as f64 * 0.9) as usize;
} else if avg_quality > 0.8 {
self.config.max_quantum_size = (self.config.max_quantum_size as f64 * 1.1) as usize;
}
}
}
pub struct VariationalHybridSolver {
num_parameters: usize,
parameters: Vec<f64>,
learning_rate: f64,
max_iterations: usize,
}
impl VariationalHybridSolver {
#[must_use]
pub fn new(num_parameters: usize, learning_rate: f64, max_iterations: usize) -> Self {
Self {
num_parameters,
parameters: vec![0.5; num_parameters],
learning_rate,
max_iterations,
}
}
pub fn optimize(&mut self, model: &IsingModel) -> IsingResult<Vec<i8>> {
for _ in 0..self.max_iterations {
let gradients = self.compute_gradients(model)?;
for i in 0..self.num_parameters {
self.parameters[i] -= self.learning_rate * gradients[i];
self.parameters[i] = self.parameters[i].max(0.0).min(1.0);
}
}
self.generate_solution(model)
}
fn compute_gradients(&self, model: &IsingModel) -> IsingResult<Vec<f64>> {
let mut gradients = vec![0.0; self.num_parameters];
let epsilon = 0.01;
for i in 0..self.num_parameters {
let mut params_plus = self.parameters.clone();
params_plus[i] += epsilon;
let energy_plus = self.evaluate_parameters(model, ¶ms_plus)?;
let mut params_minus = self.parameters.clone();
params_minus[i] -= epsilon;
let energy_minus = self.evaluate_parameters(model, ¶ms_minus)?;
gradients[i] = (energy_plus - energy_minus) / (2.0 * epsilon);
}
Ok(gradients)
}
fn evaluate_parameters(&self, model: &IsingModel, parameters: &[f64]) -> IsingResult<f64> {
let solution = self.generate_solution_with_params(model, parameters)?;
model.energy(&solution)
}
fn generate_solution(&self, model: &IsingModel) -> IsingResult<Vec<i8>> {
self.generate_solution_with_params(model, &self.parameters)
}
fn generate_solution_with_params(
&self,
model: &IsingModel,
parameters: &[f64],
) -> IsingResult<Vec<i8>> {
let n = model.num_qubits;
let mut solution = vec![1i8; n];
for i in 0..n {
let param_idx = i % parameters.len();
let prob = parameters[param_idx];
solution[i] = if thread_rng().random::<f64>() < prob {
1
} else {
-1
};
}
Ok(solution)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hybrid_solver_creation() {
let config = HybridSolverConfig::default();
let solver =
HybridQuantumClassicalSolver::new(config).expect("Failed to create hybrid solver");
assert!(solver.config.max_quantum_size > 0);
}
#[test]
fn test_small_problem_solving() {
let mut model = IsingModel::new(4);
model.set_bias(0, 1.0).expect("Failed to set bias");
model
.set_coupling(0, 1, -2.0)
.expect("Failed to set coupling");
model
.set_coupling(1, 2, -1.0)
.expect("Failed to set coupling");
let config = HybridSolverConfig {
max_quantum_size: 2,
..Default::default()
};
let mut solver =
HybridQuantumClassicalSolver::new(config).expect("Failed to create hybrid solver");
let result = solver.solve_ising(&model).expect("Failed to solve Ising");
assert_eq!(result.best_solution.len(), 4);
assert!(result.best_energy.is_finite());
}
#[test]
fn test_variational_solver() {
let model = IsingModel::new(3);
let mut solver = VariationalHybridSolver::new(3, 0.1, 10);
let solution = solver.optimize(&model).expect("Failed to optimize");
assert_eq!(solution.len(), 3);
}
}