quantrs2_tytan/solution_debugger/
energy_analyzer.rs

1//! Energy analysis functionality for the solution debugger.
2
3use scirs2_core::ndarray::Array2;
4use serde::Serialize;
5use std::collections::HashMap;
6
7/// Energy analyzer
8pub struct EnergyAnalyzer {
9    /// Energy breakdown cache
10    energy_cache: HashMap<String, EnergyBreakdown>,
11    /// Analysis depth
12    analysis_depth: usize,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct EnergyBreakdown {
17    /// Total energy
18    pub total_energy: f64,
19    /// Linear terms contribution
20    pub linear_terms: f64,
21    /// Quadratic terms contribution
22    pub quadratic_terms: f64,
23    /// Constraint penalties
24    pub constraint_penalties: f64,
25    /// Per-variable contribution
26    pub variable_contributions: HashMap<String, f64>,
27    /// Per-interaction contribution
28    pub interaction_contributions: HashMap<(String, String), f64>,
29    /// Energy landscape
30    pub energy_landscape: EnergyLandscape,
31}
32
33#[derive(Debug, Clone, Serialize)]
34pub struct EnergyLandscape {
35    /// Local minima nearby
36    pub local_minima: Vec<LocalMinimum>,
37    /// Energy barriers
38    pub barriers: Vec<EnergyBarrier>,
39    /// Basin of attraction
40    pub basin_size: usize,
41    /// Ruggedness measure
42    pub ruggedness: f64,
43}
44
45#[derive(Debug, Clone, Serialize)]
46pub struct LocalMinimum {
47    /// Solution
48    pub solution: HashMap<String, bool>,
49    /// Energy
50    pub energy: f64,
51    /// Distance from current
52    pub distance: usize,
53    /// Escape barrier
54    pub escape_barrier: f64,
55}
56
57#[derive(Debug, Clone, Serialize)]
58pub struct EnergyBarrier {
59    /// From solution
60    pub from: HashMap<String, bool>,
61    /// To solution
62    pub to: HashMap<String, bool>,
63    /// Barrier height
64    pub height: f64,
65    /// Transition path
66    pub path: Vec<HashMap<String, bool>>,
67}
68
69impl EnergyAnalyzer {
70    /// Create new energy analyzer
71    pub fn new(analysis_depth: usize) -> Self {
72        Self {
73            energy_cache: HashMap::new(),
74            analysis_depth,
75        }
76    }
77
78    /// Analyze energy breakdown for a solution
79    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    /// Compute energy breakdown
97    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        // Calculate energy contributions
110        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            // Linear terms (diagonal)
118            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            // Quadratic terms (off-diagonal)
126            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                    // Record interaction contribution
138                    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                    // Add to variable contributions
146                    *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        // Analyze energy landscape
155        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, // Would need constraint penalty information
162            variable_contributions,
163            interaction_contributions,
164            energy_landscape,
165        }
166    }
167
168    /// Analyze energy landscape around current solution
169    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        // Explore 1-flip neighborhood
179        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 analysis depth > 1, explore 2-flip neighborhood
195        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        // Sort by energy
217        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); // Keep top 10 local minima
223
224        // Calculate ruggedness (simplified measure)
225        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(), // Would need more sophisticated analysis
236            basin_size: 1,        // Simplified
237            ruggedness: energy_variance.sqrt(),
238        }
239    }
240
241    /// Calculate energy for a solution
242    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    /// Generate solution key for caching
273    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}