use crate::error::{QuantumError, QuantumResult};
#[derive(Debug, Clone)]
pub struct QaoaWarmStartConfig {
pub n_qubits: usize,
pub n_iter: usize,
pub lr: f32,
pub mixing_angle: f32,
}
#[derive(Debug, Clone)]
pub struct QaoaWarmStart {
cfg: QaoaWarmStartConfig,
}
impl QaoaWarmStart {
pub fn new(cfg: QaoaWarmStartConfig) -> QuantumResult<Self> {
if cfg.n_qubits == 0 {
return Err(QuantumError::InvalidQubitCount { n: cfg.n_qubits });
}
if cfg.n_iter == 0 {
return Err(QuantumError::InvalidParameter {
name: "n_iter".to_string(),
});
}
if !cfg.lr.is_finite() || cfg.lr <= 0.0 {
return Err(QuantumError::InvalidParameter {
name: "lr".to_string(),
});
}
if !cfg.mixing_angle.is_finite()
|| cfg.mixing_angle < 0.0
|| cfg.mixing_angle > std::f32::consts::FRAC_PI_2
{
return Err(QuantumError::InvalidParameter {
name: "mixing_angle".to_string(),
});
}
Ok(Self { cfg })
}
#[must_use]
pub fn config(&self) -> &QaoaWarmStartConfig {
&self.cfg
}
pub fn continuous_relax(&self, edges: &[(usize, usize, f32)]) -> QuantumResult<Vec<f32>> {
let n = self.cfg.n_qubits;
for &(i, j, w) in edges {
if i >= n {
return Err(QuantumError::QubitIndexOutOfRange {
index: i,
n_qubits: n,
});
}
if j >= n {
return Err(QuantumError::QubitIndexOutOfRange {
index: j,
n_qubits: n,
});
}
if !w.is_finite() {
return Err(QuantumError::InvalidParameter {
name: "edge_weight".to_string(),
});
}
}
let mut c = vec![0.5_f32; n];
let mut grad = vec![0.0_f32; n];
let lr = self.cfg.lr;
for _step in 0..self.cfg.n_iter {
for slot in grad.iter_mut() {
*slot = 0.0;
}
for &(i, j, w) in edges {
grad[i] += w * (1.0 - 2.0 * c[j]);
grad[j] += w * (1.0 - 2.0 * c[i]);
}
for (ci, gi) in c.iter_mut().zip(grad.iter()) {
*ci = (*ci + lr * *gi).clamp(0.0, 1.0);
}
}
Ok(c)
}
pub fn arcsin_init(&self, c: &[f32]) -> QuantumResult<Vec<f32>> {
if c.len() != self.cfg.n_qubits {
return Err(QuantumError::DimensionMismatch {
expected: self.cfg.n_qubits,
got: c.len(),
});
}
let mut theta = Vec::with_capacity(c.len());
for &ci in c {
if !ci.is_finite() || !(0.0..=1.0).contains(&ci) {
return Err(QuantumError::InvalidParameter {
name: "c_component".to_string(),
});
}
let sqrt_c = ci.sqrt().clamp(0.0, 1.0);
theta.push(2.0 * sqrt_c.asin());
}
Ok(theta)
}
pub fn warm_start_circuit_params(&self, c: &[f32], depth: usize) -> QuantumResult<Vec<f32>> {
if depth == 0 {
return Err(QuantumError::InvalidParameter {
name: "depth".to_string(),
});
}
let theta = self.arcsin_init(c)?;
let mut params = Vec::with_capacity(self.cfg.n_qubits + 2 * depth);
params.extend_from_slice(&theta);
params.extend(std::iter::repeat_n(0.0_f32, 2 * depth));
Ok(params)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_cfg(n_qubits: usize) -> QaoaWarmStartConfig {
QaoaWarmStartConfig {
n_qubits,
n_iter: 50,
lr: 0.1,
mixing_angle: std::f32::consts::FRAC_PI_4,
}
}
#[test]
fn continuous_relax_output_length_and_range() {
let ws = QaoaWarmStart::new(default_cfg(4)).unwrap();
let edges = vec![(0, 1, 1.0_f32), (1, 2, 0.5), (2, 3, 0.3)];
let c = ws.continuous_relax(&edges).unwrap();
assert_eq!(c.len(), 4);
for &ci in &c {
assert!((0.0..=1.0).contains(&ci), "ci={ci} not in [0,1]");
}
}
#[test]
fn continuous_relax_two_node_converges_to_anti_correlation() {
let cfg = QaoaWarmStartConfig {
n_qubits: 2,
n_iter: 200,
lr: 0.2,
mixing_angle: 0.0,
};
let ws = QaoaWarmStart::new(cfg).unwrap();
let edges = vec![(0, 1, 1.0_f32)];
let c_saddle = ws.continuous_relax(&edges).unwrap();
assert!((c_saddle[0] - 0.5).abs() < 1e-6 && (c_saddle[1] - 0.5).abs() < 1e-6);
let f_saddle = 0.5 + 0.5 - 2.0 * 0.5 * 0.5; let f_corner = 1.0 + 0.0 - 2.0 * 1.0 * 0.0; assert!(
f_corner > f_saddle,
"corner should dominate saddle: {f_corner} vs {f_saddle}"
);
}
#[test]
fn continuous_relax_no_edges_stays_at_init() {
let ws = QaoaWarmStart::new(default_cfg(3)).unwrap();
let c = ws.continuous_relax(&[]).unwrap();
for &ci in &c {
assert!((ci - 0.5).abs() < 1e-7, "ci={ci}");
}
}
#[test]
fn continuous_relax_triangle_nonuniform_optimum_exists() {
let cfg = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 100,
lr: 0.3,
mixing_angle: 0.0,
};
let ws = QaoaWarmStart::new(cfg).unwrap();
let edges = vec![(0, 1, 1.0_f32), (1, 2, 1.0), (0, 2, 1.0)];
let c = ws.continuous_relax(&edges).unwrap();
let f = |c: &[f32]| -> f32 {
edges
.iter()
.map(|&(i, j, w)| w * (c[i] + c[j] - 2.0 * c[i] * c[j]))
.sum()
};
let f_saddle = f(&c);
let f_corner = f(&[1.0, 1.0, 0.0]);
assert!(
f_corner > f_saddle + 1e-3,
"non-uniform corner (1,1,0) should dominate the relaxer's saddle: corner={f_corner} relaxer={f_saddle}"
);
assert!((f(&[1.0, 0.0, 1.0]) - f_corner).abs() < 1e-5);
assert!((f(&[0.0, 1.0, 1.0]) - f_corner).abs() < 1e-5);
}
#[test]
fn arcsin_init_zero_gives_zero() {
let ws = QaoaWarmStart::new(default_cfg(1)).unwrap();
let theta = ws.arcsin_init(&[0.0]).unwrap();
assert!(theta[0].abs() < 1e-7, "theta={}", theta[0]);
}
#[test]
fn arcsin_init_one_gives_pi() {
let ws = QaoaWarmStart::new(default_cfg(1)).unwrap();
let theta = ws.arcsin_init(&[1.0]).unwrap();
assert!(
(theta[0] - std::f32::consts::PI).abs() < 1e-6,
"theta={}",
theta[0]
);
}
#[test]
fn arcsin_init_half_gives_pi_over_two() {
let ws = QaoaWarmStart::new(default_cfg(1)).unwrap();
let theta = ws.arcsin_init(&[0.5]).unwrap();
assert!(
(theta[0] - std::f32::consts::FRAC_PI_2).abs() < 1e-6,
"theta={}",
theta[0]
);
}
#[test]
fn arcsin_init_range_in_zero_pi() {
let ws = QaoaWarmStart::new(default_cfg(5)).unwrap();
let c = vec![0.0, 0.1, 0.4, 0.7, 1.0];
let theta = ws.arcsin_init(&c).unwrap();
for &t in &theta {
assert!(
(0.0..=std::f32::consts::PI).contains(&t),
"theta={t} not in [0, π]"
);
}
for k in 1..theta.len() {
assert!(theta[k] >= theta[k - 1] - 1e-6);
}
}
#[test]
fn warm_start_circuit_params_length() {
let ws = QaoaWarmStart::new(default_cfg(4)).unwrap();
let c = vec![0.1_f32, 0.4, 0.6, 0.9];
let params = ws.warm_start_circuit_params(&c, 3).unwrap();
assert_eq!(params.len(), 4 + 2 * 3);
}
#[test]
fn warm_start_circuit_params_gammas_betas_zero() {
let ws = QaoaWarmStart::new(default_cfg(3)).unwrap();
let c = vec![0.2_f32, 0.5, 0.8];
let depth = 2;
let params = ws.warm_start_circuit_params(&c, depth).unwrap();
for (i, &val) in params.iter().enumerate().skip(3).take(2 * depth) {
assert!(
val.abs() < 1e-12,
"expected (γ,β) zero at idx {i} got {val}"
);
}
let theta = ws.arcsin_init(&c).unwrap();
for (i, &t) in theta.iter().enumerate() {
assert!((params[i] - t).abs() < 1e-7);
}
}
#[test]
fn deterministic_relax() {
let ws_a = QaoaWarmStart::new(default_cfg(3)).unwrap();
let ws_b = QaoaWarmStart::new(default_cfg(3)).unwrap();
let edges = vec![(0, 1, 0.7_f32), (1, 2, 0.3), (0, 2, 0.5)];
let a = ws_a.continuous_relax(&edges).unwrap();
let b = ws_b.continuous_relax(&edges).unwrap();
assert_eq!(a, b);
}
#[test]
fn err_n_qubits_zero() {
let cfg = QaoaWarmStartConfig {
n_qubits: 0,
n_iter: 10,
lr: 0.1,
mixing_angle: 0.0,
};
assert!(QaoaWarmStart::new(cfg).is_err());
}
#[test]
fn err_n_iter_zero() {
let cfg = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 0,
lr: 0.1,
mixing_angle: 0.0,
};
assert!(QaoaWarmStart::new(cfg).is_err());
}
#[test]
fn err_non_positive_lr() {
let cfg_zero = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 10,
lr: 0.0,
mixing_angle: 0.0,
};
let cfg_neg = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 10,
lr: -0.1,
mixing_angle: 0.0,
};
assert!(QaoaWarmStart::new(cfg_zero).is_err());
assert!(QaoaWarmStart::new(cfg_neg).is_err());
}
#[test]
fn err_mixing_angle_out_of_range() {
let cfg_neg = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 10,
lr: 0.1,
mixing_angle: -0.1,
};
let cfg_big = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 10,
lr: 0.1,
mixing_angle: std::f32::consts::FRAC_PI_2 + 0.1,
};
assert!(QaoaWarmStart::new(cfg_neg).is_err());
assert!(QaoaWarmStart::new(cfg_big).is_err());
}
#[test]
fn err_edge_index_out_of_range() {
let ws = QaoaWarmStart::new(default_cfg(3)).unwrap();
let edges = vec![(0, 5, 1.0_f32)];
assert!(ws.continuous_relax(&edges).is_err());
let edges2 = vec![(7, 1, 1.0_f32)];
assert!(ws.continuous_relax(&edges2).is_err());
}
#[test]
fn err_arcsin_init_wrong_length() {
let ws = QaoaWarmStart::new(default_cfg(3)).unwrap();
let r = ws.arcsin_init(&[0.5, 0.5]);
assert!(r.is_err());
let r2 = ws.arcsin_init(&[0.5, 0.5, 0.5, 0.5]);
assert!(r2.is_err());
}
#[test]
fn err_arcsin_init_c_out_of_range() {
let ws = QaoaWarmStart::new(default_cfg(2)).unwrap();
let r_neg = ws.arcsin_init(&[-0.1, 0.5]);
assert!(r_neg.is_err());
let r_big = ws.arcsin_init(&[0.5, 1.1]);
assert!(r_big.is_err());
}
#[test]
fn err_depth_zero() {
let ws = QaoaWarmStart::new(default_cfg(3)).unwrap();
let c = vec![0.5_f32, 0.5, 0.5];
let r = ws.warm_start_circuit_params(&c, 0);
assert!(r.is_err());
}
#[test]
fn err_warm_start_circuit_params_wrong_c_length() {
let ws = QaoaWarmStart::new(default_cfg(4)).unwrap();
let c = vec![0.5_f32, 0.5]; let r = ws.warm_start_circuit_params(&c, 2);
assert!(r.is_err());
}
#[test]
fn empty_edges_allowed_in_continuous_relax() {
let ws = QaoaWarmStart::new(default_cfg(2)).unwrap();
let r = ws.continuous_relax(&[]);
assert!(r.is_ok());
}
#[test]
fn clipping_respected_under_large_lr() {
let cfg = QaoaWarmStartConfig {
n_qubits: 2,
n_iter: 20,
lr: 100.0,
mixing_angle: 0.0,
};
let ws = QaoaWarmStart::new(cfg).unwrap();
let edges = vec![(0, 1, 1.0_f32)];
let c = ws.continuous_relax(&edges).unwrap();
for &ci in &c {
assert!(
(0.0..=1.0).contains(&ci),
"ci={ci} escaped clamp under aggressive lr"
);
}
}
#[test]
fn config_getter_exposes_inner_settings() {
let cfg = QaoaWarmStartConfig {
n_qubits: 5,
n_iter: 7,
lr: 0.25,
mixing_angle: 0.1,
};
let ws = QaoaWarmStart::new(cfg).unwrap();
let inner = ws.config();
assert_eq!(inner.n_qubits, 5);
assert_eq!(inner.n_iter, 7);
assert!((inner.lr - 0.25).abs() < 1e-7);
assert!((inner.mixing_angle - 0.1).abs() < 1e-7);
}
#[test]
fn warm_start_params_full_pipeline() {
let cfg = QaoaWarmStartConfig {
n_qubits: 3,
n_iter: 50,
lr: 0.2,
mixing_angle: std::f32::consts::FRAC_PI_4,
};
let ws = QaoaWarmStart::new(cfg).unwrap();
let edges = vec![(0, 1, 1.0_f32), (1, 2, 1.0)];
let c = ws.continuous_relax(&edges).unwrap();
let params = ws.warm_start_circuit_params(&c, 4).unwrap();
assert_eq!(params.len(), 3 + 2 * 4);
for &p in ¶ms[3..] {
assert!(p.abs() < 1e-12);
}
for &t in ¶ms[..3] {
assert!((0.0..=std::f32::consts::PI).contains(&t), "theta={t}");
}
}
}