1use crate::sampler::SampleResult;
7use scirs2_core::ndarray::Array2;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11type JobShopSchedule = Vec<(usize, usize, usize, usize)>;
13
14#[cfg(feature = "scirs")]
15use crate::scirs_stub::{
16 scirs2_graphs::{Graph, GraphLayout},
17 scirs2_plot::{ColorMap, NetworkPlot, Plot2D},
18};
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum VisualizationType {
23 TSP {
25 coordinates: Vec<(f64, f64)>,
26 city_names: Option<Vec<String>>,
27 },
28 GraphColoring {
30 adjacency_matrix: Array2<bool>,
31 node_names: Option<Vec<String>>,
32 max_colors: usize,
33 },
34 MaxCut {
36 adjacency_matrix: Array2<f64>,
37 node_names: Option<Vec<String>>,
38 },
39 JobShop {
41 n_jobs: usize,
42 n_machines: usize,
43 time_horizon: usize,
44 durations: Option<Vec<Vec<usize>>>,
50 },
51 NumberPartition { numbers: Vec<f64> },
53 Knapsack {
55 weights: Vec<f64>,
56 values: Vec<f64>,
57 capacity: f64,
58 },
59 Portfolio {
61 asset_names: Vec<String>,
62 expected_returns: Vec<f64>,
63 risk_matrix: Array2<f64>,
64 },
65 Custom {
67 plot_function: String,
68 metadata: HashMap<String, String>,
69 },
70}
71
72pub struct ProblemVisualizer {
74 problem_type: VisualizationType,
75 samples: Vec<SampleResult>,
76 config: VisualizationConfig,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct VisualizationConfig {
82 pub best_only: bool,
84 pub top_k: usize,
86 pub color_scheme: String,
88 pub node_size: f64,
90 pub edge_width: f64,
92 pub animate: bool,
94 pub animation_speed: f64,
96}
97
98impl Default for VisualizationConfig {
99 fn default() -> Self {
100 Self {
101 best_only: false,
102 top_k: 5,
103 color_scheme: "viridis".to_string(),
104 node_size: 50.0,
105 edge_width: 2.0,
106 animate: false,
107 animation_speed: 2.0,
108 }
109 }
110}
111
112impl ProblemVisualizer {
113 pub const fn new(problem_type: VisualizationType, config: VisualizationConfig) -> Self {
115 Self {
116 problem_type,
117 samples: Vec::new(),
118 config,
119 }
120 }
121
122 pub fn add_samples(&mut self, samples: Vec<SampleResult>) {
124 self.samples.extend(samples);
125 }
126
127 pub fn visualize(&self) -> Result<(), Box<dyn std::error::Error>> {
129 if self.samples.is_empty() {
130 return Err("No samples to visualize".into());
131 }
132
133 match &self.problem_type {
134 VisualizationType::TSP {
135 coordinates,
136 city_names,
137 } => self.visualize_tsp(coordinates, city_names)?,
138 VisualizationType::GraphColoring {
139 adjacency_matrix,
140 node_names,
141 max_colors,
142 } => self.visualize_graph_coloring(adjacency_matrix, node_names, *max_colors)?,
143 VisualizationType::MaxCut {
144 adjacency_matrix,
145 node_names,
146 } => self.visualize_max_cut(adjacency_matrix, node_names)?,
147 VisualizationType::JobShop {
148 n_jobs,
149 n_machines,
150 time_horizon,
151 durations,
152 } => {
153 self.visualize_job_shop(*n_jobs, *n_machines, *time_horizon, durations.as_ref())?;
154 }
155 VisualizationType::NumberPartition { numbers } => {
156 self.visualize_number_partition(numbers)?;
157 }
158 VisualizationType::Knapsack {
159 weights,
160 values,
161 capacity,
162 } => self.visualize_knapsack(weights, values, *capacity)?,
163 VisualizationType::Portfolio {
164 asset_names,
165 expected_returns,
166 risk_matrix,
167 } => self.visualize_portfolio(asset_names, expected_returns, risk_matrix)?,
168 VisualizationType::Custom {
169 plot_function,
170 metadata,
171 } => self.visualize_custom(plot_function, metadata)?,
172 }
173
174 Ok(())
175 }
176
177 fn visualize_tsp(
179 &self,
180 coordinates: &[(f64, f64)],
181 city_names: &Option<Vec<String>>,
182 ) -> Result<(), Box<dyn std::error::Error>> {
183 let n_cities = coordinates.len();
184
185 let best_samples = self.get_best_samples();
187
188 #[cfg(feature = "scirs")]
189 {
190 use crate::scirs_stub::scirs2_plot::{Figure, Subplot};
191
192 let mut fig = Figure::new();
193
194 for (idx, sample) in best_samples.iter().enumerate() {
195 if idx >= self.config.top_k {
196 break;
197 }
198
199 let subplot = fig.add_subplot(
200 (self.config.top_k as f64).sqrt().ceil() as usize,
201 (self.config.top_k as f64).sqrt().ceil() as usize,
202 idx + 1,
203 )?;
204
205 let tour = self.extract_tsp_tour(sample, n_cities)?;
207
208 let x: Vec<f64> = coordinates.iter().map(|c| c.0).collect();
210 let y: Vec<f64> = coordinates.iter().map(|c| c.1).collect();
211
212 subplot
213 .scatter(&x, &y)
214 .set_size(self.config.node_size)
215 .set_color("blue");
216
217 for i in 0..tour.len() {
219 let from = tour[i];
220 let to = tour[(i + 1) % tour.len()];
221
222 subplot
223 .plot(
224 &[coordinates[from].0, coordinates[to].0],
225 &[coordinates[from].1, coordinates[to].1],
226 )
227 .set_color("red")
228 .set_linewidth(self.config.edge_width);
229 }
230
231 if let Some(names) = city_names {
233 for (i, name) in names.iter().enumerate() {
234 subplot
235 .text(coordinates[i].0, coordinates[i].1, name)
236 .set_fontsize(8)
237 .set_ha("center");
238 }
239 }
240
241 subplot.set_title(&format!(
242 "Tour {}: Distance = {:.2}",
243 idx + 1,
244 sample.energy
245 ));
246 subplot.set_aspect("equal");
247 }
248
249 fig.suptitle("TSP Solutions");
250 fig.show()?;
251 }
252
253 #[cfg(not(feature = "scirs"))]
254 {
255 let export = TSPExport {
257 coordinates: coordinates.to_vec(),
258 city_names: city_names.clone(),
259 best_tours: best_samples
260 .iter()
261 .take(self.config.top_k)
262 .map(|s| self.extract_tsp_tour(s, n_cities))
263 .collect::<Result<Vec<_>, _>>()?,
264 tour_lengths: best_samples
265 .iter()
266 .take(self.config.top_k)
267 .map(|s| s.energy)
268 .collect(),
269 };
270
271 let json = serde_json::to_string_pretty(&export)?;
272 std::fs::write("tsp_solution.json", json)?;
273 println!("TSP solution exported to tsp_solution.json");
274 }
275
276 Ok(())
277 }
278
279 fn extract_tsp_tour(
281 &self,
282 sample: &SampleResult,
283 n_cities: usize,
284 ) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
285 let mut tour = Vec::new();
286 let mut visited = vec![false; n_cities];
287 let mut current = 0;
288
289 tour.push(current);
290 visited[current] = true;
291
292 for _ in 1..n_cities {
294 let mut next_city = None;
295
296 for (j, &is_visited) in visited.iter().enumerate().take(n_cities) {
297 if !is_visited {
298 let edge_var = format!("x_{current}_{j}");
299 if sample.assignments.get(&edge_var).copied().unwrap_or(false) {
300 next_city = Some(j);
301 break;
302 }
303 }
304 }
305
306 if let Some(next) = next_city {
307 tour.push(next);
308 visited[next] = true;
309 current = next;
310 } else {
311 for (j, is_visited) in visited.iter_mut().enumerate().take(n_cities) {
313 if !*is_visited {
314 tour.push(j);
315 *is_visited = true;
316 current = j;
317 break;
318 }
319 }
320 }
321 }
322
323 Ok(tour)
324 }
325
326 fn visualize_graph_coloring(
328 &self,
329 adjacency: &Array2<bool>,
330 node_names: &Option<Vec<String>>,
331 max_colors: usize,
332 ) -> Result<(), Box<dyn std::error::Error>> {
333 let n_nodes = adjacency.nrows();
334 let best_sample = self.get_best_sample()?;
335
336 let node_colors = self.extract_node_colors(best_sample, n_nodes, max_colors)?;
338
339 #[cfg(feature = "scirs")]
340 {
341 use crate::scirs_stub::scirs2_graphs::spring_layout;
342 use crate::scirs_stub::scirs2_plot::Figure;
343
344 let mut fig = Figure::new();
345 let ax = fig.add_subplot(1, 1, 1)?;
346
347 let mut edges = Vec::new();
349 for i in 0..n_nodes {
350 for j in i + 1..n_nodes {
351 if adjacency[[i, j]] {
352 edges.push((i, j));
353 }
354 }
355 }
356
357 let positions = spring_layout(&edges, n_nodes)?;
359
360 for (i, j) in &edges {
362 ax.plot(
363 &[positions[*i].0, positions[*j].0],
364 &[positions[*i].1, positions[*j].1],
365 )
366 .set_color("gray")
367 .set_alpha(0.5)
368 .set_linewidth(1.0);
369 }
370
371 let color_palette = ["red", "blue", "green", "yellow", "purple", "orange"];
373
374 for i in 0..n_nodes {
375 let color = color_palette[node_colors[i] % color_palette.len()];
376
377 ax.scatter(&[positions[i].0], &[positions[i].1])
378 .set_color(color)
379 .set_size(self.config.node_size)
380 .set_edgecolor("black");
381
382 let label = if let Some(names) = node_names {
384 &names[i]
385 } else {
386 &i.to_string()
387 };
388
389 ax.text(positions[i].0, positions[i].1, label)
390 .set_ha("center")
391 .set_va("center")
392 .set_fontsize(8);
393 }
394
395 ax.set_title(&format!(
396 "Graph Coloring: {} colors used",
397 node_colors.iter().max().unwrap_or(&0) + 1
398 ));
399 ax.set_aspect("equal");
400 ax.axis("off");
401
402 fig.show()?;
403 }
404
405 #[cfg(not(feature = "scirs"))]
406 {
407 let export = GraphColoringExport {
409 n_nodes,
410 edges: self.extract_edges(adjacency),
411 node_colors: node_colors.clone(),
412 node_names: node_names.clone(),
413 n_colors_used: node_colors.iter().max().copied().unwrap_or(0) + 1,
414 };
415
416 let json = serde_json::to_string_pretty(&export)?;
417 std::fs::write("graph_coloring.json", json)?;
418 println!("Graph coloring exported to graph_coloring.json");
419 }
420
421 Ok(())
422 }
423
424 fn extract_node_colors(
426 &self,
427 sample: &SampleResult,
428 n_nodes: usize,
429 max_colors: usize,
430 ) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
431 let mut colors = vec![0; n_nodes];
432
433 for (i, color) in colors.iter_mut().enumerate().take(n_nodes) {
434 for c in 0..max_colors {
435 let var_name = format!("node_{i}_color_{c}");
436 if sample.assignments.get(&var_name).copied().unwrap_or(false) {
437 *color = c;
438 break;
439 }
440 }
441 }
442
443 Ok(colors)
444 }
445
446 fn visualize_max_cut(
448 &self,
449 adjacency: &Array2<f64>,
450 node_names: &Option<Vec<String>>,
451 ) -> Result<(), Box<dyn std::error::Error>> {
452 let n_nodes = adjacency.nrows();
453 let best_sample = self.get_best_sample()?;
454
455 let partition = self.extract_partition(best_sample, n_nodes)?;
457
458 #[cfg(feature = "scirs")]
459 {
460 use crate::scirs_stub::scirs2_plot::Figure;
461
462 let mut fig = Figure::new();
463 let ax = fig.add_subplot(1, 1, 1)?;
464
465 let positions = self.compute_graph_layout(adjacency)?;
467
468 let mut cut_weight = 0.0;
470 for i in 0..n_nodes {
471 for j in i + 1..n_nodes {
472 if adjacency[[i, j]] > 0.0 {
473 let is_cut = partition[i] != partition[j];
474 let color = if is_cut { "red" } else { "gray" };
475 let width = if is_cut { 3.0 } else { 1.0 };
476
477 if is_cut {
478 cut_weight += adjacency[[i, j]];
479 }
480
481 ax.plot(
482 &[positions[i].0, positions[j].0],
483 &[positions[i].1, positions[j].1],
484 )
485 .set_color(color)
486 .set_linewidth(width)
487 .set_alpha(if is_cut { 1.0 } else { 0.3 });
488 }
489 }
490 }
491
492 for i in 0..n_nodes {
494 let color = if partition[i] { "blue" } else { "orange" };
495
496 ax.scatter(&[positions[i].0], &[positions[i].1])
497 .set_color(color)
498 .set_size(self.config.node_size)
499 .set_edgecolor("black");
500
501 let label = if let Some(names) = node_names {
503 &names[i]
504 } else {
505 &i.to_string()
506 };
507
508 ax.text(positions[i].0, positions[i].1, label)
509 .set_ha("center")
510 .set_va("center")
511 .set_fontsize(8);
512 }
513
514 ax.set_title(&format!("Max Cut: Weight = {cut_weight:.2}"));
515 ax.set_aspect("equal");
516 ax.axis("off");
517
518 fig.show()?;
519 }
520
521 Ok(())
522 }
523
524 fn extract_partition(
526 &self,
527 sample: &SampleResult,
528 n_nodes: usize,
529 ) -> Result<Vec<bool>, Box<dyn std::error::Error>> {
530 let mut partition = vec![false; n_nodes];
531
532 for (i, part) in partition.iter_mut().enumerate().take(n_nodes) {
533 let var_name = format!("x_{i}");
534 *part = sample.assignments.get(&var_name).copied().unwrap_or(false);
535 }
536
537 Ok(partition)
538 }
539
540 fn visualize_job_shop(
542 &self,
543 n_jobs: usize,
544 n_machines: usize,
545 time_horizon: usize,
546 durations: Option<&Vec<Vec<usize>>>,
547 ) -> Result<(), Box<dyn std::error::Error>> {
548 let best_sample = self.get_best_sample()?;
549
550 let schedule =
552 self.extract_schedule(best_sample, n_jobs, n_machines, time_horizon, durations)?;
553
554 #[cfg(feature = "scirs")]
555 {
556 use crate::scirs_stub::scirs2_plot::Figure;
557
558 let mut fig = Figure::new();
559 let ax = fig.add_subplot(1, 1, 1)?;
560
561 let colors = ["red", "blue", "green", "yellow", "purple", "orange"];
563
564 for (job, machine, start, duration) in &schedule {
565 let mut y = *machine as f64;
566 let color = colors[*job % colors.len()];
567
568 ax.barh(&[y], &[*duration as f64], &[*start as f64], 0.8)
569 .set_color(color)
570 .set_edgecolor("black")
571 .set_label(&format!("Job {job}"));
572 }
573
574 ax.set_xlabel("Time");
575 ax.set_ylabel("Machine");
576 ax.set_title("Job Shop Schedule");
577 ax.set_ylim(-0.5, n_machines as f64 - 0.5);
578 ax.set_xlim(0.0, time_horizon as f64);
579
580 ax.set_yticks(&(0..n_machines).map(|i| i as f64).collect::<Vec<_>>());
582 ax.set_yticklabels(&(0..n_machines).map(|i| format!("M{i}")).collect::<Vec<_>>());
583
584 ax.legend_unique();
586
587 fig.show()?;
588 }
589
590 Ok(())
591 }
592
593 fn extract_schedule(
602 &self,
603 sample: &SampleResult,
604 n_jobs: usize,
605 n_machines: usize,
606 time_horizon: usize,
607 durations: Option<&Vec<Vec<usize>>>,
608 ) -> Result<JobShopSchedule, Box<dyn std::error::Error>> {
609 let durations = durations.ok_or(
610 "Job Shop visualization requires VisualizationType::JobShop::durations \
611 (per-job/machine task duration) to render an accurate Gantt chart; \
612 none were provided",
613 )?;
614
615 let mut schedule = Vec::new();
616
617 for (j, job_durations) in durations.iter().enumerate().take(n_jobs) {
618 for m in 0..n_machines {
619 for t in 0..time_horizon {
620 let var_name = format!("x_{j}_{m}_{t}");
621 if sample.assignments.get(&var_name).copied().unwrap_or(false) {
622 let duration = *job_durations.get(m).ok_or_else(|| {
623 format!(
624 "durations[{j}] has no entry for machine {m} \
625 (expected {n_machines} entries)"
626 )
627 })?;
628 schedule.push((j, m, t, duration));
629 break;
630 }
631 }
632 }
633 }
634
635 Ok(schedule)
636 }
637
638 fn visualize_number_partition(
640 &self,
641 numbers: &[f64],
642 ) -> Result<(), Box<dyn std::error::Error>> {
643 let best_sample = self.get_best_sample()?;
644 let partition = self.extract_partition(best_sample, numbers.len())?;
645
646 #[cfg(feature = "scirs")]
647 {
648 use crate::scirs_stub::scirs2_plot::Figure;
649
650 let mut fig = Figure::new();
651 let ax = fig.add_subplot(1, 1, 1)?;
652
653 let mut set1 = Vec::new();
655 let mut set2 = Vec::new();
656
657 for (i, &num) in numbers.iter().enumerate() {
658 if partition[i] {
659 set1.push(num);
660 } else {
661 set2.push(num);
662 }
663 }
664
665 let sum1: f64 = set1.iter().sum();
666 let sum2: f64 = set2.iter().sum();
667
668 let mut x_pos = vec![1.0, 2.0];
670 let mut heights = [sum1, sum2];
671 let mut labels = ["Set 1", "Set 2"];
672
673 ax.bar(&[x_pos[0]], &[heights[0]]).set_color("blue");
675 ax.bar(&[x_pos[1]], &[heights[1]]).set_color("orange");
676
677 for (x, h, nums) in &[(1.0, sum1, &set1), (2.0, sum2, &set2)] {
679 ax.text(*x, *h + 0.5, &format!("{h:.2}")).set_ha("center");
680
681 let nums_str = nums
683 .iter()
684 .map(|n| format!("{n:.1}"))
685 .collect::<Vec<_>>()
686 .join(", ");
687 ax.text(*x, -2.0, &nums_str)
688 .set_ha("center")
689 .set_fontsize(8);
690 }
691
692 ax.set_xticks(&x_pos);
693 let string_labels: Vec<String> = labels.iter().map(|s| (*s).to_string()).collect();
694 ax.set_xticklabels(&string_labels);
695 ax.set_ylabel("Sum");
696 ax.set_title(&format!(
697 "Number Partition: |{:.2} - {:.2}| = {:.2}",
698 sum1,
699 sum2,
700 (sum1 - sum2).abs()
701 ));
702
703 fig.show()?;
704 }
705
706 Ok(())
707 }
708
709 fn visualize_knapsack(
711 &self,
712 weights: &[f64],
713 values: &[f64],
714 capacity: f64,
715 ) -> Result<(), Box<dyn std::error::Error>> {
716 let best_sample = self.get_best_sample()?;
717 let n_items = weights.len();
718
719 let mut selected = vec![false; n_items];
721 let mut total_weight = 0.0;
722 let mut total_value = 0.0;
723
724 for i in 0..n_items {
725 let var_name = format!("x_{i}");
726 if best_sample
727 .assignments
728 .get(&var_name)
729 .copied()
730 .unwrap_or(false)
731 {
732 selected[i] = true;
733 total_weight += weights[i];
734 total_value += values[i];
735 }
736 }
737
738 #[cfg(feature = "scirs")]
739 {
740 use crate::scirs_stub::scirs2_plot::Figure;
741
742 let mut fig = Figure::new();
743
744 let ax1 = fig.add_subplot(2, 1, 1)?;
746
747 let x_pos: Vec<f64> = (0..n_items).map(|i| i as f64).collect();
748 for (i, (&value, &is_selected)) in values.iter().zip(selected.iter()).enumerate() {
750 let color = if is_selected { "green" } else { "red" };
751 ax1.bar(&[i as f64], &[value])
752 .set_color(color)
753 .set_alpha(0.7);
754 }
755
756 for (i, (&w, &v)) in weights.iter().zip(values.iter()).enumerate() {
758 ax1.text(i as f64, v + 0.5, &format!("w={w:.1}"))
759 .set_ha("center")
760 .set_fontsize(8);
761 }
762
763 ax1.set_xlabel("Item");
764 ax1.set_ylabel("Value");
765 ax1.set_title(&format!(
766 "Selected Items (Green): Value = {total_value:.2}, Weight = {total_weight:.2}/{capacity:.2}"
767 ));
768
769 let ax2 = fig.add_subplot(2, 1, 2)?;
771
772 ax2.barh(&[1.0], &[total_weight], &[0.0], 0.5)
773 .set_color("blue")
774 .set_label("Used");
775
776 ax2.barh(&[1.0], &[capacity - total_weight], &[total_weight], 0.5)
777 .set_color("lightgray")
778 .set_label("Remaining");
779
780 ax2.axvline(capacity)
781 .set_color("red")
782 .set_linestyle("--")
783 .set_label("Capacity");
784 ax2.set_xlim(0.0, capacity * 1.1);
785 ax2.set_ylim(0.5, 1.5);
786 ax2.set_xlabel("Weight");
787 ax2.set_yticks(&[]);
788 ax2.legend();
789 ax2.set_title("Capacity Utilization");
790
791 fig.show()?;
792 }
793
794 Ok(())
795 }
796
797 fn visualize_portfolio(
799 &self,
800 asset_names: &[String],
801 expected_returns: &[f64],
802 risk_matrix: &Array2<f64>,
803 ) -> Result<(), Box<dyn std::error::Error>> {
804 let best_sample = self.get_best_sample()?;
805 let n_assets = asset_names.len();
806
807 let weights = self.extract_portfolio_weights(best_sample, n_assets)?;
809
810 let portfolio_return: f64 = weights
812 .iter()
813 .zip(expected_returns.iter())
814 .map(|(w, r)| w * r)
815 .sum();
816
817 let portfolio_variance: f64 = weights
818 .iter()
819 .enumerate()
820 .map(|(i, wi)| {
821 weights
822 .iter()
823 .enumerate()
824 .map(|(j, wj)| wi * wj * risk_matrix[[i, j]])
825 .sum::<f64>()
826 })
827 .sum();
828
829 let portfolio_risk = portfolio_variance.sqrt();
830
831 #[cfg(feature = "scirs")]
832 {
833 use crate::scirs_stub::scirs2_plot::Figure;
834
835 let mut fig = Figure::new();
836
837 let ax1 = fig.add_subplot(2, 2, 1)?;
839
840 let nonzero_weights: Vec<(String, f64)> = asset_names
841 .iter()
842 .zip(weights.iter())
843 .filter(|(_, &w)| w > 0.01)
844 .map(|(n, &w)| (n.clone(), w))
845 .collect();
846
847 if !nonzero_weights.is_empty() {
848 let labels: Vec<String> = nonzero_weights.iter().map(|(n, _)| n.clone()).collect();
849 let sizes: Vec<f64> = nonzero_weights.iter().map(|(_, w)| *w).collect();
850
851 ax1.pie(&sizes, &labels).set_autopct("%1.1f%%");
852 ax1.set_title("Portfolio Composition");
853 }
854
855 let ax2 = fig.add_subplot(2, 2, 2)?;
857
858 let risks: Vec<f64> = (0..n_assets).map(|i| risk_matrix[[i, i]].sqrt()).collect();
860
861 ax2.scatter(&risks, expected_returns)
862 .set_color("gray")
863 .set_alpha(0.5)
864 .set_label("Individual Assets");
865
866 ax2.scatter(&[portfolio_risk], &[portfolio_return])
868 .set_color("red")
869 .set_size(100.0)
870 .set_marker("*")
871 .set_label("Portfolio");
872
873 for (i, name) in asset_names.iter().enumerate() {
875 ax2.text(risks[i], expected_returns[i], name)
876 .set_fontsize(8)
877 .set_ha("right");
878 }
879
880 ax2.set_xlabel("Risk (Std Dev)");
881 ax2.set_ylabel("Expected Return");
882 ax2.set_title("Risk-Return Profile");
883 ax2.legend();
884
885 let ax3 = fig.add_subplot(2, 2, 3)?;
887
888 let x_pos: Vec<f64> = (0..n_assets).map(|i| i as f64).collect();
889 ax3.bar(&x_pos, &weights);
890
891 ax3.set_xticks(&x_pos);
892 ax3.set_xticklabels(asset_names);
893 ax3.set_xlabel("Asset");
894 ax3.set_ylabel("Weight");
895 ax3.set_title("Portfolio Weights");
896 ax3.set_ylim(0.0, 1.0);
897
898 for tick in ax3.get_xticklabels() {
899 tick.set_rotation(45);
900 tick.set_ha("right");
901 }
902
903 let ax4 = fig.add_subplot(2, 2, 4)?;
905
906 let summary_text = format!(
907 "Portfolio Statistics\n\n\
908 Expected Return: {:.2}%\n\
909 Risk (Std Dev): {:.2}%\n\
910 Sharpe Ratio: {:.3}\n\
911 Number of Assets: {}",
912 portfolio_return * 100.0,
913 portfolio_risk * 100.0,
914 portfolio_return / portfolio_risk,
915 nonzero_weights.len()
916 );
917
918 let _: () = ax4.trans_axes();
919 ax4.text(0.1, 0.9, &summary_text)
920 .set_fontsize(12)
921 .set_verticalalignment("top")
922 .set_transform(());
923 ax4.axis("off");
924
925 fig.suptitle("Portfolio Optimization Results");
926 fig.tight_layout();
927 fig.show()?;
928 }
929
930 Ok(())
931 }
932
933 fn extract_portfolio_weights(
935 &self,
936 sample: &SampleResult,
937 n_assets: usize,
938 ) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
939 let mut weights = vec![0.0; n_assets];
940
941 let total_selected = (0..n_assets)
947 .filter(|&i| {
948 let var_name = format!("x_{i}");
949 sample.assignments.get(&var_name).copied().unwrap_or(false)
950 })
951 .count();
952
953 if total_selected > 0 {
954 for (i, weight) in weights.iter_mut().enumerate().take(n_assets) {
955 let var_name = format!("x_{i}");
956 if sample.assignments.get(&var_name).copied().unwrap_or(false) {
957 *weight = 1.0 / total_selected as f64;
958 }
959 }
960 }
961
962 Ok(weights)
963 }
964
965 fn visualize_custom(
967 &self,
968 plot_function: &str,
969 metadata: &HashMap<String, String>,
970 ) -> Result<(), Box<dyn std::error::Error>> {
971 println!("Custom visualization: {plot_function} with metadata: {metadata:?}");
973 Ok(())
974 }
975
976 fn get_best_sample(&self) -> Result<&SampleResult, Box<dyn std::error::Error>> {
978 self.samples
979 .iter()
980 .min_by(|a, b| {
981 a.energy
982 .partial_cmp(&b.energy)
983 .unwrap_or(std::cmp::Ordering::Equal)
984 })
985 .ok_or("No samples available".into())
986 }
987
988 fn get_best_samples(&self) -> Vec<&SampleResult> {
990 let mut sorted_samples: Vec<_> = self.samples.iter().collect();
991 sorted_samples.sort_by(|a, b| {
992 a.energy
993 .partial_cmp(&b.energy)
994 .unwrap_or(std::cmp::Ordering::Equal)
995 });
996 sorted_samples
997 }
998
999 fn compute_graph_layout(
1001 &self,
1002 adjacency: &Array2<f64>,
1003 ) -> Result<Vec<(f64, f64)>, Box<dyn std::error::Error>> {
1004 let n = adjacency.nrows();
1005
1006 let mut positions = Vec::new();
1008 for i in 0..n {
1009 let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
1010 positions.push((angle.cos(), angle.sin()));
1011 }
1012
1013 Ok(positions)
1014 }
1015
1016 fn extract_edges(&self, adjacency: &Array2<bool>) -> Vec<(usize, usize)> {
1018 let mut edges = Vec::new();
1019 let n = adjacency.nrows();
1020
1021 for i in 0..n {
1022 for j in i + 1..n {
1023 if adjacency[[i, j]] {
1024 edges.push((i, j));
1025 }
1026 }
1027 }
1028
1029 edges
1030 }
1031}
1032
1033#[derive(Debug, Clone, Serialize, Deserialize)]
1036struct TSPExport {
1037 coordinates: Vec<(f64, f64)>,
1038 city_names: Option<Vec<String>>,
1039 best_tours: Vec<Vec<usize>>,
1040 tour_lengths: Vec<f64>,
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1044struct GraphColoringExport {
1045 n_nodes: usize,
1046 edges: Vec<(usize, usize)>,
1047 node_colors: Vec<usize>,
1048 node_names: Option<Vec<String>>,
1049 n_colors_used: usize,
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 fn sample_with(assignments: &[(&str, bool)]) -> SampleResult {
1057 SampleResult {
1058 assignments: assignments
1059 .iter()
1060 .map(|(name, value)| ((*name).to_string(), *value))
1061 .collect(),
1062 energy: 0.0,
1063 occurrences: 1,
1064 }
1065 }
1066
1067 #[test]
1068 fn test_extract_schedule_errors_without_durations() {
1069 let visualizer = ProblemVisualizer::new(
1070 VisualizationType::JobShop {
1071 n_jobs: 1,
1072 n_machines: 1,
1073 time_horizon: 2,
1074 durations: None,
1075 },
1076 VisualizationConfig::default(),
1077 );
1078
1079 let sample = sample_with(&[("x_0_0_0", true)]);
1080 let result = visualizer.extract_schedule(&sample, 1, 1, 2, None);
1081
1082 assert!(result.is_err());
1087 }
1088
1089 #[test]
1090 fn test_extract_schedule_uses_real_durations_not_fabricated_constant() {
1091 let durations = vec![vec![7, 3], vec![4, 9]];
1092 let visualizer = ProblemVisualizer::new(
1093 VisualizationType::JobShop {
1094 n_jobs: 2,
1095 n_machines: 2,
1096 time_horizon: 3,
1097 durations: Some(durations.clone()),
1098 },
1099 VisualizationConfig::default(),
1100 );
1101
1102 let sample = sample_with(&[("x_0_0_1", true), ("x_1_1_0", true)]);
1104
1105 let schedule = visualizer
1106 .extract_schedule(&sample, 2, 2, 3, Some(&durations))
1107 .expect("extract_schedule should succeed with real durations");
1108
1109 assert_eq!(schedule.len(), 2);
1110 assert!(schedule.contains(&(0, 0, 1, durations[0][0])));
1112 assert!(schedule.contains(&(1, 1, 0, durations[1][1])));
1113 assert!(schedule.iter().any(|&(_, _, _, d)| d != 5));
1116 }
1117}