1use scirs2_core::random::prelude::*;
8use scirs2_core::random::ChaCha8Rng;
9use scirs2_core::random::{Rng, SeedableRng};
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::Write;
13use std::path::Path;
14use thiserror::Error;
15
16use crate::ising::{IsingError, IsingModel};
17use crate::simulator::{AnnealingParams, AnnealingSolution, QuantumAnnealingSimulator};
18
19#[derive(Error, Debug)]
21pub enum VisualizationError {
22 #[error("Ising error: {0}")]
24 IsingError(#[from] IsingError),
25
26 #[error("I/O error: {0}")]
28 IoError(#[from] std::io::Error),
29
30 #[error("Invalid parameter: {0}")]
32 InvalidParameter(String),
33
34 #[error("Computation error: {0}")]
36 ComputationError(String),
37}
38
39pub type VisualizationResult<T> = Result<T, VisualizationError>;
41
42#[derive(Debug, Clone)]
44pub struct LandscapePoint {
45 pub configuration: Vec<i8>,
47
48 pub energy: f64,
50
51 pub hamming_distance: Option<usize>,
53
54 pub basin_id: Option<usize>,
56}
57
58pub struct LandscapeAnalyzer {
60 reference_config: Option<Vec<i8>>,
62
63 energy_cutoff: Option<f64>,
65
66 max_samples: usize,
68
69 seed: Option<u64>,
71}
72
73impl LandscapeAnalyzer {
74 #[must_use]
76 pub const fn new() -> Self {
77 Self {
78 reference_config: None,
79 energy_cutoff: None,
80 max_samples: 10_000,
81 seed: None,
82 }
83 }
84
85 #[must_use]
87 pub fn with_reference(mut self, config: Vec<i8>) -> Self {
88 self.reference_config = Some(config);
89 self
90 }
91
92 #[must_use]
94 pub const fn with_energy_cutoff(mut self, cutoff: f64) -> Self {
95 self.energy_cutoff = Some(cutoff);
96 self
97 }
98
99 #[must_use]
101 pub const fn with_max_samples(mut self, max_samples: usize) -> Self {
102 self.max_samples = max_samples;
103 self
104 }
105
106 #[must_use]
108 pub const fn with_seed(mut self, seed: u64) -> Self {
109 self.seed = Some(seed);
110 self
111 }
112
113 pub fn sample_landscape(&self, model: &IsingModel) -> VisualizationResult<Vec<LandscapePoint>> {
115 if model.num_qubits > 20 {
116 return Err(VisualizationError::InvalidParameter(
117 "Random sampling only practical for problems with ≤20 qubits".to_string(),
118 ));
119 }
120
121 let mut rng = match self.seed {
122 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
123 None => ChaCha8Rng::seed_from_u64(thread_rng().gen()),
124 };
125
126 let mut points = Vec::new();
127 let num_samples = std::cmp::min(self.max_samples, 2_usize.pow(model.num_qubits as u32));
128
129 for _ in 0..num_samples {
130 let config: Vec<i8> = (0..model.num_qubits)
131 .map(|_| if rng.gen_bool(0.5) { 1 } else { -1 })
132 .collect();
133
134 let energy = model.energy(&config)?;
135
136 if let Some(cutoff) = self.energy_cutoff {
138 if energy > cutoff {
139 continue;
140 }
141 }
142
143 let hamming_distance = self
144 .reference_config
145 .as_ref()
146 .map(|ref_config| hamming_distance(&config, ref_config));
147
148 points.push(LandscapePoint {
149 configuration: config,
150 energy,
151 hamming_distance,
152 basin_id: None,
153 });
154 }
155
156 Ok(points)
157 }
158
159 pub fn enumerate_landscape(
161 &self,
162 model: &IsingModel,
163 ) -> VisualizationResult<Vec<LandscapePoint>> {
164 if model.num_qubits > 16 {
165 return Err(VisualizationError::InvalidParameter(
166 "Exhaustive enumeration only practical for problems with ≤16 qubits".to_string(),
167 ));
168 }
169
170 let mut points = Vec::new();
171 let total_configs = 2_usize.pow(model.num_qubits as u32);
172
173 for i in 0..total_configs {
174 let config: Vec<i8> = (0..model.num_qubits)
175 .map(|j| if (i >> j) & 1 == 1 { 1 } else { -1 })
176 .collect();
177
178 let energy = model.energy(&config)?;
179
180 if let Some(cutoff) = self.energy_cutoff {
182 if energy > cutoff {
183 continue;
184 }
185 }
186
187 let hamming_distance = self
188 .reference_config
189 .as_ref()
190 .map(|ref_config| hamming_distance(&config, ref_config));
191
192 points.push(LandscapePoint {
193 configuration: config,
194 energy,
195 hamming_distance,
196 basin_id: None,
197 });
198 }
199
200 Ok(points)
201 }
202
203 pub fn sample_from_annealing(
205 &self,
206 model: &IsingModel,
207 num_trajectories: usize,
208 ) -> VisualizationResult<Vec<LandscapePoint>> {
209 let mut all_points = Vec::new();
210
211 for i in 0..num_trajectories {
212 let params = AnnealingParams {
213 seed: self.seed.map(|s| s + i as u64),
214 num_sweeps: 1000,
215 num_repetitions: 1,
216 ..Default::default()
217 };
218
219 let mut simulator = QuantumAnnealingSimulator::new(params)
220 .map_err(|e| VisualizationError::ComputationError(e.to_string()))?;
221
222 let trajectory = self.collect_annealing_trajectory(&mut simulator, model)?;
224
225 all_points.extend(trajectory);
226 }
227
228 Ok(all_points)
229 }
230
231 fn collect_annealing_trajectory(
233 &self,
234 _simulator: &mut QuantumAnnealingSimulator,
235 model: &IsingModel,
236 ) -> VisualizationResult<Vec<LandscapePoint>> {
237 let mut rng = ChaCha8Rng::seed_from_u64(self.seed.unwrap_or(0));
240 let mut points = Vec::new();
241
242 let mut current_config: Vec<i8> = (0..model.num_qubits)
243 .map(|_| if rng.gen_bool(0.5) { 1 } else { -1 })
244 .collect();
245
246 for step in 0..100 {
247 let energy = model.energy(¤t_config)?;
248
249 let hamming_distance = self
250 .reference_config
251 .as_ref()
252 .map(|ref_config| hamming_distance(¤t_config, ref_config));
253
254 points.push(LandscapePoint {
255 configuration: current_config.clone(),
256 energy,
257 hamming_distance,
258 basin_id: None,
259 });
260
261 if step % 10 == 0 {
263 let flip_idx = rng.gen_range(0..model.num_qubits);
264 current_config[flip_idx] *= -1;
265 }
266 }
267
268 Ok(points)
269 }
270}
271
272impl Default for LandscapeAnalyzer {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct LandscapeStats {
281 pub num_configurations: usize,
283
284 pub min_energy: f64,
286
287 pub max_energy: f64,
289
290 pub mean_energy: f64,
292
293 pub energy_std: f64,
295
296 pub num_local_minima: usize,
298
299 pub energy_gap: Option<f64>,
301
302 pub ground_state_degeneracy: usize,
304}
305
306pub fn calculate_landscape_stats(points: &[LandscapePoint]) -> LandscapeStats {
308 if points.is_empty() {
309 return LandscapeStats {
310 num_configurations: 0,
311 min_energy: 0.0,
312 max_energy: 0.0,
313 mean_energy: 0.0,
314 energy_std: 0.0,
315 num_local_minima: 0,
316 energy_gap: None,
317 ground_state_degeneracy: 0,
318 };
319 }
320
321 let energies: Vec<f64> = points.iter().map(|p| p.energy).collect();
322
323 let min_energy = energies.iter().copied().fold(f64::INFINITY, f64::min);
324 let max_energy = energies.iter().copied().fold(f64::NEG_INFINITY, f64::max);
325 let mean_energy = energies.iter().sum::<f64>() / energies.len() as f64;
326
327 let variance = energies
328 .iter()
329 .map(|&e| (e - mean_energy).powi(2))
330 .sum::<f64>()
331 / energies.len() as f64;
332 let energy_std = variance.sqrt();
333
334 let mut energy_counts = HashMap::new();
336 for &energy in &energies {
337 *energy_counts.entry(OrderedFloat(energy)).or_insert(0) += 1;
338 }
339
340 let ground_state_degeneracy = *energy_counts.get(&OrderedFloat(min_energy)).unwrap_or(&0);
341
342 let mut unique_energies: Vec<f64> = energy_counts.keys().map(|&OrderedFloat(e)| e).collect();
344 unique_energies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
345
346 let energy_gap = (unique_energies.len() > 1).then(|| unique_energies[1] - unique_energies[0]);
347
348 LandscapeStats {
349 num_configurations: points.len(),
350 min_energy,
351 max_energy,
352 mean_energy,
353 energy_std,
354 num_local_minima: 0, energy_gap,
356 ground_state_degeneracy,
357 }
358}
359
360pub fn plot_energy_landscape<P: AsRef<Path>>(
362 points: &[LandscapePoint],
363 output_path: P,
364 title: &str,
365) -> VisualizationResult<()> {
366 let mut file = File::create(output_path)?;
367
368 writeln!(file, "configuration_id,energy,hamming_distance,basin_id")?;
370
371 for (i, point) in points.iter().enumerate() {
373 writeln!(
374 file,
375 "{},{},{},{}",
376 i,
377 point.energy,
378 point
379 .hamming_distance
380 .map_or(String::new(), |d| d.to_string()),
381 point.basin_id.map_or(String::new(), |b| b.to_string())
382 )?;
383 }
384
385 writeln!(file, "# Title: {title}")?;
387 writeln!(file, "# Number of points: {}", points.len())?;
388
389 if !points.is_empty() {
390 let stats = calculate_landscape_stats(points);
391 writeln!(file, "# Min energy: {:.6}", stats.min_energy)?;
392 writeln!(file, "# Max energy: {:.6}", stats.max_energy)?;
393 writeln!(file, "# Mean energy: {:.6}", stats.mean_energy)?;
394 writeln!(file, "# Energy std: {:.6}", stats.energy_std)?;
395 }
396
397 Ok(())
398}
399
400pub fn plot_energy_histogram<P: AsRef<Path>>(
402 points: &[LandscapePoint],
403 output_path: P,
404 num_bins: usize,
405) -> VisualizationResult<()> {
406 if points.is_empty() {
407 return Err(VisualizationError::InvalidParameter(
408 "No points to plot".to_string(),
409 ));
410 }
411
412 let energies: Vec<f64> = points.iter().map(|p| p.energy).collect();
413 let min_energy = energies.iter().copied().fold(f64::INFINITY, f64::min);
414 let max_energy = energies.iter().copied().fold(f64::NEG_INFINITY, f64::max);
415
416 if min_energy == max_energy {
417 return Err(VisualizationError::InvalidParameter(
418 "All energies are the same".to_string(),
419 ));
420 }
421
422 let bin_width = (max_energy - min_energy) / num_bins as f64;
423 let mut bins = vec![0; num_bins];
424
425 for &energy in &energies {
426 let bin_idx = ((energy - min_energy) / bin_width).floor() as usize;
427 let bin_idx = std::cmp::min(bin_idx, num_bins - 1);
428 bins[bin_idx] += 1;
429 }
430
431 let mut file = File::create(output_path)?;
432 writeln!(file, "bin_center,count,frequency")?;
433
434 for (i, &count) in bins.iter().enumerate() {
435 let bin_center = (i as f64 + 0.5).mul_add(bin_width, min_energy);
436 let frequency = f64::from(count) / energies.len() as f64;
437 writeln!(file, "{bin_center:.6},{count},{frequency:.6}")?;
438 }
439
440 Ok(())
441}
442
443fn hamming_distance(config1: &[i8], config2: &[i8]) -> usize {
445 config1
446 .iter()
447 .zip(config2.iter())
448 .filter(|(&a, &b)| a != b)
449 .count()
450}
451
452#[derive(Debug, Clone, Copy, PartialEq)]
454struct OrderedFloat(f64);
455
456impl Eq for OrderedFloat {}
457
458impl std::hash::Hash for OrderedFloat {
459 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
460 self.0.to_bits().hash(state);
461 }
462}
463
464impl std::cmp::Ord for OrderedFloat {
465 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
466 self.0
467 .partial_cmp(&other.0)
468 .unwrap_or(std::cmp::Ordering::Equal)
469 }
470}
471
472impl std::cmp::PartialOrd for OrderedFloat {
473 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
474 Some(self.cmp(other))
475 }
476}
477
478pub struct BasinAnalyzer {
480 energy_tolerance: f64,
482
483 hamming_threshold: usize,
485}
486
487impl BasinAnalyzer {
488 #[must_use]
490 pub const fn new(energy_tolerance: f64, hamming_threshold: usize) -> Self {
491 Self {
492 energy_tolerance,
493 hamming_threshold,
494 }
495 }
496
497 pub fn identify_basins(&self, points: &mut [LandscapePoint]) -> VisualizationResult<usize> {
499 if points.is_empty() {
500 return Ok(0);
501 }
502
503 let mut basin_id = 0;
504
505 for i in 0..points.len() {
506 if points[i].basin_id.is_some() {
507 continue;
508 }
509
510 points[i].basin_id = Some(basin_id);
512 let mut stack = vec![i];
513
514 while let Some(current_idx) = stack.pop() {
515 let current_energy = points[current_idx].energy;
516 let current_config = points[current_idx].configuration.clone();
517
518 for j in 0..points.len() {
520 if points[j].basin_id.is_some() {
521 continue;
522 }
523
524 let energy_diff = (points[j].energy - current_energy).abs();
525 let hamming_dist = hamming_distance(¤t_config, &points[j].configuration);
526
527 if energy_diff <= self.energy_tolerance
528 && hamming_dist <= self.hamming_threshold
529 {
530 points[j].basin_id = Some(basin_id);
531 stack.push(j);
532 }
533 }
534 }
535
536 basin_id += 1;
537 }
538
539 Ok(basin_id)
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_landscape_analyzer() {
549 let mut model = IsingModel::new(4);
550 model
551 .set_coupling(0, 1, -1.0)
552 .expect("set_coupling(0,1) should succeed");
553 model
554 .set_coupling(1, 2, -1.0)
555 .expect("set_coupling(1,2) should succeed");
556 model
557 .set_coupling(2, 3, -1.0)
558 .expect("set_coupling(2,3) should succeed");
559
560 let analyzer = LandscapeAnalyzer::new().with_max_samples(100).with_seed(42);
561
562 let points = analyzer
563 .sample_landscape(&model)
564 .expect("sample_landscape should succeed");
565 assert!(!points.is_empty());
566 assert!(points.len() <= 100);
567 }
568
569 #[test]
570 fn test_landscape_stats() {
571 let points = vec![
572 LandscapePoint {
573 configuration: vec![1, 1, 1],
574 energy: -2.0,
575 hamming_distance: None,
576 basin_id: None,
577 },
578 LandscapePoint {
579 configuration: vec![-1, -1, -1],
580 energy: -2.0,
581 hamming_distance: None,
582 basin_id: None,
583 },
584 LandscapePoint {
585 configuration: vec![1, -1, 1],
586 energy: 1.0,
587 hamming_distance: None,
588 basin_id: None,
589 },
590 ];
591
592 let stats = calculate_landscape_stats(&points);
593 assert_eq!(stats.num_configurations, 3);
594 assert_eq!(stats.min_energy, -2.0);
595 assert_eq!(stats.max_energy, 1.0);
596 assert_eq!(stats.ground_state_degeneracy, 2);
597 assert_eq!(stats.energy_gap, Some(3.0));
598 }
599
600 #[test]
601 fn test_hamming_distance() {
602 let config1 = vec![1, -1, 1, -1];
603 let config2 = vec![1, 1, -1, -1];
604 assert_eq!(hamming_distance(&config1, &config2), 2);
605
606 let config3 = vec![1, -1, 1, -1];
607 assert_eq!(hamming_distance(&config1, &config3), 0);
608 }
609
610 #[test]
611 fn test_basin_analyzer() {
612 let mut points = vec![
613 LandscapePoint {
614 configuration: vec![1, 1],
615 energy: -1.0,
616 hamming_distance: None,
617 basin_id: None,
618 },
619 LandscapePoint {
620 configuration: vec![1, -1],
621 energy: -0.9,
622 hamming_distance: None,
623 basin_id: None,
624 },
625 LandscapePoint {
626 configuration: vec![-1, -1],
627 energy: 2.0,
628 hamming_distance: None,
629 basin_id: None,
630 },
631 ];
632
633 let analyzer = BasinAnalyzer::new(0.2, 1);
634 let num_basins = analyzer
635 .identify_basins(&mut points)
636 .expect("identify_basins should succeed");
637
638 assert_eq!(num_basins, 2);
639 assert_eq!(points[0].basin_id, points[1].basin_id);
640 assert_ne!(points[0].basin_id, points[2].basin_id);
641 }
642}