1use crate::error::{OptimError, Result};
7use std::collections::{HashMap, VecDeque};
8use std::io::Write;
9use std::path::Path;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Clone)]
14pub struct VisualizationConfig {
15 pub output_dir: String,
17
18 pub max_points: usize,
20
21 pub update_frequency: usize,
23
24 pub interactive_html: bool,
26
27 pub svg_output: bool,
29
30 pub color_scheme: ColorScheme,
32
33 pub figure_size: (u32, u32),
35
36 pub dpi: u32,
38
39 pub show_grid: bool,
41
42 pub show_legend: bool,
44}
45
46impl Default for VisualizationConfig {
47 fn default() -> Self {
48 Self {
49 output_dir: "optimization_plots".to_string(),
50 max_points: 10000,
51 update_frequency: 100,
52 interactive_html: true,
53 svg_output: false,
54 color_scheme: ColorScheme::Default,
55 figure_size: (800, 600),
56 dpi: 300,
57 show_grid: true,
58 show_legend: true,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy)]
65pub enum ColorScheme {
66 Default,
67 Dark,
68 Colorblind,
69 Publication,
70 Vibrant,
71}
72
73#[derive(Debug, Clone)]
75pub struct OptimizationMetric {
76 pub name: String,
78
79 pub values: VecDeque<f64>,
81
82 pub timestamps: VecDeque<u64>,
84
85 pub steps: VecDeque<usize>,
87
88 pub target: Option<f64>,
90
91 pub higher_isbetter: bool,
93
94 pub units: String,
96
97 pub smoothing_window: usize,
99}
100
101impl OptimizationMetric {
102 pub fn new(name: String, higher_isbetter: bool, units: String) -> Self {
104 Self {
105 name,
106 values: VecDeque::new(),
107 timestamps: VecDeque::new(),
108 steps: VecDeque::new(),
109 target: None,
110 higher_isbetter,
111 units,
112 smoothing_window: 10,
113 }
114 }
115
116 pub fn add_value(&mut self, value: f64, step: usize) {
118 let timestamp = SystemTime::now()
122 .duration_since(UNIX_EPOCH)
123 .unwrap_or_default()
124 .as_secs();
125
126 self.values.push_back(value);
127 self.timestamps.push_back(timestamp);
128 self.steps.push_back(step);
129
130 while self.values.len() > 50000 {
132 self.values.pop_front();
133 self.timestamps.pop_front();
134 self.steps.pop_front();
135 }
136 }
137
138 pub fn get_smoothed_values(&self) -> Vec<f64> {
140 if self.values.len() < self.smoothing_window {
141 return self.values.iter().copied().collect();
142 }
143
144 let mut smoothed = Vec::new();
145 let window = self.smoothing_window.min(self.values.len());
146
147 for i in 0..self.values.len() {
148 let start = i.saturating_sub(window / 2);
149 let end = (i + window / 2 + 1).min(self.values.len());
150
151 let sum: f64 = self.values.range(start..end).sum();
152 let avg = sum / (end - start) as f64;
153 smoothed.push(avg);
154 }
155
156 smoothed
157 }
158
159 pub fn get_recent_improvement(&self, windowsize: usize) -> Option<f64> {
161 if self.values.len() < windowsize * 2 {
162 return None;
163 }
164
165 let recent_avg: f64 =
166 self.values.iter().rev().take(windowsize).sum::<f64>() / windowsize as f64;
167 let older_avg: f64 = self
168 .values
169 .iter()
170 .rev()
171 .skip(windowsize)
172 .take(windowsize)
173 .sum::<f64>()
174 / windowsize as f64;
175
176 Some(if self.higher_isbetter {
177 recent_avg - older_avg
178 } else {
179 older_avg - recent_avg
180 })
181 }
182}
183
184#[derive(Debug, Clone)]
186pub struct OptimizerComparison {
187 pub name: String,
189
190 pub metrics: HashMap<String, Vec<f64>>,
192
193 pub hyperparameters: HashMap<String, f64>,
195
196 pub training_time: Duration,
198
199 pub memory_stats: MemoryStats,
201
202 pub convergence_info: ConvergenceInfo,
204}
205
206#[derive(Debug, Clone)]
208pub struct MemoryStats {
209 pub peak_memory_mb: f64,
211
212 pub avg_memory_mb: f64,
214
215 pub memory_efficiency: f64,
217}
218
219#[derive(Debug, Clone)]
221pub struct ConvergenceInfo {
222 pub converged: bool,
224
225 pub convergence_step: Option<usize>,
227
228 pub final_value: f64,
230
231 pub best_value: f64,
233
234 pub convergence_rate: f64,
236}
237
238pub struct OptimizationVisualizer {
240 config: VisualizationConfig,
242
243 metrics: HashMap<String, OptimizationMetric>,
245
246 comparisons: Vec<OptimizerComparison>,
248
249 dashboard_state: DashboardState,
251
252 current_step: usize,
254
255 last_update_step: usize,
257}
258
259#[derive(Debug)]
266struct DashboardState {
267 last_update: SystemTime,
269}
270
271#[derive(Debug, Clone, Copy)]
273pub enum PlotType {
274 Line,
275 Scatter,
276 Histogram,
277 Heatmap,
278 Bar,
279 Box,
280 Violin,
281 Surface3D,
282}
283
284#[derive(Debug, Clone)]
286pub struct DataSeries {
287 pub name: String,
289
290 pub x_values: Vec<f64>,
292
293 pub y_values: Vec<f64>,
295
296 pub z_values: Option<Vec<f64>>,
298
299 pub color: String,
301
302 pub line_style: LineStyle,
304
305 pub marker_style: MarkerStyle,
307}
308
309#[derive(Debug, Clone, Copy)]
311pub enum LineStyle {
312 Solid,
313 Dashed,
314 Dotted,
315 DashDot,
316 None,
317}
318
319#[derive(Debug, Clone, Copy)]
321pub enum MarkerStyle {
322 Circle,
323 Square,
324 Triangle,
325 Diamond,
326 Plus,
327 Cross,
328 None,
329}
330
331#[derive(Debug, Clone)]
333pub struct AxisConfig {
334 pub label: String,
336
337 pub scale: AxisScale,
339
340 pub range: Option<(f64, f64)>,
342
343 pub ticks: TickConfig,
345}
346
347#[derive(Debug, Clone, Copy)]
349pub enum AxisScale {
350 Linear,
351 Log,
352 Symlog,
353}
354
355#[derive(Debug, Clone)]
357pub struct TickConfig {
358 pub major_spacing: Option<f64>,
360
361 pub minor_count: usize,
363
364 pub show_labels: bool,
366}
367
368#[derive(Debug, Clone)]
370pub struct DashboardLayout {
371 pub rows: usize,
373
374 pub cols: usize,
376
377 pub plot_positions: HashMap<String, (usize, usize)>,
379}
380
381impl OptimizationVisualizer {
382 pub fn new(config: VisualizationConfig) -> Result<Self> {
384 std::fs::create_dir_all(&config.output_dir).map_err(|e| {
386 OptimError::InvalidConfig(format!("Failed to create output directory: {e}"))
387 })?;
388
389 let dashboard_state = DashboardState {
390 last_update: SystemTime::now(),
391 };
392
393 Ok(Self {
394 config,
395 metrics: HashMap::new(),
396 comparisons: Vec::new(),
397 dashboard_state,
398 current_step: 0,
399 last_update_step: 0,
400 })
401 }
402
403 pub fn add_metric(&mut self, name: String, value: f64, higher_isbetter: bool, units: String) {
405 let metric = self
406 .metrics
407 .entry(name.clone())
408 .or_insert_with(|| OptimizationMetric::new(name, higher_isbetter, units));
409
410 metric.add_value(value, self.current_step);
411 }
412
413 pub fn set_target(&mut self, metricname: &str, target: f64) {
415 if let Some(metric) = self.metrics.get_mut(metricname) {
416 metric.target = Some(target);
417 }
418 }
419
420 pub fn step(&mut self) {
422 self.current_step += 1;
423
424 if self.current_step.saturating_sub(self.last_update_step) >= self.config.update_frequency {
431 if let Err(e) = self.update_dashboard() {
432 eprintln!("Failed to update dashboard: {e}");
433 }
434 self.last_update_step = self.current_step;
435 }
436 }
437
438 pub fn plot_loss_curve(&self, metricname: &str) -> Result<String> {
440 let metric = self
441 .metrics
442 .get(metricname)
443 .ok_or_else(|| OptimError::InvalidConfig(format!("Metric '{metricname}' not found")))?;
444
445 let steps: Vec<f64> = metric.steps.iter().map(|&s| s as f64).collect();
446 let values = metric.get_smoothed_values();
447
448 let plotdata = self.create_line_plot(
449 &steps,
450 &values,
451 &format!("{} over Training Steps", metric.name),
452 "Training Steps",
453 &format!("{} ({})", metric.name, metric.units),
454 )?;
455
456 self.save_plot(&plotdata, &format!("{metricname}_curve"))
457 }
458
459 pub fn plot_learning_rate_schedule(&self) -> Result<String> {
461 if let Some(lr_metric) = self.metrics.get("learning_rate") {
462 let steps: Vec<f64> = lr_metric.steps.iter().map(|&s| s as f64).collect();
463 let values: Vec<f64> = lr_metric.values.iter().copied().collect();
464
465 let plotdata = self.create_line_plot(
466 &steps,
467 &values,
468 "Learning Rate Schedule",
469 "Training Steps",
470 "Learning Rate",
471 )?;
472
473 self.save_plot(&plotdata, "learning_rate_schedule")
474 } else {
475 Err(OptimError::InvalidConfig(
476 "Learning rate metric not found".to_string(),
477 ))
478 }
479 }
480
481 pub fn plot_optimizer_comparison(&self, metricname: &str) -> Result<String> {
483 if self.comparisons.is_empty() {
484 return Err(OptimError::InvalidConfig(
485 "No optimizer comparisons available".to_string(),
486 ));
487 }
488
489 let mut plotdata = String::new();
490
491 if self.config.interactive_html {
493 plotdata.push_str(&self.create_html_header("Optimizer Comparison")?);
494 plotdata.push_str("<div id='comparison-plot'></div>\n");
495 plotdata.push_str("<script>\n");
496 plotdata.push_str("const traces = [];\n");
497
498 for (series_index, comparison) in self.comparisons.iter().enumerate() {
499 if let Some(values) = comparison.metrics.get(metricname) {
500 let x_values: Vec<String> = (0..values.len()).map(|i| i.to_string()).collect();
501 plotdata.push_str(&format!("traces.push({{x: {:?}, y: {:?}, name: '{}', type: 'scatter', mode: 'lines', line: {{color: '{}'}}}});\n",
502 x_values, values, comparison.name, self.get_color(series_index)));
503 }
504 }
505
506 plotdata.push_str("Plotly.newPlot('comparison-plot', traces, {\n");
507 plotdata.push_str(" title: 'Optimizer Comparison',\n");
508 plotdata.push_str(" xaxis: {title: 'Training Steps'},\n");
509 plotdata.push_str(&format!(" yaxis: {{title: '{metricname}'}}\n"));
510 plotdata.push_str("});\n");
511 plotdata.push_str("</script>\n");
512 plotdata.push_str("</body></html>\n");
513 }
514
515 self.save_plot(&plotdata, &format!("{metricname}_comparison"))
516 }
517
518 pub fn plot_gradient_norm(&self) -> Result<String> {
520 if let Some(grad_metric) = self.metrics.get("gradient_norm") {
521 let steps: Vec<f64> = grad_metric.steps.iter().map(|&s| s as f64).collect();
522 let values: Vec<f64> = grad_metric.values.iter().copied().collect();
523
524 let mut plotdata = self.create_line_plot(
525 &steps,
526 &values,
527 "Gradient Norm",
528 "Training Steps",
529 "Gradient Norm",
530 )?;
531
532 let max_val = values.iter().fold(0.0f64, |a, &b| a.max(b));
534 let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
535
536 if max_val / min_val > 100.0 {
537 plotdata = plotdata.replace("yaxis: {", "yaxis: {type: 'log', ");
538 }
539
540 self.save_plot(&plotdata, "gradient_norm")
541 } else {
542 Err(OptimError::InvalidConfig(
543 "Gradient norm metric not found".to_string(),
544 ))
545 }
546 }
547
548 pub fn plot_throughput(&self) -> Result<String> {
550 if let Some(throughput_metric) = self.metrics.get("throughput") {
551 let steps: Vec<f64> = throughput_metric.steps.iter().map(|&s| s as f64).collect();
552 let values: Vec<f64> = throughput_metric.values.iter().copied().collect();
553
554 let plotdata = self.create_line_plot(
555 &steps,
556 &values,
557 "Training Throughput",
558 "Training Steps",
559 "Samples/Second",
560 )?;
561
562 self.save_plot(&plotdata, "throughput")
563 } else {
564 Err(OptimError::InvalidConfig(
565 "Throughput metric not found".to_string(),
566 ))
567 }
568 }
569
570 pub fn plot_memory_usage(&self) -> Result<String> {
572 if let Some(memory_metric) = self.metrics.get("memory_usage") {
573 let steps: Vec<f64> = memory_metric.steps.iter().map(|&s| s as f64).collect();
574 let values: Vec<f64> = memory_metric.values.iter().copied().collect();
575
576 let plotdata = self.create_line_plot(
577 &steps,
578 &values,
579 "Memory Usage",
580 "Training Steps",
581 "Memory (MB)",
582 )?;
583
584 self.save_plot(&plotdata, "memory_usage")
585 } else {
586 Err(OptimError::InvalidConfig(
587 "Memory usage metric not found".to_string(),
588 ))
589 }
590 }
591
592 pub fn plot_hyperparameter_sensitivity(
594 &self,
595 param_name: &str,
596 metricname: &str,
597 ) -> Result<String> {
598 let mut param_values = Vec::new();
599 let mut metric_values = Vec::new();
600
601 for comparison in &self.comparisons {
602 if let (Some(¶m_val), Some(metric_vals)) = (
603 comparison.hyperparameters.get(param_name),
604 comparison.metrics.get(metricname),
605 ) {
606 if let Some(&final_metric) = metric_vals.last() {
607 param_values.push(param_val);
608 metric_values.push(final_metric);
609 }
610 }
611 }
612
613 if param_values.is_empty() {
614 return Err(OptimError::InvalidConfig(format!(
615 "No data available for hyperparameter '{}' and metric '{}'",
616 param_name, metricname
617 )));
618 }
619
620 let plotdata = self.create_scatter_plot(
621 ¶m_values,
622 &metric_values,
623 &format!("Sensitivity of {} to {}", metricname, param_name),
624 param_name,
625 metricname,
626 )?;
627
628 self.save_plot(
629 &plotdata,
630 &format!("sensitivity_{}_{}", param_name, metricname),
631 )
632 }
633
634 pub fn create_dashboard(&self) -> Result<String> {
636 let mut dashboard = String::new();
637
638 if self.config.interactive_html {
639 dashboard.push_str(&self.create_html_header("Optimization Dashboard")?);
640
641 dashboard.push_str(
643 r#"
644<style>
645.dashboard-container {
646 display: grid;
647 grid-template-columns: 1fr 1fr;
648 grid-template-rows: 1fr 1fr;
649 gap: 20px;
650 height: 100vh;
651 padding: 20px;
652}
653.plot-container {
654 border: 1px solid #ddd;
655 border-radius: 8px;
656 padding: 10px;
657}
658.metrics-summary {
659 grid-column: span 2;
660 padding: 20px;
661 background-color: #f8f9fa;
662 border-radius: 8px;
663 margin-bottom: 20px;
664}
665</style>
666"#,
667 );
668
669 dashboard.push_str("<div class='metrics-summary'>\n");
671 dashboard.push_str("<h2>Current Metrics</h2>\n");
672 dashboard.push_str("<div style='display: flex; gap: 20px;'>\n");
673
674 for (name, metric) in &self.metrics {
675 if let Some(&latest_value) = metric.values.back() {
676 dashboard.push_str(&format!(
677 "<div><strong>{}:</strong> {:.4} {}</div>\n",
678 name, latest_value, metric.units
679 ));
680 }
681 }
682
683 dashboard.push_str("</div></div>\n");
684
685 dashboard.push_str("<div class='dashboard-container'>\n");
687
688 let mut plot_id = 0;
689 for _ in &self.metrics {
690 if plot_id >= 4 {
691 break;
692 } dashboard.push_str(&format!(
695 "<div class='plot-container'><div id='plot-{}'></div></div>\n",
696 plot_id
697 ));
698
699 plot_id += 1;
700 }
701
702 dashboard.push_str("</div>\n");
703
704 dashboard.push_str("<script>\n");
706
707 plot_id = 0;
708 for (name, metric) in &self.metrics {
709 if plot_id >= 4 {
710 break;
711 }
712
713 let steps: Vec<String> = metric.steps.iter().map(|&s| s.to_string()).collect();
714 let values: Vec<f64> = metric.values.iter().copied().collect();
715
716 dashboard.push_str(&format!("Plotly.newPlot('plot-{}', [{{x: {:?}, y: {:?}, type: 'scatter', mode: 'lines', name: '{}', line: {{color: '{}'}}}}], {{title: '{}', xaxis: {{title: 'Steps'}}, yaxis: {{title: '{}'}}}});\n",
717 plot_id, steps, values, name, self.get_color(plot_id), name, metric.units));
718
719 plot_id += 1;
720 }
721
722 dashboard.push_str("</script>\n");
723 dashboard.push_str("</body></html>\n");
724 }
725
726 self.save_plot(&dashboard, "dashboard")
727 }
728
729 fn update_dashboard(&mut self) -> Result<()> {
731 self.dashboard_state.last_update = SystemTime::now();
732
733 self.create_dashboard()?;
736
737 Ok(())
738 }
739
740 pub fn add_optimizer_comparison(&mut self, comparison: OptimizerComparison) {
742 self.comparisons.push(comparison);
743 }
744
745 pub fn export_all(&self) -> Result<Vec<String>> {
747 let mut exported_files = Vec::new();
748
749 for metricname in self.metrics.keys() {
751 if let Ok(filename) = self.plot_loss_curve(metricname) {
752 exported_files.push(filename);
753 }
754 }
755
756 for metricname in ["loss", "accuracy", "throughput"] {
758 if let Ok(filename) = self.plot_optimizer_comparison(metricname) {
759 exported_files.push(filename);
760 }
761 }
762
763 if let Ok(filename) = self.plot_gradient_norm() {
765 exported_files.push(filename);
766 }
767
768 if let Ok(filename) = self.plot_throughput() {
769 exported_files.push(filename);
770 }
771
772 if let Ok(filename) = self.plot_memory_usage() {
773 exported_files.push(filename);
774 }
775
776 if let Ok(filename) = self.create_dashboard() {
778 exported_files.push(filename);
779 }
780
781 Ok(exported_files)
782 }
783
784 fn create_line_plot(
786 &self,
787 x_values: &[f64],
788 y_values: &[f64],
789 title: &str,
790 x_label: &str,
791 y_label: &str,
792 ) -> Result<String> {
793 if !self.config.interactive_html {
794 return Ok(format!("# {}\nX: {:?}\nY: {:?}", title, x_values, y_values));
795 }
796
797 let mut plot = String::new();
798 plot.push_str(&self.create_html_header(title)?);
799 plot.push_str("<div id='plot'></div>\n");
800 plot.push_str("<script>\n");
801
802 plot.push_str(&format!("const trace = {{x: {:?}, y: {:?}, type: 'scatter', mode: 'lines', name: '{}', line: {{color: '{}'}}}};\n",
803 x_values, y_values, title, self.get_color(0)));
804
805 plot.push_str(&format!("Plotly.newPlot('plot', [trace], {{title: '{}', xaxis: {{title: '{}'}}, yaxis: {{title: '{}'}}}});\n",
806 title, x_label, y_label));
807
808 plot.push_str("</script></body></html>");
809
810 Ok(plot)
811 }
812
813 fn create_scatter_plot(
815 &self,
816 x_values: &[f64],
817 y_values: &[f64],
818 title: &str,
819 x_label: &str,
820 y_label: &str,
821 ) -> Result<String> {
822 if !self.config.interactive_html {
823 return Ok(format!("# {}\nX: {:?}\nY: {:?}", title, x_values, y_values));
824 }
825
826 let mut plot = String::new();
827 plot.push_str(&self.create_html_header(title)?);
828 plot.push_str("<div id='plot'></div>\n");
829 plot.push_str("<script>\n");
830
831 plot.push_str(&format!("const trace = {{x: {:?}, y: {:?}, type: 'scatter', mode: 'markers', name: '{}', marker: {{color: '{}'}}}};\n",
832 x_values, y_values, title, self.get_color(0)));
833
834 plot.push_str(&format!("Plotly.newPlot('plot', [trace], {{title: '{}', xaxis: {{title: '{}'}}, yaxis: {{title: '{}'}}}});\n",
835 title, x_label, y_label));
836
837 plot.push_str("</script></body></html>");
838
839 Ok(plot)
840 }
841
842 fn create_html_header(&self, title: &str) -> Result<String> {
844 Ok(format!(
845 r#"
846<!DOCTYPE html>
847<html>
848<head>
849 <title>{}</title>
850 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
851 <style>
852 body {{ font-family: Arial, sans-serif; margin: 20px; }}
853 #plot {{ width: 100%; height: 500px; }}
854 </style>
855</head>
856<body>
857 <h1>{}</h1>
858"#,
859 title, title
860 ))
861 }
862
863 fn save_plot(&self, plotdata: &str, filename: &str) -> Result<String> {
865 let extension = if self.config.interactive_html {
866 "html"
867 } else {
868 "txt"
869 };
870 let full_filename = format!("{}.{}", filename, extension);
871 let filepath = Path::new(&self.config.output_dir).join(&full_filename);
872
873 let mut file = std::fs::File::create(&filepath).map_err(|e| {
874 OptimError::InvalidConfig(format!(
875 "Failed to create file {}: {}",
876 filepath.display(),
877 e
878 ))
879 })?;
880
881 file.write_all(plotdata.as_bytes()).map_err(|e| {
882 OptimError::InvalidConfig(format!(
883 "Failed to write to file {}: {}",
884 filepath.display(),
885 e
886 ))
887 })?;
888
889 Ok(full_filename)
890 }
891
892 fn get_color(&self, index: usize) -> String {
894 let colors = match self.config.color_scheme {
895 ColorScheme::Default => vec![
896 "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2",
897 "#7f7f7f", "#bcbd22", "#17becf",
898 ],
899 ColorScheme::Dark => vec![
900 "#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69",
901 "#fccde5", "#d9d9d9", "#bc80bd",
902 ],
903 ColorScheme::Colorblind => vec![
904 "#000000", "#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00",
905 "#CC79A7",
906 ],
907 ColorScheme::Publication => vec!["#000000", "#333333", "#666666", "#999999", "#CCCCCC"],
908 ColorScheme::Vibrant => vec![
909 "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8",
910 "#F7DC6F", "#BB8FCE", "#85C1E9",
911 ],
912 };
913
914 colors[index % colors.len()].to_string()
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921 use std::time::Duration;
922
923 #[test]
924 fn test_visualization_config_default() {
925 let config = VisualizationConfig::default();
926 assert_eq!(config.max_points, 10000);
927 assert!(config.interactive_html);
928 assert!(config.show_grid);
929 }
930
931 #[test]
932 fn test_optimization_metric() {
933 let mut metric = OptimizationMetric::new("loss".to_string(), false, "nats".to_string());
934
935 metric.add_value(1.0, 0);
936 metric.add_value(0.8, 1);
937 metric.add_value(0.6, 2);
938 metric.add_value(0.4, 3); assert_eq!(metric.values.len(), 4);
941 assert_eq!(metric.steps.len(), 4);
942
943 let improvement = metric.get_recent_improvement(2);
944 assert!(improvement.is_some());
945 }
946
947 #[test]
948 fn test_visualizer_creation() {
949 let config = VisualizationConfig {
950 output_dir: "/tmp/test_plots".to_string(),
951 ..Default::default()
952 };
953
954 let visualizer = OptimizationVisualizer::new(config);
955 assert!(visualizer.is_ok());
956 }
957
958 #[test]
959 fn test_add_metric() {
960 let config = VisualizationConfig {
961 output_dir: "/tmp/test_plots".to_string(),
962 ..Default::default()
963 };
964
965 let mut visualizer = OptimizationVisualizer::new(config).expect("unwrap failed");
966
967 visualizer.add_metric("loss".to_string(), 1.0, false, "nats".to_string());
968 visualizer.step();
969 visualizer.add_metric("loss".to_string(), 0.8, false, "nats".to_string());
970
971 assert!(visualizer.metrics.contains_key("loss"));
972 assert_eq!(visualizer.metrics["loss"].values.len(), 2);
973 }
974
975 #[test]
976 fn test_optimizer_comparison() {
977 let comparison = OptimizerComparison {
978 name: "Adam".to_string(),
979 metrics: {
980 let mut map = HashMap::new();
981 map.insert("loss".to_string(), vec![1.0, 0.8, 0.6]);
982 map
983 },
984 hyperparameters: {
985 let mut map = HashMap::new();
986 map.insert("learning_rate".to_string(), 0.001);
987 map
988 },
989 training_time: Duration::from_secs(120),
990 memory_stats: MemoryStats {
991 peak_memory_mb: 1024.0,
992 avg_memory_mb: 512.0,
993 memory_efficiency: 100.0,
994 },
995 convergence_info: ConvergenceInfo {
996 converged: true,
997 convergence_step: Some(100),
998 final_value: 0.6,
999 best_value: 0.6,
1000 convergence_rate: 0.004,
1001 },
1002 };
1003
1004 assert_eq!(comparison.name, "Adam");
1005 assert!(comparison.convergence_info.converged);
1006 }
1007
1008 #[test]
1009 fn test_color_schemes() {
1010 let config = VisualizationConfig {
1011 color_scheme: ColorScheme::Colorblind,
1012 output_dir: "/tmp/test_plots".to_string(),
1013 ..Default::default()
1014 };
1015
1016 let visualizer = OptimizationVisualizer::new(config).expect("unwrap failed");
1017 let color = visualizer.get_color(0);
1018 assert_eq!(color, "#000000");
1019 }
1020}