quantrs2_tytan/visualization/
energy_landscape.rs

1//! Energy landscape visualization for QUBO/HOBO problems
2//!
3//! This module provides tools for visualizing the energy landscape
4//! of optimization problems including 2D/3D projections and heatmaps.
5
6use crate::sampler::SampleResult;
7use scirs2_core::ndarray::Array2;
8use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "scirs")]
11use crate::scirs_stub::{
12    scirs2_linalg::svd::SVD,
13    scirs2_plot::{ColorMap, Heatmap, Plot2D, Plot3D},
14    scirs2_statistics::kde::KernelDensityEstimator,
15};
16
17/// Energy landscape configuration
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct EnergyLandscapeConfig {
20    /// Number of bins for histograms
21    pub bins: usize,
22    /// Projection method for high-dimensional data
23    pub projection: ProjectionMethod,
24    /// Color map for visualization
25    pub colormap: String,
26    /// Include density estimation
27    pub include_density: bool,
28    /// Resolution for heatmap
29    pub resolution: usize,
30    /// Energy range limits
31    pub energy_limits: Option<(f64, f64)>,
32}
33
34/// Projection methods for dimensionality reduction
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum ProjectionMethod {
37    /// Principal Component Analysis
38    PCA,
39    /// Random projection
40    Random,
41    /// Hamming distance from best solution
42    HammingDistance,
43    /// Custom projection matrix
44    Custom,
45}
46
47impl Default for EnergyLandscapeConfig {
48    fn default() -> Self {
49        Self {
50            bins: 50,
51            projection: ProjectionMethod::PCA,
52            colormap: "viridis".to_string(),
53            include_density: true,
54            resolution: 100,
55            energy_limits: None,
56        }
57    }
58}
59
60/// Energy landscape analyzer
61pub struct EnergyLandscape {
62    config: EnergyLandscapeConfig,
63    samples: Vec<SampleResult>,
64    projection_matrix: Option<Array2<f64>>,
65}
66
67impl EnergyLandscape {
68    /// Create new energy landscape analyzer
69    pub const fn new(config: EnergyLandscapeConfig) -> Self {
70        Self {
71            config,
72            samples: Vec::new(),
73            projection_matrix: None,
74        }
75    }
76
77    /// Add samples for analysis
78    pub fn add_samples(&mut self, samples: Vec<SampleResult>) {
79        self.samples.extend(samples);
80    }
81
82    /// Generate 1D energy histogram
83    pub fn energy_histogram(&self) -> Result<HistogramData, Box<dyn std::error::Error>> {
84        let energies: Vec<f64> = self.samples.iter().map(|s| s.energy).collect();
85
86        let (min_energy, max_energy) = if let Some(limits) = self.config.energy_limits {
87            limits
88        } else {
89            let min = energies.iter().fold(f64::INFINITY, |a, &b| a.min(b));
90            let max = energies.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
91            (min, max)
92        };
93
94        let bin_width = (max_energy - min_energy) / self.config.bins as f64;
95        let mut bins = vec![0; self.config.bins];
96        let mut bin_centers = Vec::new();
97
98        for i in 0..self.config.bins {
99            bin_centers.push((i as f64 + 0.5).mul_add(bin_width, min_energy));
100        }
101
102        for &energy in &energies {
103            if energy >= min_energy && energy <= max_energy {
104                let bin_idx = ((energy - min_energy) / bin_width).floor() as usize;
105                let bin_idx = bin_idx.min(self.config.bins - 1);
106                bins[bin_idx] += 1;
107            }
108        }
109
110        Ok(HistogramData {
111            bin_centers,
112            counts: bins,
113            bin_width,
114            total_samples: energies.len(),
115        })
116    }
117
118    /// Generate 2D projected energy landscape
119    pub fn project_2d(&mut self) -> Result<ProjectedLandscape, Box<dyn std::error::Error>> {
120        // Convert samples to binary matrix
121        let (binary_matrix, _var_names) = self.samples_to_matrix()?;
122
123        // Project to 2D
124        let projected = match self.config.projection {
125            ProjectionMethod::PCA => self.project_pca(&binary_matrix, 2)?,
126            ProjectionMethod::Random => self.project_random(&binary_matrix, 2)?,
127            ProjectionMethod::HammingDistance => self.project_hamming(&binary_matrix)?,
128            ProjectionMethod::Custom => {
129                if let Some(ref proj_mat) = self.projection_matrix {
130                    binary_matrix.dot(proj_mat)
131                } else {
132                    return Err("Custom projection matrix not set".into());
133                }
134            }
135        };
136
137        // Extract coordinates and energies
138        let x_coords: Vec<f64> = (0..projected.nrows()).map(|i| projected[[i, 0]]).collect();
139        let y_coords: Vec<f64> = (0..projected.nrows()).map(|i| projected[[i, 1]]).collect();
140        let energies: Vec<f64> = self.samples.iter().map(|s| s.energy).collect();
141
142        // Generate density estimation if requested
143        let density_map = if self.config.include_density {
144            #[cfg(feature = "scirs")]
145            {
146                Some(self.estimate_density_2d(&x_coords, &y_coords, &energies)?)
147            }
148            #[cfg(not(feature = "scirs"))]
149            None
150        } else {
151            None
152        };
153
154        Ok(ProjectedLandscape {
155            x_coords,
156            y_coords,
157            energies,
158            density_map,
159            projection_info: format!("{:?} projection", self.config.projection),
160        })
161    }
162
163    /// Convert samples to binary matrix
164    fn samples_to_matrix(&self) -> Result<(Array2<f64>, Vec<String>), Box<dyn std::error::Error>> {
165        if self.samples.is_empty() {
166            return Err("No samples to analyze".into());
167        }
168
169        // Get all variable names
170        let mut all_vars = std::collections::HashSet::new();
171        for sample in &self.samples {
172            for var in sample.assignments.keys() {
173                all_vars.insert(var.clone());
174            }
175        }
176
177        let var_names: Vec<String> = all_vars.into_iter().collect();
178        let n_vars = var_names.len();
179        let n_samples = self.samples.len();
180
181        // Create binary matrix
182        let mut matrix = Array2::zeros((n_samples, n_vars));
183
184        for (i, sample) in self.samples.iter().enumerate() {
185            for (j, var_name) in var_names.iter().enumerate() {
186                if let Some(&value) = sample.assignments.get(var_name) {
187                    matrix[[i, j]] = if value { 1.0 } else { 0.0 };
188                }
189            }
190        }
191
192        Ok((matrix, var_names))
193    }
194
195    /// PCA projection
196    fn project_pca(
197        &self,
198        data: &Array2<f64>,
199        n_components: usize,
200    ) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
201        #[cfg(feature = "scirs")]
202        {
203            use crate::scirs_stub::scirs2_linalg::pca::PCA;
204
205            let pca = PCA::new(n_components);
206            let projected = pca.fit_transform(data)?;
207            Ok(projected)
208        }
209
210        #[cfg(not(feature = "scirs"))]
211        {
212            // Simple centering and truncation
213            let n_samples = data.nrows();
214            let n_features = n_components.min(data.ncols());
215            let mut result = Array2::zeros((n_samples, n_components));
216
217            // Center the data
218            let means = data
219                .mean_axis(scirs2_core::ndarray::Axis(0))
220                .ok_or("Failed to compute mean axis")?;
221            for i in 0..n_samples {
222                for j in 0..n_features {
223                    result[[i, j]] = data[[i, j]] - means[j];
224                }
225            }
226
227            Ok(result)
228        }
229    }
230
231    /// Random projection
232    fn project_random(
233        &self,
234        data: &Array2<f64>,
235        n_components: usize,
236    ) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
237        use scirs2_core::random::prelude::*;
238
239        let n_features = data.ncols();
240        let mut rng = StdRng::seed_from_u64(42);
241
242        // Generate random projection matrix
243        let mut proj_matrix = Array2::<f64>::zeros((n_features, n_components));
244        for i in 0..n_features {
245            for j in 0..n_components {
246                proj_matrix[[i, j]] = rng.gen_range(-1.0..1.0);
247            }
248        }
249
250        // Normalize columns
251        for j in 0..n_components {
252            let col_norm = (0..n_features)
253                .map(|i| proj_matrix[[i, j]].powi(2))
254                .sum::<f64>()
255                .sqrt();
256
257            if col_norm > 0.0 {
258                for i in 0..n_features {
259                    proj_matrix[[i, j]] /= col_norm;
260                }
261            }
262        }
263
264        Ok(data.dot(&proj_matrix))
265    }
266
267    /// Hamming distance projection
268    fn project_hamming(
269        &self,
270        data: &Array2<f64>,
271    ) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
272        let n_samples = data.nrows();
273
274        // Find best solution
275        let best_idx = self
276            .samples
277            .iter()
278            .enumerate()
279            .min_by(|(_, a), (_, b)| {
280                a.energy
281                    .partial_cmp(&b.energy)
282                    .unwrap_or(std::cmp::Ordering::Equal)
283            })
284            .map_or(0, |(i, _)| i);
285
286        let best_solution = data.row(best_idx);
287
288        // Calculate Hamming distances
289        let mut result = Array2::zeros((n_samples, 2));
290
291        for i in 0..n_samples {
292            let hamming_dist: f64 = data
293                .row(i)
294                .iter()
295                .zip(best_solution.iter())
296                .filter(|(&a, &b)| (a - b).abs() > 0.5)
297                .count() as f64;
298
299            result[[i, 0]] = hamming_dist;
300            result[[i, 1]] = self.samples[i].energy;
301        }
302
303        Ok(result)
304    }
305
306    #[cfg(feature = "scirs")]
307    /// Estimate density in 2D
308    fn estimate_density_2d(
309        &self,
310        x: &[f64],
311        y: &[f64],
312        energies: &[f64],
313    ) -> Result<DensityMap, Box<dyn std::error::Error>> {
314        let kde = KernelDensityEstimator::new("gaussian")?;
315
316        // Create grid
317        let x_min = x.iter().fold(f64::INFINITY, |a, &b| a.min(b));
318        let x_max = x.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
319        let y_min = y.iter().fold(f64::INFINITY, |a, &b| a.min(b));
320        let y_max = y.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
321
322        let x_range = x_max - x_min;
323        let y_range = y_max - y_min;
324
325        let mut grid_x = Vec::new();
326        let mut grid_y = Vec::new();
327        let mut grid_density = Vec::new();
328
329        for i in 0..self.config.resolution {
330            for j in 0..self.config.resolution {
331                let gx = (i as f64 / self.config.resolution as f64).mul_add(x_range, x_min);
332                let gy = (j as f64 / self.config.resolution as f64).mul_add(y_range, y_min);
333
334                grid_x.push(gx);
335                grid_y.push(gy);
336
337                // Estimate density at this point
338                let density = kde.estimate_2d(x, y, gx, gy)?;
339                grid_density.push(density);
340            }
341        }
342
343        Ok(DensityMap {
344            x: grid_x,
345            y: grid_y,
346            density: grid_density,
347            resolution: self.config.resolution,
348        })
349    }
350
351    /// Set custom projection matrix
352    pub fn set_projection_matrix(&mut self, matrix: Array2<f64>) {
353        self.projection_matrix = Some(matrix);
354    }
355}
356
357/// Histogram data
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct HistogramData {
360    pub bin_centers: Vec<f64>,
361    pub counts: Vec<usize>,
362    pub bin_width: f64,
363    pub total_samples: usize,
364}
365
366/// Projected landscape data
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct ProjectedLandscape {
369    pub x_coords: Vec<f64>,
370    pub y_coords: Vec<f64>,
371    pub energies: Vec<f64>,
372    pub density_map: Option<DensityMap>,
373    pub projection_info: String,
374}
375
376/// Density map data
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct DensityMap {
379    pub x: Vec<f64>,
380    pub y: Vec<f64>,
381    pub density: Vec<f64>,
382    pub resolution: usize,
383}
384
385/// Plot energy landscape
386pub fn plot_energy_landscape(
387    samples: Vec<SampleResult>,
388    config: Option<EnergyLandscapeConfig>,
389) -> Result<(), Box<dyn std::error::Error>> {
390    let config = config.unwrap_or_default();
391    let mut landscape = EnergyLandscape::new(config);
392    landscape.add_samples(samples);
393
394    #[cfg(feature = "scirs")]
395    {
396        use crate::scirs_stub::scirs2_plot::{Figure, Subplot};
397
398        let mut fig = Figure::new();
399
400        // Add energy histogram
401        let hist_data = landscape.energy_histogram()?;
402        let counts_f64: Vec<f64> = hist_data.counts.iter().map(|&c| c as f64).collect();
403        fig.add_subplot(2, 2, 1)?
404            .bar(&hist_data.bin_centers, &counts_f64)
405            .set_xlabel("Energy")
406            .set_ylabel("Count")
407            .set_title("Energy Distribution");
408
409        // Add 2D projection
410        let proj_data = landscape.project_2d()?;
411        let subplot = fig.add_subplot(2, 2, 2)?;
412        subplot
413            .scatter(&proj_data.x_coords, &proj_data.y_coords)
414            .set_color_data(&proj_data.energies)
415            .set_colormap(&landscape.config.colormap)
416            .set_xlabel("Component 1")
417            .set_ylabel("Component 2")
418            .set_title(&proj_data.projection_info);
419
420        // Add density plot if available
421        if let Some(density) = proj_data.density_map {
422            fig.add_subplot(2, 2, 3)?
423                .contourf(&density.x, &density.y, &density.density)
424                .set_colormap("plasma")
425                .set_xlabel("Component 1")
426                .set_ylabel("Component 2")
427                .set_title("Solution Density");
428        }
429
430        fig.show()?;
431    }
432
433    #[cfg(not(feature = "scirs"))]
434    {
435        // Export data for external plotting
436        let hist_data = landscape.energy_histogram()?;
437        let proj_data = landscape.project_2d()?;
438
439        export_landscape_data(&hist_data, &proj_data, "energy_landscape.json")?;
440        println!("Energy landscape data exported to energy_landscape.json");
441    }
442
443    Ok(())
444}
445
446/// Export landscape data
447fn export_landscape_data(
448    histogram: &HistogramData,
449    projection: &ProjectedLandscape,
450    path: &str,
451) -> Result<(), Box<dyn std::error::Error>> {
452    let export = LandscapeExport {
453        histogram: histogram.clone(),
454        projection: projection.clone(),
455        timestamp: std::time::SystemTime::now(),
456    };
457
458    let json = serde_json::to_string_pretty(&export)?;
459    std::fs::write(path, json)?;
460
461    Ok(())
462}
463
464/// Landscape export format
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct LandscapeExport {
467    pub histogram: HistogramData,
468    pub projection: ProjectedLandscape,
469    pub timestamp: std::time::SystemTime,
470}