1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ExportFormat {
13 JSON,
15 CSV,
17 NPY,
19 MAT,
21 Plotly,
23 Matplotlib,
25 HTML,
27}
28
29pub struct VisualizationExporter {
31 format: ExportFormat,
32 include_metadata: bool,
33 compression: bool,
34}
35
36impl VisualizationExporter {
37 pub const fn new(format: ExportFormat) -> Self {
39 Self {
40 format,
41 include_metadata: true,
42 compression: false,
43 }
44 }
45
46 pub const fn with_metadata(mut self, include: bool) -> Self {
48 self.include_metadata = include;
49 self
50 }
51
52 pub const fn with_compression(mut self, compress: bool) -> Self {
54 self.compression = compress;
55 self
56 }
57
58 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 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 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 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 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 writeln!(file, "x,y,energy,density")?;
140
141 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 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 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 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 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!(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 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 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 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 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 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 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 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 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 fn export_plotly_distribution(
421 &self,
422 _data: &DistributionData,
423 _path: &str,
424 ) -> Result<(), Box<dyn std::error::Error>> {
425 Ok(())
427 }
428
429 fn export_matplotlib_distribution(
430 &self,
431 _data: &DistributionData,
432 _path: &str,
433 ) -> Result<(), Box<dyn std::error::Error>> {
434 Ok(())
436 }
437
438 fn export_html_distribution(
439 &self,
440 _data: &DistributionData,
441 _path: &str,
442 ) -> Result<(), Box<dyn std::error::Error>> {
443 Ok(())
445 }
446
447 fn export_plotly_convergence(
448 &self,
449 _data: &ConvergenceData,
450 _path: &str,
451 ) -> Result<(), Box<dyn std::error::Error>> {
452 Ok(())
454 }
455
456 fn export_matplotlib_convergence(
457 &self,
458 _data: &ConvergenceData,
459 _path: &str,
460 ) -> Result<(), Box<dyn std::error::Error>> {
461 Ok(())
463 }
464
465 fn export_html_convergence(
466 &self,
467 _data: &ConvergenceData,
468 _path: &str,
469 ) -> Result<(), Box<dyn std::error::Error>> {
470 Ok(())
472 }
473}
474
475#[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#[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#[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#[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
563pub 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
586pub 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}