problemreductions/rules/
spinglass_qubo.rs1use crate::models::optimization::{SpinGlass, QUBO};
9use crate::rules::traits::{ReduceTo, ReductionResult};
10use crate::traits::Problem;
11use crate::types::ProblemSize;
12
13#[derive(Debug, Clone)]
15pub struct ReductionQUBOToSG {
16 target: SpinGlass<f64>,
17 source_size: ProblemSize,
18}
19
20impl ReductionResult for ReductionQUBOToSG {
21 type Source = QUBO<f64>;
22 type Target = SpinGlass<f64>;
23
24 fn target_problem(&self) -> &Self::Target {
25 &self.target
26 }
27
28 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
30 target_solution.to_vec()
31 }
32
33 fn source_size(&self) -> ProblemSize {
34 self.source_size.clone()
35 }
36
37 fn target_size(&self) -> ProblemSize {
38 self.target.problem_size()
39 }
40}
41
42impl ReduceTo<SpinGlass<f64>> for QUBO<f64> {
43 type Result = ReductionQUBOToSG;
44
45 fn reduce_to(&self) -> Self::Result {
46 let n = self.num_vars();
47 let matrix = self.matrix();
48
49 let mut interactions = Vec::new();
61 let mut onsite = vec![0.0; n];
62
63 for i in 0..n {
64 for j in i..n {
65 let q = matrix[i][j];
66 if q.abs() < 1e-10 {
67 continue;
68 }
69
70 if i == j {
71 onsite[i] += q / 2.0;
73 } else {
74 let j_ij = q / 4.0;
77 if j_ij.abs() > 1e-10 {
78 interactions.push(((i, j), j_ij));
79 }
80 onsite[i] += q / 4.0;
82 onsite[j] += q / 4.0;
83 }
84 }
85 }
86
87 let target = SpinGlass::new(n, interactions, onsite);
88
89 ReductionQUBOToSG {
90 target,
91 source_size: self.problem_size(),
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct ReductionSGToQUBO {
99 target: QUBO<f64>,
100 source_size: ProblemSize,
101}
102
103impl ReductionResult for ReductionSGToQUBO {
104 type Source = SpinGlass<f64>;
105 type Target = QUBO<f64>;
106
107 fn target_problem(&self) -> &Self::Target {
108 &self.target
109 }
110
111 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
112 target_solution.to_vec()
113 }
114
115 fn source_size(&self) -> ProblemSize {
116 self.source_size.clone()
117 }
118
119 fn target_size(&self) -> ProblemSize {
120 self.target.problem_size()
121 }
122}
123
124impl ReduceTo<QUBO<f64>> for SpinGlass<f64> {
125 type Result = ReductionSGToQUBO;
126
127 fn reduce_to(&self) -> Self::Result {
128 let n = self.num_spins();
129 let mut matrix = vec![vec![0.0; n]; n];
130
131 for &((i, j), j_val) in self.interactions() {
140 matrix[i][j] += 4.0 * j_val;
142 matrix[i][i] -= 2.0 * j_val;
144 matrix[j][j] -= 2.0 * j_val;
145 }
146
147 for (i, &h) in self.fields().iter().enumerate() {
149 matrix[i][i] += 2.0 * h;
151 }
152
153 let target = QUBO::from_matrix(matrix);
154
155 ReductionSGToQUBO {
156 target,
157 source_size: self.problem_size(),
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::solvers::{BruteForce, Solver};
166
167 #[test]
168 fn test_qubo_to_spinglass() {
169 let qubo = QUBO::from_matrix(vec![vec![1.0, -2.0], vec![0.0, 1.0]]);
172 let reduction = ReduceTo::<SpinGlass<f64>>::reduce_to(&qubo);
173 let sg = reduction.target_problem();
174
175 let solver = BruteForce::new();
176 let sg_solutions = solver.find_best(sg);
177 let qubo_solutions: Vec<_> = sg_solutions
178 .iter()
179 .map(|s| reduction.extract_solution(s))
180 .collect();
181
182 assert!(!qubo_solutions.is_empty());
184
185 for sol in &qubo_solutions {
188 let val = qubo.solution_size(sol).size;
189 assert!(val <= 0.0 + 1e-6, "Expected optimal value near 0, got {}", val);
190 }
191 }
192
193 #[test]
194 fn test_spinglass_to_qubo() {
195 let sg = SpinGlass::new(2, vec![((0, 1), -1.0)], vec![0.0, 0.0]);
200 let reduction = ReduceTo::<QUBO<f64>>::reduce_to(&sg);
201 let qubo = reduction.target_problem();
202
203 let solver = BruteForce::new();
204 let qubo_solutions = solver.find_best(qubo);
205
206 for sol in &qubo_solutions {
208 assert_eq!(sol[0], sol[1], "Ferromagnetic should have aligned spins");
209 }
210 }
211
212 #[test]
213 fn test_roundtrip_qubo_sg_qubo() {
214 let original = QUBO::from_matrix(vec![vec![-1.0, 2.0], vec![0.0, -1.0]]);
215 let solver = BruteForce::new();
216 let original_solutions = solver.find_best(&original);
217 let _original_val = original.solution_size(&original_solutions[0]).size;
218
219 let reduction1 = ReduceTo::<SpinGlass<f64>>::reduce_to(&original);
221 let sg = reduction1.target_problem().clone();
222 let reduction2 = ReduceTo::<QUBO<f64>>::reduce_to(&sg);
223 let roundtrip = reduction2.target_problem();
224
225 let roundtrip_solutions = solver.find_best(roundtrip);
226 let _roundtrip_val = roundtrip.solution_size(&roundtrip_solutions[0]).size;
227
228 let orig_configs: std::collections::HashSet<_> = original_solutions.iter().collect();
231 let rt_configs: std::collections::HashSet<_> = roundtrip_solutions.iter().collect();
232 assert!(
233 orig_configs.intersection(&rt_configs).count() > 0,
234 "At least one optimal solution should match"
235 );
236 }
237
238 #[test]
239 fn test_antiferromagnetic() {
240 let sg = SpinGlass::new(2, vec![((0, 1), 1.0)], vec![0.0, 0.0]);
242 let reduction = ReduceTo::<QUBO<f64>>::reduce_to(&sg);
243 let qubo = reduction.target_problem();
244
245 let solver = BruteForce::new();
246 let solutions = solver.find_best(qubo);
247
248 for sol in &solutions {
250 assert_ne!(sol[0], sol[1], "Antiferromagnetic should have opposite spins");
251 }
252 }
253
254 #[test]
255 fn test_with_onsite_fields() {
256 let sg = SpinGlass::new(1, vec![], vec![1.0]);
260 let reduction = ReduceTo::<QUBO<f64>>::reduce_to(&sg);
261 let qubo = reduction.target_problem();
262
263 let solver = BruteForce::new();
264 let solutions = solver.find_best(qubo);
265
266 assert_eq!(solutions.len(), 1);
267 assert_eq!(solutions[0], vec![0], "Should prefer x=0 (s=-1)");
268 }
269}
270
271use crate::poly;
273use crate::rules::registry::{ReductionEntry, ReductionOverhead};
274
275inventory::submit! {
276 ReductionEntry {
277 source_name: "QUBO",
278 target_name: "SpinGlass",
279 source_graph: "QUBOMatrix",
280 target_graph: "SpinGlassGraph",
281 overhead_fn: || ReductionOverhead::new(vec![
282 ("num_spins", poly!(num_vars)),
283 ]),
284 }
285}
286
287inventory::submit! {
288 ReductionEntry {
289 source_name: "SpinGlass",
290 target_name: "QUBO",
291 source_graph: "SpinGlassGraph",
292 target_graph: "QUBOMatrix",
293 overhead_fn: || ReductionOverhead::new(vec![
294 ("num_vars", poly!(num_spins)),
295 ]),
296 }
297}