use scirs2_core::ndarray::{Array2, ArrayD, ArrayView3, ArrayView4, Dimension, Ix3, Ix4, IxDyn};
use scirs2_core::parallel_ops::*;
use std::collections::HashMap;
pub fn build_dense_q(n: usize, edges: &HashMap<(usize, usize), f64>) -> Vec<f64> {
let mut q = vec![0.0f64; n * n];
for (&(i, j), &val) in edges {
assert!(
i < n && j < n,
"edge index out of bounds: ({i},{j}) for n={n}"
);
q[i * n + j] += val;
}
q
}
pub fn build_dense_q_from_array(q_array: &Array2<f64>) -> Result<Vec<f64>, String> {
let (rows, cols) = q_array.dim();
if rows != cols {
return Err(format!("QUBO matrix must be square, got {}x{}", rows, cols));
}
let q_flat = q_array
.as_slice()
.ok_or_else(|| "Non-contiguous QUBO matrix — cannot extract flat slice".to_string())?
.to_vec();
Ok(q_flat)
}
#[inline]
pub fn energy_full(state: &[bool], q_matrix: &[f64], n: usize) -> f64 {
debug_assert_eq!(state.len(), n, "state length mismatch");
debug_assert_eq!(q_matrix.len(), n * n, "q_matrix length mismatch");
let mut energy = 0.0f64;
for i in 0..n {
if !state[i] {
continue;
}
for j in 0..n {
if state[j] {
energy += q_matrix[i * n + j];
}
}
}
energy
}
#[inline]
pub fn energy_full_simd(state: &[bool], q_matrix: &[f64], n: usize) -> f64 {
energy_full(state, q_matrix, n)
}
#[inline]
pub fn compute_influence(state: &[bool], q_matrix: &[f64], n: usize) -> Vec<f64> {
let mut g = vec![0.0f64; n];
for i in 0..n {
g[i] = q_matrix[i * n + i];
for j in 0..n {
if j != i && state[j] {
g[i] += q_matrix[i * n + j] + q_matrix[j * n + i];
}
}
}
g
}
#[inline]
pub fn compute_influence_simd(state: &[bool], q_matrix: &[f64], n: usize) -> Vec<f64> {
compute_influence(state, q_matrix, n)
}
#[inline]
pub fn energy_delta(state: &[bool], q_matrix: &[f64], n: usize, k: usize) -> f64 {
let g_k = {
let mut g = q_matrix[k * n + k];
for j in 0..n {
if j != k && state[j] {
g += q_matrix[k * n + j] + q_matrix[j * n + k];
}
}
g
};
(1.0 - 2.0 * if state[k] { 1.0 } else { 0.0 }) * g_k
}
#[inline]
pub fn energy_delta_simd(state: &[bool], q_matrix: &[f64], n: usize, k: usize) -> f64 {
energy_delta(state, q_matrix, n, k)
}
#[inline]
pub fn update_influence(g: &mut [f64], q_matrix: &[f64], n: usize, k: usize, new_val: bool) {
let delta = if new_val { 1.0 } else { -1.0 };
for i in 0..n {
if i != k {
g[i] += (q_matrix[i * n + k] + q_matrix[k * n + i]) * delta;
}
}
}
#[inline]
pub fn update_influence_simd(g: &mut [f64], q_matrix: &[f64], n: usize, k: usize, new_val: bool) {
update_influence(g, q_matrix, n, k, new_val)
}
pub fn energy_full_from_array(state: &[bool], q_array: &Array2<f64>) -> Result<f64, String> {
let q_flat = build_dense_q_from_array(q_array)?;
let n = q_array.dim().0;
Ok(energy_full_simd(state, &q_flat, n))
}
pub fn energy_delta_from_array(
state: &[bool],
q_array: &Array2<f64>,
k: usize,
) -> Result<f64, String> {
let q_flat = build_dense_q_from_array(q_array)?;
let n = q_array.dim().0;
Ok(energy_delta_simd(state, &q_flat, n, k))
}
pub fn hobo_energy_full(state: &[bool], tensor: &ArrayD<f64>) -> f64 {
let mut energy = 0.0f64;
for (indices, &coeff) in tensor.indexed_iter() {
if coeff == 0.0 {
continue;
}
let mut active = true;
for &idx in indices.slice() {
if !state[idx] {
active = false;
break;
}
}
if active {
energy += coeff;
}
}
energy
}
const HOBO_3BODY_PARALLEL_THRESHOLD: usize = 32;
pub fn hobo_energy_full_3body(state: &[bool], tensor: ArrayView3<f64>) -> f64 {
let n = tensor.dim().0;
if n >= HOBO_3BODY_PARALLEL_THRESHOLD {
(0..n)
.into_par_iter()
.filter(|&i| state[i])
.map(|i| {
let mut partial = 0.0f64;
for j in 0..n {
if !state[j] {
continue;
}
for l in 0..n {
if state[l] {
partial += tensor[[i, j, l]];
}
}
}
partial
})
.sum()
} else {
let mut energy = 0.0f64;
for i in 0..n {
if !state[i] {
continue;
}
for j in 0..n {
if !state[j] {
continue;
}
for l in 0..n {
if state[l] {
energy += tensor[[i, j, l]];
}
}
}
}
energy
}
}
const HOBO_4BODY_PARALLEL_THRESHOLD: usize = 16;
pub fn hobo_energy_full_4body(state: &[bool], tensor: ArrayView4<f64>) -> f64 {
let n = tensor.dim().0;
if n >= HOBO_4BODY_PARALLEL_THRESHOLD {
(0..n)
.into_par_iter()
.filter(|&i| state[i])
.map(|i| {
let mut partial = 0.0f64;
for j in 0..n {
if !state[j] {
continue;
}
for l in 0..n {
if !state[l] {
continue;
}
for m in 0..n {
if state[m] {
partial += tensor[[i, j, l, m]];
}
}
}
}
partial
})
.sum()
} else {
let mut energy = 0.0f64;
for i in 0..n {
if !state[i] {
continue;
}
for j in 0..n {
if !state[j] {
continue;
}
for l in 0..n {
if !state[l] {
continue;
}
for m in 0..n {
if state[m] {
energy += tensor[[i, j, l, m]];
}
}
}
}
}
energy
}
}
pub fn hobo_energy_full_dispatch(state: &[bool], tensor: &ArrayD<f64>) -> f64 {
let ndim = tensor.ndim();
let n = state.len();
match ndim {
2 => {
let shape = tensor.shape();
if shape[0] == n && shape[1] == n {
if let Some(flat) = tensor.as_slice() {
return energy_full_simd(state, flat, n);
}
}
hobo_energy_full(state, tensor)
}
3 => {
if let Ok(view3) = tensor.view().into_dimensionality::<Ix3>() {
hobo_energy_full_3body(state, view3)
} else {
hobo_energy_full(state, tensor)
}
}
4 => {
if let Ok(view4) = tensor.view().into_dimensionality::<Ix4>() {
hobo_energy_full_4body(state, view4)
} else {
hobo_energy_full(state, tensor)
}
}
_ => hobo_energy_full(state, tensor),
}
}
pub fn hobo_energy_delta(state: &[bool], tensor: &ArrayD<f64>, k: usize) -> f64 {
let g_k = hobo_influence_at(state, tensor, k);
(1.0 - 2.0 * if state[k] { 1.0 } else { 0.0 }) * g_k
}
pub fn hobo_energy_delta_3body(state: &[bool], tensor: ArrayView3<f64>, k: usize) -> f64 {
let n = tensor.dim().0;
let mut g_k = 0.0f64;
for i in 0..n {
for j in 0..n {
for l in 0..n {
if i != k && j != k && l != k {
continue;
}
let coeff = tensor[[i, j, l]];
if coeff == 0.0 {
continue;
}
if (i != k && !state[i]) || (j != k && !state[j]) || (l != k && !state[l]) {
continue;
}
g_k += coeff;
}
}
}
(1.0 - 2.0 * if state[k] { 1.0 } else { 0.0 }) * g_k
}
pub fn hobo_energy_delta_4body(state: &[bool], tensor: ArrayView4<f64>, k: usize) -> f64 {
let n = tensor.dim().0;
let mut g_k = 0.0f64;
for i in 0..n {
for j in 0..n {
for l in 0..n {
for m in 0..n {
if i != k && j != k && l != k && m != k {
continue;
}
let coeff = tensor[[i, j, l, m]];
if coeff == 0.0 {
continue;
}
if (i != k && !state[i])
|| (j != k && !state[j])
|| (l != k && !state[l])
|| (m != k && !state[m])
{
continue;
}
g_k += coeff;
}
}
}
}
(1.0 - 2.0 * if state[k] { 1.0 } else { 0.0 }) * g_k
}
pub fn hobo_compute_influence(state: &[bool], tensor: &ArrayD<f64>) -> Vec<f64> {
let n = state.len();
let mut g = vec![0.0f64; n];
for k in 0..n {
g[k] = hobo_influence_at(state, tensor, k);
}
g
}
pub fn hobo_update_influence(g: &mut [f64], tensor: &ArrayD<f64>, k: usize, new_val: bool) {
let n = g.len();
let delta = if new_val { 1.0 } else { -1.0 };
for q in 0..n {
if q == k {
continue;
}
let mut dg_q = 0.0f64;
for (indices, &coeff) in tensor.indexed_iter() {
if coeff == 0.0 {
continue;
}
let idx_slice = indices.slice();
let has_q = idx_slice.contains(&q);
let has_k = idx_slice.contains(&k);
if !has_q || !has_k {
continue;
}
let cnt_q = idx_slice.iter().filter(|&&i| i == q).count();
let mut prod = 1.0f64;
let mut feasible = true;
for &idx in idx_slice {
if idx == q || idx == k {
continue;
}
let _ = idx;
feasible = false;
prod = 0.0;
break;
}
if feasible {
dg_q += coeff * (cnt_q as f64) * prod * delta;
}
}
g[q] += dg_q;
}
}
pub fn hobo_recompute_influence(g: &mut [f64], state: &[bool], tensor: &ArrayD<f64>) {
let n = g.len();
for k in 0..n {
g[k] = hobo_influence_at(state, tensor, k);
}
}
#[inline]
fn hobo_influence_at(state: &[bool], tensor: &ArrayD<f64>, k: usize) -> f64 {
let mut g_k = 0.0f64;
for (indices, &coeff) in tensor.indexed_iter() {
if coeff == 0.0 {
continue;
}
let idx_slice = indices.slice();
let cnt = idx_slice.iter().filter(|&&i| i == k).count();
if cnt == 0 {
continue;
}
let mut all_other_active = true;
for &idx in idx_slice {
if idx != k && !state[idx] {
all_other_active = false;
break;
}
}
if all_other_active {
g_k += coeff;
}
}
g_k
}
pub fn hobo_to_qubo(
tensor: &ArrayD<f64>,
var_map: &HashMap<String, usize>,
) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
let ndim = tensor.ndim();
if ndim == 2 {
let q = tensor
.view()
.into_dimensionality::<scirs2_core::ndarray::Ix2>()
.map_err(|e| format!("QUBO conversion: {e}"))?
.to_owned();
return Ok((q, var_map.clone()));
}
if ndim != 3 {
return Err(format!(
"hobo_to_qubo: only 2D and 3D tensors are supported, got {ndim}D"
));
}
let shape = tensor.shape();
let n = shape[0];
if shape[1] != n || shape[2] != n {
return Err(format!("hobo_to_qubo: tensor must be cubic, got {shape:?}"));
}
let mut pair_to_aux: HashMap<(usize, usize), usize> = HashMap::new();
for i in 0..n {
for j in (i + 1)..n {
for k in (j + 1)..n {
if tensor[[i, j, k]].abs() > 1e-15
|| tensor[[i, k, j]].abs() > 1e-15
|| tensor[[j, i, k]].abs() > 1e-15
|| tensor[[j, k, i]].abs() > 1e-15
|| tensor[[k, i, j]].abs() > 1e-15
|| tensor[[k, j, i]].abs() > 1e-15
{
for &(a, b) in &[(i, j), (i, k), (j, k)] {
let count = pair_to_aux.len();
pair_to_aux.entry((a, b)).or_insert(n + count);
}
}
}
}
}
let n_aux = pair_to_aux.len();
let n_total = n + n_aux;
let max_coeff = tensor
.iter()
.fold(0.0_f64, |m, &v| if v.abs() > m { v.abs() } else { m });
let penalty = (1.0 + max_coeff) * (n as f64);
let mut q = Array2::<f64>::zeros((n_total, n_total));
for (&(i, j), &y) in &pair_to_aux {
q[[y, y]] += 3.0 * penalty;
let (r, c) = if i <= j { (i, j) } else { (j, i) };
q[[r, c]] += penalty;
let (r2, c2) = if i <= y { (i, y) } else { (y, i) };
q[[r2, c2]] -= 2.0 * penalty;
let (r3, c3) = if j <= y { (j, y) } else { (y, j) };
q[[r3, c3]] -= 2.0 * penalty;
}
for i in 0..n {
for j in (i + 1)..n {
for k in (j + 1)..n {
let t_sym = tensor[[i, j, k]]
+ tensor[[i, k, j]]
+ tensor[[j, i, k]]
+ tensor[[j, k, i]]
+ tensor[[k, i, j]]
+ tensor[[k, j, i]];
if t_sym.abs() < 1e-15 {
continue;
}
let t_third = t_sym / 3.0;
for &(a, b, c) in &[(i, j, k), (i, k, j), (j, k, i)] {
if let Some(&y_ab) = pair_to_aux.get(&(a, b)) {
let (r, col) = if y_ab <= c { (y_ab, c) } else { (c, y_ab) };
q[[r, col]] += t_third;
}
}
}
}
}
let mut ext_map = var_map.clone();
for (&(i, j), &aux_idx) in &pair_to_aux {
ext_map.insert(format!("_aux_{i}_{j}"), aux_idx);
}
Ok((q, ext_map))
}
#[cfg(test)]
#[allow(
clippy::unreadable_literal,
clippy::suboptimal_flops,
clippy::identity_op,
clippy::erasing_op,
clippy::needless_range_loop,
clippy::redundant_clone
)]
mod tests {
use super::*;
fn make_qubo(n: usize, seed: u64) -> Vec<f64> {
let mut q = vec![0.0f64; n * n];
let mut state = seed;
let lcg = |s: &mut u64| -> f64 {
*s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*s >> 33) as f64) / (u32::MAX as f64) * 4.0 - 2.0
};
for i in 0..n {
q[i * n + i] = lcg(&mut state);
for j in (i + 1)..n {
let v = lcg(&mut state);
q[i * n + j] = v;
q[j * n + i] = v;
}
}
q
}
fn flip(state: &[bool], k: usize) -> Vec<bool> {
let mut s = state.to_vec();
s[k] = !s[k];
s
}
fn zeros(n: usize) -> Vec<bool> {
vec![false; n]
}
fn ones(n: usize) -> Vec<bool> {
vec![true; n]
}
#[test]
fn test_energy_full_all_zeros() {
let q = make_qubo(8, 1);
let s = zeros(8);
assert!((energy_full(&s, &q, 8) - 0.0).abs() < 1e-15);
}
#[test]
fn test_energy_full_all_ones_diagonal_only() {
let n = 4;
let mut q = vec![0.0f64; n * n];
q[0 * n + 0] = 1.0;
q[1 * n + 1] = 2.0;
q[2 * n + 2] = 3.0;
q[3 * n + 3] = 4.0;
let s = ones(n);
let e = energy_full(&s, &q, n);
assert!((e - 10.0).abs() < 1e-12, "Expected 10.0, got {e}");
}
#[test]
fn test_energy_full_single_quadratic() {
let n = 4;
let mut q = vec![0.0f64; n * n];
q[0 * n + 1] = 1.0;
q[1 * n + 0] = 1.0;
let mut s = zeros(n);
s[0] = true;
s[1] = true;
let e = energy_full(&s, &q, n);
assert!((e - 2.0).abs() < 1e-12, "Expected 2.0, got {e}");
}
#[test]
fn test_energy_delta_matches_full_diff_n4() {
let n = 4;
let q = make_qubo(n, 42);
for bits in 0u16..16 {
let state: Vec<bool> = (0..n).map(|i| (bits >> i) & 1 == 1).collect();
for k in 0..n {
let delta = energy_delta(&state, &q, n, k);
let flipped = flip(&state, k);
let e0 = energy_full(&state, &q, n);
let e1 = energy_full(&flipped, &q, n);
let expected = e1 - e0;
assert!(
(delta - expected).abs() < 1e-12,
"n=4, bits={bits:#06b}, k={k}: delta={delta}, expected={expected}"
);
}
}
}
#[test]
fn test_energy_delta_matches_full_diff_n8() {
let n = 8;
let q = make_qubo(n, 77);
let mut state = zeros(n);
state[0] = true;
state[3] = true;
state[7] = true;
for k in 0..n {
let delta = energy_delta(&state, &q, n, k);
let e0 = energy_full(&state, &q, n);
let e1 = energy_full(&flip(&state, k), &q, n);
let expected = e1 - e0;
assert!(
(delta - expected).abs() < 1e-12,
"n=8, k={k}: delta={delta}, expected={expected}"
);
}
}
#[test]
fn test_simd_energy_full_matches_scalar_n32() {
let n = 32;
let q = make_qubo(n, 111);
let mut state = zeros(n);
for i in (0..n).step_by(3) {
state[i] = true;
}
let scalar = energy_full(&state, &q, n);
let simd = energy_full_simd(&state, &q, n);
assert!(
(simd - scalar).abs() < 1e-10,
"n=32 SIMD vs scalar: {simd} vs {scalar}"
);
}
#[test]
fn test_simd_energy_full_matches_scalar_n64() {
let n = 64;
let q = make_qubo(n, 222);
let state: Vec<bool> = (0..n).map(|i| i % 2 == 0).collect();
let scalar = energy_full(&state, &q, n);
let simd = energy_full_simd(&state, &q, n);
assert!(
(simd - scalar).abs() < 1e-10,
"n=64 SIMD vs scalar: {simd} vs {scalar}"
);
}
#[test]
fn test_simd_energy_delta_matches_scalar_n32() {
let n = 32;
let q = make_qubo(n, 333);
let state: Vec<bool> = (0..n).map(|i| i % 3 != 0).collect();
for k in 0..n {
let scalar = energy_delta(&state, &q, n, k);
let simd = energy_delta_simd(&state, &q, n, k);
assert!(
(simd - scalar).abs() < 1e-10,
"n=32, k={k}: simd={simd} scalar={scalar}"
);
}
}
#[test]
fn test_simd_energy_delta_matches_scalar_n64() {
let n = 64;
let q = make_qubo(n, 444);
let state: Vec<bool> = (0..n).map(|i| (i * 7 + 3) % 5 > 2).collect();
for k in 0..n {
let scalar = energy_delta(&state, &q, n, k);
let simd = energy_delta_simd(&state, &q, n, k);
assert!(
(simd - scalar).abs() < 1e-10,
"n=64, k={k}: simd={simd} scalar={scalar}"
);
}
}
#[test]
fn test_simd_energy_delta_matches_scalar_n128() {
let n = 128;
let q = make_qubo(n, 555);
let state: Vec<bool> = (0..n).map(|i| i % 4 < 2).collect();
for k in [0, 1, 31, 32, 63, 64, 127] {
let scalar = energy_delta(&state, &q, n, k);
let simd = energy_delta_simd(&state, &q, n, k);
assert!(
(simd - scalar).abs() < 1e-10,
"n=128, k={k}: simd={simd} scalar={scalar}"
);
}
}
#[test]
fn test_compute_influence_correctness_n4() {
let n = 4;
let q = make_qubo(n, 999);
let state: Vec<bool> = vec![true, false, true, false];
let g = compute_influence(&state, &q, n);
for k in 0..n {
let delta_from_g = (1.0 - 2.0 * if state[k] { 1.0 } else { 0.0 }) * g[k];
let delta_direct = energy_delta(&state, &q, n, k);
assert!(
(delta_from_g - delta_direct).abs() < 1e-12,
"k={k}: from_g={delta_from_g}, direct={delta_direct}"
);
}
}
#[test]
fn test_compute_influence_simd_matches_scalar_n32() {
let n = 32;
let q = make_qubo(n, 888);
let state: Vec<bool> = (0..n).map(|i| i % 2 == 0).collect();
let g_scalar = compute_influence(&state, &q, n);
let g_simd = compute_influence_simd(&state, &q, n);
for i in 0..n {
assert!(
(g_simd[i] - g_scalar[i]).abs() < 1e-10,
"i={i}: simd={} scalar={}",
g_simd[i],
g_scalar[i]
);
}
}
#[test]
fn test_update_influence_matches_recompute_n4() {
let n = 4;
let q = make_qubo(n, 1234);
let state: Vec<bool> = vec![true, false, true, false];
let mut g = compute_influence(&state, &q, n);
let k = 1;
let new_val = !state[k];
update_influence(&mut g, &q, n, k, new_val);
let mut new_state = state.clone();
new_state[k] = new_val;
let g_expected = compute_influence(&new_state, &q, n);
for i in 0..n {
assert!(
(g[i] - g_expected[i]).abs() < 1e-12,
"i={i}: updated={}, expected={}",
g[i],
g_expected[i]
);
}
}
#[test]
fn test_update_influence_simd_matches_scalar_n32() {
let n = 32;
let q = make_qubo(n, 5678);
let state: Vec<bool> = (0..n).map(|i| i % 3 == 0).collect();
let g_ref = compute_influence(&state, &q, n);
for k in [0, 1, 15, 16, 31] {
let mut g_scalar = g_ref.clone();
let mut g_simd = g_ref.clone();
let new_val = !state[k];
update_influence(&mut g_scalar, &q, n, k, new_val);
update_influence_simd(&mut g_simd, &q, n, k, new_val);
for i in 0..n {
assert!(
(g_simd[i] - g_scalar[i]).abs() < 1e-10,
"k={k}, i={i}: simd={} scalar={}",
g_simd[i],
g_scalar[i]
);
}
}
}
#[test]
fn test_build_dense_q_basic() {
let mut edges = HashMap::new();
edges.insert((0, 0), -1.0f64);
edges.insert((1, 1), -1.0f64);
edges.insert((0, 1), 2.0f64);
let q = build_dense_q(2, &edges);
assert_eq!(q.len(), 4);
assert!((q[0 * 2 + 0] - (-1.0)).abs() < 1e-12);
assert!((q[0 * 2 + 1] - 2.0).abs() < 1e-12);
assert!((q[1 * 2 + 0] - 0.0).abs() < 1e-12); assert!((q[1 * 2 + 1] - (-1.0)).abs() < 1e-12);
}
#[test]
fn test_energy_full_from_array_matches_flat() {
use scirs2_core::ndarray::Array2;
let n = 4;
let q = make_qubo(n, 777);
let q_array = Array2::from_shape_vec((n, n), q.clone()).expect("Array2 creation failed");
let state: Vec<bool> = vec![true, false, true, true];
let e_flat = energy_full_simd(&state, &q, n);
let e_array = energy_full_from_array(&state, &q_array).expect("Array energy failed");
assert!(
(e_flat - e_array).abs() < 1e-12,
"flat={e_flat}, array={e_array}"
);
}
}