quantrs2_tytan/visualization/
export.rs

1//! Export functionality for visualization data
2//!
3//! This module provides export capabilities for visualization data in various
4//! formats including JSON, CSV, and custom formats for external plotting tools.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10/// Export format types
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ExportFormat {
13    /// JSON format
14    JSON,
15    /// CSV format
16    CSV,
17    /// Python numpy arrays
18    NPY,
19    /// MATLAB .mat files
20    MAT,
21    /// Plotly JSON format
22    Plotly,
23    /// Matplotlib script
24    Matplotlib,
25    /// HTML with embedded visualization
26    HTML,
27}
28
29/// Visualization exporter
30pub struct VisualizationExporter {
31    format: ExportFormat,
32    include_metadata: bool,
33    compression: bool,
34}
35
36impl VisualizationExporter {
37    /// Create new exporter
38    pub const fn new(format: ExportFormat) -> Self {
39        Self {
40            format,
41            include_metadata: true,
42            compression: false,
43        }
44    }
45
46    /// Enable metadata inclusion
47    pub const fn with_metadata(mut self, include: bool) -> Self {
48        self.include_metadata = include;
49        self
50    }
51
52    /// Enable compression
53    pub const fn with_compression(mut self, compress: bool) -> Self {
54        self.compression = compress;
55        self
56    }
57
58    /// Export energy landscape data
59    pub fn export_energy_landscape(
60        &self,
61        data: &EnergyLandscapeData,
62        path: &str,
63    ) -> Result<(), Box<dyn std::error::Error>> {
64        match self.format {
65            ExportFormat::JSON => self.export_json(data, path)?,
66            ExportFormat::CSV => self.export_energy_csv(data, path)?,
67            ExportFormat::Plotly => self.export_plotly_energy(data, path)?,
68            ExportFormat::Matplotlib => self.export_matplotlib_energy(data, path)?,
69            ExportFormat::HTML => self.export_html_energy(data, path)?,
70            _ => return Err("Unsupported format for energy landscape".into()),
71        }
72
73        Ok(())
74    }
75
76    /// Export solution distribution data
77    pub fn export_distribution(
78        &self,
79        data: &DistributionData,
80        path: &str,
81    ) -> Result<(), Box<dyn std::error::Error>> {
82        match self.format {
83            ExportFormat::JSON => self.export_json(data, path)?,
84            ExportFormat::CSV => self.export_distribution_csv(data, path)?,
85            ExportFormat::Plotly => self.export_plotly_distribution(data, path)?,
86            ExportFormat::Matplotlib => self.export_matplotlib_distribution(data, path)?,
87            ExportFormat::HTML => self.export_html_distribution(data, path)?,
88            _ => return Err("Unsupported format for distribution data".into()),
89        }
90
91        Ok(())
92    }
93
94    /// Export convergence data
95    pub fn export_convergence(
96        &self,
97        data: &ConvergenceData,
98        path: &str,
99    ) -> Result<(), Box<dyn std::error::Error>> {
100        match self.format {
101            ExportFormat::JSON => self.export_json(data, path)?,
102            ExportFormat::CSV => self.export_convergence_csv(data, path)?,
103            ExportFormat::Plotly => self.export_plotly_convergence(data, path)?,
104            ExportFormat::Matplotlib => self.export_matplotlib_convergence(data, path)?,
105            ExportFormat::HTML => self.export_html_convergence(data, path)?,
106            _ => return Err("Unsupported format for convergence data".into()),
107        }
108
109        Ok(())
110    }
111
112    /// Generic JSON export
113    fn export_json<T: Serialize>(
114        &self,
115        data: &T,
116        path: &str,
117    ) -> Result<(), Box<dyn std::error::Error>> {
118        let json = if self.compression {
119            serde_json::to_string(data)?
120        } else {
121            serde_json::to_string_pretty(data)?
122        };
123
124        std::fs::write(path, json)?;
125        Ok(())
126    }
127
128    /// Export energy landscape as CSV
129    fn export_energy_csv(
130        &self,
131        data: &EnergyLandscapeData,
132        path: &str,
133    ) -> Result<(), Box<dyn std::error::Error>> {
134        use std::io::Write;
135
136        let mut file = std::fs::File::create(path)?;
137
138        // Write header
139        writeln!(file, "x,y,energy,density")?;
140
141        // Write data points
142        for i in 0..data.x_coords.len() {
143            let density = data
144                .density
145                .as_ref()
146                .and_then(|d| d.get(i))
147                .copied()
148                .unwrap_or(0.0);
149
150            writeln!(
151                file,
152                "{},{},{},{}",
153                data.x_coords[i], data.y_coords[i], data.energies[i], density
154            )?;
155        }
156
157        Ok(())
158    }
159
160    /// Export distribution as CSV
161    fn export_distribution_csv(
162        &self,
163        data: &DistributionData,
164        path: &str,
165    ) -> Result<(), Box<dyn std::error::Error>> {
166        use std::io::Write;
167
168        let base_path = Path::new(path);
169        let stem = base_path.file_stem().unwrap_or_default();
170        let parent = base_path.parent().unwrap_or(Path::new("."));
171
172        // Export statistics
173        let stats_path = parent.join(format!("{}_stats.csv", stem.to_string_lossy()));
174        let mut stats_file = std::fs::File::create(stats_path)?;
175
176        writeln!(stats_file, "metric,value")?;
177        writeln!(stats_file, "n_samples,{}", data.n_samples)?;
178        writeln!(stats_file, "n_unique,{}", data.n_unique)?;
179        writeln!(stats_file, "mean_energy,{}", data.mean_energy)?;
180        writeln!(stats_file, "std_energy,{}", data.std_energy)?;
181        writeln!(stats_file, "min_energy,{}", data.min_energy)?;
182        writeln!(stats_file, "max_energy,{}", data.max_energy)?;
183
184        // Export cluster information if available
185        if let Some(clusters) = &data.clusters {
186            let cluster_path = parent.join(format!("{}_clusters.csv", stem.to_string_lossy()));
187            let mut cluster_file = std::fs::File::create(cluster_path)?;
188
189            writeln!(cluster_file, "sample_id,cluster_id")?;
190            for (i, &cluster) in clusters.iter().enumerate() {
191                writeln!(cluster_file, "{i},{cluster}")?;
192            }
193        }
194
195        Ok(())
196    }
197
198    /// Export convergence as CSV
199    fn export_convergence_csv(
200        &self,
201        data: &ConvergenceData,
202        path: &str,
203    ) -> Result<(), Box<dyn std::error::Error>> {
204        use std::io::Write;
205
206        let mut file = std::fs::File::create(path)?;
207
208        // Write header
209        write!(file, "iteration,objective,best_so_far")?;
210        if !data.constraint_names.is_empty() {
211            for name in &data.constraint_names {
212                write!(file, ",constraint_{name}")?;
213            }
214        }
215        writeln!(file)?;
216
217        // Calculate best so far
218        let mut best_so_far = vec![f64::INFINITY; data.objectives.len()];
219        let mut current_best = f64::INFINITY;
220        for (i, &obj) in data.objectives.iter().enumerate() {
221            current_best = current_best.min(obj);
222            best_so_far[i] = current_best;
223        }
224
225        // Write data
226        for i in 0..data.objectives.len() {
227            write!(file, "{},{},{}", i, data.objectives[i], best_so_far[i])?;
228
229            if let Some(constraints) = &data.constraints {
230                for name in &data.constraint_names {
231                    let value = constraints[i].get(name).copied().unwrap_or(0.0);
232                    write!(file, ",{value}")?;
233                }
234            }
235
236            writeln!(file)?;
237        }
238
239        Ok(())
240    }
241
242    /// Export Plotly energy landscape
243    fn export_plotly_energy(
244        &self,
245        data: &EnergyLandscapeData,
246        path: &str,
247    ) -> Result<(), Box<dyn std::error::Error>> {
248        let plotly_data = PlotlyData {
249            data: vec![PlotlyTrace {
250                trace_type: "scatter".to_string(),
251                mode: Some("markers".to_string()),
252                x: Some(data.x_coords.clone()),
253                y: Some(data.y_coords.clone()),
254                z: None,
255                marker: Some(PlotlyMarker {
256                    size: Some(8),
257                    color: Some(data.energies.clone()),
258                    colorscale: Some("Viridis".to_string()),
259                    showscale: Some(true),
260                }),
261                name: Some("Energy Landscape".to_string()),
262                text: None,
263            }],
264            layout: PlotlyLayout {
265                title: Some("Energy Landscape Projection".to_string()),
266                xaxis: Some(PlotlyAxis {
267                    title: Some("Component 1".to_string()),
268                    ..Default::default()
269                }),
270                yaxis: Some(PlotlyAxis {
271                    title: Some("Component 2".to_string()),
272                    ..Default::default()
273                }),
274                ..Default::default()
275            },
276        };
277
278        self.export_json(&plotly_data, path)?;
279        Ok(())
280    }
281
282    /// Export Matplotlib energy script
283    fn export_matplotlib_energy(
284        &self,
285        data: &EnergyLandscapeData,
286        path: &str,
287    ) -> Result<(), Box<dyn std::error::Error>> {
288        use std::io::Write;
289
290        let mut file = std::fs::File::create(path)?;
291
292        writeln!(file, "#!/usr/bin/env python3")?;
293        writeln!(file, "import matplotlib.pyplot as plt")?;
294        writeln!(file, "import numpy as np")?;
295        writeln!(file)?;
296        writeln!(file, "# Energy landscape data")?;
297        writeln!(file, "x = np.array({:?})", data.x_coords)?;
298        writeln!(file, "y = np.array({:?})", data.y_coords)?;
299        writeln!(file, "energies = np.array({:?})", data.energies)?;
300        writeln!(file)?;
301        writeln!(file, "# Create scatter plot")?;
302        writeln!(file, "plt.figure(figsize=(10, 8))")?;
303        writeln!(
304            file,
305            "scatter = plt.scatter(x, y, c=energies, cmap='viridis', s=50)"
306        )?;
307        writeln!(file, "plt.colorbar(scatter, label='Energy')")?;
308        writeln!(file, "plt.xlabel('Component 1')")?;
309        writeln!(file, "plt.ylabel('Component 2')")?;
310        writeln!(file, "plt.title('Energy Landscape Projection')")?;
311        writeln!(file)?;
312
313        if let Some(_density) = &data.density {
314            writeln!(file, "# Add density contours")?;
315            writeln!(file, "if len(x) > 100:")?;
316            writeln!(file, "    from scipy.stats import gaussian_kde")?;
317            writeln!(file, "    xy = np.vstack([x, y])")?;
318            writeln!(file, "    z = gaussian_kde(xy)(xy)")?;
319            writeln!(file, "    plt.contour(x, y, z, colors='black', alpha=0.3)")?;
320            writeln!(file)?;
321        }
322
323        writeln!(file, "plt.tight_layout()")?;
324        writeln!(file, "plt.savefig('energy_landscape.png', dpi=300)")?;
325        writeln!(file, "plt.show()")?;
326
327        Ok(())
328    }
329
330    /// Export HTML with embedded visualization
331    fn export_html_energy(
332        &self,
333        data: &EnergyLandscapeData,
334        path: &str,
335    ) -> Result<(), Box<dyn std::error::Error>> {
336        use std::io::Write;
337
338        let mut file = std::fs::File::create(path)?;
339
340        // Write HTML header
341        writeln!(file, "<!DOCTYPE html>")?;
342        writeln!(file, "<html>")?;
343        writeln!(file, "<head>")?;
344        writeln!(file, "    <title>Energy Landscape Visualization</title>")?;
345        writeln!(
346            file,
347            "    <script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>"
348        )?;
349        writeln!(file, "    <style>")?;
350        writeln!(
351            file,
352            "        body {{ font-family: Arial, sans-serif; margin: 20px; }}"
353        )?;
354        writeln!(file, "        #plot {{ width: 100%; height: 600px; }}")?;
355        writeln!(file, "    </style>")?;
356        writeln!(file, "</head>")?;
357        writeln!(file, "<body>")?;
358        writeln!(file, "    <h1>Energy Landscape Visualization</h1>")?;
359        writeln!(file, "    <div id=\"plot\"></div>")?;
360        writeln!(file, "    <script>")?;
361
362        // Embed data and Plotly code
363        writeln!(file, "        var data = [{{")?;
364        writeln!(file, "            type: 'scatter',")?;
365        writeln!(file, "            mode: 'markers',")?;
366        writeln!(file, "            x: {:?},", data.x_coords)?;
367        writeln!(file, "            y: {:?},", data.y_coords)?;
368        writeln!(file, "            marker: {{")?;
369        writeln!(file, "                size: 8,")?;
370        writeln!(file, "                color: {:?},", data.energies)?;
371        writeln!(file, "                colorscale: 'Viridis',")?;
372        writeln!(file, "                showscale: true")?;
373        writeln!(file, "            }},")?;
374        writeln!(file, "            text: 'Energy'")?;
375        writeln!(file, "        }}];")?;
376        writeln!(file)?;
377        writeln!(file, "        var layout = {{")?;
378        writeln!(file, "            title: 'Energy Landscape Projection',")?;
379        writeln!(file, "            xaxis: {{ title: 'Component 1' }},")?;
380        writeln!(file, "            yaxis: {{ title: 'Component 2' }}")?;
381        writeln!(file, "        }};")?;
382        writeln!(file)?;
383        writeln!(file, "        Plotly.newPlot('plot', data, layout);")?;
384        writeln!(file, "    </script>")?;
385
386        // Add summary statistics
387        writeln!(file, "    <h2>Summary Statistics</h2>")?;
388        writeln!(file, "    <ul>")?;
389        writeln!(
390            file,
391            "        <li>Number of samples: {}</li>",
392            data.x_coords.len()
393        )?;
394        writeln!(
395            file,
396            "        <li>Min energy: {:.4}</li>",
397            data.energies.iter().fold(f64::INFINITY, |a, &b| a.min(b))
398        )?;
399        writeln!(
400            file,
401            "        <li>Max energy: {:.4}</li>",
402            data.energies
403                .iter()
404                .fold(f64::NEG_INFINITY, |a, &b| a.max(b))
405        )?;
406        writeln!(
407            file,
408            "        <li>Projection method: {}</li>",
409            data.projection_method
410        )?;
411        writeln!(file, "    </ul>")?;
412
413        writeln!(file, "</body>")?;
414        writeln!(file, "</html>")?;
415
416        Ok(())
417    }
418
419    /// Similar implementations for distribution and convergence...
420    fn export_plotly_distribution(
421        &self,
422        _data: &DistributionData,
423        _path: &str,
424    ) -> Result<(), Box<dyn std::error::Error>> {
425        // Implementation for distribution Plotly export
426        Ok(())
427    }
428
429    fn export_matplotlib_distribution(
430        &self,
431        _data: &DistributionData,
432        _path: &str,
433    ) -> Result<(), Box<dyn std::error::Error>> {
434        // Implementation for distribution Matplotlib export
435        Ok(())
436    }
437
438    fn export_html_distribution(
439        &self,
440        _data: &DistributionData,
441        _path: &str,
442    ) -> Result<(), Box<dyn std::error::Error>> {
443        // Implementation for distribution HTML export
444        Ok(())
445    }
446
447    fn export_plotly_convergence(
448        &self,
449        _data: &ConvergenceData,
450        _path: &str,
451    ) -> Result<(), Box<dyn std::error::Error>> {
452        // Implementation for convergence Plotly export
453        Ok(())
454    }
455
456    fn export_matplotlib_convergence(
457        &self,
458        _data: &ConvergenceData,
459        _path: &str,
460    ) -> Result<(), Box<dyn std::error::Error>> {
461        // Implementation for convergence Matplotlib export
462        Ok(())
463    }
464
465    fn export_html_convergence(
466        &self,
467        _data: &ConvergenceData,
468        _path: &str,
469    ) -> Result<(), Box<dyn std::error::Error>> {
470        // Implementation for convergence HTML export
471        Ok(())
472    }
473}
474
475// Data structures for export
476
477/// Energy landscape export data
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct EnergyLandscapeData {
480    pub x_coords: Vec<f64>,
481    pub y_coords: Vec<f64>,
482    pub energies: Vec<f64>,
483    pub density: Option<Vec<f64>>,
484    pub projection_method: String,
485    pub metadata: HashMap<String, String>,
486}
487
488/// Distribution export data
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct DistributionData {
491    pub n_samples: usize,
492    pub n_unique: usize,
493    pub mean_energy: f64,
494    pub std_energy: f64,
495    pub min_energy: f64,
496    pub max_energy: f64,
497    pub clusters: Option<Vec<usize>>,
498    pub cluster_energies: Option<Vec<f64>>,
499    pub metadata: HashMap<String, String>,
500}
501
502/// Convergence export data
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct ConvergenceData {
505    pub objectives: Vec<f64>,
506    pub constraints: Option<Vec<HashMap<String, f64>>>,
507    pub parameters: Option<Vec<HashMap<String, f64>>>,
508    pub iterations: Vec<usize>,
509    pub times: Option<Vec<f64>>,
510    pub constraint_names: Vec<String>,
511    pub parameter_names: Vec<String>,
512    pub metadata: HashMap<String, String>,
513}
514
515// Plotly data structures
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
518struct PlotlyData {
519    data: Vec<PlotlyTrace>,
520    layout: PlotlyLayout,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
524struct PlotlyTrace {
525    #[serde(rename = "type")]
526    trace_type: String,
527    mode: Option<String>,
528    x: Option<Vec<f64>>,
529    y: Option<Vec<f64>>,
530    z: Option<Vec<f64>>,
531    marker: Option<PlotlyMarker>,
532    name: Option<String>,
533    text: Option<Vec<String>>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
537struct PlotlyMarker {
538    size: Option<u32>,
539    color: Option<Vec<f64>>,
540    colorscale: Option<String>,
541    showscale: Option<bool>,
542}
543
544#[derive(Debug, Clone, Default, Serialize, Deserialize)]
545struct PlotlyLayout {
546    title: Option<String>,
547    xaxis: Option<PlotlyAxis>,
548    yaxis: Option<PlotlyAxis>,
549    zaxis: Option<PlotlyAxis>,
550    showlegend: Option<bool>,
551    height: Option<u32>,
552    width: Option<u32>,
553}
554
555#[derive(Debug, Clone, Default, Serialize, Deserialize)]
556struct PlotlyAxis {
557    title: Option<String>,
558    range: Option<[f64; 2]>,
559    #[serde(rename = "type")]
560    axis_type: Option<String>,
561}
562
563/// Export visualization data in specified format
564pub fn export_visualization<T: Serialize>(
565    data: &T,
566    path: &str,
567    format: ExportFormat,
568) -> Result<(), Box<dyn std::error::Error>> {
569    let _exporter = VisualizationExporter::new(format);
570
571    match format {
572        ExportFormat::JSON => {
573            let json = serde_json::to_string_pretty(data)?;
574            std::fs::write(path, json)?;
575        }
576        _ => {
577            return Err(
578                format!("Export format {format:?} not implemented for generic data").into(),
579            );
580        }
581    }
582
583    Ok(())
584}
585
586/// Create export path with proper extension
587pub fn create_export_path(base_path: &str, format: ExportFormat) -> String {
588    let path = Path::new(base_path);
589    let stem = path.file_stem().unwrap_or_default();
590    let parent = path.parent().unwrap_or(Path::new("."));
591
592    let extension = match format {
593        ExportFormat::JSON => "json",
594        ExportFormat::CSV => "csv",
595        ExportFormat::NPY => "npy",
596        ExportFormat::MAT => "mat",
597        ExportFormat::Plotly => "plotly.json",
598        ExportFormat::Matplotlib => "py",
599        ExportFormat::HTML => "html",
600    };
601
602    parent
603        .join(format!("{}.{}", stem.to_string_lossy(), extension))
604        .to_string_lossy()
605        .to_string()
606}