1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct EnergyLandscapeConfig {
20 pub bins: usize,
22 pub projection: ProjectionMethod,
24 pub colormap: String,
26 pub include_density: bool,
28 pub resolution: usize,
30 pub energy_limits: Option<(f64, f64)>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum ProjectionMethod {
37 PCA,
39 Random,
41 HammingDistance,
43 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
60pub struct EnergyLandscape {
62 config: EnergyLandscapeConfig,
63 samples: Vec<SampleResult>,
64 projection_matrix: Option<Array2<f64>>,
65}
66
67impl EnergyLandscape {
68 pub const fn new(config: EnergyLandscapeConfig) -> Self {
70 Self {
71 config,
72 samples: Vec::new(),
73 projection_matrix: None,
74 }
75 }
76
77 pub fn add_samples(&mut self, samples: Vec<SampleResult>) {
79 self.samples.extend(samples);
80 }
81
82 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 pub fn project_2d(&mut self) -> Result<ProjectedLandscape, Box<dyn std::error::Error>> {
120 let (binary_matrix, _var_names) = self.samples_to_matrix()?;
122
123 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn set_projection_matrix(&mut self, matrix: Array2<f64>) {
353 self.projection_matrix = Some(matrix);
354 }
355}
356
357#[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#[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#[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
385pub 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 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 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 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 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
446fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct LandscapeExport {
467 pub histogram: HistogramData,
468 pub projection: ProjectedLandscape,
469 pub timestamp: std::time::SystemTime,
470}