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    },
45    /// Number Partitioning
46    NumberPartition { numbers: Vec<f64> },
47    /// Knapsack Problem
48    Knapsack {
49        weights: Vec<f64>,
50        values: Vec<f64>,
51        capacity: f64,
52    },
53    /// Portfolio Optimization
54    Portfolio {
55        asset_names: Vec<String>,
56        expected_returns: Vec<f64>,
57        risk_matrix: Array2<f64>,
58    },
59    /// Custom visualization
60    Custom {
61        plot_function: String,
62        metadata: HashMap<String, String>,
63    },
64}
65
66/// Problem visualizer
67pub struct ProblemVisualizer {
68    problem_type: VisualizationType,
69    samples: Vec<SampleResult>,
70    config: VisualizationConfig,
71}
72
73/// Visualization configuration
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct VisualizationConfig {
76    /// Show best solution only
77    pub best_only: bool,
78    /// Number of top solutions to show
79    pub top_k: usize,
80    /// Color scheme
81    pub color_scheme: String,
82    /// Node size for graph problems
83    pub node_size: f64,
84    /// Edge width for graph problems
85    pub edge_width: f64,
86    /// Animation settings
87    pub animate: bool,
88    /// Animation speed (fps)
89    pub animation_speed: f64,
90}
91
92impl Default for VisualizationConfig {
93    fn default() -> Self {
94        Self {
95            best_only: false,
96            top_k: 5,
97            color_scheme: "viridis".to_string(),
98            node_size: 50.0,
99            edge_width: 2.0,
100            animate: false,
101            animation_speed: 2.0,
102        }
103    }
104}
105
106impl ProblemVisualizer {
107    /// Create new problem visualizer
108    pub const fn new(problem_type: VisualizationType, config: VisualizationConfig) -> Self {
109        Self {
110            problem_type,
111            samples: Vec::new(),
112            config,
113        }
114    }
115
116    /// Add sample results
117    pub fn add_samples(&mut self, samples: Vec<SampleResult>) {
118        self.samples.extend(samples);
119    }
120
121    /// Visualize the problem and solutions
122    pub fn visualize(&self) -> Result<(), Box<dyn std::error::Error>> {
123        if self.samples.is_empty() {
124            return Err("No samples to visualize".into());
125        }
126
127        match &self.problem_type {
128            VisualizationType::TSP {
129                coordinates,
130                city_names,
131            } => self.visualize_tsp(coordinates, city_names)?,
132            VisualizationType::GraphColoring {
133                adjacency_matrix,
134                node_names,
135                max_colors,
136            } => self.visualize_graph_coloring(adjacency_matrix, node_names, *max_colors)?,
137            VisualizationType::MaxCut {
138                adjacency_matrix,
139                node_names,
140            } => self.visualize_max_cut(adjacency_matrix, node_names)?,
141            VisualizationType::JobShop {
142                n_jobs,
143                n_machines,
144                time_horizon,
145            } => self.visualize_job_shop(*n_jobs, *n_machines, *time_horizon)?,
146            VisualizationType::NumberPartition { numbers } => {
147                self.visualize_number_partition(numbers)?;
148            }
149            VisualizationType::Knapsack {
150                weights,
151                values,
152                capacity,
153            } => self.visualize_knapsack(weights, values, *capacity)?,
154            VisualizationType::Portfolio {
155                asset_names,
156                expected_returns,
157                risk_matrix,
158            } => self.visualize_portfolio(asset_names, expected_returns, risk_matrix)?,
159            VisualizationType::Custom {
160                plot_function,
161                metadata,
162            } => self.visualize_custom(plot_function, metadata)?,
163        }
164
165        Ok(())
166    }
167
168    /// Visualize TSP solution
169    fn visualize_tsp(
170        &self,
171        coordinates: &[(f64, f64)],
172        city_names: &Option<Vec<String>>,
173    ) -> Result<(), Box<dyn std::error::Error>> {
174        let n_cities = coordinates.len();
175
176        // Get best solutions
177        let best_samples = self.get_best_samples();
178
179        #[cfg(feature = "scirs")]
180        {
181            use crate::scirs_stub::scirs2_plot::{Figure, Subplot};
182
183            let mut fig = Figure::new();
184
185            for (idx, sample) in best_samples.iter().enumerate() {
186                if idx >= self.config.top_k {
187                    break;
188                }
189
190                let subplot = fig.add_subplot(
191                    (self.config.top_k as f64).sqrt().ceil() as usize,
192                    (self.config.top_k as f64).sqrt().ceil() as usize,
193                    idx + 1,
194                )?;
195
196                // Extract tour from binary variables
197                let tour = self.extract_tsp_tour(sample, n_cities)?;
198
199                // Plot cities
200                let x: Vec<f64> = coordinates.iter().map(|c| c.0).collect();
201                let y: Vec<f64> = coordinates.iter().map(|c| c.1).collect();
202
203                subplot
204                    .scatter(&x, &y)
205                    .set_size(self.config.node_size)
206                    .set_color("blue");
207
208                // Plot tour edges
209                for i in 0..tour.len() {
210                    let from = tour[i];
211                    let to = tour[(i + 1) % tour.len()];
212
213                    subplot
214                        .plot(
215                            &[coordinates[from].0, coordinates[to].0],
216                            &[coordinates[from].1, coordinates[to].1],
217                        )
218                        .set_color("red")
219                        .set_linewidth(self.config.edge_width);
220                }
221
222                // Add city labels if provided
223                if let Some(names) = city_names {
224                    for (i, name) in names.iter().enumerate() {
225                        subplot
226                            .text(coordinates[i].0, coordinates[i].1, name)
227                            .set_fontsize(8)
228                            .set_ha("center");
229                    }
230                }
231
232                subplot.set_title(&format!(
233                    "Tour {}: Distance = {:.2}",
234                    idx + 1,
235                    sample.energy
236                ));
237                subplot.set_aspect("equal");
238            }
239
240            fig.suptitle("TSP Solutions");
241            fig.show()?;
242        }
243
244        #[cfg(not(feature = "scirs"))]
245        {
246            // Export TSP data
247            let export = TSPExport {
248                coordinates: coordinates.to_vec(),
249                city_names: city_names.clone(),
250                best_tours: best_samples
251                    .iter()
252                    .take(self.config.top_k)
253                    .map(|s| self.extract_tsp_tour(s, n_cities))
254                    .collect::<Result<Vec<_>, _>>()?,
255                tour_lengths: best_samples
256                    .iter()
257                    .take(self.config.top_k)
258                    .map(|s| s.energy)
259                    .collect(),
260            };
261
262            let json = serde_json::to_string_pretty(&export)?;
263            std::fs::write("tsp_solution.json", json)?;
264            println!("TSP solution exported to tsp_solution.json");
265        }
266
267        Ok(())
268    }
269
270    /// Extract TSP tour from binary variables
271    fn extract_tsp_tour(
272        &self,
273        sample: &SampleResult,
274        n_cities: usize,
275    ) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
276        let mut tour = Vec::new();
277        let mut visited = vec![false; n_cities];
278        let mut current = 0;
279
280        tour.push(current);
281        visited[current] = true;
282
283        // Follow edges to build tour
284        for _ in 1..n_cities {
285            let mut next_city = None;
286
287            for (j, &is_visited) in visited.iter().enumerate().take(n_cities) {
288                if !is_visited {
289                    let edge_var = format!("x_{current}_{j}");
290                    if sample.assignments.get(&edge_var).copied().unwrap_or(false) {
291                        next_city = Some(j);
292                        break;
293                    }
294                }
295            }
296
297            if let Some(next) = next_city {
298                tour.push(next);
299                visited[next] = true;
300                current = next;
301            } else {
302                // Find first unvisited city as fallback
303                for (j, is_visited) in visited.iter_mut().enumerate().take(n_cities) {
304                    if !*is_visited {
305                        tour.push(j);
306                        *is_visited = true;
307                        current = j;
308                        break;
309                    }
310                }
311            }
312        }
313
314        Ok(tour)
315    }
316
317    /// Visualize graph coloring solution
318    fn visualize_graph_coloring(
319        &self,
320        adjacency: &Array2<bool>,
321        node_names: &Option<Vec<String>>,
322        max_colors: usize,
323    ) -> Result<(), Box<dyn std::error::Error>> {
324        let n_nodes = adjacency.nrows();
325        let best_sample = self.get_best_sample()?;
326
327        // Extract node colors
328        let node_colors = self.extract_node_colors(best_sample, n_nodes, max_colors)?;
329
330        #[cfg(feature = "scirs")]
331        {
332            use crate::scirs_stub::scirs2_graphs::spring_layout;
333            use crate::scirs_stub::scirs2_plot::Figure;
334
335            let mut fig = Figure::new();
336            let ax = fig.add_subplot(1, 1, 1)?;
337
338            // Create graph
339            let mut edges = Vec::new();
340            for i in 0..n_nodes {
341                for j in i + 1..n_nodes {
342                    if adjacency[[i, j]] {
343                        edges.push((i, j));
344                    }
345                }
346            }
347
348            // Compute layout
349            let positions = spring_layout(&edges, n_nodes)?;
350
351            // Plot edges
352            for (i, j) in &edges {
353                ax.plot(
354                    &[positions[*i].0, positions[*j].0],
355                    &[positions[*i].1, positions[*j].1],
356                )
357                .set_color("gray")
358                .set_alpha(0.5)
359                .set_linewidth(1.0);
360            }
361
362            // Plot nodes with colors
363            let color_palette = ["red", "blue", "green", "yellow", "purple", "orange"];
364
365            for i in 0..n_nodes {
366                let color = color_palette[node_colors[i] % color_palette.len()];
367
368                ax.scatter(&[positions[i].0], &[positions[i].1])
369                    .set_color(color)
370                    .set_size(self.config.node_size)
371                    .set_edgecolor("black");
372
373                // Add labels
374                let label = if let Some(names) = node_names {
375                    &names[i]
376                } else {
377                    &i.to_string()
378                };
379
380                ax.text(positions[i].0, positions[i].1, label)
381                    .set_ha("center")
382                    .set_va("center")
383                    .set_fontsize(8);
384            }
385
386            ax.set_title(&format!(
387                "Graph Coloring: {} colors used",
388                node_colors.iter().max().unwrap_or(&0) + 1
389            ));
390            ax.set_aspect("equal");
391            ax.axis("off");
392
393            fig.show()?;
394        }
395
396        #[cfg(not(feature = "scirs"))]
397        {
398            // Export coloring data
399            let export = GraphColoringExport {
400                n_nodes,
401                edges: self.extract_edges(adjacency),
402                node_colors: node_colors.clone(),
403                node_names: node_names.clone(),
404                n_colors_used: node_colors.iter().max().copied().unwrap_or(0) + 1,
405            };
406
407            let json = serde_json::to_string_pretty(&export)?;
408            std::fs::write("graph_coloring.json", json)?;
409            println!("Graph coloring exported to graph_coloring.json");
410        }
411
412        Ok(())
413    }
414
415    /// Extract node colors from solution
416    fn extract_node_colors(
417        &self,
418        sample: &SampleResult,
419        n_nodes: usize,
420        max_colors: usize,
421    ) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
422        let mut colors = vec![0; n_nodes];
423
424        for (i, color) in colors.iter_mut().enumerate().take(n_nodes) {
425            for c in 0..max_colors {
426                let var_name = format!("node_{i}_color_{c}");
427                if sample.assignments.get(&var_name).copied().unwrap_or(false) {
428                    *color = c;
429                    break;
430                }
431            }
432        }
433
434        Ok(colors)
435    }
436
437    /// Visualize max cut solution
438    fn visualize_max_cut(
439        &self,
440        adjacency: &Array2<f64>,
441        node_names: &Option<Vec<String>>,
442    ) -> Result<(), Box<dyn std::error::Error>> {
443        let n_nodes = adjacency.nrows();
444        let best_sample = self.get_best_sample()?;
445
446        // Extract partition
447        let partition = self.extract_partition(best_sample, n_nodes)?;
448
449        #[cfg(feature = "scirs")]
450        {
451            use crate::scirs_stub::scirs2_plot::Figure;
452
453            let mut fig = Figure::new();
454            let ax = fig.add_subplot(1, 1, 1)?;
455
456            // Compute layout with force-directed algorithm
457            let positions = self.compute_graph_layout(adjacency)?;
458
459            // Plot edges with cut edges highlighted
460            let mut cut_weight = 0.0;
461            for i in 0..n_nodes {
462                for j in i + 1..n_nodes {
463                    if adjacency[[i, j]] > 0.0 {
464                        let is_cut = partition[i] != partition[j];
465                        let color = if is_cut { "red" } else { "gray" };
466                        let width = if is_cut { 3.0 } else { 1.0 };
467
468                        if is_cut {
469                            cut_weight += adjacency[[i, j]];
470                        }
471
472                        ax.plot(
473                            &[positions[i].0, positions[j].0],
474                            &[positions[i].1, positions[j].1],
475                        )
476                        .set_color(color)
477                        .set_linewidth(width)
478                        .set_alpha(if is_cut { 1.0 } else { 0.3 });
479                    }
480                }
481            }
482
483            // Plot nodes
484            for i in 0..n_nodes {
485                let color = if partition[i] { "blue" } else { "orange" };
486
487                ax.scatter(&[positions[i].0], &[positions[i].1])
488                    .set_color(color)
489                    .set_size(self.config.node_size)
490                    .set_edgecolor("black");
491
492                // Add labels
493                let label = if let Some(names) = node_names {
494                    &names[i]
495                } else {
496                    &i.to_string()
497                };
498
499                ax.text(positions[i].0, positions[i].1, label)
500                    .set_ha("center")
501                    .set_va("center")
502                    .set_fontsize(8);
503            }
504
505            ax.set_title(&format!("Max Cut: Weight = {cut_weight:.2}"));
506            ax.set_aspect("equal");
507            ax.axis("off");
508
509            fig.show()?;
510        }
511
512        Ok(())
513    }
514
515    /// Extract partition from solution
516    fn extract_partition(
517        &self,
518        sample: &SampleResult,
519        n_nodes: usize,
520    ) -> Result<Vec<bool>, Box<dyn std::error::Error>> {
521        let mut partition = vec![false; n_nodes];
522
523        for (i, part) in partition.iter_mut().enumerate().take(n_nodes) {
524            let var_name = format!("x_{i}");
525            *part = sample.assignments.get(&var_name).copied().unwrap_or(false);
526        }
527
528        Ok(partition)
529    }
530
531    /// Visualize job shop scheduling solution
532    fn visualize_job_shop(
533        &self,
534        n_jobs: usize,
535        n_machines: usize,
536        time_horizon: usize,
537    ) -> Result<(), Box<dyn std::error::Error>> {
538        let best_sample = self.get_best_sample()?;
539
540        // Extract schedule
541        let schedule = self.extract_schedule(best_sample, n_jobs, n_machines, time_horizon)?;
542
543        #[cfg(feature = "scirs")]
544        {
545            use crate::scirs_stub::scirs2_plot::Figure;
546
547            let mut fig = Figure::new();
548            let ax = fig.add_subplot(1, 1, 1)?;
549
550            // Create Gantt chart
551            let colors = ["red", "blue", "green", "yellow", "purple", "orange"];
552
553            for (job, machine, start, duration) in &schedule {
554                let mut y = *machine as f64;
555                let color = colors[*job % colors.len()];
556
557                ax.barh(&[y], &[*duration as f64], &[*start as f64], 0.8)
558                    .set_color(color)
559                    .set_edgecolor("black")
560                    .set_label(&format!("Job {job}"));
561            }
562
563            ax.set_xlabel("Time");
564            ax.set_ylabel("Machine");
565            ax.set_title("Job Shop Schedule");
566            ax.set_ylim(-0.5, n_machines as f64 - 0.5);
567            ax.set_xlim(0.0, time_horizon as f64);
568
569            // Set y-ticks
570            ax.set_yticks(&(0..n_machines).map(|i| i as f64).collect::<Vec<_>>());
571            ax.set_yticklabels(&(0..n_machines).map(|i| format!("M{i}")).collect::<Vec<_>>());
572
573            // Remove duplicate labels in legend
574            ax.legend_unique();
575
576            fig.show()?;
577        }
578
579        Ok(())
580    }
581
582    /// Extract schedule from solution
583    fn extract_schedule(
584        &self,
585        sample: &SampleResult,
586        n_jobs: usize,
587        n_machines: usize,
588        time_horizon: usize,
589    ) -> Result<JobShopSchedule, Box<dyn std::error::Error>> {
590        let mut schedule = Vec::new();
591
592        // This is problem-specific and would need the actual encoding scheme
593        // For now, return a placeholder
594        for j in 0..n_jobs {
595            for m in 0..n_machines {
596                for t in 0..time_horizon {
597                    let var_name = format!("x_{j}_{m}_{t}");
598                    if sample.assignments.get(&var_name).copied().unwrap_or(false) {
599                        // Find duration (would need problem-specific logic)
600                        let duration = 5; // Placeholder
601                        schedule.push((j, m, t, duration));
602                        break;
603                    }
604                }
605            }
606        }
607
608        Ok(schedule)
609    }
610
611    /// Visualize number partition solution
612    fn visualize_number_partition(
613        &self,
614        numbers: &[f64],
615    ) -> Result<(), Box<dyn std::error::Error>> {
616        let best_sample = self.get_best_sample()?;
617        let partition = self.extract_partition(best_sample, numbers.len())?;
618
619        #[cfg(feature = "scirs")]
620        {
621            use crate::scirs_stub::scirs2_plot::Figure;
622
623            let mut fig = Figure::new();
624            let ax = fig.add_subplot(1, 1, 1)?;
625
626            // Separate numbers into two sets
627            let mut set1 = Vec::new();
628            let mut set2 = Vec::new();
629
630            for (i, &num) in numbers.iter().enumerate() {
631                if partition[i] {
632                    set1.push(num);
633                } else {
634                    set2.push(num);
635                }
636            }
637
638            let sum1: f64 = set1.iter().sum();
639            let sum2: f64 = set2.iter().sum();
640
641            // Create bar chart
642            let mut x_pos = vec![1.0, 2.0];
643            let mut heights = [sum1, sum2];
644            let mut labels = ["Set 1", "Set 2"];
645
646            // Draw each bar with its own color
647            ax.bar(&[x_pos[0]], &[heights[0]]).set_color("blue");
648            ax.bar(&[x_pos[1]], &[heights[1]]).set_color("orange");
649
650            // Add value labels on bars
651            for (x, h, nums) in &[(1.0, sum1, &set1), (2.0, sum2, &set2)] {
652                ax.text(*x, *h + 0.5, &format!("{h:.2}")).set_ha("center");
653
654                // Show individual numbers
655                let nums_str = nums
656                    .iter()
657                    .map(|n| format!("{n:.1}"))
658                    .collect::<Vec<_>>()
659                    .join(", ");
660                ax.text(*x, -2.0, &nums_str)
661                    .set_ha("center")
662                    .set_fontsize(8);
663            }
664
665            ax.set_xticks(&x_pos);
666            let string_labels: Vec<String> = labels.iter().map(|s| (*s).to_string()).collect();
667            ax.set_xticklabels(&string_labels);
668            ax.set_ylabel("Sum");
669            ax.set_title(&format!(
670                "Number Partition: |{:.2} - {:.2}| = {:.2}",
671                sum1,
672                sum2,
673                (sum1 - sum2).abs()
674            ));
675
676            fig.show()?;
677        }
678
679        Ok(())
680    }
681
682    /// Visualize knapsack solution
683    fn visualize_knapsack(
684        &self,
685        weights: &[f64],
686        values: &[f64],
687        capacity: f64,
688    ) -> Result<(), Box<dyn std::error::Error>> {
689        let best_sample = self.get_best_sample()?;
690        let n_items = weights.len();
691
692        // Extract selected items
693        let mut selected = vec![false; n_items];
694        let mut total_weight = 0.0;
695        let mut total_value = 0.0;
696
697        for i in 0..n_items {
698            let var_name = format!("x_{i}");
699            if best_sample
700                .assignments
701                .get(&var_name)
702                .copied()
703                .unwrap_or(false)
704            {
705                selected[i] = true;
706                total_weight += weights[i];
707                total_value += values[i];
708            }
709        }
710
711        #[cfg(feature = "scirs")]
712        {
713            use crate::scirs_stub::scirs2_plot::Figure;
714
715            let mut fig = Figure::new();
716
717            // Item selection visualization
718            let ax1 = fig.add_subplot(2, 1, 1)?;
719
720            let x_pos: Vec<f64> = (0..n_items).map(|i| i as f64).collect();
721            // Draw bars with individual colors
722            for (i, (&value, &is_selected)) in values.iter().zip(selected.iter()).enumerate() {
723                let color = if is_selected { "green" } else { "red" };
724                ax1.bar(&[i as f64], &[value])
725                    .set_color(color)
726                    .set_alpha(0.7);
727            }
728
729            // Add weight labels
730            for (i, (&w, &v)) in weights.iter().zip(values.iter()).enumerate() {
731                ax1.text(i as f64, v + 0.5, &format!("w={w:.1}"))
732                    .set_ha("center")
733                    .set_fontsize(8);
734            }
735
736            ax1.set_xlabel("Item");
737            ax1.set_ylabel("Value");
738            ax1.set_title(&format!(
739                "Selected Items (Green): Value = {total_value:.2}, Weight = {total_weight:.2}/{capacity:.2}"
740            ));
741
742            // Capacity utilization
743            let ax2 = fig.add_subplot(2, 1, 2)?;
744
745            ax2.barh(&[1.0], &[total_weight], &[0.0], 0.5)
746                .set_color("blue")
747                .set_label("Used");
748
749            ax2.barh(&[1.0], &[capacity - total_weight], &[total_weight], 0.5)
750                .set_color("lightgray")
751                .set_label("Remaining");
752
753            ax2.axvline(capacity)
754                .set_color("red")
755                .set_linestyle("--")
756                .set_label("Capacity");
757            ax2.set_xlim(0.0, capacity * 1.1);
758            ax2.set_ylim(0.5, 1.5);
759            ax2.set_xlabel("Weight");
760            ax2.set_yticks(&[]);
761            ax2.legend();
762            ax2.set_title("Capacity Utilization");
763
764            fig.show()?;
765        }
766
767        Ok(())
768    }
769
770    /// Visualize portfolio optimization solution
771    fn visualize_portfolio(
772        &self,
773        asset_names: &[String],
774        expected_returns: &[f64],
775        risk_matrix: &Array2<f64>,
776    ) -> Result<(), Box<dyn std::error::Error>> {
777        let best_sample = self.get_best_sample()?;
778        let n_assets = asset_names.len();
779
780        // Extract portfolio weights
781        let weights = self.extract_portfolio_weights(best_sample, n_assets)?;
782
783        // Calculate portfolio metrics
784        let portfolio_return: f64 = weights
785            .iter()
786            .zip(expected_returns.iter())
787            .map(|(w, r)| w * r)
788            .sum();
789
790        let portfolio_variance: f64 = weights
791            .iter()
792            .enumerate()
793            .map(|(i, wi)| {
794                weights
795                    .iter()
796                    .enumerate()
797                    .map(|(j, wj)| wi * wj * risk_matrix[[i, j]])
798                    .sum::<f64>()
799            })
800            .sum();
801
802        let portfolio_risk = portfolio_variance.sqrt();
803
804        #[cfg(feature = "scirs")]
805        {
806            use crate::scirs_stub::scirs2_plot::Figure;
807
808            let mut fig = Figure::new();
809
810            // Portfolio composition pie chart
811            let ax1 = fig.add_subplot(2, 2, 1)?;
812
813            let nonzero_weights: Vec<(String, f64)> = asset_names
814                .iter()
815                .zip(weights.iter())
816                .filter(|(_, &w)| w > 0.01)
817                .map(|(n, &w)| (n.clone(), w))
818                .collect();
819
820            if !nonzero_weights.is_empty() {
821                let labels: Vec<String> = nonzero_weights.iter().map(|(n, _)| n.clone()).collect();
822                let sizes: Vec<f64> = nonzero_weights.iter().map(|(_, w)| *w).collect();
823
824                ax1.pie(&sizes, &labels).set_autopct("%1.1f%%");
825                ax1.set_title("Portfolio Composition");
826            }
827
828            // Risk-return scatter
829            let ax2 = fig.add_subplot(2, 2, 2)?;
830
831            // Plot individual assets
832            let risks: Vec<f64> = (0..n_assets).map(|i| risk_matrix[[i, i]].sqrt()).collect();
833
834            ax2.scatter(&risks, expected_returns)
835                .set_color("gray")
836                .set_alpha(0.5)
837                .set_label("Individual Assets");
838
839            // Plot portfolio
840            ax2.scatter(&[portfolio_risk], &[portfolio_return])
841                .set_color("red")
842                .set_size(100.0)
843                .set_marker("*")
844                .set_label("Portfolio");
845
846            // Add asset labels
847            for (i, name) in asset_names.iter().enumerate() {
848                ax2.text(risks[i], expected_returns[i], name)
849                    .set_fontsize(8)
850                    .set_ha("right");
851            }
852
853            ax2.set_xlabel("Risk (Std Dev)");
854            ax2.set_ylabel("Expected Return");
855            ax2.set_title("Risk-Return Profile");
856            ax2.legend();
857
858            // Weight distribution
859            let ax3 = fig.add_subplot(2, 2, 3)?;
860
861            let x_pos: Vec<f64> = (0..n_assets).map(|i| i as f64).collect();
862            ax3.bar(&x_pos, &weights);
863
864            ax3.set_xticks(&x_pos);
865            ax3.set_xticklabels(asset_names);
866            ax3.set_xlabel("Asset");
867            ax3.set_ylabel("Weight");
868            ax3.set_title("Portfolio Weights");
869            ax3.set_ylim(0.0, 1.0);
870
871            for tick in ax3.get_xticklabels() {
872                tick.set_rotation(45);
873                tick.set_ha("right");
874            }
875
876            // Summary statistics
877            let ax4 = fig.add_subplot(2, 2, 4)?;
878
879            let summary_text = format!(
880                "Portfolio Statistics\n\n\
881                 Expected Return: {:.2}%\n\
882                 Risk (Std Dev): {:.2}%\n\
883                 Sharpe Ratio: {:.3}\n\
884                 Number of Assets: {}",
885                portfolio_return * 100.0,
886                portfolio_risk * 100.0,
887                portfolio_return / portfolio_risk,
888                nonzero_weights.len()
889            );
890
891            let _: () = ax4.trans_axes();
892            ax4.text(0.1, 0.9, &summary_text)
893                .set_fontsize(12)
894                .set_verticalalignment("top")
895                .set_transform(());
896            ax4.axis("off");
897
898            fig.suptitle("Portfolio Optimization Results");
899            fig.tight_layout();
900            fig.show()?;
901        }
902
903        Ok(())
904    }
905
906    /// Extract portfolio weights from solution
907    fn extract_portfolio_weights(
908        &self,
909        sample: &SampleResult,
910        n_assets: usize,
911    ) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
912        let mut weights = vec![0.0; n_assets];
913
914        // This depends on the encoding used
915        // For discrete allocation, might be binary variables
916        // For continuous, might need decoding from binary representation
917
918        // Simple binary allocation example
919        let total_selected = (0..n_assets)
920            .filter(|&i| {
921                let var_name = format!("x_{i}");
922                sample.assignments.get(&var_name).copied().unwrap_or(false)
923            })
924            .count();
925
926        if total_selected > 0 {
927            for (i, weight) in weights.iter_mut().enumerate().take(n_assets) {
928                let var_name = format!("x_{i}");
929                if sample.assignments.get(&var_name).copied().unwrap_or(false) {
930                    *weight = 1.0 / total_selected as f64;
931                }
932            }
933        }
934
935        Ok(weights)
936    }
937
938    /// Visualize custom problem
939    fn visualize_custom(
940        &self,
941        plot_function: &str,
942        metadata: &HashMap<String, String>,
943    ) -> Result<(), Box<dyn std::error::Error>> {
944        // This would call a user-provided plotting function
945        println!("Custom visualization: {plot_function} with metadata: {metadata:?}");
946        Ok(())
947    }
948
949    /// Get best sample
950    fn get_best_sample(&self) -> Result<&SampleResult, Box<dyn std::error::Error>> {
951        self.samples
952            .iter()
953            .min_by(|a, b| {
954                a.energy
955                    .partial_cmp(&b.energy)
956                    .unwrap_or(std::cmp::Ordering::Equal)
957            })
958            .ok_or("No samples available".into())
959    }
960
961    /// Get best k samples
962    fn get_best_samples(&self) -> Vec<&SampleResult> {
963        let mut sorted_samples: Vec<_> = self.samples.iter().collect();
964        sorted_samples.sort_by(|a, b| {
965            a.energy
966                .partial_cmp(&b.energy)
967                .unwrap_or(std::cmp::Ordering::Equal)
968        });
969        sorted_samples
970    }
971
972    /// Compute graph layout
973    fn compute_graph_layout(
974        &self,
975        adjacency: &Array2<f64>,
976    ) -> Result<Vec<(f64, f64)>, Box<dyn std::error::Error>> {
977        let n = adjacency.nrows();
978
979        // Simple circular layout as fallback
980        let mut positions = Vec::new();
981        for i in 0..n {
982            let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
983            positions.push((angle.cos(), angle.sin()));
984        }
985
986        Ok(positions)
987    }
988
989    /// Extract edges from adjacency matrix
990    fn extract_edges(&self, adjacency: &Array2<bool>) -> Vec<(usize, usize)> {
991        let mut edges = Vec::new();
992        let n = adjacency.nrows();
993
994        for i in 0..n {
995            for j in i + 1..n {
996                if adjacency[[i, j]] {
997                    edges.push((i, j));
998                }
999            }
1000        }
1001
1002        edges
1003    }
1004}
1005
1006// Export structures for non-SciRS builds
1007
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1009struct TSPExport {
1010    coordinates: Vec<(f64, f64)>,
1011    city_names: Option<Vec<String>>,
1012    best_tours: Vec<Vec<usize>>,
1013    tour_lengths: Vec<f64>,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017struct GraphColoringExport {
1018    n_nodes: usize,
1019    edges: Vec<(usize, usize)>,
1020    node_colors: Vec<usize>,
1021    node_names: Option<Vec<String>>,
1022    n_colors_used: usize,
1023}