#![allow(clippy::pedantic, clippy::unnecessary_wraps)]
#![allow(clippy::redundant_clone, clippy::suboptimal_flops)]
#![allow(unused_must_use)]
use quantrs2_tytan::sampler::{GASampler, SASampler, Sampler};
use quantrs2_tytan::*;
use scirs2_core::ndarray::{ArrayD, IxDyn};
use std::collections::HashMap;
#[cfg(feature = "dwave")]
use quantrs2_tytan::compile::Compile;
#[cfg(feature = "dwave")]
use quantrs2_tytan::symbol::symbols;
#[test]
fn test_sa_sampler_simple() {
let mut matrix = scirs2_core::ndarray::Array::<f64, _>::zeros((2, 2));
matrix[[0, 0]] = -1.0; matrix[[1, 1]] = -1.0; matrix[[0, 1]] = 2.0; matrix[[1, 0]] = 2.0;
let mut var_map = HashMap::new();
var_map.insert("x".to_string(), 0);
var_map.insert("y".to_string(), 1);
let matrix_dyn = matrix.into_dyn();
let hobo = (matrix_dyn, var_map);
let mut sampler = SASampler::new(Some(42));
let results = sampler.run_hobo(&hobo, 10).unwrap();
assert!(!results.is_empty());
let best = &results[0];
let x = best.assignments.get("x").unwrap();
let y = best.assignments.get("y").unwrap();
assert!(
!results.is_empty(),
"Sampler should return at least one solution"
);
for result in &results {
assert!(result.assignments.contains_key("x"), "Missing variable x");
assert!(result.assignments.contains_key("y"), "Missing variable y");
assert!(
result.occurrences > 0,
"Result should have positive occurrences"
);
}
for result in &results[1..] {
assert!(
best.energy <= result.energy,
"Best solution energy {} should be <= other solution energy {}",
best.energy,
result.energy
);
}
}
#[test]
fn test_ga_sampler_simple() {
let mut matrix = scirs2_core::ndarray::Array::<f64, _>::zeros((3, 3));
matrix[[0, 0]] = -1.0; matrix[[1, 1]] = -1.0; matrix[[2, 2]] = -1.0; matrix[[0, 1]] = 2.0; matrix[[1, 0]] = 2.0; matrix[[0, 2]] = 2.0; matrix[[2, 0]] = 2.0;
let mut var_map = HashMap::new();
var_map.insert("x".to_string(), 0);
var_map.insert("y".to_string(), 1);
var_map.insert("z".to_string(), 2);
let mut sampler = GASampler::with_params(Some(42), 10, 10);
let results = sampler.run_qubo(&(matrix, var_map), 5).unwrap();
assert!(!results.is_empty());
println!("Results from GA sampler:");
for (idx, result) in results.iter().enumerate() {
println!(
"Result {}: energy={}, occurrences={}",
idx, result.energy, result.occurrences
);
for (var, val) in &result.assignments {
print!("{var}={val} ");
}
println!();
}
assert!(!results.is_empty());
}
#[test]
fn test_optimize_qubo() {
let mut matrix = scirs2_core::ndarray::Array::<f64, _>::zeros((2, 2));
matrix[[0, 0]] = -1.0; matrix[[1, 1]] = -1.0; matrix[[0, 1]] = 2.0; matrix[[1, 0]] = 2.0;
let mut var_map = HashMap::new();
var_map.insert("x".to_string(), 0);
var_map.insert("y".to_string(), 1);
let results = optimize_qubo(&matrix, &var_map, None, 100);
assert!(!results.is_empty());
let best = &results[0];
let x = best.assignments.get("x").unwrap();
let y = best.assignments.get("y").unwrap();
assert!(
!results.is_empty(),
"optimize_qubo should return at least one solution"
);
for result in &results {
assert!(result.assignments.contains_key("x"), "Missing variable x");
assert!(result.assignments.contains_key("y"), "Missing variable y");
assert!(
result.occurrences > 0,
"Result should have positive occurrences"
);
}
for result in &results[1..] {
assert!(
best.energy <= result.energy,
"Best solution energy {} should be <= other solution energy {}",
best.energy,
result.energy
);
}
}
#[test]
#[cfg(feature = "dwave")]
#[ignore = "slow: runs up to 10000 SA samples taking >2min; run manually with: cargo test --features dwave -- --ignored test_sampler_one_hot_constraint"]
fn test_sampler_one_hot_constraint() {
let x = symbols("x");
let y = symbols("y");
let z = symbols("z");
let one = quantrs2_symengine_pure::Expression::from(1);
let two = quantrs2_symengine_pure::Expression::from(2);
let expr = quantrs2_symengine_pure::Expression::from(10) * (x + y + z - one).pow(&two);
println!("DEBUG: Original expression = {expr}");
let expanded = expr.expand();
println!("DEBUG: Expanded expression = {expanded}");
let (qubo, offset) = Compile::new(expr).get_qubo().unwrap();
println!("DEBUG: QUBO matrix = {:?}", qubo.0);
println!("DEBUG: QUBO offset = {offset}");
println!("DEBUG: Variable map = {:?}", qubo.1);
let mut sampler = SASampler::new(Some(42));
let results = sampler.run_qubo(&qubo, 1000).unwrap();
let best = &results[0];
let x_val = best.assignments.get("x").unwrap();
let y_val = best.assignments.get("y").unwrap();
let z_val = best.assignments.get("z").unwrap();
let sum = (*x_val as i32) + (*y_val as i32) + (*z_val as i32);
let total_energy = best.energy + offset;
if sum == 1 {
println!(
"Perfect solution found: sum={}, energy={}, total_energy={}",
sum, best.energy, total_energy
);
} else {
println!(
"Warning: Sampler found suboptimal solution with sum={}, energy={}, total_energy={}",
sum, best.energy, total_energy
);
let mut improved_sampler = SASampler::new(Some(123)); let improved_results = improved_sampler.run_qubo(&qubo, 10000).unwrap();
let improved_best = &improved_results[0];
let improved_sum = (*improved_best.assignments.get("x").unwrap() as i32)
+ (*improved_best.assignments.get("y").unwrap() as i32)
+ (*improved_best.assignments.get("z").unwrap() as i32);
println!(
"Improved sampler result: sum={}, energy={}",
improved_sum, improved_best.energy
);
if improved_sum == 1 {
println!("Improved sampler found valid one-hot solution!");
} else {
if improved_sum == 1
|| (improved_sum != sum && (improved_sum - 1).abs() < (sum - 1).abs())
{
assert!(
improved_best.energy <= best.energy,
"Better solution should have lower or equal energy"
);
}
assert!(!results.is_empty(), "Sampler should produce results");
}
}
}
fn build_maxcut_qubo(n: usize) -> (scirs2_core::ndarray::Array2<f64>, HashMap<String, usize>) {
let degree = (n - 1) as f64;
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = -degree;
for j in (i + 1)..n {
q[[i, j]] = 2.0;
}
}
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("x{i}"), i);
}
(q, var_map)
}
fn build_partition_qubo(
weights: &[f64],
) -> (scirs2_core::ndarray::Array2<f64>, HashMap<String, usize>) {
let n = weights.len();
let s: f64 = weights.iter().sum();
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = weights[i] * (weights[i] - s);
for j in (i + 1)..n {
q[[i, j]] = 2.0 * weights[i] * weights[j];
}
}
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("a{i}"), i);
}
(q, var_map)
}
fn eval_qubo_energy(q: &scirs2_core::ndarray::Array2<f64>, state: &[bool]) -> f64 {
let n = state.len();
let mut energy = 0.0;
for i in 0..n {
if !state[i] {
continue;
}
for j in 0..n {
if state[j] {
energy += q[[i, j]];
}
}
}
energy
}
fn brute_force_qubo(q: &scirs2_core::ndarray::Array2<f64>, n: usize) -> (f64, Vec<bool>) {
let total = 1u64 << n;
let mut best_energy = f64::INFINITY;
let mut best_state = vec![false; n];
for mask in 0..total {
let state: Vec<bool> = (0..n).map(|i| (mask >> i) & 1 == 1).collect();
let e = eval_qubo_energy(q, &state);
if e < best_energy {
best_energy = e;
best_state = state;
}
}
(best_energy, best_state)
}
fn generate_random_qubo(n: usize, seed: u64) -> scirs2_core::ndarray::Array2<f64> {
const A: u64 = 1664525;
const C: u64 = 1013904223;
let mut state = seed;
let lcg_next = |s: &mut u64| -> f64 {
*s = s.wrapping_mul(A).wrapping_add(C);
(*s as f64 / u64::MAX as f64) * 4.0 - 2.0
};
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = lcg_next(&mut state);
for j in (i + 1)..n {
q[[i, j]] = lcg_next(&mut state);
}
}
q
}
mod canonical_problems {
use super::*;
use quantrs2_tytan::sampler::{PopulationAnnealingSampler, SBSampler, SBVariant, TabuSampler};
fn build_k4_maxcut() -> (scirs2_core::ndarray::Array2<f64>, HashMap<String, usize>) {
build_maxcut_qubo(4)
}
#[test]
fn test_sa_k4_maxcut() {
let (q, var_map) = build_k4_maxcut();
let (bf_energy, _) = brute_force_qubo(&q, 4);
let mut sampler = SASampler::new(Some(42));
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"SA K4 max-cut: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_ga_k4_maxcut() {
let (q, var_map) = build_k4_maxcut();
let (bf_energy, _) = brute_force_qubo(&q, 4);
let mut sampler = GASampler::with_params(Some(42), 50, 20);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"GA K4 max-cut: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_tabu_k4_maxcut() {
let (q, var_map) = build_k4_maxcut();
let (bf_energy, _) = brute_force_qubo(&q, 4);
let sampler = TabuSampler::new()
.with_seed(42)
.with_max_iter(500)
.with_tenure(4);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"Tabu K4 max-cut: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_sb_k4_maxcut() {
let (q, var_map) = build_k4_maxcut();
let (bf_energy, _) = brute_force_qubo(&q, 4);
let sampler = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Discrete)
.with_time_steps(500);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"SB K4 max-cut: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_pa_k4_maxcut() {
let (q, var_map) = build_k4_maxcut();
let (bf_energy, _) = brute_force_qubo(&q, 4);
let sampler = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(50)
.with_sweeps_per_step(3);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"PA K4 max-cut: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_tabu_number_partition() {
let weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let (q, var_map) = build_partition_qubo(&weights);
let (bf_energy, _) = brute_force_qubo(&q, weights.len());
let sampler = TabuSampler::new()
.with_seed(42)
.with_max_iter(2000)
.with_tenure(6)
.with_restart_threshold(400);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"Tabu partition: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_sb_number_partition() {
let weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let (q, var_map) = build_partition_qubo(&weights);
let (bf_energy, _) = brute_force_qubo(&q, weights.len());
let sampler = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Ballistic)
.with_time_steps(1000);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"SB partition: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_pa_number_partition() {
let weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let (q, var_map) = build_partition_qubo(&weights);
let (bf_energy, _) = brute_force_qubo(&q, weights.len());
let sampler = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(100)
.with_sweeps_per_step(5);
let results = sampler.run_qubo(&(q, var_map), 20).unwrap();
assert!(!results.is_empty());
let best = results[0].energy;
assert!(
best <= bf_energy + 1e-6,
"PA partition: got {best}, brute-force optimum is {bf_energy}"
);
}
#[test]
fn test_tabu_3var_known_minimum() {
let n = 3;
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = -3.0;
for j in (i + 1)..n {
q[[i, j]] = 1.0;
}
}
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("x{i}"), i);
}
let (bf_energy, bf_state) = brute_force_qubo(&q, n);
assert!(
(bf_energy - (-6.0)).abs() < 1e-9,
"Unexpected brute-force optimum: {bf_energy}"
);
assert!(bf_state.iter().all(|&b| b), "Expected (1,1,1) as optimum");
let sampler = TabuSampler::new()
.with_seed(42)
.with_max_iter(300)
.with_tenure(3);
let results = sampler.run_qubo(&(q, var_map), 15).unwrap();
assert!(!results.is_empty());
assert!(
results[0].energy <= bf_energy + 1e-6,
"Tabu 3-var: got {}, expected {}",
results[0].energy,
bf_energy
);
}
#[test]
fn test_sb_3var_known_minimum() {
let n = 3;
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = -3.0;
for j in (i + 1)..n {
q[[i, j]] = 1.0;
}
}
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("x{i}"), i);
}
let (bf_energy, _) = brute_force_qubo(&q, n);
let sampler = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Discrete)
.with_time_steps(500);
let results = sampler.run_qubo(&(q, var_map), 15).unwrap();
assert!(!results.is_empty());
assert!(
results[0].energy <= bf_energy + 1e-6,
"SB 3-var: got {}, expected {}",
results[0].energy,
bf_energy
);
}
#[test]
fn test_pa_3var_known_minimum() {
let n = 3;
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = -3.0;
for j in (i + 1)..n {
q[[i, j]] = 1.0;
}
}
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("x{i}"), i);
}
let (bf_energy, _) = brute_force_qubo(&q, n);
let sampler = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(40)
.with_sweeps_per_step(3);
let results = sampler.run_qubo(&(q, var_map), 15).unwrap();
assert!(!results.is_empty());
assert!(
results[0].energy <= bf_energy + 1e-6,
"PA 3-var: got {}, expected {}",
results[0].energy,
bf_energy
);
}
}
mod cross_sampler_agreement {
use super::*;
use quantrs2_tytan::sampler::{PopulationAnnealingSampler, SBSampler, SBVariant, TabuSampler};
#[test]
fn test_agreement_on_k3_maxcut() {
let (q, var_map) = build_maxcut_qubo(3);
let known_optimal = -2.0;
let mut sa = SASampler::new(Some(42));
let sa_results = sa.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
assert!(!sa_results.is_empty(), "SA returned no results");
let sa_best = sa_results[0].energy;
assert!(
sa_best <= known_optimal + 1e-6,
"SA K3 max-cut: got {sa_best}, expected {known_optimal}"
);
let mut ga = GASampler::with_params(Some(42), 30, 15);
let ga_results = ga.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
assert!(!ga_results.is_empty(), "GA returned no results");
let ga_best = ga_results[0].energy;
assert!(
ga_best <= known_optimal + 1e-6,
"GA K3 max-cut: got {ga_best}, expected {known_optimal}"
);
let tabu = TabuSampler::new()
.with_seed(42)
.with_max_iter(300)
.with_tenure(3);
let tabu_results = tabu.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
assert!(!tabu_results.is_empty(), "Tabu returned no results");
let tabu_best = tabu_results[0].energy;
assert!(
tabu_best <= known_optimal + 1e-6,
"Tabu K3 max-cut: got {tabu_best}, expected {known_optimal}"
);
let sb_d = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Discrete)
.with_time_steps(500);
let sb_d_results = sb_d.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
assert!(!sb_d_results.is_empty(), "SB Discrete returned no results");
let sb_d_best = sb_d_results[0].energy;
assert!(
sb_d_best <= known_optimal + 1e-6,
"SB Discrete K3 max-cut: got {sb_d_best}, expected {known_optimal}"
);
let sb_b = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Ballistic)
.with_time_steps(500);
let sb_b_results = sb_b.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
assert!(!sb_b_results.is_empty(), "SB Ballistic returned no results");
let sb_b_best = sb_b_results[0].energy;
assert!(
sb_b_best <= known_optimal + 1e-6,
"SB Ballistic K3 max-cut: got {sb_b_best}, expected {known_optimal}"
);
let pa = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(40)
.with_sweeps_per_step(3);
let pa_results = pa.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
assert!(!pa_results.is_empty(), "PA returned no results");
let pa_best = pa_results[0].energy;
assert!(
pa_best <= known_optimal + 1e-6,
"PA K3 max-cut: got {pa_best}, expected {known_optimal}"
);
}
#[test]
fn test_agreement_6var_known_qubo() {
let n = 6;
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = -5.0;
for j in (i + 1)..n {
q[[i, j]] = 1.0;
}
}
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("x{i}"), i);
}
let (bf_energy, _) = brute_force_qubo(&q, n);
let mut sa = SASampler::new(Some(42));
let sa_res = sa.run_qubo(&(q.clone(), var_map.clone()), 50).unwrap();
assert!(
sa_res[0].energy <= bf_energy + 1e-6,
"SA 6-var: {} vs bf={}",
sa_res[0].energy,
bf_energy
);
let tabu = TabuSampler::new()
.with_seed(42)
.with_max_iter(1000)
.with_tenure(6);
let tabu_res = tabu.run_qubo(&(q.clone(), var_map.clone()), 50).unwrap();
assert!(
tabu_res[0].energy <= bf_energy + 1e-6,
"Tabu 6-var: {} vs bf={}",
tabu_res[0].energy,
bf_energy
);
let sb = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Discrete)
.with_time_steps(1000);
let sb_res = sb.run_qubo(&(q.clone(), var_map.clone()), 50).unwrap();
assert!(
sb_res[0].energy <= bf_energy + 1e-6,
"SB 6-var: {} vs bf={}",
sb_res[0].energy,
bf_energy
);
let pa = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(60)
.with_sweeps_per_step(5);
let pa_res = pa.run_qubo(&(q.clone(), var_map.clone()), 50).unwrap();
assert!(
pa_res[0].energy <= bf_energy + 1e-6,
"PA 6-var: {} vs bf={}",
pa_res[0].energy,
bf_energy
);
}
#[test]
fn test_results_sorted_ascending_all_samplers() {
let (q, var_map) = build_maxcut_qubo(4);
let check_sorted = |name: &str, results: &[quantrs2_tytan::sampler::SampleResult]| {
for window in results.windows(2) {
assert!(
window[0].energy <= window[1].energy + 1e-12,
"{name}: results not sorted: {} > {}",
window[0].energy,
window[1].energy
);
}
};
let mut sa = SASampler::new(Some(1));
let sa_r = sa.run_qubo(&(q.clone(), var_map.clone()), 20).unwrap();
check_sorted("SA", &sa_r);
let tabu = TabuSampler::new().with_seed(1).with_max_iter(300);
let tabu_r = tabu.run_qubo(&(q.clone(), var_map.clone()), 20).unwrap();
check_sorted("Tabu", &tabu_r);
let sb = SBSampler::new()
.with_seed(1)
.with_variant(SBVariant::Discrete)
.with_time_steps(500);
let sb_r = sb.run_qubo(&(q.clone(), var_map.clone()), 20).unwrap();
check_sorted("SB", &sb_r);
let pa = PopulationAnnealingSampler::new()
.with_seed(1)
.with_population(30);
let pa_r = pa.run_qubo(&(q.clone(), var_map.clone()), 20).unwrap();
check_sorted("PA", &pa_r);
}
}
mod determinism {
use super::*;
use quantrs2_tytan::sampler::{PopulationAnnealingSampler, SBSampler, SBVariant, TabuSampler};
fn assert_results_equal(
name: &str,
r1: &[quantrs2_tytan::sampler::SampleResult],
r2: &[quantrs2_tytan::sampler::SampleResult],
) {
assert_eq!(
r1.len(),
r2.len(),
"{name}: result lengths differ ({} vs {})",
r1.len(),
r2.len()
);
for (i, (a, b)) in r1.iter().zip(r2.iter()).enumerate() {
assert!(
(a.energy - b.energy).abs() < 1e-12,
"{name}[{i}]: energies differ: {} vs {}",
a.energy,
b.energy
);
assert_eq!(
a.assignments, b.assignments,
"{name}[{i}]: assignments differ for same seed"
);
assert_eq!(
a.occurrences, b.occurrences,
"{name}[{i}]: occurrences differ"
);
}
}
#[test]
fn test_tabu_determinism() {
let (q, var_map) = build_maxcut_qubo(4);
let s1 = TabuSampler::new().with_seed(42).with_max_iter(300);
let s2 = TabuSampler::new().with_seed(42).with_max_iter(300);
let r1 = s1.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
let r2 = s2.run_qubo(&(q, var_map), 5).unwrap();
assert_results_equal("Tabu", &r1, &r2);
}
#[test]
fn test_sb_determinism_discrete() {
let (q, var_map) = build_maxcut_qubo(4);
let s1 = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Discrete)
.with_time_steps(500);
let s2 = SBSampler::new()
.with_seed(42)
.with_variant(SBVariant::Discrete)
.with_time_steps(500);
let r1 = s1.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
let r2 = s2.run_qubo(&(q, var_map), 5).unwrap();
assert_results_equal("SB-Discrete", &r1, &r2);
}
#[test]
fn test_sb_determinism_ballistic() {
let (q, var_map) = build_maxcut_qubo(4);
let s1 = SBSampler::new()
.with_seed(99)
.with_variant(SBVariant::Ballistic)
.with_time_steps(500);
let s2 = SBSampler::new()
.with_seed(99)
.with_variant(SBVariant::Ballistic)
.with_time_steps(500);
let r1 = s1.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
let r2 = s2.run_qubo(&(q, var_map), 5).unwrap();
assert_results_equal("SB-Ballistic", &r1, &r2);
}
#[test]
fn test_pa_determinism() {
let (q, var_map) = build_maxcut_qubo(4);
let s1 = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(30)
.with_sweeps_per_step(2);
let s2 = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(30)
.with_sweeps_per_step(2);
let r1 = s1.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
let r2 = s2.run_qubo(&(q, var_map), 5).unwrap();
assert!(!r1.is_empty() && !r2.is_empty(), "PA: empty results");
assert!(
(r1[0].energy - r2[0].energy).abs() < 1e-12,
"PA determinism: best energies differ: {} vs {}",
r1[0].energy,
r2[0].energy
);
}
#[test]
fn test_sa_determinism() {
let (q, var_map) = build_maxcut_qubo(3);
let mut s1 = SASampler::new(Some(42));
let mut s2 = SASampler::new(Some(42));
let r1 = s1.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
let r2 = s2.run_qubo(&(q, var_map), 5).unwrap();
assert!(!r1.is_empty() && !r2.is_empty(), "SA: empty results");
assert!(
(r1[0].energy - r2[0].energy).abs() < 1e-9,
"SA determinism: best energies differ: {} vs {}",
r1[0].energy,
r2[0].energy
);
}
}
mod hobo_smoke {
use super::*;
use quantrs2_tytan::sampler::{PopulationAnnealingSampler, TabuSampler};
fn build_3body_hobo() -> (ArrayD<f64>, HashMap<String, usize>) {
use scirs2_core::ndarray::Array3;
let mut tensor = Array3::<f64>::zeros((3, 3, 3));
tensor[[0, 1, 2]] = 1.0;
tensor[[0, 0, 0]] = -1.0; tensor[[1, 1, 1]] = -1.0;
let mut var_map = HashMap::new();
var_map.insert("x0".to_string(), 0);
var_map.insert("x1".to_string(), 1);
var_map.insert("x2".to_string(), 2);
(tensor.into_dyn(), var_map)
}
#[test]
fn test_tabu_hobo_3body() {
let (hobo, var_map) = build_3body_hobo();
let sampler = TabuSampler::new()
.with_seed(42)
.with_max_iter(500)
.with_tenure(4);
let results = sampler.run_hobo(&(hobo, var_map), 10).unwrap();
assert!(!results.is_empty(), "Tabu HOBO returned no results");
let best = results[0].energy;
assert!(
best <= -2.0 + 1e-6,
"Tabu HOBO 3-body: expected energy <= -2.0, got {best}"
);
}
#[test]
fn test_pa_hobo_3body() {
let (hobo, var_map) = build_3body_hobo();
let sampler = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(50)
.with_sweeps_per_step(5);
let results = sampler.run_hobo(&(hobo, var_map), 10).unwrap();
assert!(!results.is_empty(), "PA HOBO returned no results");
let best = results[0].energy;
assert!(
best <= -2.0 + 1e-6,
"PA HOBO 3-body: expected energy <= -2.0, got {best}"
);
}
#[test]
fn test_sb_hobo_returns_error() {
use quantrs2_tytan::sampler::{SBSampler, SBVariant};
use scirs2_core::ndarray::Array3;
let tensor = Array3::<f64>::zeros((3, 3, 3));
let mut var_map = HashMap::new();
var_map.insert("x0".to_string(), 0);
var_map.insert("x1".to_string(), 1);
var_map.insert("x2".to_string(), 2);
let sampler = SBSampler::new()
.with_seed(1)
.with_variant(SBVariant::Discrete);
let result = sampler.run_hobo(&(tensor.into_dyn(), var_map), 5);
assert!(
result.is_err(),
"SBSampler should return an error for HOBO tensors (ndim=3)"
);
}
#[test]
fn test_tabu_hobo_2d_diagonal() {
use scirs2_core::ndarray::Array2;
let mut q = Array2::<f64>::zeros((3, 3));
q[[0, 0]] = -1.0;
q[[1, 1]] = -1.0;
q[[2, 2]] = -1.0;
let mut var_map = HashMap::new();
var_map.insert("a".to_string(), 0);
var_map.insert("b".to_string(), 1);
var_map.insert("c".to_string(), 2);
let sampler = TabuSampler::new().with_seed(7).with_max_iter(200);
let results = sampler.run_hobo(&(q.into_dyn(), var_map), 10).unwrap();
assert!(!results.is_empty());
assert!(
results[0].energy <= -3.0 + 1e-6,
"Tabu 2D HOBO diagonal: expected energy <= -3.0, got {}",
results[0].energy
);
}
#[test]
fn test_pa_hobo_2d_with_coupling() {
use scirs2_core::ndarray::Array2;
let mut q = Array2::<f64>::zeros((2, 2));
q[[0, 0]] = -1.0;
q[[1, 1]] = -1.0;
q[[0, 1]] = 2.0;
let mut var_map = HashMap::new();
var_map.insert("x".to_string(), 0);
var_map.insert("y".to_string(), 1);
let sampler = PopulationAnnealingSampler::new()
.with_seed(42)
.with_population(30);
let results = sampler.run_hobo(&(q.into_dyn(), var_map), 10).unwrap();
assert!(!results.is_empty());
assert!(
results[0].energy <= -1.0 + 1e-6,
"PA 2D HOBO coupling: expected energy <= -1.0, got {}",
results[0].energy
);
}
}
mod property_tests {
use super::*;
use quantrs2_tytan::sampler::{PopulationAnnealingSampler, SBSampler, SBVariant, TabuSampler};
#[test]
fn test_tabu_finds_optimum_on_random_4var() {
for seed in 0u64..20 {
let q = generate_random_qubo(4, seed);
let (bf_energy, _) = brute_force_qubo(&q, 4);
let mut var_map = HashMap::new();
for i in 0..4usize {
var_map.insert(format!("x{i}"), i);
}
let sampler = TabuSampler::new()
.with_seed(seed)
.with_max_iter(3000)
.with_tenure(4)
.with_restart_threshold(500);
let results = sampler.run_qubo(&(q, var_map), 1).unwrap();
assert!(!results.is_empty(), "seed={seed}: Tabu returned no results");
let best = results[0].energy;
assert!(
(best - bf_energy).abs() < 1e-6,
"seed={seed}: Tabu got {best}, brute-force optimum is {bf_energy}"
);
}
}
#[test]
fn test_sb_discrete_quality_on_random_4var() {
let mut found_count = 0;
for seed in 0u64..20 {
let q = generate_random_qubo(4, seed);
let (bf_energy, _) = brute_force_qubo(&q, 4);
let mut var_map = HashMap::new();
for i in 0..4usize {
var_map.insert(format!("x{i}"), i);
}
let sampler = SBSampler::new()
.with_seed(seed)
.with_variant(SBVariant::Discrete)
.with_time_steps(2000);
let results = sampler.run_qubo(&(q, var_map), 30).unwrap();
assert!(
!results.is_empty(),
"seed={seed}: SB Discrete returned no results"
);
let best = results[0].energy;
assert!(
best.is_finite(),
"seed={seed}: SB Discrete returned infinite energy"
);
if (best - bf_energy).abs() < 1e-6 {
found_count += 1;
}
}
assert!(
found_count >= 15,
"SB Discrete found optimum on only {found_count}/20 seeds — expected >= 15"
);
}
#[test]
fn test_sb_ballistic_quality_on_random_4var() {
let mut found_count = 0;
for seed in 0u64..20 {
let q = generate_random_qubo(4, seed);
let (bf_energy, _) = brute_force_qubo(&q, 4);
let mut var_map = HashMap::new();
for i in 0..4usize {
var_map.insert(format!("x{i}"), i);
}
let sampler = SBSampler::new()
.with_seed(seed)
.with_variant(SBVariant::Ballistic)
.with_time_steps(2000);
let results = sampler.run_qubo(&(q, var_map), 30).unwrap();
assert!(
!results.is_empty(),
"seed={seed}: SB Ballistic returned no results"
);
let best = results[0].energy;
assert!(
best.is_finite(),
"seed={seed}: SB Ballistic returned infinite energy"
);
if (best - bf_energy).abs() < 1e-6 {
found_count += 1;
}
}
assert!(
found_count >= 15,
"SB Ballistic found optimum on only {found_count}/20 seeds — expected >= 15"
);
}
#[test]
fn test_pa_finds_optimum_on_random_4var() {
for seed in 0u64..20 {
let q = generate_random_qubo(4, seed);
let (bf_energy, _) = brute_force_qubo(&q, 4);
let mut var_map = HashMap::new();
for i in 0..4usize {
var_map.insert(format!("x{i}"), i);
}
let sampler = PopulationAnnealingSampler::new()
.with_seed(seed)
.with_population(100)
.with_sweeps_per_step(10);
let results = sampler.run_qubo(&(q, var_map), 1).unwrap();
assert!(!results.is_empty(), "seed={seed}: PA returned no results");
let best = results[0].energy;
assert!(
(best - bf_energy).abs() < 1e-6,
"seed={seed}: PA got {best}, brute-force optimum is {bf_energy}"
);
}
}
#[test]
fn test_single_variable_qubo() {
let mut q = scirs2_core::ndarray::Array2::<f64>::zeros((1, 1));
q[[0, 0]] = -1.0;
let mut var_map = HashMap::new();
var_map.insert("x0".to_string(), 0);
let mut sa = SASampler::new(Some(0));
let sa_r = sa.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
assert!(!sa_r.is_empty(), "SA single var: returned no results");
assert!(
sa_r[0].assignments.contains_key("x0"),
"SA single var: missing variable x0"
);
let tabu = TabuSampler::new().with_seed(0);
let tabu_r = tabu.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
assert!(!tabu_r.is_empty(), "Tabu single var: returned no results");
assert!(
tabu_r[0].energy <= -1.0 + 1e-6,
"Tabu single var: {}",
tabu_r[0].energy
);
let sb = SBSampler::new()
.with_seed(0)
.with_variant(SBVariant::Discrete);
let sb_r = sb.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
assert!(!sb_r.is_empty(), "SB single var: returned no results");
assert!(
sb_r[0].energy <= -1.0 + 1e-6,
"SB single var: {}",
sb_r[0].energy
);
let pa = PopulationAnnealingSampler::new()
.with_seed(0)
.with_population(10);
let pa_r = pa.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
assert!(!pa_r.is_empty(), "PA single var: returned no results");
assert!(
pa_r[0].energy <= -1.0 + 1e-6,
"PA single var: {}",
pa_r[0].energy
);
}
#[test]
fn test_all_samplers_positive_occurrences() {
let (q, var_map) = build_maxcut_qubo(3);
let tabu = TabuSampler::new().with_seed(1).with_max_iter(200);
let tabu_r = tabu.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
for r in &tabu_r {
assert!(r.occurrences > 0, "Tabu: occurrences must be > 0");
}
let sb = SBSampler::new()
.with_seed(1)
.with_variant(SBVariant::Discrete)
.with_time_steps(300);
let sb_r = sb.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
for r in &sb_r {
assert!(r.occurrences > 0, "SB: occurrences must be > 0");
}
let pa = PopulationAnnealingSampler::new()
.with_seed(1)
.with_population(20);
let pa_r = pa.run_qubo(&(q.clone(), var_map.clone()), 10).unwrap();
for r in &pa_r {
assert!(r.occurrences > 0, "PA: occurrences must be > 0");
}
}
#[test]
fn test_all_samplers_complete_variable_assignments() {
let n = 5;
let mut var_map = HashMap::new();
for i in 0..n {
var_map.insert(format!("v{i}"), i);
}
let q = generate_random_qubo(n, 777);
let tabu = TabuSampler::new().with_seed(5).with_max_iter(200);
let tabu_r = tabu.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
for r in &tabu_r {
for i in 0..n {
assert!(
r.assignments.contains_key(&format!("v{i}")),
"Tabu missing variable v{i}"
);
}
}
let sb = SBSampler::new()
.with_seed(5)
.with_variant(SBVariant::Ballistic)
.with_time_steps(300);
let sb_r = sb.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
for r in &sb_r {
for i in 0..n {
assert!(
r.assignments.contains_key(&format!("v{i}")),
"SB missing variable v{i}"
);
}
}
let pa = PopulationAnnealingSampler::new()
.with_seed(5)
.with_population(20);
let pa_r = pa.run_qubo(&(q.clone(), var_map.clone()), 5).unwrap();
for r in &pa_r {
for i in 0..n {
assert!(
r.assignments.contains_key(&format!("v{i}")),
"PA missing variable v{i}"
);
}
}
}
}