use crate::model::QuboModel;
pub const DEFAULT_MAX_COEFF: i64 = i32::MAX as i64;
#[derive(Debug, Clone, PartialEq)]
pub struct QuantizedQubo {
n: usize,
q: Vec<i64>,
pub scale: f64,
}
impl QuantizedQubo {
pub fn quantize(qubo: &QuboModel<f64>, max_coeff: i64) -> Self {
assert!(max_coeff > 0, "max_coeff must be positive");
let n = qubo.n();
let mut max_abs = 0.0f64;
for i in 0..n {
for j in i..n {
let v = qubo.term(i, j);
assert!(v.is_finite(), "QUBO coefficient ({i},{j}) is not finite");
max_abs = max_abs.max(v.abs());
}
}
let scale = if max_abs == 0.0 {
1.0
} else {
max_coeff as f64 / max_abs
};
let mut q = vec![0i64; n * n];
for i in 0..n {
for j in i..n {
q[i * n + j] = (qubo.term(i, j) * scale).round() as i64;
}
}
Self { n, q, scale }
}
pub fn n(&self) -> usize {
self.n
}
pub fn term(&self, i: usize, j: usize) -> i64 {
let (a, b) = if i <= j { (i, j) } else { (j, i) };
self.q[a * self.n + b]
}
pub fn objective(&self, bits: &[u8]) -> i128 {
assert_eq!(bits.len(), self.n, "bit vector length mismatch");
let mut total: i128 = 0;
for i in 0..self.n {
if bits[i] == 0 {
continue;
}
let row = &self.q[i * self.n..(i + 1) * self.n];
total += row[i] as i128;
for (&coeff, &bit) in row[(i + 1)..].iter().zip(bits[(i + 1)..].iter()) {
if bit != 0 {
total += coeff as i128;
}
}
}
total
}
pub fn dequantize(&self, objective: i128) -> f64 {
objective as f64 / self.scale
}
pub fn to_qubo(&self) -> QuboModel<f64> {
let mut qubo = QuboModel::new(self.n);
for i in 0..self.n {
for j in i..self.n {
let v = self.q[i * self.n + j];
if v != 0 {
qubo.set_term(i, j, v as f64);
}
}
}
qubo
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quantize_and_score() {
let mut q = QuboModel::<f64>::new(2);
q.set_term(0, 0, -1.0);
q.set_term(1, 1, 0.5);
q.set_term(0, 1, 0.25);
let qq = QuantizedQubo::quantize(&q, 1000);
assert_eq!(qq.term(0, 0), -1000);
assert_eq!(qq.term(1, 1), 500);
assert_eq!(qq.term(0, 1), 250);
assert_eq!(qq.objective(&[1, 0]), -1000);
assert_eq!(qq.objective(&[1, 1]), -1000 + 500 + 250);
assert!((qq.dequantize(qq.objective(&[1, 1])) - (-0.25)).abs() < 1e-9);
}
#[test]
fn zero_qubo_scale_is_one() {
let q = QuboModel::<f64>::new(3);
let qq = QuantizedQubo::quantize(&q, DEFAULT_MAX_COEFF);
assert_eq!(qq.scale, 1.0);
assert_eq!(qq.objective(&[1, 1, 1]), 0);
}
#[test]
fn integer_roundtrip_matches_float_model() {
let mut q = QuboModel::<f64>::new(4);
q.set_term(0, 0, 3.7);
q.set_term(1, 2, -1.9);
q.set_term(3, 3, 0.61);
q.set_term(0, 3, 2.2);
let qq = QuantizedQubo::quantize(&q, DEFAULT_MAX_COEFF);
let as_float = qq.to_qubo();
let bits = [1u8, 1, 1, 1];
assert_eq!(as_float.objective(&bits), qq.objective(&bits) as f64);
}
}