1use crate::error::OptimError;
7use crate::error::Result;
8use crate::research::experiments::{Experiment, RunStatus};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Publication {
17 pub id: String,
19 pub title: String,
21 pub abstracttext: String,
23 pub authors: Vec<Author>,
25 pub publication_type: PublicationType,
27 pub venue: Option<Venue>,
29 pub status: PublicationStatus,
31 pub keywords: Vec<String>,
33 pub sections: Vec<ManuscriptSection>,
35 pub bibliography: Bibliography,
37 pub experiment_ids: Vec<String>,
39 pub submission_history: Vec<SubmissionRecord>,
41 pub reviews: Vec<Review>,
43 pub metadata: PublicationMetadata,
45 pub created_at: DateTime<Utc>,
47 pub modified_at: DateTime<Utc>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Author {
54 pub name: String,
56 pub email: String,
58 pub affiliations: Vec<Affiliation>,
60 pub orcid: Option<String>,
62 pub position: AuthorPosition,
64 pub contributions: Vec<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Affiliation {
71 pub institution: String,
73 pub department: Option<String>,
75 pub address: String,
77 pub country: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub enum AuthorPosition {
84 First,
86 Corresponding,
88 Senior,
90 EqualContribution,
92 Regular,
94}
95
96#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
98pub enum PublicationType {
99 ConferencePaper,
101 JournalArticle,
103 WorkshopPaper,
105 TechnicalReport,
107 Preprint,
109 Thesis,
111 BookChapter,
113 Patent,
115 SoftwarePaper,
117 DatasetPaper,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Venue {
124 pub name: String,
126 pub venue_type: VenueType,
128 pub abbreviation: Option<String>,
130 pub publisher: Option<String>,
132 pub impact_factor: Option<f64>,
134 pub h_index: Option<u32>,
136 pub acceptance_rate: Option<f64>,
138 pub ranking: Option<String>,
140 pub url: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146pub enum VenueType {
147 Conference,
149 Journal,
151 Workshop,
153 Symposium,
155 PreprintServer,
157 Repository,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub enum PublicationStatus {
164 Draft,
166 ReadyForSubmission,
168 Submitted,
170 UnderReview,
172 RevisionRequested,
174 Accepted,
176 Published,
178 Rejected,
180 Withdrawn,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ManuscriptSection {
187 pub title: String,
189 pub content: String,
191 pub order: usize,
193 pub section_type: SectionType,
195 pub word_count: usize,
197 pub figures: Vec<Figure>,
199 pub tables: Vec<Table>,
200 pub references: Vec<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206pub enum SectionType {
207 Abstract,
209 Introduction,
211 RelatedWork,
213 Methodology,
215 Experiments,
217 Results,
219 Discussion,
221 Conclusion,
223 Acknowledgments,
225 References,
227 Appendix,
229 Custom(String),
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct Figure {
236 pub caption: String,
238 pub file_path: PathBuf,
240 pub figure_type: FigureType,
242 pub number: usize,
244 pub width: Option<f64>,
246 pub height: Option<f64>,
248 pub experiment_id: Option<String>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
254pub enum FigureType {
255 Plot,
257 Diagram,
259 Flowchart,
261 Architecture,
263 Screenshot,
265 Photo,
267 Other,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct Table {
274 pub caption: String,
276 pub data: Vec<Vec<String>>,
278 pub headers: Vec<String>,
280 pub number: usize,
282 pub experiment_id: Option<String>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct Bibliography {
289 pub entries: HashMap<String, BibTeXEntry>,
291 pub citation_style: CitationStyle,
293 pub file_path: Option<PathBuf>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct BibTeXEntry {
300 pub key: String,
302 pub entry_type: String,
304 pub fields: HashMap<String, String>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
310pub enum CitationStyle {
311 APA,
313 IEEE,
315 ACM,
317 Nature,
319 Science,
321 Chicago,
323 MLA,
325 Harvard,
327 Custom(String),
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct SubmissionRecord {
334 pub submitted_at: DateTime<Utc>,
336 pub venue: Venue,
338 pub submission_id: Option<String>,
340 pub status: SubmissionStatus,
342 pub decision_date: Option<DateTime<Utc>>,
344 pub decision: Option<Decision>,
346 pub editor_comments: Option<String>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
352pub enum SubmissionStatus {
353 Submitted,
355 UnderReview,
357 Decided,
359 Withdrawn,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
365pub enum Decision {
366 Accept,
368 AcceptMinorRevisions,
370 AcceptMajorRevisions,
372 RejectAndResubmit,
374 Reject,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct Review {
381 pub id: String,
383 pub reviewer: ReviewerInfo,
385 pub overall_score: Option<f64>,
387 pub confidence_score: Option<f64>,
389 pub detailed_scores: HashMap<String, f64>,
391 pub reviewtext: String,
393 pub strengths: Vec<String>,
395 pub weaknesses: Vec<String>,
397 pub questions: Vec<String>,
399 pub recommendation: ReviewRecommendation,
401 pub submitted_at: DateTime<Utc>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ReviewerInfo {
408 pub anonymous_id: String,
410 pub expertise_level: ExpertiseLevel,
412 pub research_areas: Vec<String>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
418pub enum ExpertiseLevel {
419 Expert,
421 Knowledgeable,
423 SomeKnowledge,
425 LimitedKnowledge,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
431pub enum ReviewRecommendation {
432 StrongAccept,
434 Accept,
436 WeakAccept,
438 Borderline,
440 WeakReject,
442 Reject,
444 StrongReject,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize, Default)]
450pub struct PublicationMetadata {
451 pub doi: Option<String>,
453 pub arxiv_id: Option<String>,
455 pub pages: Option<String>,
457 pub volume: Option<String>,
459 pub issue: Option<String>,
461 pub year: Option<u32>,
463 pub month: Option<u32>,
465 pub isbn_issn: Option<String>,
467 pub license: Option<String>,
469 pub open_access: bool,
471}
472
473#[derive(Debug)]
475pub struct PublicationGenerator {
476 templates: HashMap<PublicationType, PublicationTemplate>,
478 default_citation_style: CitationStyle,
480 output_dir: PathBuf,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct PublicationTemplate {
487 pub name: String,
489 pub sections: Vec<SectionTemplate>,
491 pub formatting: FormattingOptions,
493 pub venue_constraints: VenueConstraints,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct SectionTemplate {
500 pub section_type: SectionType,
502 pub template: String,
504 pub required_fields: Vec<String>,
506 pub target_word_count: Option<usize>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct FormattingOptions {
513 pub format: DocumentFormat,
515 pub font_size: u32,
517 pub line_spacing: f64,
519 pub margins: Margins,
521 pub citation_format: CitationFormat,
523 pub figure_numbering: NumberingStyle,
525 pub table_numbering: NumberingStyle,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
531pub enum DocumentFormat {
532 LaTeX,
534 Markdown,
536 HTML,
538 Word,
540 PDF,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct Margins {
547 pub top: f64,
549 pub bottom: f64,
551 pub left: f64,
553 pub right: f64,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
559pub enum CitationFormat {
560 Numbered,
562 AuthorYear,
564 Superscript,
566 Footnote,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
572pub enum NumberingStyle {
573 Arabic,
575 Roman,
577 Letters,
579 None,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
585pub struct VenueConstraints {
586 pub max_word_count: Option<usize>,
588 pub max_page_count: Option<usize>,
590 pub required_sections: Vec<SectionType>,
592 pub forbidden_sections: Vec<SectionType>,
594 pub max_figures: Option<usize>,
596 pub max_tables: Option<usize>,
598 pub max_references: Option<usize>,
600}
601
602impl Publication {
603 pub fn new(title: &str) -> Self {
605 let now = Utc::now();
606 Self {
607 id: uuid::Uuid::new_v4().to_string(),
608 title: title.to_string(),
609 abstracttext: String::new(),
610 authors: Vec::new(),
611 publication_type: PublicationType::ConferencePaper,
612 venue: None,
613 status: PublicationStatus::Draft,
614 keywords: Vec::new(),
615 sections: Vec::new(),
616 bibliography: Bibliography::new(),
617 experiment_ids: Vec::new(),
618 submission_history: Vec::new(),
619 reviews: Vec::new(),
620 metadata: PublicationMetadata::default(),
621 created_at: now,
622 modified_at: now,
623 }
624 }
625
626 pub fn abstracttext(mut self, abstracttext: &str) -> Self {
628 self.abstracttext = abstracttext.to_string();
629 self.modified_at = Utc::now();
630 self
631 }
632
633 pub fn add_author(mut self, author: Author) -> Self {
635 self.authors.push(author);
636 self.modified_at = Utc::now();
637 self
638 }
639
640 pub fn publication_type(mut self, pubtype: PublicationType) -> Self {
642 self.publication_type = pubtype;
643 self.modified_at = Utc::now();
644 self
645 }
646
647 pub fn venue(mut self, venue: Venue) -> Self {
649 self.venue = Some(venue);
650 self.modified_at = Utc::now();
651 self
652 }
653
654 pub fn keywords(mut self, keywords: Vec<String>) -> Self {
656 self.keywords = keywords;
657 self.modified_at = Utc::now();
658 self
659 }
660
661 pub fn add_experiment(&mut self, experiment_id: &str) {
663 self.experiment_ids.push(experiment_id.to_string());
664 self.modified_at = Utc::now();
665 }
666
667 pub fn add_section(&mut self, section: ManuscriptSection) {
669 self.sections.push(section);
670 self.modified_at = Utc::now();
671 }
672
673 pub fn generate_latex(&self) -> Result<String> {
675 let mut latex = String::new();
676
677 latex.push_str("\\documentclass[conference]{IEEEtran}\n");
679 latex.push_str("\\usepackage{graphicx}\n");
680 latex.push_str("\\usepackage{booktabs}\n");
681 latex.push_str("\\usepackage{amsmath}\n");
682 latex.push_str("\\usepackage{url}\n\n");
683
684 latex.push_str("\\begin{document}\n\n");
685
686 latex.push_str(&format!("\\title{{{}}}\n\n", self.title));
688
689 latex.push_str("\\author{\n");
690 for (i, author) in self.authors.iter().enumerate() {
691 if i > 0 {
692 latex.push_str("\\and\n");
693 }
694 latex.push_str(&format!("\\IEEEauthorblockN{{{}}}\n", author.name));
695 if !author.affiliations.is_empty() {
696 latex.push_str(&format!(
697 "\\IEEEauthorblockA{{{}}}\n",
698 author.affiliations[0].institution
699 ));
700 }
701 }
702 latex.push_str("}\n\n");
703
704 latex.push_str("\\maketitle\n\n");
705
706 if !self.abstracttext.is_empty() {
708 latex.push_str("\\begin{abstract}\n");
709 latex.push_str(&self.abstracttext);
710 latex.push_str("\n\\end{abstract}\n\n");
711 }
712
713 if !self.keywords.is_empty() {
715 latex.push_str("\\begin{IEEEkeywords}\n");
716 latex.push_str(&self.keywords.join(", "));
717 latex.push_str("\n\\end{IEEEkeywords}\n\n");
718 }
719
720 let mut sorted_sections = self.sections.clone();
722 sorted_sections.sort_by_key(|s| s.order);
723
724 for section in sorted_sections {
725 match section.section_type {
726 SectionType::Abstract => continue, SectionType::References => continue, _ => {
729 latex.push_str(&format!("\\section{{{}}}\n", section.title));
730 latex.push_str(§ion.content);
731 latex.push_str("\n\n");
732 }
733 }
734 }
735
736 if !self.bibliography.entries.is_empty() {
738 latex.push_str("\\begin{thebibliography}{99}\n");
739 for entry in self.bibliography.entries.values() {
740 latex.push_str(&self.format_bibtex_entry_latex(entry));
741 }
742 latex.push_str("\\end{thebibliography}\n\n");
743 }
744
745 latex.push_str("\\end{document}\n");
746
747 Ok(latex)
748 }
749
750 fn format_bibtex_entry_latex(&self, entry: &BibTeXEntry) -> String {
751 format!(
752 "\\bibitem{{{}}}\n{}\n\n",
753 entry.key,
754 self.format_bibtex_fields(&entry.fields)
755 )
756 }
757
758 fn format_bibtex_fields(&self, fields: &HashMap<String, String>) -> String {
759 let mut result = String::new();
760
761 if let Some(author) = fields.get("author") {
762 result.push_str(author);
763 }
764
765 if let Some(title) = fields.get("title") {
766 result.push_str(&format!(", ``{}'', ", title));
767 }
768
769 if let Some(journal) = fields.get("journal") {
770 result.push_str(&format!("\\emph{{{}}}, ", journal));
771 } else if let Some(booktitle) = fields.get("booktitle") {
772 result.push_str(&format!("in \\emph{{{}}}, ", booktitle));
773 }
774
775 if let Some(year) = fields.get("year") {
776 result.push_str(year);
777 }
778
779 result
780 }
781
782 pub fn generate_markdown(&self) -> Result<String> {
784 let mut markdown = String::new();
785
786 markdown.push_str(&format!("# {}\n\n", self.title));
788
789 if !self.authors.is_empty() {
791 markdown.push_str("**Authors**: ");
792 let author_names: Vec<String> = self.authors.iter().map(|a| a.name.clone()).collect();
793 markdown.push_str(&author_names.join(", "));
794 markdown.push_str("\n\n");
795 }
796
797 if !self.abstracttext.is_empty() {
799 markdown.push_str("## Abstract\n\n");
800 markdown.push_str(&self.abstracttext);
801 markdown.push_str("\n\n");
802 }
803
804 if !self.keywords.is_empty() {
806 markdown.push_str("**Keywords**: ");
807 markdown.push_str(&self.keywords.join(", "));
808 markdown.push_str("\n\n");
809 }
810
811 let mut sorted_sections = self.sections.clone();
813 sorted_sections.sort_by_key(|s| s.order);
814
815 for section in sorted_sections {
816 match section.section_type {
817 SectionType::Abstract => continue, _ => {
819 markdown.push_str(&format!("## {}\n\n", section.title));
820 markdown.push_str(§ion.content);
821 markdown.push_str("\n\n");
822 }
823 }
824 }
825
826 if !self.bibliography.entries.is_empty() {
828 markdown.push_str("## References\n\n");
829 for (i, entry) in self.bibliography.entries.values().enumerate() {
830 markdown.push_str(&format!(
831 "{}. {}\n",
832 i + 1,
833 self.format_bibtex_entry_markdown(entry)
834 ));
835 }
836 }
837
838 Ok(markdown)
839 }
840
841 fn format_bibtex_entry_markdown(&self, entry: &BibTeXEntry) -> String {
842 let mut result = String::new();
843
844 if let Some(author) = entry.fields.get("author") {
845 result.push_str(author);
846 }
847
848 if let Some(title) = entry.fields.get("title") {
849 result.push_str(&format!(". \"{}\". ", title));
850 }
851
852 if let Some(journal) = entry.fields.get("journal") {
853 result.push_str(&format!("*{}*. ", journal));
854 } else if let Some(booktitle) = entry.fields.get("booktitle") {
855 result.push_str(&format!("In *{}*. ", booktitle));
856 }
857
858 if let Some(year) = entry.fields.get("year") {
859 result.push_str(year);
860 }
861
862 result
863 }
864
865 pub fn submission_statistics(&self) -> SubmissionStatistics {
867 let total_submissions = self.submission_history.len();
868 let accepted = self
869 .submission_history
870 .iter()
871 .filter(|s| {
872 matches!(
873 s.decision,
874 Some(Decision::Accept)
875 | Some(Decision::AcceptMinorRevisions)
876 | Some(Decision::AcceptMajorRevisions)
877 )
878 })
879 .count();
880 let rejected = self
881 .submission_history
882 .iter()
883 .filter(|s| matches!(s.decision, Some(Decision::Reject)))
884 .count();
885
886 let avg_review_time = if !self.submission_history.is_empty() {
887 let total_days: i64 = self
888 .submission_history
889 .iter()
890 .filter_map(|s| {
891 s.decision_date
892 .map(|decision_date| (decision_date - s.submitted_at).num_days())
893 })
894 .sum();
895 total_days as f64 / self.submission_history.len() as f64
896 } else {
897 0.0
898 };
899
900 SubmissionStatistics {
901 total_submissions,
902 accepted,
903 rejected,
904 pending: total_submissions - accepted - rejected,
905 acceptance_rate: if total_submissions > 0 {
906 accepted as f64 / total_submissions as f64
907 } else {
908 0.0
909 },
910 avg_review_time_days: avg_review_time,
911 }
912 }
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct SubmissionStatistics {
918 pub total_submissions: usize,
920 pub accepted: usize,
922 pub rejected: usize,
924 pub pending: usize,
926 pub acceptance_rate: f64,
928 pub avg_review_time_days: f64,
930}
931
932impl Default for Bibliography {
933 fn default() -> Self {
934 Self::new()
935 }
936}
937
938impl Bibliography {
939 pub fn new() -> Self {
941 Self {
942 entries: HashMap::new(),
943 citation_style: CitationStyle::IEEE,
944 file_path: None,
945 }
946 }
947
948 pub fn add_entry(&mut self, entry: BibTeXEntry) {
950 self.entries.insert(entry.key.clone(), entry);
951 }
952
953 pub fn load_bibtex_file(&mut self, filepath: &PathBuf) -> Result<()> {
955 let content = std::fs::read_to_string(filepath)?;
956 self.parse_bibtex(&content)?;
957 self.file_path = Some(filepath.clone());
958 Ok(())
959 }
960
961 pub fn parse_bibtex(&mut self, content: &str) -> Result<()> {
968 for (entry_type, key, fields) in crate::research::citations::parse_bibtex_entries(content) {
969 let entry = BibTeXEntry {
970 key: key.clone(),
971 entry_type,
972 fields,
973 };
974 self.entries.insert(key, entry);
975 }
976
977 Ok(())
978 }
979}
980
981impl PublicationGenerator {
982 pub fn new(output_dir: PathBuf) -> Self {
984 Self {
985 templates: HashMap::new(),
986 default_citation_style: CitationStyle::IEEE,
987 output_dir,
988 }
989 }
990
991 pub fn output_dir(&self) -> &Path {
993 &self.output_dir
994 }
995
996 pub fn default_citation_style(&self) -> &CitationStyle {
998 &self.default_citation_style
999 }
1000
1001 pub fn set_default_citation_style(&mut self, style: CitationStyle) {
1003 self.default_citation_style = style;
1004 }
1005
1006 pub fn register_template(
1013 &mut self,
1014 publication_type: PublicationType,
1015 template: PublicationTemplate,
1016 ) {
1017 self.templates.insert(publication_type, template);
1018 }
1019
1020 pub fn template(&self, publication_type: &PublicationType) -> Option<&PublicationTemplate> {
1022 self.templates.get(publication_type)
1023 }
1024
1025 pub fn generate_registered(
1033 &self,
1034 experiments: &[Experiment],
1035 publication_type: PublicationType,
1036 ) -> Result<Publication> {
1037 let template = self.templates.get(&publication_type).ok_or_else(|| {
1038 OptimError::InvalidConfig(format!(
1039 "no template is registered for {publication_type:?}; call register_template first"
1040 ))
1041 })?;
1042 let mut publication = self.generate_from_experiments(experiments, template)?;
1043 publication.publication_type = publication_type;
1044 Ok(publication)
1045 }
1046
1047 pub fn write_markdown(&self, publication: &Publication) -> Result<PathBuf> {
1050 std::fs::create_dir_all(&self.output_dir)?;
1051 let file_name = publication
1052 .title
1053 .chars()
1054 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1055 .collect::<String>();
1056 let path = self.output_dir.join(format!("{file_name}.md"));
1057 std::fs::write(&path, publication.generate_markdown()?)?;
1058 Ok(path)
1059 }
1060
1061 pub fn generate_from_experiments(
1063 &self,
1064 experiments: &[Experiment],
1065 template: &PublicationTemplate,
1066 ) -> Result<Publication> {
1067 let mut publication = Publication::new("Generated Publication");
1068
1069 let abstracttext = self.generate_abstract(experiments)?;
1071 publication.abstracttext = abstracttext;
1072
1073 for (index, section_template) in template.sections.iter().enumerate() {
1077 let mut section = self.generate_section(section_template, experiments)?;
1078 section.order = index;
1079 section.word_count = section.content.split_whitespace().count();
1080 publication.add_section(section);
1081 }
1082
1083 Ok(publication)
1084 }
1085
1086 fn generate_abstract(&self, experiments: &[Experiment]) -> Result<String> {
1087 let mut abstracttext = String::new();
1089
1090 abstracttext.push_str(
1091 "This paper presents experimental results comparing various optimization algorithms. ",
1092 );
1093
1094 if !experiments.is_empty() {
1095 abstracttext.push_str(&format!(
1096 "We conducted {} experiments evaluating the performance of different optimizers. ",
1097 experiments.len()
1098 ));
1099 }
1100
1101 abstracttext.push_str("Our results demonstrate significant differences in convergence behavior and final performance across different optimization methods.");
1102
1103 Ok(abstracttext)
1104 }
1105
1106 fn generate_section(
1107 &self,
1108 template: &SectionTemplate,
1109 experiments: &[Experiment],
1110 ) -> Result<ManuscriptSection> {
1111 let content = match template.section_type {
1112 SectionType::Introduction => self.generate_introduction(experiments)?,
1113 SectionType::Methodology => self.generate_methodology(experiments)?,
1114 SectionType::Experiments => self.generate_experiments_section(experiments)?,
1115 SectionType::Results => self.generate_results(experiments)?,
1116 SectionType::Conclusion => self.generate_conclusion(experiments)?,
1117 _ => template.template.clone(),
1118 };
1119
1120 Ok(ManuscriptSection {
1121 title: match template.section_type {
1122 SectionType::Abstract => "Abstract".to_string(),
1123 SectionType::Introduction => "Introduction".to_string(),
1124 SectionType::RelatedWork => "Related Work".to_string(),
1125 SectionType::Methodology => "Methodology".to_string(),
1126 SectionType::Experiments => "Experiments".to_string(),
1127 SectionType::Results => "Results".to_string(),
1128 SectionType::Discussion => "Discussion".to_string(),
1129 SectionType::Conclusion => "Conclusion".to_string(),
1130 SectionType::Acknowledgments => "Acknowledgments".to_string(),
1131 SectionType::References => "References".to_string(),
1132 SectionType::Appendix => "Appendix".to_string(),
1133 SectionType::Custom(ref name) => name.clone(),
1134 },
1135 content,
1136 order: 0,
1140 section_type: template.section_type.clone(),
1141 word_count: 0,
1142 figures: Vec::new(),
1143 tables: Vec::new(),
1144 references: Vec::new(),
1145 })
1146 }
1147
1148 fn generate_introduction(&self, experiments: &[Experiment]) -> Result<String> {
1149 let mut content = String::from(
1150 "This section introduces the research problem and motivation for comparing optimization algorithms.",
1151 );
1152
1153 let hypotheses: Vec<&str> = experiments
1154 .iter()
1155 .map(|e| e.hypothesis.as_str())
1156 .filter(|h| !h.is_empty())
1157 .collect();
1158 if !hypotheses.is_empty() {
1159 content.push_str("\n\nThis work investigates the following hypotheses:\n\n");
1160 for hypothesis in hypotheses {
1161 content.push_str(&format!("- {hypothesis}\n"));
1162 }
1163 }
1164
1165 let questions: Vec<&str> = experiments
1166 .iter()
1167 .map(|e| e.metadata.research_question.as_str())
1168 .filter(|q| !q.is_empty())
1169 .collect();
1170 if !questions.is_empty() {
1171 content.push_str("\nThe research questions addressed are:\n\n");
1172 for question in questions {
1173 content.push_str(&format!("- {question}\n"));
1174 }
1175 }
1176
1177 Ok(content)
1178 }
1179
1180 fn generate_methodology(&self, experiments: &[Experiment]) -> Result<String> {
1181 let mut content = String::new();
1182 content.push_str("We evaluate the following optimization algorithms:\n\n");
1183
1184 for experiment in experiments {
1185 for optimizer_name in experiment.optimizer_configs.keys() {
1186 content.push_str(&format!("- {}\n", optimizer_name));
1187 }
1188 }
1189
1190 Ok(content)
1191 }
1192
1193 fn generate_experiments_section(&self, experiments: &[Experiment]) -> Result<String> {
1194 let mut content = String::new();
1195 content.push_str("We conducted the following experiments:\n\n");
1196
1197 for experiment in experiments {
1198 content.push_str(&format!(
1199 "**{}**: {}\n\n",
1200 experiment.name, experiment.description
1201 ));
1202 }
1203
1204 Ok(content)
1205 }
1206
1207 fn generate_results(&self, experiments: &[Experiment]) -> Result<String> {
1208 let mut content = String::new();
1209 content.push_str("The experimental results are summarized below:\n\n");
1210
1211 for experiment in experiments {
1212 if !experiment.results.is_empty() {
1213 content.push_str(&format!("### {}\n\n", experiment.name));
1214 content.push_str(&format!("Number of runs: {}\n\n", experiment.results.len()));
1215 }
1216 }
1217
1218 Ok(content)
1219 }
1220
1221 fn generate_conclusion(&self, experiments: &[Experiment]) -> Result<String> {
1222 let mut content = String::from(
1223 "This section summarizes the key findings and implications of the experimental results.",
1224 );
1225
1226 let total_runs: usize = experiments.iter().map(|e| e.results.len()).sum();
1227 if total_runs > 0 {
1228 let successful_runs = experiments
1229 .iter()
1230 .flat_map(|e| e.results.iter())
1231 .filter(|r| r.status == RunStatus::Success)
1232 .count();
1233 let optimizer_names: std::collections::BTreeSet<&str> = experiments
1234 .iter()
1235 .flat_map(|e| e.optimizer_configs.keys())
1236 .map(String::as_str)
1237 .collect();
1238
1239 content.push_str(&format!(
1240 "\n\nAcross {} experiment(s) and {} run(s), {} completed successfully ({:.1}%).",
1241 experiments.len(),
1242 total_runs,
1243 successful_runs,
1244 100.0 * successful_runs as f64 / total_runs as f64
1245 ));
1246 if !optimizer_names.is_empty() {
1247 content.push_str(&format!(
1248 " Optimizers evaluated: {}.",
1249 optimizer_names.into_iter().collect::<Vec<_>>().join(", ")
1250 ));
1251 }
1252 }
1253
1254 Ok(content)
1255 }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260 use super::*;
1261 use crate::research::experiments::{ExperimentResult, ResourceUsage, TrainingHistory};
1262
1263 #[test]
1264 fn test_publication_creation() {
1265 let publication = Publication::new("Test Publication")
1266 .abstracttext("Test abstract")
1267 .publication_type(PublicationType::ConferencePaper)
1268 .keywords(vec![
1269 "optimization".to_string(),
1270 "machine learning".to_string(),
1271 ]);
1272
1273 assert_eq!(publication.title, "Test Publication");
1274 assert_eq!(publication.abstracttext, "Test abstract");
1275 assert_eq!(
1276 publication.publication_type,
1277 PublicationType::ConferencePaper
1278 );
1279 assert_eq!(publication.keywords.len(), 2);
1280 }
1281
1282 #[test]
1283 fn test_bibliography() {
1284 let mut bibliography = Bibliography::new();
1285
1286 let entry = BibTeXEntry {
1287 key: "test2023".to_string(),
1288 entry_type: "article".to_string(),
1289 fields: {
1290 let mut fields = HashMap::new();
1291 fields.insert("author".to_string(), "Test Author".to_string());
1292 fields.insert("title".to_string(), "Test Title".to_string());
1293 fields.insert("year".to_string(), "2023".to_string());
1294 fields
1295 },
1296 };
1297
1298 bibliography.add_entry(entry);
1299 assert_eq!(bibliography.entries.len(), 1);
1300 assert!(bibliography.entries.contains_key("test2023"));
1301 }
1302
1303 #[test]
1304 fn test_markdown_generation() {
1305 let mut publication = Publication::new("Test Paper");
1306 publication.abstracttext = "This is a test abstract.".to_string();
1307 publication.keywords = vec!["test".to_string(), "paper".to_string()];
1308
1309 let markdown = publication.generate_markdown().expect("unwrap failed");
1310 assert!(markdown.contains("# Test Paper"));
1311 assert!(markdown.contains("## Abstract"));
1312 assert!(markdown.contains("This is a test abstract."));
1313 assert!(markdown.contains("**Keywords**: test, paper"));
1314 }
1315
1316 #[test]
1321 fn test_bibliography_parse_bibtex_handles_multiline_values() {
1322 let mut bibliography = Bibliography::new();
1323 let bibtex =
1324 "@article{multi2024,\n title = {Spans\nmultiple\nlines},\n year = {2024},\n}\n";
1325
1326 bibliography
1327 .parse_bibtex(bibtex)
1328 .expect("parse should succeed");
1329
1330 assert_eq!(bibliography.entries.len(), 1);
1331 let entry = bibliography
1332 .entries
1333 .get("multi2024")
1334 .expect("entry should be present");
1335 assert_eq!(entry.entry_type, "article");
1336 assert_eq!(
1337 entry.fields.get("title").map(String::as_str),
1338 Some("Spans multiple lines")
1339 );
1340 assert_eq!(entry.fields.get("year").map(String::as_str), Some("2024"));
1341 }
1342
1343 fn minimal_template(section_types: Vec<SectionType>) -> PublicationTemplate {
1344 PublicationTemplate {
1345 name: "Test Template".to_string(),
1346 sections: section_types
1347 .into_iter()
1348 .map(|section_type| SectionTemplate {
1349 section_type,
1350 template: String::new(),
1351 required_fields: Vec::new(),
1352 target_word_count: None,
1353 })
1354 .collect(),
1355 formatting: FormattingOptions {
1356 format: DocumentFormat::Markdown,
1357 font_size: 12,
1358 line_spacing: 1.0,
1359 margins: Margins {
1360 top: 2.5,
1361 bottom: 2.5,
1362 left: 2.5,
1363 right: 2.5,
1364 },
1365 citation_format: CitationFormat::Numbered,
1366 figure_numbering: NumberingStyle::Arabic,
1367 table_numbering: NumberingStyle::Arabic,
1368 },
1369 venue_constraints: VenueConstraints {
1370 max_word_count: None,
1371 max_page_count: None,
1372 required_sections: Vec::new(),
1373 forbidden_sections: Vec::new(),
1374 max_figures: None,
1375 max_tables: None,
1376 max_references: None,
1377 },
1378 }
1379 }
1380
1381 #[test]
1386 fn test_generate_from_experiments_sets_real_order_and_word_count() {
1387 let generator = PublicationGenerator::new(PathBuf::from("."));
1388 let template = minimal_template(vec![
1389 SectionType::Introduction,
1390 SectionType::Methodology,
1391 SectionType::Conclusion,
1392 ]);
1393
1394 let mut experiment = Experiment::new("Adam vs SGD");
1395 experiment.hypothesis = "Adam converges faster than SGD on this benchmark".to_string();
1396
1397 let publication = generator
1398 .generate_from_experiments(&[experiment], &template)
1399 .expect("generation should succeed");
1400
1401 assert_eq!(publication.sections.len(), 3);
1402 let orders: Vec<usize> = publication.sections.iter().map(|s| s.order).collect();
1403 assert_eq!(orders, vec![0, 1, 2], "sections should keep template order");
1404
1405 for section in &publication.sections {
1406 let expected_word_count = section.content.split_whitespace().count();
1407 assert_eq!(section.word_count, expected_word_count);
1408 assert!(
1409 expected_word_count > 0,
1410 "generated section content should be non-empty"
1411 );
1412 }
1413
1414 let introduction = &publication.sections[0];
1418 assert_eq!(introduction.section_type, SectionType::Introduction);
1419 assert!(
1420 introduction
1421 .content
1422 .contains("Adam converges faster than SGD on this benchmark"),
1423 "introduction should quote the real hypothesis: {:?}",
1424 introduction.content
1425 );
1426 }
1427
1428 #[test]
1429 fn test_generate_conclusion_reports_real_success_rate() {
1430 let generator = PublicationGenerator::new(PathBuf::from("."));
1431 let mut experiment = Experiment::new("Conclusion Test");
1432 experiment.optimizer_configs.insert(
1433 "adam".to_string(),
1434 crate::unified_api::OptimizerConfig::default(),
1435 );
1436 experiment.results.push(ExperimentResult {
1437 run_id: "run-1".to_string(),
1438 optimizer_name: "adam".to_string(),
1439 start_time: Utc::now(),
1440 end_time: None,
1441 status: RunStatus::Success,
1442 final_metrics: HashMap::new(),
1443 training_history: TrainingHistory {
1444 epochs: vec![],
1445 train_metrics: HashMap::new(),
1446 val_metrics: HashMap::new(),
1447 learning_rates: vec![],
1448 gradient_norms: vec![],
1449 parameter_norms: vec![],
1450 step_times: vec![],
1451 },
1452 resource_usage: ResourceUsage::default(),
1453 error_info: None,
1454 metadata: HashMap::new(),
1455 });
1456
1457 let conclusion = generator
1458 .generate_conclusion(std::slice::from_ref(&experiment))
1459 .expect("conclusion generation should succeed");
1460
1461 assert!(conclusion.contains("1 run(s)"));
1462 assert!(conclusion.contains("100.0%"));
1463 assert!(conclusion.contains("adam"));
1464 }
1465}