Skip to main content

problemreductions/rules/
spinglass_qubo.rs

1//! Reductions between SpinGlass and QUBO problems.
2//!
3//! QUBO: minimize x^T Q x where x in {0, 1}^n
4//! SpinGlass: minimize sum J_ij s_i s_j + sum h_i s_i where s in {-1, +1}^n
5//!
6//! Transformation: s = 2x - 1 (so x=0 -> s=-1, x=1 -> s=+1)
7
8use crate::models::algebraic::QUBO;
9use crate::models::graph::SpinGlass;
10use crate::reduction;
11use crate::rules::traits::{ReduceTo, ReductionResult};
12use crate::topology::SimpleGraph;
13
14/// Result of reducing QUBO to SpinGlass.
15#[derive(Debug, Clone)]
16pub struct ReductionQUBOToSG {
17    target: SpinGlass<SimpleGraph, f64>,
18}
19
20impl ReductionResult for ReductionQUBOToSG {
21    type Source = QUBO<f64>;
22    type Target = SpinGlass<SimpleGraph, f64>;
23
24    fn target_problem(&self) -> &Self::Target {
25        &self.target
26    }
27
28    /// Solution maps directly (same binary encoding).
29    fn extract_solution(
30        &self,
31        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
32    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
33        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
34
35        Ok(target_solution.iter().map(|&spin| spin == 1).collect())
36    }
37}
38
39#[reduction(
40    transform = exact {
41        num_spins = "num_vars",
42        num_interactions = "num_vars^2",
43    },
44)]
45impl ReduceTo<SpinGlass<SimpleGraph, f64>> for QUBO<f64> {
46    type Result = ReductionQUBOToSG;
47
48    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
49        let n = self.num_vars();
50        let matrix = self.matrix();
51
52        // Convert Q matrix to J interactions and h fields
53        // Using substitution s = 2x - 1:
54        // x = (s + 1) / 2
55        // x_i * x_j = ((s_i + 1)/2) * ((s_j + 1)/2) = (s_i*s_j + s_i + s_j + 1) / 4
56        //
57        // For off-diagonal terms Q_ij x_i x_j:
58        //   Q_ij * (s_i*s_j + s_i + s_j + 1) / 4
59        //   = Q_ij/4 * s_i*s_j + Q_ij/4 * s_i + Q_ij/4 * s_j + Q_ij/4
60        //
61        // For diagonal terms Q_ii x_i:
62        //   Q_ii * (s_i + 1) / 2 = Q_ii/2 * s_i + Q_ii/2
63        let mut interactions = Vec::new();
64        let mut onsite = vec![0.0; n];
65
66        for i in 0..n {
67            for j in i..n {
68                let q = matrix[i][j];
69                if q.abs() < 1e-10 {
70                    continue;
71                }
72
73                if i == j {
74                    // Diagonal: Q_ii * x_i = Q_ii/2 * s_i + Q_ii/2 (constant)
75                    onsite[i] += q / 2.0;
76                } else {
77                    // Off-diagonal: Q_ij * x_i * x_j
78                    // J_ij contribution
79                    let j_ij = q / 4.0;
80                    if j_ij.abs() > 1e-10 {
81                        interactions.push(((i, j), j_ij));
82                    }
83                    // h_i and h_j contributions
84                    onsite[i] += q / 4.0;
85                    onsite[j] += q / 4.0;
86                }
87            }
88        }
89
90        let target = SpinGlass::<SimpleGraph, f64>::new(n, interactions, onsite).map_err(
91            |cause| {
92                crate::rules::ReductionError::construction::<
93                    QUBO<f64>,
94                    SpinGlass<SimpleGraph, f64>,
95                >(cause)
96            },
97        )?;
98
99        Ok(ReductionQUBOToSG { target })
100    }
101}
102
103/// Result of reducing SpinGlass to QUBO.
104#[derive(Debug, Clone)]
105pub struct ReductionSGToQUBO<W = f64> {
106    target: QUBO<W>,
107}
108
109impl<W> ReductionResult for ReductionSGToQUBO<W>
110where
111    W: crate::types::WeightElement + crate::types::NumericSize + crate::variant::VariantParam,
112{
113    type Source = SpinGlass<SimpleGraph, W>;
114    type Target = QUBO<W>;
115
116    fn target_problem(&self) -> &Self::Target {
117        &self.target
118    }
119
120    fn extract_solution(
121        &self,
122        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
123    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
124        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
125
126        Ok(target_solution
127            .iter()
128            .map(|&bit| if bit { 1 } else { -1 })
129            .collect())
130    }
131}
132
133#[reduction(
134    transform = exact {
135        num_vars = "num_spins",
136    }
137)]
138impl ReduceTo<QUBO<f64>> for SpinGlass<SimpleGraph, f64> {
139    type Result = ReductionSGToQUBO<f64>;
140
141    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
142        let n = self.num_spins();
143        let mut matrix = vec![vec![0.0; n]; n];
144
145        // Convert using s = 2x - 1:
146        // s_i * s_j = (2x_i - 1)(2x_j - 1) = 4x_i*x_j - 2x_i - 2x_j + 1
147        // s_i = 2x_i - 1
148        //
149        // J_ij * s_i * s_j = J_ij * (4x_i*x_j - 2x_i - 2x_j + 1)
150        //                  = 4*J_ij*x_i*x_j - 2*J_ij*x_i - 2*J_ij*x_j + J_ij
151        //
152        // h_i * s_i = h_i * (2x_i - 1) = 2*h_i*x_i - h_i
153        for ((i, j), j_val) in self.interactions() {
154            // Off-diagonal: 4 * J_ij
155            matrix[i][j] += 4.0 * j_val;
156            // Diagonal contributions: -2 * J_ij
157            matrix[i][i] -= 2.0 * j_val;
158            matrix[j][j] -= 2.0 * j_val;
159        }
160
161        // Convert h fields to diagonal
162        for (i, &h) in self.fields().iter().enumerate() {
163            // h_i * s_i -> 2*h_i*x_i
164            matrix[i][i] += 2.0 * h;
165        }
166
167        let target = QUBO::from_matrix(matrix).map_err(|message| {
168            crate::rules::ReductionError::construction::<SpinGlass<SimpleGraph, f64>, QUBO<f64>>(
169                message,
170            )
171        })?;
172
173        Ok(ReductionSGToQUBO { target })
174    }
175}
176
177#[reduction(
178    transform = exact {
179        num_vars = "num_spins",
180    }
181)]
182impl ReduceTo<QUBO<i64>> for SpinGlass<SimpleGraph, i64> {
183    type Result = ReductionSGToQUBO<i64>;
184
185    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
186        let n = self.num_spins();
187        let mut matrix = vec![vec![0_i64; n]; n];
188        let overflow = |operation| {
189            crate::rules::ReductionError::integer_overflow::<SpinGlass<SimpleGraph, i64>, QUBO<i64>>(
190                operation,
191            )
192        };
193
194        for ((i, j), coupling) in self.interactions() {
195            let interaction = coupling
196                .checked_mul(4)
197                .ok_or_else(|| overflow("scaling a spin-glass interaction"))?;
198            matrix[i][j] = matrix[i][j]
199                .checked_add(interaction)
200                .ok_or_else(|| overflow("summing QUBO interaction coefficients"))?;
201            let diagonal = coupling
202                .checked_mul(2)
203                .ok_or_else(|| overflow("scaling a spin-glass diagonal contribution"))?;
204            matrix[i][i] = matrix[i][i]
205                .checked_sub(diagonal)
206                .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?;
207            matrix[j][j] = matrix[j][j]
208                .checked_sub(diagonal)
209                .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?;
210        }
211
212        for (i, &field) in self.fields().iter().enumerate() {
213            let diagonal = field
214                .checked_mul(2)
215                .ok_or_else(|| overflow("scaling a spin-glass field"))?;
216            matrix[i][i] = matrix[i][i]
217                .checked_add(diagonal)
218                .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?;
219        }
220
221        Ok(ReductionSGToQUBO {
222            target:
223                QUBO::from_matrix(matrix).map_err(
224                    crate::rules::ReductionError::construction::<
225                        SpinGlass<SimpleGraph, i64>,
226                        QUBO<i64>,
227                    >,
228                )?,
229        })
230    }
231}
232
233#[cfg(feature = "example-db")]
234pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
235    use crate::export::SolutionPair;
236
237    vec![
238        crate::example_db::specs::RuleExampleSpec {
239            id: "qubo_to_spinglass",
240            build: || {
241                let (n, edges) = crate::topology::small_graphs::petersen();
242                let mut matrix = vec![vec![0.0; n]; n];
243                for (i, row) in matrix.iter_mut().enumerate() {
244                    row[i] = -1.0 + 0.2 * i as f64;
245                }
246                for (idx, &(u, v)) in edges.iter().enumerate() {
247                    let (i, j) = if u < v { (u, v) } else { (v, u) };
248                    matrix[i][j] = if idx % 2 == 0 { 2.0 } else { -1.5 };
249                }
250                let source = QUBO::from_matrix(matrix).unwrap();
251                crate::example_db::specs::rule_example_with_witness::<_, SpinGlass<SimpleGraph, f64>>(
252                    source,
253                    SolutionPair {
254                        source_config: serde_json::json!(vec![
255                            true, false, true, true, true, false, true, false, false, true
256                        ]),
257                        target_config: serde_json::json!(vec![1, -1, 1, 1, 1, -1, 1, -1, -1, 1]),
258                    },
259                )
260            },
261        },
262        crate::example_db::specs::RuleExampleSpec {
263            id: "spinglass_to_qubo",
264            build: || {
265                let (n, edges) = crate::topology::small_graphs::petersen();
266                let couplings: Vec<((usize, usize), f64)> = edges
267                    .iter()
268                    .enumerate()
269                    .map(|(i, &(u, v))| ((u, v), if i % 2 == 0 { 1.0 } else { -1.0 }))
270                    .collect();
271                let source = SpinGlass::new(n, couplings, vec![0.0; n]).unwrap();
272                crate::example_db::specs::rule_example_with_witness::<_, QUBO<f64>>(
273                    source,
274                    SolutionPair {
275                        source_config: serde_json::json!(vec![1, -1, 1, 1, 1, -1, 1, -1, -1, 1]),
276                        target_config: serde_json::json!(vec![
277                            true, false, true, true, true, false, true, false, false, true
278                        ]),
279                    },
280                )
281            },
282        },
283        crate::example_db::specs::RuleExampleSpec {
284            id: "integer_spinglass_to_qubo",
285            build: || {
286                let source =
287                    SpinGlass::<SimpleGraph, i64>::new(2, vec![((0, 1), 1)], vec![0, 0]).unwrap();
288                crate::example_db::specs::rule_example_with_witness::<_, QUBO<i64>>(
289                    source,
290                    SolutionPair {
291                        source_config: serde_json::json!([1, -1]),
292                        target_config: serde_json::json!([true, false]),
293                    },
294                )
295            },
296        },
297    ]
298}
299
300#[cfg(test)]
301#[path = "../unit_tests/rules/spinglass_qubo.rs"]
302mod tests;