1use crate::embedding::Embedding;
8use crate::ising::{IsingError, IsingResult};
9use scirs2_core::random::{thread_rng, ChaCha8Rng, Rng, RngExt, SeedableRng};
10use std::collections::{HashMap, HashSet};
11
12#[derive(Debug, Clone)]
14pub struct HardwareSolution {
15 pub spins: Vec<i8>,
17 pub energy: f64,
19 pub occurrences: usize,
21}
22
23#[derive(Debug, Clone)]
25pub struct ResolvedSolution {
26 pub logical_spins: Vec<i8>,
28 pub chain_breaks: usize,
30 pub energy: f64,
38 pub hardware_solution: HardwareSolution,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ResolutionMethod {
45 MajorityVote,
47 EnergyMinimization,
49 WeightedMajority,
51 Discard,
53}
54
55pub struct ChainBreakResolver {
57 pub method: ResolutionMethod,
59 pub tie_break_random: bool,
61 pub seed: Option<u64>,
63}
64
65impl Default for ChainBreakResolver {
66 fn default() -> Self {
67 Self {
68 method: ResolutionMethod::MajorityVote,
69 tie_break_random: true,
70 seed: None,
71 }
72 }
73}
74
75impl ChainBreakResolver {
76 pub fn resolve_solution(
78 &self,
79 hardware_solution: &HardwareSolution,
80 embedding: &Embedding,
81 logical_problem: Option<&LogicalProblem>,
82 ) -> IsingResult<ResolvedSolution> {
83 match self.method {
84 ResolutionMethod::MajorityVote => {
85 self.resolve_majority_vote(hardware_solution, embedding, logical_problem)
86 }
87 ResolutionMethod::WeightedMajority => {
88 self.resolve_weighted_majority(hardware_solution, embedding, logical_problem)
89 }
90 ResolutionMethod::EnergyMinimization => {
91 let problem = logical_problem.ok_or_else(|| {
92 IsingError::InvalidValue(
93 "Energy minimization requires logical problem".to_string(),
94 )
95 })?;
96 self.resolve_energy_minimization(hardware_solution, embedding, problem)
97 }
98 ResolutionMethod::Discard => self.resolve_discard(hardware_solution, embedding),
99 }
100 }
101
102 pub fn resolve_solutions(
104 &self,
105 hardware_solutions: &[HardwareSolution],
106 embedding: &Embedding,
107 logical_problem: Option<&LogicalProblem>,
108 ) -> IsingResult<Vec<ResolvedSolution>> {
109 let mut resolved = Vec::new();
110
111 for hw_solution in hardware_solutions {
112 match self.resolve_solution(hw_solution, embedding, logical_problem) {
113 Ok(solution) => resolved.push(solution),
114 Err(_) if self.method == ResolutionMethod::Discard => {
115 continue;
117 }
118 Err(e) => return Err(e),
119 }
120 }
121
122 resolved.sort_by(|a, b| a.energy.total_cmp(&b.energy));
124
125 Ok(resolved)
126 }
127
128 fn resolve_majority_vote(
130 &self,
131 hardware_solution: &HardwareSolution,
132 embedding: &Embedding,
133 logical_problem: Option<&LogicalProblem>,
134 ) -> IsingResult<ResolvedSolution> {
135 let mut logical_spins = Vec::new();
136 let mut chain_breaks = 0;
137 let num_vars = embedding.chains.len();
138 let mut rng = self.tie_break_rng();
139
140 for var in 0..num_vars {
141 let chain = embedding
142 .chains
143 .get(&var)
144 .ok_or_else(|| IsingError::InvalidQubit(var))?;
145
146 let mut plus_votes = 0;
148 let mut minus_votes = 0;
149
150 for &qubit in chain {
151 if qubit >= hardware_solution.spins.len() {
152 return Err(IsingError::InvalidQubit(qubit));
153 }
154
155 match hardware_solution.spins[qubit] {
156 1 => plus_votes += 1,
157 -1 => minus_votes += 1,
158 _ => return Err(IsingError::InvalidValue("Invalid spin value".to_string())),
159 }
160 }
161
162 let logical_value = if plus_votes > minus_votes {
164 1
165 } else if minus_votes > plus_votes {
166 -1
167 } else {
168 self.break_tie(&mut rng)
171 };
172
173 let unanimous = plus_votes == 0 || minus_votes == 0;
175 if !unanimous {
176 chain_breaks += 1;
177 }
178
179 logical_spins.push(logical_value);
180 }
181
182 let energy = Self::resolved_energy(&logical_spins, hardware_solution, logical_problem);
183
184 Ok(ResolvedSolution {
185 logical_spins,
186 chain_breaks,
187 energy,
188 hardware_solution: hardware_solution.clone(),
189 })
190 }
191
192 fn resolve_weighted_majority(
194 &self,
195 hardware_solution: &HardwareSolution,
196 embedding: &Embedding,
197 logical_problem: Option<&LogicalProblem>,
198 ) -> IsingResult<ResolvedSolution> {
199 let num_vars = embedding.chains.len();
204 let mut logical_spins = vec![0i8; num_vars];
205 let mut chain_breaks = 0;
206 let mut rng = self.tie_break_rng();
207
208 for var in 0..num_vars {
209 if let Some(chain) = embedding.chains.get(&var) {
210 if chain.is_empty() {
211 return Err(IsingError::InvalidValue(format!(
212 "Empty chain for variable {var}"
213 )));
214 }
215
216 if chain.len() == 1 {
217 logical_spins[var] = hardware_solution.spins[chain[0]];
219 continue;
220 }
221
222 let mut weight_plus = 0.0;
224 let mut weight_minus = 0.0;
225 let mut has_disagreement = false;
226
227 for &qubit_i in chain {
228 let spin_i = hardware_solution.spins[qubit_i];
229
230 let mut agreement_count = 0.0;
232 for &qubit_j in chain {
233 if qubit_i != qubit_j && hardware_solution.spins[qubit_j] == spin_i {
234 agreement_count += 1.0;
235 }
236 }
237
238 let weight = 1.0 + agreement_count;
240
241 if spin_i == 1 {
242 weight_plus += weight;
243 } else if spin_i == -1 {
244 weight_minus += weight;
245 }
246
247 if hardware_solution.spins[chain[0]] != spin_i {
249 has_disagreement = true;
250 }
251 }
252
253 if weight_plus > weight_minus {
255 logical_spins[var] = 1;
256 } else if weight_minus > weight_plus {
257 logical_spins[var] = -1;
258 } else {
259 logical_spins[var] = if self.tie_break_random {
263 self.break_tie(&mut rng)
264 } else {
265 hardware_solution.spins[chain[0]]
266 };
267 }
268
269 if has_disagreement {
270 chain_breaks += 1;
271 }
272 }
273 }
274
275 let energy = Self::resolved_energy(&logical_spins, hardware_solution, logical_problem);
276
277 Ok(ResolvedSolution {
278 logical_spins,
279 chain_breaks,
280 energy,
281 hardware_solution: hardware_solution.clone(),
282 })
283 }
284
285 fn resolve_energy_minimization(
287 &self,
288 hardware_solution: &HardwareSolution,
289 embedding: &Embedding,
290 logical_problem: &LogicalProblem,
291 ) -> IsingResult<ResolvedSolution> {
292 let mut resolved =
293 self.resolve_majority_vote(hardware_solution, embedding, Some(logical_problem))?;
294
295 for var in 0..resolved.logical_spins.len() {
297 if self.is_chain_broken(var, hardware_solution, embedding)? {
298 let current_energy = logical_problem.calculate_energy(&resolved.logical_spins);
300
301 resolved.logical_spins[var] *= -1;
303 let flipped_energy = logical_problem.calculate_energy(&resolved.logical_spins);
304
305 if flipped_energy >= current_energy {
307 resolved.logical_spins[var] *= -1; }
309 }
310 }
311
312 resolved.energy = logical_problem.calculate_energy(&resolved.logical_spins);
314
315 Ok(resolved)
316 }
317
318 fn resolve_discard(
320 &self,
321 hardware_solution: &HardwareSolution,
322 embedding: &Embedding,
323 ) -> IsingResult<ResolvedSolution> {
324 let resolved = self.resolve_majority_vote(hardware_solution, embedding, None)?;
325
326 if resolved.chain_breaks > 0 {
327 Err(IsingError::HardwareConstraint(format!(
328 "Solution has {} broken chains",
329 resolved.chain_breaks
330 )))
331 } else {
332 Ok(resolved)
333 }
334 }
335
336 fn is_chain_broken(
338 &self,
339 var: usize,
340 hardware_solution: &HardwareSolution,
341 embedding: &Embedding,
342 ) -> IsingResult<bool> {
343 let chain = embedding
344 .chains
345 .get(&var)
346 .ok_or_else(|| IsingError::InvalidQubit(var))?;
347
348 if chain.is_empty() {
349 return Ok(false);
350 }
351
352 let first_spin = hardware_solution.spins[chain[0]];
353
354 for &qubit in &chain[1..] {
355 if hardware_solution.spins[qubit] != first_spin {
356 return Ok(true);
357 }
358 }
359
360 Ok(false)
361 }
362
363 fn tie_break_rng(&self) -> ChaCha8Rng {
369 match self.seed {
370 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
371 None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
372 }
373 }
374
375 fn break_tie(&self, rng: &mut ChaCha8Rng) -> i8 {
378 if self.tie_break_random {
379 if rng.random_bool(0.5) {
380 1
381 } else {
382 -1
383 }
384 } else {
385 1
386 }
387 }
388
389 fn resolved_energy(
396 logical_spins: &[i8],
397 hardware_solution: &HardwareSolution,
398 logical_problem: Option<&LogicalProblem>,
399 ) -> f64 {
400 match logical_problem {
401 Some(problem) => problem.calculate_energy(logical_spins),
402 None => hardware_solution.energy,
403 }
404 }
405}
406
407#[derive(Debug, Clone)]
409pub struct LogicalProblem {
410 pub linear: Vec<f64>,
412 pub quadratic: HashMap<(usize, usize), f64>,
414 pub offset: f64,
416}
417
418impl LogicalProblem {
419 #[must_use]
421 pub fn new(num_vars: usize) -> Self {
422 Self {
423 linear: vec![0.0; num_vars],
424 quadratic: HashMap::new(),
425 offset: 0.0,
426 }
427 }
428
429 #[must_use]
431 pub fn calculate_energy(&self, spins: &[i8]) -> f64 {
432 let mut energy = self.offset;
433
434 for (i, &h) in self.linear.iter().enumerate() {
436 if i < spins.len() {
437 energy += h * f64::from(spins[i]);
438 }
439 }
440
441 for (&(i, j), &J) in &self.quadratic {
443 if i < spins.len() && j < spins.len() {
444 energy += J * f64::from(spins[i]) * f64::from(spins[j]);
445 }
446 }
447
448 energy
449 }
450
451 pub fn from_qubo(qubo_matrix: &[Vec<f64>], offset: f64) -> IsingResult<Self> {
453 let n = qubo_matrix.len();
454 let mut problem = Self::new(n);
455 problem.offset = offset;
456
457 for i in 0..n {
462 for j in i..n {
463 let q_ij = qubo_matrix[i][j];
464 if q_ij.abs() > 1e-10 {
465 problem.offset += q_ij / 4.0;
466 if i == j {
467 problem.linear[i] += q_ij / 2.0;
469 } else {
470 problem.quadratic.insert((i, j), q_ij / 4.0);
472 problem.linear[i] += q_ij / 4.0;
473 problem.linear[j] += q_ij / 4.0;
474 }
475 }
476 }
477 }
478
479 Ok(problem)
480 }
481}
482
483pub struct ChainStrengthOptimizer {
485 pub min_strength: f64,
487 pub max_strength: f64,
489 pub num_tries: usize,
491}
492
493impl Default for ChainStrengthOptimizer {
494 fn default() -> Self {
495 Self {
496 min_strength: 0.1,
497 max_strength: 10.0,
498 num_tries: 10,
499 }
500 }
501}
502
503impl ChainStrengthOptimizer {
504 #[must_use]
506 pub fn find_optimal_strength(&self, logical_problem: &LogicalProblem) -> f64 {
507 let mut all_coeffs = Vec::new();
509
510 for &h in &logical_problem.linear {
512 if h.abs() > 1e-10 {
513 all_coeffs.push(h.abs());
514 }
515 }
516
517 for &J in logical_problem.quadratic.values() {
519 if J.abs() > 1e-10 {
520 all_coeffs.push(J.abs());
521 }
522 }
523
524 if all_coeffs.is_empty() {
525 return 1.0; }
527
528 all_coeffs.sort_by(|a, b| a.total_cmp(b));
530
531 let median = if all_coeffs.len() % 2 == 0 {
533 f64::midpoint(
534 all_coeffs[all_coeffs.len() / 2 - 1],
535 all_coeffs[all_coeffs.len() / 2],
536 )
537 } else {
538 all_coeffs[all_coeffs.len() / 2]
539 };
540
541 (median * 1.5).max(self.min_strength).min(self.max_strength)
544 }
545
546 #[must_use]
548 pub fn optimize_strength(
549 &self,
550 logical_problem: &LogicalProblem,
551 test_solutions: &[Vec<i8>],
552 ) -> f64 {
553 let mut best_strength = self.find_optimal_strength(logical_problem);
554 let mut best_score = f64::INFINITY;
555
556 let step = (self.max_strength - self.min_strength) / (self.num_tries as f64);
558
559 for i in 0..self.num_tries {
560 let strength = (i as f64).mul_add(step, self.min_strength);
561
562 let score = self.evaluate_strength(strength, logical_problem, test_solutions);
564
565 if score < best_score {
566 best_score = score;
567 best_strength = strength;
568 }
569 }
570
571 best_strength
572 }
573
574 fn evaluate_strength(
593 &self,
594 strength: f64,
595 logical_problem: &LogicalProblem,
596 test_solutions: &[Vec<i8>],
597 ) -> f64 {
598 let num_vars = logical_problem.linear.len();
599
600 let mut required_strength = 0.0_f64;
602
603 if test_solutions.is_empty() {
604 for i in 0..num_vars {
606 let mut field = logical_problem.linear[i].abs();
607 for (&(a, b), &j) in &logical_problem.quadratic {
608 if a == i || b == i {
609 field += j.abs();
610 }
611 }
612 required_strength = required_strength.max(field);
613 }
614 } else {
615 for solution in test_solutions {
616 for i in 0..num_vars.min(solution.len()) {
617 let mut field = logical_problem.linear[i];
618 for (&(a, b), &j) in &logical_problem.quadratic {
619 if a == i {
620 if let Some(&s) = solution.get(b) {
621 field += j * f64::from(s);
622 }
623 } else if b == i {
624 if let Some(&s) = solution.get(a) {
625 field += j * f64::from(s);
626 }
627 }
628 }
629 required_strength = required_strength.max(field.abs());
630 }
631 }
632 }
633
634 if required_strength <= 0.0 {
635 return strength;
638 }
639
640 const BREAK_PENALTY: f64 = 10.0;
642 let under = (required_strength - strength).max(0.0) / required_strength;
643
644 let over = (strength - required_strength).max(0.0) / required_strength;
646
647 BREAK_PENALTY.mul_add(under, over)
648 }
649}
650
651#[derive(Debug, Clone, Default)]
653pub struct ChainBreakStats {
654 pub total_chains: usize,
656 pub broken_chains: Vec<usize>,
658 pub break_rate: f64,
660 pub frequent_breaks: Vec<(usize, usize)>,
662}
663
664impl ChainBreakStats {
665 pub fn analyze(
667 hardware_solutions: &[HardwareSolution],
668 embedding: &Embedding,
669 ) -> IsingResult<Self> {
670 let total_chains = embedding.chains.len();
671 let mut broken_chains = Vec::new();
672 let mut break_counts: HashMap<usize, usize> = HashMap::new();
673
674 for hw_solution in hardware_solutions {
675 let mut breaks_in_solution = 0;
676
677 for (&var, chain) in &embedding.chains {
678 if chain.len() > 1 {
679 let first_spin = hw_solution.spins[chain[0]];
680 let is_broken = chain[1..]
681 .iter()
682 .any(|&q| hw_solution.spins[q] != first_spin);
683
684 if is_broken {
685 breaks_in_solution += 1;
686 *break_counts.entry(var).or_insert(0) += 1;
687 }
688 }
689 }
690
691 broken_chains.push(breaks_in_solution);
692 }
693
694 let total_breaks: usize = broken_chains.iter().sum();
696 let break_rate = if hardware_solutions.is_empty() || total_chains == 0 {
697 0.0
698 } else {
699 total_breaks as f64 / (hardware_solutions.len() * total_chains) as f64
700 };
701
702 let mut frequent_breaks: Vec<(usize, usize)> = break_counts.into_iter().collect();
704 frequent_breaks.sort_by_key(|&(_, count)| std::cmp::Reverse(count));
705 frequent_breaks.truncate(10); Ok(Self {
708 total_chains,
709 broken_chains,
710 break_rate,
711 frequent_breaks,
712 })
713 }
714
715 #[must_use]
717 pub fn get_recommendations(&self) -> Vec<String> {
718 let mut recommendations = Vec::new();
719
720 if self.break_rate > 0.5 {
721 recommendations.push(
722 "High chain break rate detected. Consider increasing chain strength.".to_string(),
723 );
724 }
725
726 if self.break_rate > 0.2 {
727 recommendations.push(
728 "Moderate chain breaks. Try optimizing embedding or chain strength.".to_string(),
729 );
730 }
731
732 if !self.frequent_breaks.is_empty() {
733 let vars: Vec<String> = self
734 .frequent_breaks
735 .iter()
736 .take(3)
737 .map(|(var, _)| var.to_string())
738 .collect();
739 recommendations.push(format!(
740 "Variables {} frequently have broken chains. Check embedding quality.",
741 vars.join(", ")
742 ));
743 }
744
745 recommendations
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn test_majority_vote_resolution() {
755 let mut embedding = Embedding::new();
756 embedding
757 .add_chain(0, vec![0, 1, 2])
758 .expect("failed to add chain in test");
759 embedding
760 .add_chain(1, vec![3, 4, 5])
761 .expect("failed to add chain in test");
762
763 let hw_solution = HardwareSolution {
764 spins: vec![1, 1, -1, -1, -1, -1], energy: -1.0,
766 occurrences: 1,
767 };
768
769 let resolver = ChainBreakResolver::default();
770 let resolved = resolver
771 .resolve_solution(&hw_solution, &embedding, None)
772 .expect("failed to resolve solution in test");
773
774 assert_eq!(resolved.logical_spins, vec![1, -1]);
775 assert_eq!(resolved.chain_breaks, 1); }
777
778 #[test]
779 fn test_chain_strength_optimizer() {
780 let mut problem = LogicalProblem::new(3);
781 problem.linear = vec![1.0, -0.5, 0.0];
782 problem.quadratic.insert((0, 1), -2.0);
783 problem.quadratic.insert((1, 2), 1.5);
784
785 let optimizer = ChainStrengthOptimizer::default();
786 let strength = optimizer.find_optimal_strength(&problem);
787
788 assert!(strength > 0.5 && strength < 5.0);
790 }
791
792 #[test]
793 fn test_evaluate_strength_uses_required_field() {
794 let mut problem = LogicalProblem::new(2);
798 problem.linear = vec![0.0, 0.0];
799 problem.quadratic.insert((0, 1), 2.0);
800
801 let optimizer = ChainStrengthOptimizer::default();
802 let test_solutions = vec![vec![1_i8, 1]];
803
804 let score_weak = optimizer.evaluate_strength(0.5, &problem, &test_solutions);
807 let score_matched = optimizer.evaluate_strength(2.0, &problem, &test_solutions);
808 let score_strong = optimizer.evaluate_strength(6.0, &problem, &test_solutions);
809
810 assert!(score_matched < score_weak);
812 assert!(score_matched < score_strong);
813 assert!(score_matched.abs() < 1e-12);
815 let score_under = optimizer.evaluate_strength(1.0, &problem, &test_solutions); let score_over = optimizer.evaluate_strength(3.0, &problem, &test_solutions); assert!(score_under > score_over);
820
821 let best = optimizer.optimize_strength(&problem, &test_solutions);
823 assert!(
824 best >= 1.5,
825 "expected strength resisting the field, got {best}"
826 );
827 }
828
829 #[test]
830 fn test_chain_break_stats() {
831 let mut embedding = Embedding::new();
832 embedding
833 .add_chain(0, vec![0, 1])
834 .expect("failed to add chain in test");
835 embedding
836 .add_chain(1, vec![2, 3])
837 .expect("failed to add chain in test");
838
839 let solutions = vec![
840 HardwareSolution {
841 spins: vec![1, 1, -1, -1], energy: -1.0,
843 occurrences: 1,
844 },
845 HardwareSolution {
846 spins: vec![1, -1, -1, -1], energy: -0.5,
848 occurrences: 1,
849 },
850 ];
851
852 let stats = ChainBreakStats::analyze(&solutions, &embedding)
853 .expect("failed to analyze chain break stats in test");
854
855 assert_eq!(stats.total_chains, 2);
856 assert_eq!(stats.broken_chains, vec![0, 1]);
857 assert_eq!(stats.break_rate, 0.25); }
859}