Skip to main content

quantrs2_tytan/visualization/
problem_specific.rs

1//! Problem-specific visualizations for quantum annealing
2//!
3//! This module provides specialized visualization routines for common
4//! optimization problem types including TSP, graph coloring, scheduling, etc.
5
6use crate::sampler::SampleResult;
7use scirs2_core::ndarray::Array2;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Type alias for job shop schedule representation (job, machine, start_time, duration)
12type 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/// Problem visualization types
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum VisualizationType {
23    /// Traveling Salesman Problem
24    TSP {
25        coordinates: Vec<(f64, f64)>,
26        city_names: Option<Vec<String>>,
27    },
28    /// Graph Coloring
29    GraphColoring {
30        adjacency_matrix: Array2<bool>,
31        node_names: Option<Vec<String>>,
32        max_colors: usize,
33    },
34    /// Maximum Cut
35    MaxCut {
36        adjacency_matrix: Array2<f64>,
37        node_names: Option<Vec<String>>,
38    },
39    /// Job Shop Scheduling
40    JobShop {
41        n_jobs: usize,
42        n_machines: usize,
43        time_horizon: usize,
44        /// Per-(job, machine) task duration, indexed `durations[job][machine]`.
45        /// Real Gantt-chart rendering needs the actual task durations from
46        /// the problem encoding; without them, `extract_schedule` honestly
47        /// errors instead of fabricating a placeholder duration for every
48        /// task.
49        durations: Option<Vec<Vec<usize>>>,
50    },
51    /// Number Partitioning
52    NumberPartition { numbers: Vec<f64> },
53    /// Knapsack Problem
54    Knapsack {
55        weights: Vec<f64>,
56        values: Vec<f64>,
57        capacity: f64,
58    },
59    /// Portfolio Optimization
60    Portfolio {
61        asset_names: Vec<String>,
62        expected_returns: Vec<f64>,
63        risk_matrix: Array2<f64>,
64    },
65    /// Custom visualization
66    Custom {
67        plot_function: String,
68        metadata: HashMap<String, String>,
69    },
70}
71
72/// Problem visualizer
73pub struct ProblemVisualizer {
74    problem_type: VisualizationType,
75    samples: Vec<SampleResult>,
76    config: VisualizationConfig,
77}
78
79/// Visualization configuration
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct VisualizationConfig {
82    /// Show best solution only
83    pub best_only: bool,
84    /// Number of top solutions to show
85    pub top_k: usize,
86    /// Color scheme
87    pub color_scheme: String,
88    /// Node size for graph problems
89    pub node_size: f64,
90    /// Edge width for graph problems
91    pub edge_width: f64,
92    /// Animation settings
93    pub animate: bool,
94    /// Animation speed (fps)
95    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    /// Create new problem visualizer
114    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    /// Add sample results
123    pub fn add_samples(&mut self, samples: Vec<SampleResult>) {
124        self.samples.extend(samples);
125    }
126
127    /// Visualize the problem and solutions
128    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    /// Visualize TSP solution
178    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        // Get best solutions
186        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                // Extract tour from binary variables
206                let tour = self.extract_tsp_tour(sample, n_cities)?;
207
208                // Plot cities
209                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                // Plot tour edges
218                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                // Add city labels if provided
232                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            // Export TSP data
256            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    /// Extract TSP tour from binary variables
280    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        // Follow edges to build tour
293        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                // Find first unvisited city as fallback
312                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    /// Visualize graph coloring solution
327    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        // Extract node colors
337        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            // Create graph
348            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            // Compute layout
358            let positions = spring_layout(&edges, n_nodes)?;
359
360            // Plot edges
361            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            // Plot nodes with colors
372            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                // Add labels
383                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            // Export coloring data
408            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    /// Extract node colors from solution
425    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    /// Visualize max cut solution
447    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        // Extract partition
456        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            // Compute layout with force-directed algorithm
466            let positions = self.compute_graph_layout(adjacency)?;
467
468            // Plot edges with cut edges highlighted
469            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            // Plot nodes
493            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                // Add labels
502                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    /// Extract partition from solution
525    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    /// Visualize job shop scheduling solution
541    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        // Extract schedule
551        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            // Create Gantt chart
562            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            // Set y-ticks
581            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            // Remove duplicate labels in legend
585            ax.legend_unique();
586
587            fig.show()?;
588        }
589
590        Ok(())
591    }
592
593    /// Extract schedule from solution
594    ///
595    /// Requires the real per-(job, machine) task durations via `durations`
596    /// (see [`VisualizationType::JobShop::durations`]); without them there
597    /// is no honest way to know how wide each Gantt-chart bar should be, so
598    /// this returns an error instead of fabricating a constant duration for
599    /// every task (the previous behavior hardcoded `duration = 5` for
600    /// every single task regardless of the real problem).
601    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    /// Visualize number partition solution
639    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            // Separate numbers into two sets
654            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            // Create bar chart
669            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            // Draw each bar with its own color
674            ax.bar(&[x_pos[0]], &[heights[0]]).set_color("blue");
675            ax.bar(&[x_pos[1]], &[heights[1]]).set_color("orange");
676
677            // Add value labels on bars
678            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                // Show individual numbers
682                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    /// Visualize knapsack solution
710    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        // Extract selected items
720        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            // Item selection visualization
745            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            // Draw bars with individual colors
749            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            // Add weight labels
757            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            // Capacity utilization
770            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    /// Visualize portfolio optimization solution
798    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        // Extract portfolio weights
808        let weights = self.extract_portfolio_weights(best_sample, n_assets)?;
809
810        // Calculate portfolio metrics
811        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            // Portfolio composition pie chart
838            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            // Risk-return scatter
856            let ax2 = fig.add_subplot(2, 2, 2)?;
857
858            // Plot individual assets
859            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            // Plot portfolio
867            ax2.scatter(&[portfolio_risk], &[portfolio_return])
868                .set_color("red")
869                .set_size(100.0)
870                .set_marker("*")
871                .set_label("Portfolio");
872
873            // Add asset labels
874            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            // Weight distribution
886            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            // Summary statistics
904            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    /// Extract portfolio weights from solution
934    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        // This depends on the encoding used
942        // For discrete allocation, might be binary variables
943        // For continuous, might need decoding from binary representation
944
945        // Simple binary allocation example
946        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    /// Visualize custom problem
966    fn visualize_custom(
967        &self,
968        plot_function: &str,
969        metadata: &HashMap<String, String>,
970    ) -> Result<(), Box<dyn std::error::Error>> {
971        // This would call a user-provided plotting function
972        println!("Custom visualization: {plot_function} with metadata: {metadata:?}");
973        Ok(())
974    }
975
976    /// Get best sample
977    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    /// Get best k samples
989    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    /// Compute graph layout
1000    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        // Simple circular layout as fallback
1007        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    /// Extract edges from adjacency matrix
1017    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// Export structures for non-SciRS builds
1034
1035#[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        // The old fabricated implementation always returned Ok(..) with a
1083        // hardcoded `duration = 5` for every scheduled task regardless of
1084        // the real problem; without real durations this must now be an
1085        // honest error instead.
1086        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        // Job 0 scheduled on machine 0 at t=1; Job 1 scheduled on machine 1 at t=0.
1103        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        // (job, machine, start, duration)
1111        assert!(schedule.contains(&(0, 0, 1, durations[0][0])));
1112        assert!(schedule.contains(&(1, 1, 0, durations[1][1])));
1113        // The old fabricated implementation always used duration=5
1114        // regardless of the real per-job/machine durations.
1115        assert!(schedule.iter().any(|&(_, _, _, d)| d != 5));
1116    }
1117}