quantrs2_tytan/solution_debugger/
energy_analyzer.rs1use scirs2_core::ndarray::Array2;
4use serde::Serialize;
5use std::collections::HashMap;
6
7pub struct EnergyAnalyzer {
9 energy_cache: HashMap<String, EnergyBreakdown>,
11 analysis_depth: usize,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct EnergyBreakdown {
17 pub total_energy: f64,
19 pub linear_terms: f64,
21 pub quadratic_terms: f64,
23 pub constraint_penalties: f64,
25 pub variable_contributions: HashMap<String, f64>,
27 pub interaction_contributions: HashMap<(String, String), f64>,
29 pub energy_landscape: EnergyLandscape,
31}
32
33#[derive(Debug, Clone, Serialize)]
34pub struct EnergyLandscape {
35 pub local_minima: Vec<LocalMinimum>,
37 pub barriers: Vec<EnergyBarrier>,
39 pub basin_size: usize,
41 pub ruggedness: f64,
43}
44
45#[derive(Debug, Clone, Serialize)]
46pub struct LocalMinimum {
47 pub solution: HashMap<String, bool>,
49 pub energy: f64,
51 pub distance: usize,
53 pub escape_barrier: f64,
55}
56
57#[derive(Debug, Clone, Serialize)]
58pub struct EnergyBarrier {
59 pub from: HashMap<String, bool>,
61 pub to: HashMap<String, bool>,
63 pub height: f64,
65 pub path: Vec<HashMap<String, bool>>,
67}
68
69impl EnergyAnalyzer {
70 pub fn new(analysis_depth: usize) -> Self {
72 Self {
73 energy_cache: HashMap::new(),
74 analysis_depth,
75 }
76 }
77
78 pub fn analyze(
80 &mut self,
81 qubo: &Array2<f64>,
82 assignments: &HashMap<String, bool>,
83 var_map: &HashMap<String, usize>,
84 ) -> EnergyBreakdown {
85 let solution_key = self.solution_key(assignments);
86
87 if let Some(cached) = self.energy_cache.get(&solution_key) {
88 return cached.clone();
89 }
90
91 let breakdown = self.compute_energy_breakdown(qubo, assignments, var_map);
92 self.energy_cache.insert(solution_key, breakdown.clone());
93 breakdown
94 }
95
96 fn compute_energy_breakdown(
98 &self,
99 qubo: &Array2<f64>,
100 assignments: &HashMap<String, bool>,
101 var_map: &HashMap<String, usize>,
102 ) -> EnergyBreakdown {
103 let mut total_energy = 0.0;
104 let mut linear_terms = 0.0;
105 let mut quadratic_terms = 0.0;
106 let mut variable_contributions = HashMap::new();
107 let mut interaction_contributions = HashMap::new();
108
109 for (var1, &idx1) in var_map {
111 let val1 = if assignments.get(var1).copied().unwrap_or(false) {
112 1.0
113 } else {
114 0.0
115 };
116
117 if idx1 < qubo.nrows() && idx1 < qubo.ncols() {
119 let linear_contrib = qubo[[idx1, idx1]] * val1;
120 linear_terms += linear_contrib;
121 total_energy += linear_contrib;
122 *variable_contributions.entry(var1.clone()).or_insert(0.0) += linear_contrib;
123 }
124
125 for (var2, &idx2) in var_map {
127 if idx1 < idx2 && idx1 < qubo.nrows() && idx2 < qubo.ncols() {
128 let val2 = if assignments.get(var2).copied().unwrap_or(false) {
129 1.0
130 } else {
131 0.0
132 };
133 let quad_contrib = qubo[[idx1, idx2]] * val1 * val2;
134 quadratic_terms += quad_contrib;
135 total_energy += quad_contrib;
136
137 let key = if var1 < var2 {
139 (var1.clone(), var2.clone())
140 } else {
141 (var2.clone(), var1.clone())
142 };
143 interaction_contributions.insert(key, quad_contrib);
144
145 *variable_contributions.entry(var1.clone()).or_insert(0.0) +=
147 quad_contrib / 2.0;
148 *variable_contributions.entry(var2.clone()).or_insert(0.0) +=
149 quad_contrib / 2.0;
150 }
151 }
152 }
153
154 let energy_landscape = self.analyze_energy_landscape(qubo, assignments, var_map);
156
157 EnergyBreakdown {
158 total_energy,
159 linear_terms,
160 quadratic_terms,
161 constraint_penalties: 0.0, variable_contributions,
163 interaction_contributions,
164 energy_landscape,
165 }
166 }
167
168 fn analyze_energy_landscape(
170 &self,
171 qubo: &Array2<f64>,
172 assignments: &HashMap<String, bool>,
173 var_map: &HashMap<String, usize>,
174 ) -> EnergyLandscape {
175 let mut local_minima = Vec::new();
176 let current_energy = self.calculate_energy(qubo, assignments, var_map);
177
178 for var in assignments.keys() {
180 let mut neighbor = assignments.clone();
181 neighbor.insert(var.clone(), !assignments[var]);
182
183 let neighbor_energy = self.calculate_energy(qubo, &neighbor, var_map);
184 if neighbor_energy < current_energy {
185 local_minima.push(LocalMinimum {
186 solution: neighbor,
187 energy: neighbor_energy,
188 distance: 1,
189 escape_barrier: (current_energy - neighbor_energy).max(0.0),
190 });
191 }
192 }
193
194 if self.analysis_depth > 1 {
196 let vars: Vec<_> = assignments.keys().collect();
197 for i in 0..vars.len() {
198 for j in i + 1..vars.len() {
199 let mut neighbor = assignments.clone();
200 neighbor.insert(vars[i].clone(), !assignments[vars[i]]);
201 neighbor.insert(vars[j].clone(), !assignments[vars[j]]);
202
203 let neighbor_energy = self.calculate_energy(qubo, &neighbor, var_map);
204 if neighbor_energy < current_energy {
205 local_minima.push(LocalMinimum {
206 solution: neighbor,
207 energy: neighbor_energy,
208 distance: 2,
209 escape_barrier: (current_energy - neighbor_energy).max(0.0),
210 });
211 }
212 }
213 }
214 }
215
216 local_minima.sort_by(|a, b| {
218 a.energy
219 .partial_cmp(&b.energy)
220 .unwrap_or(std::cmp::Ordering::Equal)
221 });
222 local_minima.truncate(10); let energy_variance = if local_minima.len() > 1 {
226 let energies: Vec<f64> = local_minima.iter().map(|m| m.energy).collect();
227 let mean = energies.iter().sum::<f64>() / energies.len() as f64;
228 energies.iter().map(|e| (e - mean).powi(2)).sum::<f64>() / energies.len() as f64
229 } else {
230 0.0
231 };
232
233 EnergyLandscape {
234 local_minima,
235 barriers: Vec::new(), basin_size: 1, ruggedness: energy_variance.sqrt(),
238 }
239 }
240
241 fn calculate_energy(
243 &self,
244 qubo: &Array2<f64>,
245 assignments: &HashMap<String, bool>,
246 var_map: &HashMap<String, usize>,
247 ) -> f64 {
248 let mut energy = 0.0;
249
250 for (var1, &idx1) in var_map {
251 let val1 = if assignments.get(var1).copied().unwrap_or(false) {
252 1.0
253 } else {
254 0.0
255 };
256
257 for (var2, &idx2) in var_map {
258 if idx1 < qubo.nrows() && idx2 < qubo.ncols() {
259 let val2 = if assignments.get(var2).copied().unwrap_or(false) {
260 1.0
261 } else {
262 0.0
263 };
264 energy += qubo[[idx1, idx2]] * val1 * val2;
265 }
266 }
267 }
268
269 energy
270 }
271
272 fn solution_key(&self, assignments: &HashMap<String, bool>) -> String {
274 let mut vars: Vec<_> = assignments.iter().collect();
275 vars.sort_by_key(|(name, _)| *name);
276 vars.iter()
277 .map(|(name, &val)| format!("{}:{}", name, if val { "1" } else { "0" }))
278 .collect::<Vec<_>>()
279 .join(",")
280 }
281}