1#![allow(dead_code)]
10
11use crate::{
12 architecture_analysis::ArchitectureAnalysisReport,
13 gradient_debugger::GradientDebugReport,
14 profiler::ProfilerReport,
15 visualization::{DebugVisualizer, PlotData, VisualizationConfig},
16};
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub enum ReportFormat {
24 Pdf,
26 Markdown,
28 Html,
30 Json,
32 Jupyter,
34 Latex,
36 Excel,
38 PowerPoint,
40}
41
42impl ReportFormat {
43 pub fn is_implemented(&self) -> bool {
52 !matches!(
53 self,
54 ReportFormat::Pdf | ReportFormat::Excel | ReportFormat::PowerPoint
55 )
56 }
57
58 fn label(&self) -> &'static str {
61 match self {
62 ReportFormat::Pdf => "PDF",
63 ReportFormat::Markdown => "Markdown",
64 ReportFormat::Html => "HTML",
65 ReportFormat::Json => "JSON",
66 ReportFormat::Jupyter => "Jupyter",
67 ReportFormat::Latex => "LaTeX",
68 ReportFormat::Excel => "Excel",
69 ReportFormat::PowerPoint => "PowerPoint",
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum ReportType {
77 DebugReport,
79 PerformanceReport,
81 TrainingReport,
83 GradientReport,
85 MemoryReport,
87 ComprehensiveReport,
89 CustomReport(Vec<ReportSection>),
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub enum ReportSection {
96 Summary,
98 Architecture,
100 Performance,
102 Memory,
104 Gradients,
106 Training,
108 Errors,
110 Recommendations,
112 Visualizations,
114 RawData,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ReportConfig {
121 pub title: String,
123 pub subtitle: Option<String>,
125 pub author: String,
127 pub organization: Option<String>,
129 pub format: ReportFormat,
131 pub report_type: ReportType,
133 pub include_visualizations: bool,
135 pub include_raw_data: bool,
137 pub output_path: String,
139 pub metadata: HashMap<String, String>,
141}
142
143impl Default for ReportConfig {
144 fn default() -> Self {
145 Self {
146 title: "TrustformeRS Debug Report".to_string(),
147 subtitle: None,
148 author: "TrustformeRS Debugger".to_string(),
149 organization: None,
150 format: ReportFormat::Html,
151 report_type: ReportType::ComprehensiveReport,
152 include_visualizations: true,
153 include_raw_data: false,
154 output_path: "debug_report".to_string(),
155 metadata: HashMap::new(),
156 }
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Report {
163 pub metadata: ReportMetadata,
165 pub sections: Vec<GeneratedSection>,
167 pub visualizations: HashMap<String, PlotData>,
169 pub raw_data: HashMap<String, serde_json::Value>,
171 pub generated_at: DateTime<Utc>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ReportMetadata {
178 pub title: String,
180 pub subtitle: Option<String>,
182 pub author: String,
184 pub organization: Option<String>,
186 pub version: String,
188 pub generation_time_ms: f64,
190 pub additional_metadata: HashMap<String, String>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct GeneratedSection {
197 pub section_type: ReportSection,
199 pub title: String,
201 pub content: String,
203 pub data: HashMap<String, serde_json::Value>,
205 pub visualizations: Vec<String>,
207}
208
209#[derive(Debug)]
211pub struct ReportGenerator {
212 config: ReportConfig,
214 debug_data: Option<GradientDebugReport>,
216 profiling_data: Option<ProfilerReport>,
218 architecture_data: Option<ArchitectureAnalysisReport>,
223 visualizer: DebugVisualizer,
225}
226
227impl ReportGenerator {
228 pub fn new(config: ReportConfig) -> Result<Self, ReportError> {
237 if !config.format.is_implemented() {
238 return Err(ReportError::UnsupportedFormat(format!(
239 "{} export is not implemented; choose one of Markdown, Html, Json, Jupyter, or \
240 Latex",
241 config.format.label()
242 )));
243 }
244 Ok(Self {
245 config,
246 debug_data: None,
247 profiling_data: None,
248 architecture_data: None,
249 visualizer: DebugVisualizer::new(VisualizationConfig::default()),
250 })
251 }
252
253 pub fn with_debug_data(mut self, data: GradientDebugReport) -> Self {
255 self.debug_data = Some(data);
256 self
257 }
258
259 pub fn with_profiling_data(mut self, data: ProfilerReport) -> Self {
261 self.profiling_data = Some(data);
262 self
263 }
264
265 pub fn with_architecture_data(mut self, data: ArchitectureAnalysisReport) -> Self {
270 self.architecture_data = Some(data);
271 self
272 }
273
274 pub fn generate(&self) -> Result<Report, ReportError> {
276 let start_time = std::time::Instant::now();
277
278 let sections = match &self.config.report_type {
279 ReportType::DebugReport => self.generate_debug_sections()?,
280 ReportType::PerformanceReport => self.generate_performance_sections()?,
281 ReportType::TrainingReport => self.generate_training_sections()?,
282 ReportType::GradientReport => self.generate_gradient_sections()?,
283 ReportType::MemoryReport => self.generate_memory_sections()?,
284 ReportType::ComprehensiveReport => self.generate_comprehensive_sections()?,
285 ReportType::CustomReport(section_types) => {
286 self.generate_custom_sections(section_types)?
287 },
288 };
289
290 let visualizations = if self.config.include_visualizations {
291 self.generate_visualizations()?
292 } else {
293 HashMap::new()
294 };
295
296 let raw_data = if self.config.include_raw_data {
297 self.generate_raw_data()?
298 } else {
299 HashMap::new()
300 };
301
302 let generation_time = start_time.elapsed().as_secs_f64() * 1000.0;
303
304 let report = Report {
305 metadata: ReportMetadata {
306 title: self.config.title.clone(),
307 subtitle: self.config.subtitle.clone(),
308 author: self.config.author.clone(),
309 organization: self.config.organization.clone(),
310 version: "1.0".to_string(),
311 generation_time_ms: generation_time,
312 additional_metadata: self.config.metadata.clone(),
313 },
314 sections,
315 visualizations,
316 raw_data,
317 generated_at: Utc::now(),
318 };
319
320 Ok(report)
321 }
322
323 fn generate_debug_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
325 let mut sections = Vec::new();
326
327 sections.push(self.generate_summary_section()?);
329
330 sections.push(self.generate_architecture_section()?);
332
333 if self.debug_data.is_some() {
335 sections.push(self.generate_gradients_section()?);
336 }
337
338 sections.push(self.generate_errors_section()?);
340
341 sections.push(self.generate_recommendations_section()?);
343
344 Ok(sections)
345 }
346
347 fn generate_performance_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
349 let mut sections = Vec::new();
350
351 sections.push(self.generate_summary_section()?);
352 sections.push(self.generate_performance_section()?);
353
354 if self.profiling_data.is_some() {
355 sections.push(self.generate_memory_section()?);
356 }
357
358 sections.push(self.generate_recommendations_section()?);
359
360 Ok(sections)
361 }
362
363 fn generate_training_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
365 let mut sections = Vec::new();
366
367 sections.push(self.generate_summary_section()?);
368 sections.push(self.generate_training_section()?);
369 sections.push(self.generate_gradients_section()?);
370 sections.push(self.generate_recommendations_section()?);
371
372 Ok(sections)
373 }
374
375 fn generate_gradient_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
377 let mut sections = Vec::new();
378
379 sections.push(self.generate_summary_section()?);
380 sections.push(self.generate_gradients_section()?);
381 sections.push(self.generate_recommendations_section()?);
382
383 Ok(sections)
384 }
385
386 fn generate_memory_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
388 let mut sections = Vec::new();
389
390 sections.push(self.generate_summary_section()?);
391 sections.push(self.generate_memory_section()?);
392 sections.push(self.generate_recommendations_section()?);
393
394 Ok(sections)
395 }
396
397 fn generate_comprehensive_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
399 let mut sections = Vec::new();
400
401 sections.push(self.generate_summary_section()?);
402 sections.push(self.generate_architecture_section()?);
403 sections.push(self.generate_performance_section()?);
404 sections.push(self.generate_memory_section()?);
405 sections.push(self.generate_gradients_section()?);
406 sections.push(self.generate_training_section()?);
407 sections.push(self.generate_errors_section()?);
408 sections.push(self.generate_recommendations_section()?);
409
410 Ok(sections)
411 }
412
413 fn generate_custom_sections(
415 &self,
416 section_types: &[ReportSection],
417 ) -> Result<Vec<GeneratedSection>, ReportError> {
418 let mut sections = Vec::new();
419
420 for section_type in section_types {
421 let section = match section_type {
422 ReportSection::Summary => self.generate_summary_section()?,
423 ReportSection::Architecture => self.generate_architecture_section()?,
424 ReportSection::Performance => self.generate_performance_section()?,
425 ReportSection::Memory => self.generate_memory_section()?,
426 ReportSection::Gradients => self.generate_gradients_section()?,
427 ReportSection::Training => self.generate_training_section()?,
428 ReportSection::Errors => self.generate_errors_section()?,
429 ReportSection::Recommendations => self.generate_recommendations_section()?,
430 ReportSection::Visualizations => self.generate_visualizations_section()?,
431 ReportSection::RawData => self.generate_raw_data_section()?,
432 };
433 sections.push(section);
434 }
435
436 Ok(sections)
437 }
438
439 fn generate_summary_section(&self) -> Result<GeneratedSection, ReportError> {
441 let mut content = String::new();
442 let mut data = HashMap::new();
443
444 content.push_str("## Executive Summary\n\n");
445 content.push_str("This report provides a comprehensive analysis of the TrustformeRS model debugging session.\n\n");
446
447 if let Some(debug_data) = &self.debug_data {
449 content.push_str(&format!(
450 "- **Total Layers Analyzed**: {}\n",
451 debug_data.flow_analysis.layer_analyses.len()
452 ));
453
454 let healthy_layers = debug_data
455 .flow_analysis
456 .layer_analyses
457 .iter()
458 .filter(|(_name, l)| !l.is_vanishing && !l.is_exploding)
459 .count();
460 content.push_str(&format!("- **Healthy Layers**: {}\n", healthy_layers));
461
462 data.insert(
463 "total_layers".to_string(),
464 serde_json::json!(debug_data.flow_analysis.layer_analyses.len()),
465 );
466 data.insert(
467 "healthy_layers".to_string(),
468 serde_json::json!(healthy_layers),
469 );
470 }
471
472 if let Some(profiling_data) = &self.profiling_data {
473 content.push_str(&format!(
474 "- **Total Memory Usage**: {:.2} MB\n",
475 profiling_data.memory_efficiency.peak_memory_mb
476 ));
477 content.push_str(&format!(
478 "- **Execution Time**: {:.2} ms\n",
479 profiling_data.total_runtime.as_millis() as f64
480 ));
481
482 data.insert(
483 "peak_memory_mb".to_string(),
484 serde_json::json!(profiling_data.memory_efficiency.peak_memory_mb),
485 );
486 data.insert(
487 "total_time_ms".to_string(),
488 serde_json::json!(profiling_data.total_runtime.as_millis() as f64),
489 );
490 }
491
492 Ok(GeneratedSection {
493 section_type: ReportSection::Summary,
494 title: "Executive Summary".to_string(),
495 content,
496 data,
497 visualizations: Vec::new(),
498 })
499 }
500
501 fn generate_architecture_section(&self) -> Result<GeneratedSection, ReportError> {
503 let mut content = String::new();
504 let data = HashMap::new();
505
506 content.push_str("## Model Architecture Analysis\n\n");
507 content.push_str("This section provides detailed analysis of the model architecture.\n\n");
508
509 let parameter_counts: HashMap<&str, usize> = self
515 .architecture_data
516 .as_ref()
517 .map(|arch| arch.layers.iter().map(|l| (l.name.as_str(), l.parameters)).collect())
518 .unwrap_or_default();
519
520 content.push_str("### Layer Structure\n\n");
522 if let Some(debug_data) = &self.debug_data {
523 content.push_str("| Layer | Type | Parameters | Health Status |\n");
524 content.push_str("|-------|------|------------|---------------|\n");
525
526 for (i, (layer_name, layer)) in
527 debug_data.flow_analysis.layer_analyses.iter().enumerate()
528 {
529 let health = if layer.is_vanishing {
530 "Vanishing"
531 } else if layer.is_exploding {
532 "Exploding"
533 } else {
534 "Healthy"
535 };
536 let parameters = parameter_counts
537 .get(layer_name.as_str())
538 .map(|count| count.to_string())
539 .unwrap_or_else(|| "N/A".to_string());
540 content.push_str(&format!(
541 "| {} | {} | {} | {} |\n",
542 i, layer_name, parameters, health
543 ));
544 }
545 } else {
546 content.push_str("No architecture data available.\n");
547 }
548
549 Ok(GeneratedSection {
550 section_type: ReportSection::Architecture,
551 title: "Model Architecture Analysis".to_string(),
552 content,
553 data,
554 visualizations: vec!["architecture_diagram".to_string()],
555 })
556 }
557
558 fn generate_performance_section(&self) -> Result<GeneratedSection, ReportError> {
560 let mut content = String::new();
561 let mut data = HashMap::new();
562
563 content.push_str("## Performance Analysis\n\n");
564
565 if let Some(profiling_data) = &self.profiling_data {
566 content.push_str("### Timing Statistics\n\n");
567 content.push_str(&format!(
568 "- **Total Execution Time**: {:.2} ms\n",
569 profiling_data.total_runtime.as_millis() as f64
570 ));
571 content.push_str(&format!(
572 "- **Forward Pass Time**: {:.2} ms\n",
573 profiling_data.total_runtime.as_millis() as f64 * 0.6
574 )); content.push_str(&format!(
576 "- **Backward Pass Time**: {:.2} ms\n",
577 profiling_data.total_runtime.as_millis() as f64 * 0.4
578 )); content.push_str("\n### Throughput\n\n");
581 let tokens_per_sec = 1000.0 / (profiling_data.total_runtime.as_millis() as f64 + 1.0); content.push_str(&format!("- **Tokens per Second**: {:.2}\n", tokens_per_sec));
583 content.push_str(&format!(
584 "- **Samples per Second**: {:.2}\n",
585 tokens_per_sec * 10.0
586 )); let timing_stats = serde_json::json!({
590 "total_time_ms": profiling_data.total_runtime.as_millis() as f64,
591 "forward_pass_ms": profiling_data.total_runtime.as_millis() as f64 * 0.6,
592 "backward_pass_ms": profiling_data.total_runtime.as_millis() as f64 * 0.4
593 });
594 let throughput_stats = serde_json::json!({
595 "tokens_per_second": tokens_per_sec,
596 "samples_per_second": tokens_per_sec * 10.0
597 });
598 data.insert("timing_stats".to_string(), timing_stats);
599 data.insert("throughput_stats".to_string(), throughput_stats);
600 } else {
601 content.push_str("No performance data available.\n");
602 }
603
604 Ok(GeneratedSection {
605 section_type: ReportSection::Performance,
606 title: "Performance Analysis".to_string(),
607 content,
608 data,
609 visualizations: vec!["performance_chart".to_string()],
610 })
611 }
612
613 fn generate_memory_section(&self) -> Result<GeneratedSection, ReportError> {
615 let mut content = String::new();
616 let mut data = HashMap::new();
617
618 content.push_str("## Memory Analysis\n\n");
619
620 if let Some(profiling_data) = &self.profiling_data {
621 content.push_str("### Memory Usage\n\n");
622 content.push_str(&format!(
623 "- **Peak Memory**: {:.2} MB\n",
624 profiling_data.memory_efficiency.peak_memory_mb
625 ));
626 content.push_str(&format!(
627 "- **Current Memory**: {:.2} MB\n",
628 profiling_data.memory_efficiency.avg_memory_mb
629 ));
630 content.push_str(&format!(
631 "- **Memory Efficiency**: {:.2}%\n",
632 profiling_data.memory_efficiency.efficiency_score
633 ));
634
635 data.insert(
636 "memory_stats".to_string(),
637 serde_json::to_value(&profiling_data.memory_efficiency)
638 .map_err(|e| ReportError::SerializationError(e.to_string()))?,
639 );
640 } else {
641 content.push_str("No memory data available.\n");
642 }
643
644 Ok(GeneratedSection {
645 section_type: ReportSection::Memory,
646 title: "Memory Analysis".to_string(),
647 content,
648 data,
649 visualizations: vec!["memory_chart".to_string()],
650 })
651 }
652
653 fn generate_gradients_section(&self) -> Result<GeneratedSection, ReportError> {
655 let mut content = String::new();
656 let mut data = HashMap::new();
657
658 content.push_str("## Gradient Analysis\n\n");
659
660 if let Some(debug_data) = &self.debug_data {
661 content.push_str("### Gradient Health Summary\n\n");
662
663 let healthy_count = debug_data
664 .flow_analysis
665 .layer_analyses
666 .iter()
667 .filter(|(_name, l)| !l.is_vanishing && !l.is_exploding)
668 .count();
669 let problematic_count = debug_data.flow_analysis.layer_analyses.len() - healthy_count;
670
671 content.push_str(&format!("- **Healthy Layers**: {}\n", healthy_count));
672 content.push_str(&format!(
673 "- **Problematic Layers**: {}\n",
674 problematic_count
675 ));
676
677 if problematic_count > 0 {
678 content.push_str("\n### Issues Detected\n\n");
679 for (i, (layer_name, layer)) in
680 debug_data.flow_analysis.layer_analyses.iter().enumerate()
681 {
682 if layer.is_vanishing || layer.is_exploding {
683 let status = if layer.is_vanishing {
684 "Vanishing gradients"
685 } else {
686 "Exploding gradients"
687 };
688 content
689 .push_str(&format!("- **Layer {}** ({}): {}\n", i, layer_name, status));
690 }
691 }
692 }
693
694 data.insert(
695 "gradient_analysis".to_string(),
696 serde_json::to_value(debug_data)
697 .map_err(|e| ReportError::SerializationError(e.to_string()))?,
698 );
699 } else {
700 content.push_str("No gradient data available.\n");
701 }
702
703 Ok(GeneratedSection {
704 section_type: ReportSection::Gradients,
705 title: "Gradient Analysis".to_string(),
706 content,
707 data,
708 visualizations: vec!["gradient_flow_chart".to_string()],
709 })
710 }
711
712 fn generate_training_section(&self) -> Result<GeneratedSection, ReportError> {
714 let content =
715 "## Training Dynamics\n\nTraining dynamics analysis would go here.".to_string();
716 let data = HashMap::new();
717
718 Ok(GeneratedSection {
719 section_type: ReportSection::Training,
720 title: "Training Dynamics".to_string(),
721 content,
722 data,
723 visualizations: vec!["training_curves".to_string()],
724 })
725 }
726
727 fn generate_errors_section(&self) -> Result<GeneratedSection, ReportError> {
729 let content = "## Error Analysis\n\nError analysis would go here.".to_string();
730 let data = HashMap::new();
731
732 Ok(GeneratedSection {
733 section_type: ReportSection::Errors,
734 title: "Error Analysis".to_string(),
735 content,
736 data,
737 visualizations: Vec::new(),
738 })
739 }
740
741 fn generate_recommendations_section(&self) -> Result<GeneratedSection, ReportError> {
743 let mut content = String::new();
744 let data = HashMap::new();
745
746 content.push_str("## Recommendations\n\n");
747 content.push_str("Based on the analysis, here are our recommendations:\n\n");
748
749 if let Some(debug_data) = &self.debug_data {
751 let problematic_layers = debug_data
752 .flow_analysis
753 .layer_analyses
754 .iter()
755 .filter(|(_name, l)| l.is_vanishing || l.is_exploding)
756 .count();
757
758 if problematic_layers > 0 {
759 content.push_str("### Gradient Issues\n\n");
760 content.push_str("- Consider adjusting learning rate\n");
761 content.push_str("- Review gradient clipping settings\n");
762 content.push_str("- Check for numerical instabilities\n\n");
763 }
764 }
765
766 if let Some(profiling_data) = &self.profiling_data {
767 if profiling_data.memory_efficiency.efficiency_score < 80.0 {
768 content.push_str("### Memory Optimization\n\n");
769 content.push_str("- Consider using gradient checkpointing\n");
770 content.push_str("- Review batch size settings\n");
771 content.push_str("- Consider model quantization\n\n");
772 }
773 }
774
775 content.push_str("### General Recommendations\n\n");
776 content.push_str("- Monitor training regularly\n");
777 content.push_str("- Validate on diverse test sets\n");
778 content.push_str("- Keep detailed training logs\n");
779
780 Ok(GeneratedSection {
781 section_type: ReportSection::Recommendations,
782 title: "Recommendations".to_string(),
783 content,
784 data,
785 visualizations: Vec::new(),
786 })
787 }
788
789 fn generate_visualizations_section(&self) -> Result<GeneratedSection, ReportError> {
791 let content = "## Visualizations\n\nVisualization section content.".to_string();
792 let data = HashMap::new();
793
794 Ok(GeneratedSection {
795 section_type: ReportSection::Visualizations,
796 title: "Visualizations".to_string(),
797 content,
798 data,
799 visualizations: vec!["all_charts".to_string()],
800 })
801 }
802
803 fn generate_raw_data_section(&self) -> Result<GeneratedSection, ReportError> {
805 let content = "## Raw Data\n\nRaw data section content.".to_string();
806 let data = HashMap::new();
807
808 Ok(GeneratedSection {
809 section_type: ReportSection::RawData,
810 title: "Raw Data".to_string(),
811 content,
812 data,
813 visualizations: Vec::new(),
814 })
815 }
816
817 fn generate_visualizations(&self) -> Result<HashMap<String, PlotData>, ReportError> {
826 let mut visualizations = HashMap::new();
827
828 if let Some(profiling_data) = &self.profiling_data {
829 if !profiling_data.slowest_layers.is_empty() {
830 let x_values: Vec<f64> =
831 (0..profiling_data.slowest_layers.len()).map(|i| i as f64).collect();
832 let y_values: Vec<f64> = profiling_data
833 .slowest_layers
834 .iter()
835 .map(|(_, duration)| duration.as_secs_f64() * 1000.0)
836 .collect();
837 let labels: Vec<String> =
838 profiling_data.slowest_layers.iter().map(|(name, _)| name.clone()).collect();
839
840 let plot_data = PlotData {
841 x_values,
842 y_values,
843 labels,
844 title: "Performance Chart".to_string(),
845 x_label: "Layer Rank (slowest first)".to_string(),
846 y_label: "Duration (ms)".to_string(),
847 };
848 visualizations.insert("performance_chart".to_string(), plot_data);
849 }
850 }
851
852 Ok(visualizations)
853 }
854
855 fn generate_raw_data(&self) -> Result<HashMap<String, serde_json::Value>, ReportError> {
857 let mut raw_data = HashMap::new();
858
859 if let Some(debug_data) = &self.debug_data {
860 raw_data.insert(
861 "debug_data".to_string(),
862 serde_json::to_value(debug_data)
863 .map_err(|e| ReportError::SerializationError(e.to_string()))?,
864 );
865 }
866
867 if let Some(profiling_data) = &self.profiling_data {
868 raw_data.insert(
869 "profiling_data".to_string(),
870 serde_json::to_value(profiling_data)
871 .map_err(|e| ReportError::SerializationError(e.to_string()))?,
872 );
873 }
874
875 Ok(raw_data)
876 }
877
878 pub fn export_report(&self, report: &Report) -> Result<(), ReportError> {
880 match self.config.format {
881 ReportFormat::Html => self.export_html(report),
882 ReportFormat::Markdown => self.export_markdown(report),
883 ReportFormat::Json => self.export_json(report),
884 ReportFormat::Pdf => self.export_pdf(report),
885 ReportFormat::Jupyter => self.export_jupyter(report),
886 ReportFormat::Latex => self.export_latex(report),
887 ReportFormat::Excel => self.export_excel(report),
888 ReportFormat::PowerPoint => self.export_powerpoint(report),
889 }
890 }
891
892 fn export_html(&self, report: &Report) -> Result<(), ReportError> {
894 let mut html = String::new();
895
896 html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
897 html.push_str(&format!("<title>{}</title>\n", report.metadata.title));
898 html.push_str("<style>body { font-family: Arial, sans-serif; margin: 40px; }</style>\n");
899 html.push_str("</head>\n<body>\n");
900
901 html.push_str(&format!("<h1>{}</h1>\n", report.metadata.title));
902 if let Some(subtitle) = &report.metadata.subtitle {
903 html.push_str(&format!("<h2>{}</h2>\n", subtitle));
904 }
905
906 for section in &report.sections {
907 html.push_str(§ion.content);
908 }
909
910 html.push_str("</body>\n</html>");
911
912 std::fs::write(format!("{}.html", self.config.output_path), html)
913 .map_err(|e| ReportError::FileError(e.to_string()))?;
914
915 Ok(())
916 }
917
918 fn export_markdown(&self, report: &Report) -> Result<(), ReportError> {
920 let mut markdown = String::new();
921
922 markdown.push_str(&format!("# {}\n\n", report.metadata.title));
923 if let Some(subtitle) = &report.metadata.subtitle {
924 markdown.push_str(&format!("## {}\n\n", subtitle));
925 }
926
927 markdown.push_str(&format!("**Author**: {}\n", report.metadata.author));
928 markdown.push_str(&format!(
929 "**Generated**: {}\n\n",
930 report.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
931 ));
932
933 for section in &report.sections {
934 markdown.push_str(§ion.content);
935 markdown.push('\n');
936 }
937
938 std::fs::write(format!("{}.md", self.config.output_path), markdown)
939 .map_err(|e| ReportError::FileError(e.to_string()))?;
940
941 Ok(())
942 }
943
944 fn export_json(&self, report: &Report) -> Result<(), ReportError> {
946 let json = serde_json::to_string_pretty(report)
947 .map_err(|e| ReportError::SerializationError(e.to_string()))?;
948
949 std::fs::write(format!("{}.json", self.config.output_path), json)
950 .map_err(|e| ReportError::FileError(e.to_string()))?;
951
952 Ok(())
953 }
954
955 fn export_pdf(&self, _report: &Report) -> Result<(), ReportError> {
961 Err(ReportError::UnsupportedFormat(
962 "PDF export not implemented: trustformers-debug links no PDF writer. Export HTML \
963 or LaTeX and convert externally."
964 .to_string(),
965 ))
966 }
967
968 fn export_jupyter(&self, report: &Report) -> Result<(), ReportError> {
970 let mut notebook = serde_json::json!({
971 "cells": [],
972 "metadata": {
973 "kernelspec": {
974 "display_name": "Python 3",
975 "language": "python",
976 "name": "python3"
977 }
978 },
979 "nbformat": 4,
980 "nbformat_minor": 4
981 });
982
983 let title_cell = serde_json::json!({
985 "cell_type": "markdown",
986 "metadata": {},
987 "source": [format!("# {}\n\n**Generated**: {}",
988 report.metadata.title,
989 report.generated_at.format("%Y-%m-%d %H:%M:%S UTC"))]
990 });
991 let cells = notebook["cells"].as_array_mut().ok_or_else(|| {
992 ReportError::SerializationError("notebook cells should be an array".to_string())
993 })?;
994 cells.push(title_cell);
995
996 for section in &report.sections {
998 let cell = serde_json::json!({
999 "cell_type": "markdown",
1000 "metadata": {},
1001 "source": [section.content]
1002 });
1003 cells.push(cell);
1004 }
1005
1006 let notebook_str = serde_json::to_string_pretty(¬ebook)
1007 .map_err(|e| ReportError::SerializationError(e.to_string()))?;
1008
1009 std::fs::write(format!("{}.ipynb", self.config.output_path), notebook_str)
1010 .map_err(|e| ReportError::FileError(e.to_string()))?;
1011
1012 Ok(())
1013 }
1014
1015 fn export_latex(&self, report: &Report) -> Result<(), ReportError> {
1017 let mut latex = String::new();
1018
1019 latex.push_str("\\documentclass{article}\n");
1020 latex.push_str("\\begin{document}\n");
1021 latex.push_str(&format!("\\title{{{}}}\n", report.metadata.title));
1022 latex.push_str(&format!("\\author{{{}}}\n", report.metadata.author));
1023 latex.push_str("\\maketitle\n\n");
1024
1025 for section in &report.sections {
1026 latex.push_str(&markdown_headings_to_latex(§ion.content));
1027 }
1028
1029 latex.push_str("\\end{document}\n");
1030
1031 std::fs::write(format!("{}.tex", self.config.output_path), latex)
1032 .map_err(|e| ReportError::FileError(e.to_string()))?;
1033
1034 Ok(())
1035 }
1036
1037 fn export_excel(&self, _report: &Report) -> Result<(), ReportError> {
1042 Err(ReportError::UnsupportedFormat(
1043 "Excel export not implemented for narrative reports; see crate::data_export for \
1044 real .xlsx output of tabular data."
1045 .to_string(),
1046 ))
1047 }
1048
1049 fn export_powerpoint(&self, _report: &Report) -> Result<(), ReportError> {
1051 Err(ReportError::UnsupportedFormat(
1052 "PowerPoint export not implemented: trustformers-debug links no OOXML presentation \
1053 writer."
1054 .to_string(),
1055 ))
1056 }
1057}
1058
1059fn markdown_headings_to_latex(content: &str) -> String {
1073 let mut out = String::with_capacity(content.len() + 32);
1074 for line in content.lines() {
1075 let trimmed = line.trim_start();
1076 let level = trimmed.chars().take_while(|&c| c == '#').count();
1077 if (1..=3).contains(&level) && trimmed.chars().nth(level) == Some(' ') {
1078 let command = match level {
1079 1 => "section",
1080 2 => "subsection",
1081 _ => "subsubsection",
1082 };
1083 let title = escape_latex(trimmed[level + 1..].trim());
1084 out.push_str(&format!("\\{}{{{}}}\n", command, title));
1085 } else {
1086 out.push_str(&escape_latex(line));
1087 out.push('\n');
1088 }
1089 }
1090 out
1091}
1092
1093fn escape_latex(text: &str) -> String {
1096 let mut out = String::with_capacity(text.len());
1097 for ch in text.chars() {
1098 match ch {
1099 '\\' => out.push_str("\\textbackslash{}"),
1100 '{' => out.push_str("\\{"),
1101 '}' => out.push_str("\\}"),
1102 '$' | '&' | '%' | '#' | '_' => {
1103 out.push('\\');
1104 out.push(ch);
1105 },
1106 '~' => out.push_str("\\textasciitilde{}"),
1107 '^' => out.push_str("\\textasciicircum{}"),
1108 _ => out.push(ch),
1109 }
1110 }
1111 out
1112}
1113
1114#[derive(Debug, Clone)]
1116pub enum ReportError {
1117 FileError(String),
1119 SerializationError(String),
1121 UnsupportedFormat(String),
1123 MissingData(String),
1125 GenerationError(String),
1127}
1128
1129impl std::fmt::Display for ReportError {
1130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131 match self {
1132 ReportError::FileError(msg) => write!(f, "File error: {}", msg),
1133 ReportError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
1134 ReportError::UnsupportedFormat(msg) => write!(f, "Unsupported format: {}", msg),
1135 ReportError::MissingData(msg) => write!(f, "Missing data: {}", msg),
1136 ReportError::GenerationError(msg) => write!(f, "Generation error: {}", msg),
1137 }
1138 }
1139}
1140
1141impl std::error::Error for ReportError {}
1142
1143#[cfg(test)]
1144mod tests {
1145 use super::*;
1146
1147 #[test]
1148 fn markdown_headings_become_closed_latex_sections() {
1149 let latex = markdown_headings_to_latex("# One\n## Two\n### Three\nbody\n");
1150 assert!(latex.contains("\\section{One}"), "{latex}");
1151 assert!(latex.contains("\\subsection{Two}"), "{latex}");
1152 assert!(latex.contains("\\subsubsection{Three}"), "{latex}");
1153 assert_eq!(
1156 latex.matches('{').count(),
1157 latex.matches('}').count(),
1158 "every brace must be closed:\n{latex}"
1159 );
1160 assert!(latex.contains("body"));
1161 }
1162
1163 #[test]
1164 fn latex_specials_in_body_text_are_escaped() {
1165 let latex = markdown_headings_to_latex("100% of $x_1 & y#2");
1166 assert!(latex.contains("100\\%"), "{latex}");
1167 assert!(latex.contains("\\$x\\_1"), "{latex}");
1168 assert!(latex.contains("\\&"), "{latex}");
1169 assert!(latex.contains("y\\#2"), "{latex}");
1170 assert!(!latex.contains("\\section"), "{latex}");
1172 }
1173
1174 #[test]
1175 fn unimplemented_exports_name_what_is_missing() {
1176 let generator =
1177 ReportGenerator::new(ReportConfig::default()).expect("generator construction");
1178 let report = Report {
1179 metadata: ReportMetadata {
1180 title: "t".to_string(),
1181 subtitle: None,
1182 author: "a".to_string(),
1183 organization: None,
1184 version: "1".to_string(),
1185 generation_time_ms: 0.0,
1186 additional_metadata: HashMap::new(),
1187 },
1188 sections: Vec::new(),
1189 visualizations: HashMap::new(),
1190 raw_data: HashMap::new(),
1191 generated_at: chrono::Utc::now(),
1192 };
1193 let pdf = generator.export_pdf(&report).expect_err("pdf must be refused");
1194 assert!(format!("{pdf:?}").contains("PDF writer"), "{pdf:?}");
1195 let xls = generator.export_excel(&report).expect_err("excel must be refused");
1196 assert!(format!("{xls:?}").contains("data_export"), "{xls:?}");
1197 let ppt = generator.export_powerpoint(&report).expect_err("pptx must be refused");
1198 assert!(format!("{ppt:?}").contains("OOXML"), "{ppt:?}");
1199 }
1200 use crate::DebugConfig;
1201
1202 #[test]
1203 fn test_report_config_default() {
1204 let config = ReportConfig::default();
1205 assert_eq!(config.title, "TrustformeRS Debug Report");
1206 assert_eq!(config.author, "TrustformeRS Debugger");
1207 assert!(matches!(config.format, ReportFormat::Html));
1208 assert!(matches!(
1209 config.report_type,
1210 ReportType::ComprehensiveReport
1211 ));
1212 }
1213
1214 #[test]
1215 fn test_report_generator_creation() {
1216 let config = ReportConfig::default();
1217 let generator = ReportGenerator::new(config).expect("format should be implemented");
1218 assert!(generator.debug_data.is_none());
1219 assert!(generator.profiling_data.is_none());
1220 }
1221
1222 #[test]
1227 fn test_new_rejects_pdf_format_immediately_instead_of_at_export_time() {
1228 let config = ReportConfig {
1229 format: ReportFormat::Pdf,
1230 ..Default::default()
1231 };
1232
1233 let err = ReportGenerator::new(config)
1234 .expect_err("PDF must be rejected at construction, not accepted and failed later");
1235 let message = err.to_string();
1236 assert!(
1237 message.contains("PDF"),
1238 "error should name the rejected format: {message}"
1239 );
1240 }
1241
1242 #[test]
1246 fn test_new_rejects_excel_and_powerpoint_formats() {
1247 for format in [ReportFormat::Excel, ReportFormat::PowerPoint] {
1248 let config = ReportConfig {
1249 format,
1250 ..Default::default()
1251 };
1252 assert!(
1253 ReportGenerator::new(config).is_err(),
1254 "unimplemented export formats must be rejected at construction"
1255 );
1256 }
1257 }
1258
1259 #[test]
1264 fn test_new_accepts_every_implemented_format() {
1265 for format in [
1266 ReportFormat::Markdown,
1267 ReportFormat::Html,
1268 ReportFormat::Json,
1269 ReportFormat::Jupyter,
1270 ReportFormat::Latex,
1271 ] {
1272 let config = ReportConfig {
1273 format,
1274 ..Default::default()
1275 };
1276 assert!(
1277 ReportGenerator::new(config).is_ok(),
1278 "implemented export formats must not be rejected"
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn test_report_generation() {
1285 let config = ReportConfig {
1286 title: "Test Report".to_string(),
1287 format: ReportFormat::Json,
1288 report_type: ReportType::DebugReport,
1289 ..Default::default()
1290 };
1291
1292 let generator = ReportGenerator::new(config).expect("format should be implemented");
1293 let report = generator.generate().expect("operation failed in test");
1294
1295 assert_eq!(report.metadata.title, "Test Report");
1296 assert!(!report.sections.is_empty());
1297 }
1298
1299 #[test]
1300 fn test_section_generation() {
1301 let config = ReportConfig::default();
1302 let generator = ReportGenerator::new(config).expect("format should be implemented");
1303
1304 let summary = generator.generate_summary_section().expect("operation failed in test");
1305 assert!(matches!(summary.section_type, ReportSection::Summary));
1306 assert_eq!(summary.title, "Executive Summary");
1307 assert!(!summary.content.is_empty());
1308 }
1309
1310 #[test]
1311 fn test_custom_report_type() {
1312 let config = ReportConfig {
1313 report_type: ReportType::CustomReport(vec![
1314 ReportSection::Summary,
1315 ReportSection::Performance,
1316 ]),
1317 ..Default::default()
1318 };
1319
1320 let generator = ReportGenerator::new(config).expect("format should be implemented");
1321 let report = generator.generate().expect("operation failed in test");
1322
1323 assert_eq!(report.sections.len(), 2);
1324 assert!(matches!(
1325 report.sections[0].section_type,
1326 ReportSection::Summary
1327 ));
1328 assert!(matches!(
1329 report.sections[1].section_type,
1330 ReportSection::Performance
1331 ));
1332 }
1333
1334 #[test]
1335 fn test_report_serialization() {
1336 let config = ReportConfig::default();
1337 let generator = ReportGenerator::new(config).expect("format should be implemented");
1338 let report = generator.generate().expect("operation failed in test");
1339
1340 let json = serde_json::to_string(&report).expect("JSON serialization failed");
1341 let deserialized: Report =
1342 serde_json::from_str(&json).expect("JSON deserialization failed");
1343
1344 assert_eq!(report.metadata.title, deserialized.metadata.title);
1345 assert_eq!(report.sections.len(), deserialized.sections.len());
1346 }
1347
1348 #[tokio::test]
1355 async fn test_architecture_section_uses_real_parameter_counts_not_na() {
1356 use crate::architecture_analysis::{
1357 ArchitectureAnalysisConfig, ArchitectureAnalyzer, LayerInfo, LayerType,
1358 };
1359 use crate::gradient_debugger::debugger::{FlowAnalysis, LayerFlowAnalysis};
1360 use crate::gradient_debugger::GradientDebugger;
1361
1362 let debugger = GradientDebugger::new(DebugConfig::default());
1363 let mut gradient_report =
1364 debugger.generate_report().await.expect("gradient report should generate");
1365 let mut layer_analyses = HashMap::new();
1366 layer_analyses.insert(
1367 "encoder.layer0".to_string(),
1368 LayerFlowAnalysis {
1369 layer_name: "encoder.layer0".to_string(),
1370 is_vanishing: false,
1371 is_exploding: false,
1372 gradient_norm: 0.5,
1373 flow_consistency: 0.9,
1374 },
1375 );
1376 gradient_report.flow_analysis = FlowAnalysis { layer_analyses };
1377
1378 let mut analyzer = ArchitectureAnalyzer::new(ArchitectureAnalysisConfig::default());
1379 analyzer.register_layer(LayerInfo {
1380 id: "0".to_string(),
1381 name: "encoder.layer0".to_string(),
1382 layer_type: LayerType::Linear,
1383 input_shape: vec![768],
1384 output_shape: vec![768],
1385 parameters: 590_592,
1386 trainable_parameters: 590_592,
1387 memory_usage: 0,
1388 flops: 0,
1389 receptive_field: None,
1390 });
1391 let architecture_report =
1392 analyzer.analyze().await.expect("architecture analysis should succeed");
1393
1394 let generator = ReportGenerator::new(ReportConfig::default())
1395 .expect("Html is implemented")
1396 .with_debug_data(gradient_report)
1397 .with_architecture_data(architecture_report);
1398
1399 let section = generator
1400 .generate_architecture_section()
1401 .expect("architecture section generation should succeed");
1402
1403 assert!(
1404 section.content.contains("590592"),
1405 "must show the real parameter count from architecture data, not N/A: {}",
1406 section.content
1407 );
1408 }
1409
1410 #[tokio::test]
1415 async fn test_architecture_section_reports_na_without_architecture_data() {
1416 use crate::gradient_debugger::debugger::{FlowAnalysis, LayerFlowAnalysis};
1417 use crate::gradient_debugger::GradientDebugger;
1418
1419 let debugger = GradientDebugger::new(DebugConfig::default());
1420 let mut gradient_report =
1421 debugger.generate_report().await.expect("gradient report should generate");
1422 let mut layer_analyses = HashMap::new();
1423 layer_analyses.insert(
1424 "encoder.layer0".to_string(),
1425 LayerFlowAnalysis {
1426 layer_name: "encoder.layer0".to_string(),
1427 is_vanishing: false,
1428 is_exploding: false,
1429 gradient_norm: 0.5,
1430 flow_consistency: 0.9,
1431 },
1432 );
1433 gradient_report.flow_analysis = FlowAnalysis { layer_analyses };
1434
1435 let generator = ReportGenerator::new(ReportConfig::default())
1436 .expect("Html is implemented")
1437 .with_debug_data(gradient_report);
1438 let section = generator
1439 .generate_architecture_section()
1440 .expect("architecture section generation should succeed");
1441
1442 assert!(section.content.contains("N/A"));
1443 }
1444
1445 #[test]
1450 fn test_visualizations_reflect_real_slowest_layers_not_fixed_points() {
1451 use crate::profiler::{MemoryEfficiencyAnalysis, ProfilerReport};
1452 use std::time::Duration;
1453
1454 let profiling_data = ProfilerReport {
1455 total_events: 2,
1456 total_runtime: Duration::from_millis(42),
1457 statistics: HashMap::new(),
1458 bottlenecks: Vec::new(),
1459 slowest_layers: vec![
1460 ("attention.0".to_string(), Duration::from_millis(30)),
1461 ("mlp.0".to_string(), Duration::from_millis(12)),
1462 ],
1463 memory_efficiency: MemoryEfficiencyAnalysis::default(),
1464 recommendations: Vec::new(),
1465 };
1466
1467 let generator = ReportGenerator::new(ReportConfig::default())
1468 .expect("Html is implemented")
1469 .with_profiling_data(profiling_data);
1470 let visualizations = generator
1471 .generate_visualizations()
1472 .expect("visualization generation should succeed");
1473
1474 let chart = visualizations
1475 .get("performance_chart")
1476 .expect("a performance chart should be produced from real slowest_layers data");
1477 assert_eq!(
1478 chart.y_values,
1479 vec![30.0, 12.0],
1480 "must reflect real layer durations in ms"
1481 );
1482 assert_ne!(
1483 chart.y_values,
1484 vec![10.0, 15.0, 12.0],
1485 "must not be the old fabricated placeholder points"
1486 );
1487 assert_eq!(
1488 chart.labels,
1489 vec!["attention.0".to_string(), "mlp.0".to_string()]
1490 );
1491 }
1492}