problemreductions/rules/
spinglass_maxcut.rs

1//! Reductions between SpinGlass and MaxCut problems.
2//!
3//! MaxCut → SpinGlass: Direct mapping, edge weights become J couplings.
4//! SpinGlass → MaxCut: Requires ancilla vertex for onsite terms.
5
6use crate::models::graph::MaxCut;
7use crate::models::optimization::SpinGlass;
8use crate::rules::traits::{ReduceTo, ReductionResult};
9use crate::traits::Problem;
10use crate::types::ProblemSize;
11use num_traits::{Num, Zero};
12use std::ops::AddAssign;
13
14/// Result of reducing MaxCut to SpinGlass.
15#[derive(Debug, Clone)]
16pub struct ReductionMaxCutToSG<W> {
17    target: SpinGlass<W>,
18    source_size: ProblemSize,
19}
20
21impl<W> ReductionResult for ReductionMaxCutToSG<W>
22where
23    W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From<i32> + 'static,
24{
25    type Source = MaxCut<W>;
26    type Target = SpinGlass<W>;
27
28    fn target_problem(&self) -> &Self::Target {
29        &self.target
30    }
31
32    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
33        target_solution.to_vec()
34    }
35
36    fn source_size(&self) -> ProblemSize {
37        self.source_size.clone()
38    }
39
40    fn target_size(&self) -> ProblemSize {
41        self.target.problem_size()
42    }
43}
44
45impl<W> ReduceTo<SpinGlass<W>> for MaxCut<W>
46where
47    W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From<i32> + 'static,
48{
49    type Result = ReductionMaxCutToSG<W>;
50
51    fn reduce_to(&self) -> Self::Result {
52        let n = self.num_vertices();
53        let edges_with_weights = self.edges();
54
55        // MaxCut: maximize sum of w_ij for edges (i,j) where s_i != s_j
56        // SpinGlass: minimize sum of J_ij * s_i * s_j
57        //
58        // For MaxCut, we want to maximize cut, which means:
59        // - When s_i != s_j (opposite spins), edge contributes to cut
60        // - s_i * s_j = -1 when opposite, +1 when same
61        //
62        // To convert: maximize sum(w_ij * [s_i != s_j])
63        //           = maximize sum(w_ij * (1 - s_i*s_j)/2)
64        //           = constant - minimize sum(w_ij * s_i*s_j / 2)
65        //
66        // So J_ij = -w_ij / 2 would work, but since we need to relate
67        // the problems directly, we use J_ij = w_ij and negate.
68        // Actually, for a proper reduction, we set J_ij = w_ij.
69        // MaxCut wants to maximize edges cut, SpinGlass minimizes energy.
70        // When J > 0 (antiferromagnetic), opposite spins lower energy.
71        // So maximizing cut = minimizing Ising energy with J = w.
72        let interactions: Vec<((usize, usize), W)> = edges_with_weights
73            .into_iter()
74            .map(|(u, v, w)| ((u, v), w))
75            .collect();
76
77        // No onsite terms for pure MaxCut
78        let onsite = vec![W::zero(); n];
79
80        let target = SpinGlass::new(n, interactions, onsite);
81
82        ReductionMaxCutToSG {
83            target,
84            source_size: self.problem_size(),
85        }
86    }
87}
88
89/// Result of reducing SpinGlass to MaxCut.
90#[derive(Debug, Clone)]
91pub struct ReductionSGToMaxCut<W> {
92    target: MaxCut<W>,
93    source_size: ProblemSize,
94    /// Ancilla vertex index (None if no ancilla needed).
95    ancilla: Option<usize>,
96}
97
98impl<W> ReductionResult for ReductionSGToMaxCut<W>
99where
100    W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From<i32> + 'static,
101{
102    type Source = SpinGlass<W>;
103    type Target = MaxCut<W>;
104
105    fn target_problem(&self) -> &Self::Target {
106        &self.target
107    }
108
109    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
110        match self.ancilla {
111            None => target_solution.to_vec(),
112            Some(anc) => {
113                // If ancilla is 1, flip all bits; then remove ancilla
114                let mut sol = target_solution.to_vec();
115                if sol[anc] == 1 {
116                    for x in sol.iter_mut() {
117                        *x = 1 - *x;
118                    }
119                }
120                sol.remove(anc);
121                sol
122            }
123        }
124    }
125
126    fn source_size(&self) -> ProblemSize {
127        self.source_size.clone()
128    }
129
130    fn target_size(&self) -> ProblemSize {
131        self.target.problem_size()
132    }
133}
134
135impl<W> ReduceTo<MaxCut<W>> for SpinGlass<W>
136where
137    W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From<i32> + 'static,
138{
139    type Result = ReductionSGToMaxCut<W>;
140
141    fn reduce_to(&self) -> Self::Result {
142        let n = self.num_spins();
143        let interactions = self.interactions();
144        let fields = self.fields();
145
146        // Check if we need an ancilla vertex for onsite terms
147        let need_ancilla = fields.iter().any(|h| !h.is_zero());
148        let total_vertices = if need_ancilla { n + 1 } else { n };
149        let ancilla_idx = if need_ancilla { Some(n) } else { None };
150
151        let mut edges = Vec::new();
152        let mut weights = Vec::new();
153
154        // Add interaction edges
155        for &((i, j), ref w) in interactions {
156            edges.push((i, j));
157            weights.push(w.clone());
158        }
159
160        // Add onsite terms as edges to ancilla
161        // h_i * s_i can be modeled as an edge to ancilla with weight h_i
162        // When s_i and s_ancilla are opposite, the edge is cut
163        if need_ancilla {
164            for (i, h) in fields.iter().enumerate() {
165                if !h.is_zero() {
166                    edges.push((i, n));
167                    weights.push(h.clone());
168                }
169            }
170        }
171
172        let target = MaxCut::with_weights(total_vertices, edges, weights);
173
174        ReductionSGToMaxCut {
175            target,
176            source_size: self.problem_size(),
177            ancilla: ancilla_idx,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::solvers::{BruteForce, Solver};
186
187    #[test]
188    fn test_maxcut_to_spinglass() {
189        // Simple triangle MaxCut
190        let mc = MaxCut::<i32>::unweighted(3, vec![(0, 1), (1, 2), (0, 2)]);
191        let reduction = ReduceTo::<SpinGlass<i32>>::reduce_to(&mc);
192        let sg = reduction.target_problem();
193
194        let solver = BruteForce::new();
195        let solutions = solver.find_best(sg);
196
197        assert!(!solutions.is_empty());
198    }
199
200    #[test]
201    fn test_spinglass_to_maxcut_no_onsite() {
202        // SpinGlass without onsite terms
203        let sg = SpinGlass::new(3, vec![((0, 1), 1), ((1, 2), 1)], vec![0, 0, 0]);
204        let reduction = ReduceTo::<MaxCut<i32>>::reduce_to(&sg);
205        let mc = reduction.target_problem();
206
207        assert_eq!(mc.num_vertices(), 3); // No ancilla needed
208        assert!(reduction.ancilla.is_none());
209    }
210
211    #[test]
212    fn test_spinglass_to_maxcut_with_onsite() {
213        // SpinGlass with onsite terms
214        let sg = SpinGlass::new(2, vec![((0, 1), 1)], vec![1, 0]);
215        let reduction = ReduceTo::<MaxCut<i32>>::reduce_to(&sg);
216        let mc = reduction.target_problem();
217
218        assert_eq!(mc.num_vertices(), 3); // Ancilla added
219        assert_eq!(reduction.ancilla, Some(2));
220    }
221
222    #[test]
223    fn test_solution_extraction_no_ancilla() {
224        let sg = SpinGlass::new(2, vec![((0, 1), 1)], vec![0, 0]);
225        let reduction = ReduceTo::<MaxCut<i32>>::reduce_to(&sg);
226
227        let mc_sol = vec![0, 1];
228        let extracted = reduction.extract_solution(&mc_sol);
229        assert_eq!(extracted, vec![0, 1]);
230    }
231
232    #[test]
233    fn test_solution_extraction_with_ancilla() {
234        let sg = SpinGlass::new(2, vec![((0, 1), 1)], vec![1, 0]);
235        let reduction = ReduceTo::<MaxCut<i32>>::reduce_to(&sg);
236
237        // If ancilla is 0, don't flip
238        let mc_sol = vec![0, 1, 0];
239        let extracted = reduction.extract_solution(&mc_sol);
240        assert_eq!(extracted, vec![0, 1]);
241
242        // If ancilla is 1, flip all
243        let mc_sol = vec![0, 1, 1];
244        let extracted = reduction.extract_solution(&mc_sol);
245        assert_eq!(extracted, vec![1, 0]); // flipped and ancilla removed
246    }
247
248    #[test]
249    fn test_weighted_maxcut() {
250        let mc = MaxCut::new(3, vec![(0, 1, 10), (1, 2, 20)]);
251        let reduction = ReduceTo::<SpinGlass<i32>>::reduce_to(&mc);
252        let sg = reduction.target_problem();
253
254        // Verify interactions have correct weights
255        let interactions = sg.interactions();
256        assert_eq!(interactions.len(), 2);
257    }
258}
259
260// Register reductions with inventory for auto-discovery
261use crate::poly;
262use crate::rules::registry::{ReductionEntry, ReductionOverhead};
263
264inventory::submit! {
265    ReductionEntry {
266        source_name: "MaxCut",
267        target_name: "SpinGlass",
268        source_graph: "SimpleGraph",
269        target_graph: "SpinGlassGraph",
270        overhead_fn: || ReductionOverhead::new(vec![
271            ("num_spins", poly!(num_vertices)),
272            ("num_interactions", poly!(num_edges)),
273        ]),
274    }
275}
276
277inventory::submit! {
278    ReductionEntry {
279        source_name: "SpinGlass",
280        target_name: "MaxCut",
281        source_graph: "SpinGlassGraph",
282        target_graph: "SimpleGraph",
283        overhead_fn: || ReductionOverhead::new(vec![
284            ("num_vertices", poly!(num_spins)),
285            ("num_edges", poly!(num_interactions)),
286        ]),
287    }
288}