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