Skip to main content

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::graph::SpinGlass;
8use crate::reduction;
9use crate::rules::traits::{ReduceTo, ReductionResult};
10use crate::topology::{Graph, SimpleGraph};
11use crate::types::WeightElement;
12use num_traits::Zero;
13
14/// Result of reducing MaxCut to SpinGlass.
15#[derive(Debug, Clone)]
16pub struct ReductionMaxCutToSG<W> {
17    target: SpinGlass<SimpleGraph, W>,
18}
19
20impl<W> ReductionResult for ReductionMaxCutToSG<W>
21where
22    W: WeightElement
23        + crate::variant::VariantParam
24        + PartialOrd
25        + num_traits::Num
26        + num_traits::Zero
27        + num_traits::Bounded
28        + std::ops::AddAssign
29        + std::ops::Mul<Output = W>,
30{
31    type Source = MaxCut<SimpleGraph, W>;
32    type Target = SpinGlass<SimpleGraph, W>;
33
34    fn target_problem(&self) -> &Self::Target {
35        &self.target
36    }
37
38    fn extract_solution(
39        &self,
40        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
41    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
42        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
43
44        Ok(target_solution.iter().map(|&spin| spin == 1).collect())
45    }
46}
47
48#[reduction(
49    transform = exact {
50        num_spins = "num_vertices",
51        num_interactions = "num_edges",
52    }
53)]
54impl ReduceTo<SpinGlass<SimpleGraph, i64>> for MaxCut<SimpleGraph, i64> {
55    type Result = ReductionMaxCutToSG<i64>;
56
57    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
58        let n = self.graph().num_vertices();
59        let edges_with_weights = self.edges();
60
61        // MaxCut: maximize sum of w_ij for edges (i,j) where s_i != s_j
62        // SpinGlass: minimize sum of J_ij * s_i * s_j
63        //
64        // For MaxCut, we want to maximize cut, which means:
65        // - When s_i != s_j (opposite spins), edge contributes to cut
66        // - s_i * s_j = -1 when opposite, +1 when same
67        //
68        // To convert: maximize sum(w_ij * [s_i != s_j])
69        //           = maximize sum(w_ij * (1 - s_i*s_j)/2)
70        //           = constant - minimize sum(w_ij * s_i*s_j / 2)
71        //
72        // So J_ij = -w_ij / 2 would work, but since we need to relate
73        // the problems directly, we use J_ij = w_ij and negate.
74        // Actually, for a proper reduction, we set J_ij = w_ij.
75        // MaxCut wants to maximize edges cut, SpinGlass minimizes energy.
76        // When J > 0 (antiferromagnetic), opposite spins lower energy.
77        // So maximizing cut = minimizing Ising energy with J = w.
78        let interactions: Vec<((usize, usize), i64)> = edges_with_weights
79            .into_iter()
80            .map(|(u, v, w)| ((u, v), w))
81            .collect();
82
83        // No onsite terms for pure MaxCut
84        let onsite = vec![0i64; n];
85
86        let target =
87            SpinGlass::<SimpleGraph, i64>::new(n, interactions, onsite).map_err(|cause| {
88                crate::rules::ReductionError::construction::<
89                    MaxCut<SimpleGraph, i64>,
90                    SpinGlass<SimpleGraph, i64>,
91                >(cause)
92            })?;
93
94        Ok(ReductionMaxCutToSG { target })
95    }
96}
97
98/// Result of reducing SpinGlass to MaxCut.
99#[derive(Debug, Clone)]
100pub struct ReductionSGToMaxCut<W> {
101    target: MaxCut<SimpleGraph, W>,
102    /// Ancilla vertex index (None if no ancilla needed).
103    ancilla: Option<usize>,
104}
105
106impl<W> ReductionResult for ReductionSGToMaxCut<W>
107where
108    W: WeightElement
109        + crate::variant::VariantParam
110        + PartialOrd
111        + num_traits::Num
112        + num_traits::Zero
113        + num_traits::Bounded
114        + std::ops::AddAssign
115        + std::ops::Mul<Output = W>,
116{
117    type Source = SpinGlass<SimpleGraph, W>;
118    type Target = MaxCut<SimpleGraph, W>;
119
120    fn target_problem(&self) -> &Self::Target {
121        &self.target
122    }
123
124    fn extract_solution(
125        &self,
126        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
127    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
128        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
129
130        Ok({
131            match self.ancilla {
132                None => target_solution
133                    .iter()
134                    .map(|&side| if side { 1 } else { -1 })
135                    .collect(),
136                Some(anc) => {
137                    // Normalize the ancilla to spin +1 so its edge contributes h_i * s_i.
138                    let mut sol = target_solution.to_vec();
139                    if !sol[anc] {
140                        for x in sol.iter_mut() {
141                            *x = !*x;
142                        }
143                    }
144                    sol.remove(anc);
145                    sol.into_iter()
146                        .map(|side| if side { 1 } else { -1 })
147                        .collect()
148                }
149            }
150        })
151    }
152}
153
154#[reduction(
155    transform = upper_bound {
156        num_vertices = "num_spins + 1",
157        num_edges = "num_interactions + num_spins",
158    }
159)]
160impl ReduceTo<MaxCut<SimpleGraph, i64>> for SpinGlass<SimpleGraph, i64> {
161    type Result = ReductionSGToMaxCut<i64>;
162
163    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
164        let n = self.num_spins();
165        let interactions = self.interactions();
166        let fields = self.fields();
167
168        // Check if we need an ancilla vertex for onsite terms
169        let need_ancilla = fields.iter().any(|h| !h.is_zero());
170        let total_vertices = if need_ancilla { n + 1 } else { n };
171        let ancilla_idx = if need_ancilla { Some(n) } else { None };
172
173        let mut edges = Vec::new();
174        let mut weights = Vec::new();
175
176        // Add interaction edges
177        for ((i, j), w) in interactions {
178            edges.push((i, j));
179            weights.push(w);
180        }
181
182        // Add onsite terms as edges to ancilla
183        // h_i * s_i can be modeled as an edge to ancilla with weight h_i
184        // When s_i and s_ancilla are opposite, the edge is cut
185        if need_ancilla {
186            for (i, h) in fields.iter().enumerate() {
187                if !h.is_zero() {
188                    edges.push((i, n));
189                    weights.push(*h);
190                }
191            }
192        }
193
194        let target = MaxCut::new(SimpleGraph::new(total_vertices, edges), weights);
195
196        Ok(ReductionSGToMaxCut {
197            target,
198            ancilla: ancilla_idx,
199        })
200    }
201}
202
203#[cfg(feature = "example-db")]
204pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
205    use crate::export::SolutionPair;
206
207    vec![
208        crate::example_db::specs::RuleExampleSpec {
209            id: "maxcut_to_spinglass",
210            build: || {
211                let (n, edges) = crate::topology::small_graphs::petersen();
212                let source = MaxCut::unweighted(SimpleGraph::new(n, edges));
213                crate::example_db::specs::rule_example_with_witness::<_, SpinGlass<SimpleGraph, i64>>(
214                    source,
215                    SolutionPair {
216                        source_config: serde_json::json!(vec![
217                            false, true, false, true, false, true, false, false, false, true
218                        ]),
219                        target_config: serde_json::json!(vec![-1, 1, -1, 1, -1, 1, -1, -1, -1, 1]),
220                    },
221                )
222            },
223        },
224        crate::example_db::specs::RuleExampleSpec {
225            id: "spinglass_to_maxcut",
226            build: || {
227                let (n, edges) = crate::topology::small_graphs::petersen();
228                let couplings: Vec<((usize, usize), i64)> = edges
229                    .iter()
230                    .enumerate()
231                    .map(|(i, &(u, v))| ((u, v), if i % 2 == 0 { 1 } else { -1 }))
232                    .collect();
233                let source = SpinGlass::new(n, couplings, vec![0; n]).unwrap();
234                crate::example_db::specs::rule_example_with_witness::<_, MaxCut<SimpleGraph, i64>>(
235                    source,
236                    SolutionPair {
237                        source_config: serde_json::json!(vec![1, -1, 1, 1, 1, -1, 1, -1, -1, 1]),
238                        target_config: serde_json::json!(vec![
239                            true, false, true, true, true, false, true, false, false, true
240                        ]),
241                    },
242                )
243            },
244        },
245    ]
246}
247
248#[cfg(test)]
249#[path = "../unit_tests/rules/spinglass_maxcut.rs"]
250mod tests;