Skip to main content

optirs_core/research/
publications.rs

1// Publication generation and management for academic research
2//
3// This module provides tools for generating academic publications from experimental
4// results, managing bibliographies, and formatting papers for various venues.
5
6use 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/// Academic publication representation
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Publication {
17    /// Publication identifier
18    pub id: String,
19    /// Publication title
20    pub title: String,
21    /// Publication abstract
22    pub abstracttext: String,
23    /// Authors
24    pub authors: Vec<Author>,
25    /// Publication type
26    pub publication_type: PublicationType,
27    /// Venue information
28    pub venue: Option<Venue>,
29    /// Publication status
30    pub status: PublicationStatus,
31    /// Keywords
32    pub keywords: Vec<String>,
33    /// Manuscript sections
34    pub sections: Vec<ManuscriptSection>,
35    /// Bibliography
36    pub bibliography: Bibliography,
37    /// Associated experiments
38    pub experiment_ids: Vec<String>,
39    /// Submission history
40    pub submission_history: Vec<SubmissionRecord>,
41    /// Review information
42    pub reviews: Vec<Review>,
43    /// Publication metadata
44    pub metadata: PublicationMetadata,
45    /// Creation timestamp
46    pub created_at: DateTime<Utc>,
47    /// Last modified timestamp
48    pub modified_at: DateTime<Utc>,
49}
50
51/// Author information
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Author {
54    /// Full name
55    pub name: String,
56    /// Email address
57    pub email: String,
58    /// Affiliations
59    pub affiliations: Vec<Affiliation>,
60    /// ORCID identifier
61    pub orcid: Option<String>,
62    /// Author position (first, corresponding, etc.)
63    pub position: AuthorPosition,
64    /// Contribution description
65    pub contributions: Vec<String>,
66}
67
68/// Author affiliation
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Affiliation {
71    /// Institution name
72    pub institution: String,
73    /// Department
74    pub department: Option<String>,
75    /// Address
76    pub address: String,
77    /// Country
78    pub country: String,
79}
80
81/// Author position/role
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub enum AuthorPosition {
84    /// First author
85    First,
86    /// Corresponding author
87    Corresponding,
88    /// Senior author
89    Senior,
90    /// Equal contribution
91    EqualContribution,
92    /// Regular author
93    Regular,
94}
95
96/// Publication types
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
98pub enum PublicationType {
99    /// Conference paper
100    ConferencePaper,
101    /// Journal article
102    JournalArticle,
103    /// Workshop paper
104    WorkshopPaper,
105    /// Technical report
106    TechnicalReport,
107    /// Preprint
108    Preprint,
109    /// Thesis
110    Thesis,
111    /// Book chapter
112    BookChapter,
113    /// Patent
114    Patent,
115    /// Software paper
116    SoftwarePaper,
117    /// Dataset paper
118    DatasetPaper,
119}
120
121/// Publication venue
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Venue {
124    /// Venue name
125    pub name: String,
126    /// Venue type
127    pub venue_type: VenueType,
128    /// Abbreviation
129    pub abbreviation: Option<String>,
130    /// Publisher
131    pub publisher: Option<String>,
132    /// Impact factor
133    pub impact_factor: Option<f64>,
134    /// H-index
135    pub h_index: Option<u32>,
136    /// Acceptance rate
137    pub acceptance_rate: Option<f64>,
138    /// Ranking (A*, A, B, C)
139    pub ranking: Option<String>,
140    /// Venue URL
141    pub url: Option<String>,
142}
143
144/// Venue types
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146pub enum VenueType {
147    /// Academic conference
148    Conference,
149    /// Academic journal
150    Journal,
151    /// Workshop
152    Workshop,
153    /// Symposium
154    Symposium,
155    /// Preprint server
156    PreprintServer,
157    /// Repository
158    Repository,
159}
160
161/// Publication status
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub enum PublicationStatus {
164    /// Draft in preparation
165    Draft,
166    /// Ready for submission
167    ReadyForSubmission,
168    /// Submitted
169    Submitted,
170    /// Under review
171    UnderReview,
172    /// Revision requested
173    RevisionRequested,
174    /// Accepted
175    Accepted,
176    /// Published
177    Published,
178    /// Rejected
179    Rejected,
180    /// Withdrawn
181    Withdrawn,
182}
183
184/// Manuscript section
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ManuscriptSection {
187    /// Section title
188    pub title: String,
189    /// Section content
190    pub content: String,
191    /// Section order
192    pub order: usize,
193    /// Section type
194    pub section_type: SectionType,
195    /// Word count
196    pub word_count: usize,
197    /// Figures and tables
198    pub figures: Vec<Figure>,
199    pub tables: Vec<Table>,
200    /// References in this section
201    pub references: Vec<String>,
202}
203
204/// Section types
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206pub enum SectionType {
207    /// Abstract
208    Abstract,
209    /// Introduction
210    Introduction,
211    /// Background/Related Work
212    RelatedWork,
213    /// Methodology
214    Methodology,
215    /// Experiments
216    Experiments,
217    /// Results
218    Results,
219    /// Discussion
220    Discussion,
221    /// Conclusion
222    Conclusion,
223    /// Acknowledgments
224    Acknowledgments,
225    /// References
226    References,
227    /// Appendix
228    Appendix,
229    /// Custom section
230    Custom(String),
231}
232
233/// Figure information
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct Figure {
236    /// Figure caption
237    pub caption: String,
238    /// Figure file path
239    pub file_path: PathBuf,
240    /// Figure type
241    pub figure_type: FigureType,
242    /// Figure number
243    pub number: usize,
244    /// Width (in publication units)
245    pub width: Option<f64>,
246    /// Height (in publication units)
247    pub height: Option<f64>,
248    /// Associated experiment ID
249    pub experiment_id: Option<String>,
250}
251
252/// Figure types
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
254pub enum FigureType {
255    /// Plot/graph
256    Plot,
257    /// Diagram
258    Diagram,
259    /// Algorithm flowchart
260    Flowchart,
261    /// Architecture diagram
262    Architecture,
263    /// Screenshot
264    Screenshot,
265    /// Photo
266    Photo,
267    /// Other
268    Other,
269}
270
271/// Table information
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct Table {
274    /// Table caption
275    pub caption: String,
276    /// Table data
277    pub data: Vec<Vec<String>>,
278    /// Column headers
279    pub headers: Vec<String>,
280    /// Table number
281    pub number: usize,
282    /// Associated experiment ID
283    pub experiment_id: Option<String>,
284}
285
286/// Bibliography management
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct Bibliography {
289    /// BibTeX entries
290    pub entries: HashMap<String, BibTeXEntry>,
291    /// Citation style
292    pub citation_style: CitationStyle,
293    /// Bibliography file path
294    pub file_path: Option<PathBuf>,
295}
296
297/// BibTeX entry
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct BibTeXEntry {
300    /// Entry key
301    pub key: String,
302    /// Entry type (article, inproceedings, etc.)
303    pub entry_type: String,
304    /// Fields (title, author, year, etc.)
305    pub fields: HashMap<String, String>,
306}
307
308/// Citation styles
309#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
310pub enum CitationStyle {
311    /// APA style
312    APA,
313    /// IEEE style
314    IEEE,
315    /// ACM style
316    ACM,
317    /// Nature style
318    Nature,
319    /// Science style
320    Science,
321    /// Chicago style
322    Chicago,
323    /// MLA style
324    MLA,
325    /// Harvard style
326    Harvard,
327    /// Custom style
328    Custom(String),
329}
330
331/// Submission record
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct SubmissionRecord {
334    /// Submission timestamp
335    pub submitted_at: DateTime<Utc>,
336    /// Venue submitted to
337    pub venue: Venue,
338    /// Submission ID
339    pub submission_id: Option<String>,
340    /// Submission status
341    pub status: SubmissionStatus,
342    /// Decision date
343    pub decision_date: Option<DateTime<Utc>>,
344    /// Decision outcome
345    pub decision: Option<Decision>,
346    /// Comments from editors
347    pub editor_comments: Option<String>,
348}
349
350/// Submission status
351#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
352pub enum SubmissionStatus {
353    /// Submitted
354    Submitted,
355    /// Under review
356    UnderReview,
357    /// Decision made
358    Decided,
359    /// Withdrawn
360    Withdrawn,
361}
362
363/// Review decision
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
365pub enum Decision {
366    /// Accept
367    Accept,
368    /// Accept with minor revisions
369    AcceptMinorRevisions,
370    /// Accept with major revisions
371    AcceptMajorRevisions,
372    /// Reject and resubmit
373    RejectAndResubmit,
374    /// Reject
375    Reject,
376}
377
378/// Review information
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct Review {
381    /// Review ID
382    pub id: String,
383    /// Reviewer information (anonymous)
384    pub reviewer: ReviewerInfo,
385    /// Overall score
386    pub overall_score: Option<f64>,
387    /// Confidence score
388    pub confidence_score: Option<f64>,
389    /// Detailed scores
390    pub detailed_scores: HashMap<String, f64>,
391    /// Written review
392    pub reviewtext: String,
393    /// Strengths
394    pub strengths: Vec<String>,
395    /// Weaknesses
396    pub weaknesses: Vec<String>,
397    /// Questions for authors
398    pub questions: Vec<String>,
399    /// Recommendation
400    pub recommendation: ReviewRecommendation,
401    /// Review timestamp
402    pub submitted_at: DateTime<Utc>,
403}
404
405/// Reviewer information (anonymized)
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ReviewerInfo {
408    /// Anonymous reviewer ID
409    pub anonymous_id: String,
410    /// Expertise level
411    pub expertise_level: ExpertiseLevel,
412    /// Research areas
413    pub research_areas: Vec<String>,
414}
415
416/// Expertise levels
417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
418pub enum ExpertiseLevel {
419    /// Expert in the field
420    Expert,
421    /// Knowledgeable
422    Knowledgeable,
423    /// Some knowledge
424    SomeKnowledge,
425    /// Limited knowledge
426    LimitedKnowledge,
427}
428
429/// Review recommendation
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
431pub enum ReviewRecommendation {
432    /// Strong accept
433    StrongAccept,
434    /// Accept
435    Accept,
436    /// Weak accept
437    WeakAccept,
438    /// Borderline
439    Borderline,
440    /// Weak reject
441    WeakReject,
442    /// Reject
443    Reject,
444    /// Strong reject
445    StrongReject,
446}
447
448/// Publication metadata
449#[derive(Debug, Clone, Serialize, Deserialize, Default)]
450pub struct PublicationMetadata {
451    /// DOI
452    pub doi: Option<String>,
453    /// ArXiv ID
454    pub arxiv_id: Option<String>,
455    /// Page numbers
456    pub pages: Option<String>,
457    /// Volume
458    pub volume: Option<String>,
459    /// Issue/Number
460    pub issue: Option<String>,
461    /// Publication year
462    pub year: Option<u32>,
463    /// Publication month
464    pub month: Option<u32>,
465    /// ISBN/ISSN
466    pub isbn_issn: Option<String>,
467    /// License
468    pub license: Option<String>,
469    /// Open access status
470    pub open_access: bool,
471}
472
473/// Publication generator for creating publications from experiments
474#[derive(Debug)]
475pub struct PublicationGenerator {
476    /// Template repository
477    templates: HashMap<PublicationType, PublicationTemplate>,
478    /// Default citation style
479    default_citation_style: CitationStyle,
480    /// Output directory
481    output_dir: PathBuf,
482}
483
484/// Publication template
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct PublicationTemplate {
487    /// Template name
488    pub name: String,
489    /// Template sections
490    pub sections: Vec<SectionTemplate>,
491    /// Default formatting options
492    pub formatting: FormattingOptions,
493    /// Target venue constraints
494    pub venue_constraints: VenueConstraints,
495}
496
497/// Section template
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct SectionTemplate {
500    /// Section type
501    pub section_type: SectionType,
502    /// Template content
503    pub template: String,
504    /// Required fields
505    pub required_fields: Vec<String>,
506    /// Word count target
507    pub target_word_count: Option<usize>,
508}
509
510/// Formatting options
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct FormattingOptions {
513    /// Document format
514    pub format: DocumentFormat,
515    /// Font size
516    pub font_size: u32,
517    /// Line spacing
518    pub line_spacing: f64,
519    /// Margins (in cm)
520    pub margins: Margins,
521    /// Citation format
522    pub citation_format: CitationFormat,
523    /// Figure numbering
524    pub figure_numbering: NumberingStyle,
525    /// Table numbering
526    pub table_numbering: NumberingStyle,
527}
528
529/// Document formats
530#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
531pub enum DocumentFormat {
532    /// LaTeX
533    LaTeX,
534    /// Markdown
535    Markdown,
536    /// HTML
537    HTML,
538    /// Microsoft Word
539    Word,
540    /// PDF
541    PDF,
542}
543
544/// Page margins
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct Margins {
547    /// Top margin
548    pub top: f64,
549    /// Bottom margin
550    pub bottom: f64,
551    /// Left margin
552    pub left: f64,
553    /// Right margin
554    pub right: f64,
555}
556
557/// Citation format
558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
559pub enum CitationFormat {
560    /// Numbered citations \[1\]
561    Numbered,
562    /// Author-year citations (Author, 2023)
563    AuthorYear,
564    /// Superscript citations¹
565    Superscript,
566    /// Footnote citations
567    Footnote,
568}
569
570/// Numbering styles
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
572pub enum NumberingStyle {
573    /// Arabic numerals (1, 2, 3)
574    Arabic,
575    /// Roman numerals (I, II, III)
576    Roman,
577    /// Letters (a, b, c)
578    Letters,
579    /// No numbering
580    None,
581}
582
583/// Venue constraints
584#[derive(Debug, Clone, Serialize, Deserialize)]
585pub struct VenueConstraints {
586    /// Maximum word count
587    pub max_word_count: Option<usize>,
588    /// Maximum page count
589    pub max_page_count: Option<usize>,
590    /// Required sections
591    pub required_sections: Vec<SectionType>,
592    /// Forbidden sections
593    pub forbidden_sections: Vec<SectionType>,
594    /// Figure limits
595    pub max_figures: Option<usize>,
596    /// Table limits
597    pub max_tables: Option<usize>,
598    /// Reference limits
599    pub max_references: Option<usize>,
600}
601
602impl Publication {
603    /// Create a new publication
604    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    /// Set publication abstract
627    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    /// Add an author
634    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    /// Set publication type
641    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    /// Set target venue
648    pub fn venue(mut self, venue: Venue) -> Self {
649        self.venue = Some(venue);
650        self.modified_at = Utc::now();
651        self
652    }
653
654    /// Add keywords
655    pub fn keywords(mut self, keywords: Vec<String>) -> Self {
656        self.keywords = keywords;
657        self.modified_at = Utc::now();
658        self
659    }
660
661    /// Associate with experiment
662    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    /// Add a manuscript section
668    pub fn add_section(&mut self, section: ManuscriptSection) {
669        self.sections.push(section);
670        self.modified_at = Utc::now();
671    }
672
673    /// Generate LaTeX document
674    pub fn generate_latex(&self) -> Result<String> {
675        let mut latex = String::new();
676
677        // Document class and packages
678        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        // Title and authors
687        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        // Abstract
707        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        // Keywords
714        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        // Sections
721        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,   // Already handled
727                SectionType::References => continue, // Handle at end
728                _ => {
729                    latex.push_str(&format!("\\section{{{}}}\n", section.title));
730                    latex.push_str(&section.content);
731                    latex.push_str("\n\n");
732                }
733            }
734        }
735
736        // Bibliography
737        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    /// Generate markdown document
783    pub fn generate_markdown(&self) -> Result<String> {
784        let mut markdown = String::new();
785
786        // Title
787        markdown.push_str(&format!("# {}\n\n", self.title));
788
789        // Authors
790        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        // Abstract
798        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        // Keywords
805        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        // Sections
812        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, // Already handled
818                _ => {
819                    markdown.push_str(&format!("## {}\n\n", section.title));
820                    markdown.push_str(&section.content);
821                    markdown.push_str("\n\n");
822                }
823            }
824        }
825
826        // References
827        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    /// Generate submission statistics
866    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/// Submission statistics
916#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct SubmissionStatistics {
918    /// Total number of submissions
919    pub total_submissions: usize,
920    /// Number of accepted submissions
921    pub accepted: usize,
922    /// Number of rejected submissions
923    pub rejected: usize,
924    /// Number of pending submissions
925    pub pending: usize,
926    /// Acceptance rate (0.0 to 1.0)
927    pub acceptance_rate: f64,
928    /// Average review time in days
929    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    /// Create a new bibliography
940    pub fn new() -> Self {
941        Self {
942            entries: HashMap::new(),
943            citation_style: CitationStyle::IEEE,
944            file_path: None,
945        }
946    }
947
948    /// Add a BibTeX entry
949    pub fn add_entry(&mut self, entry: BibTeXEntry) {
950        self.entries.insert(entry.key.clone(), entry);
951    }
952
953    /// Load from BibTeX file
954    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    /// Parse BibTeX content.
962    ///
963    /// Delegates to `crate::research::citations::parse_bibtex_entries`, a
964    /// brace-depth-aware tokenizer that (unlike a line-oriented scanner)
965    /// correctly captures field values spanning multiple physical lines and
966    /// values containing nested braces.
967    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    /// Create a new publication generator writing into `output_dir`.
983    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    /// The directory generated manuscripts are written to.
992    pub fn output_dir(&self) -> &Path {
993        &self.output_dir
994    }
995
996    /// The citation style applied when a template does not name one.
997    pub fn default_citation_style(&self) -> &CitationStyle {
998        &self.default_citation_style
999    }
1000
1001    /// Set the fallback citation style.
1002    pub fn set_default_citation_style(&mut self, style: CitationStyle) {
1003        self.default_citation_style = style;
1004    }
1005
1006    /// Register a reusable template under `publication_type`.
1007    ///
1008    /// Until 0.3.2 `templates` was an empty map nothing could populate and
1009    /// nothing read: [`Self::generate_from_experiments`] required the caller to
1010    /// hand in a template every time, so the repository the field documents did
1011    /// not exist.
1012    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    /// The template registered for `publication_type`, if any.
1021    pub fn template(&self, publication_type: &PublicationType) -> Option<&PublicationTemplate> {
1022        self.templates.get(publication_type)
1023    }
1024
1025    /// Generate a publication using the registered template for
1026    /// `publication_type`.
1027    ///
1028    /// # Errors
1029    ///
1030    /// [`OptimError::InvalidConfig`] when no template has been registered for
1031    /// that publication type.
1032    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    /// Render a publication to Markdown under [`Self::output_dir`] and return
1048    /// the path written.
1049    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    /// Generate publication from experiments
1062    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        // Generate abstract from experiments
1070        let abstracttext = self.generate_abstract(experiments)?;
1071        publication.abstracttext = abstracttext;
1072
1073        // Generate sections, assigning each its real position in the
1074        // template (used for ordering in `generate_latex`/`generate_markdown`)
1075        // and its real word count.
1076        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        // Generate abstract based on experiments
1088        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`/`word_count` are filled in by the caller
1137            // (`generate_from_experiments`), which knows this section's real
1138            // position in the template and can see the finished content.
1139            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    // Regression test for F77: `Bibliography::parse_bibtex` duplicated the
1317    // same line-oriented parser as `BibTeXProcessor::parse_bibtex` (and the
1318    // same multiline-truncation bug). It now delegates to the shared
1319    // brace-depth-aware tokenizer in `research::citations`.
1320    #[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    // Regression test for F79: generated sections always had `order: 0` and
1382    // `word_count: 0` ("will be set"/"will be calculated" comments that were
1383    // never followed through), so every section tied for first place and
1384    // reported zero length regardless of actual content.
1385    #[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        // Regression for the "canned prose" half of F79: the introduction
1415        // must incorporate the experiment's actual hypothesis, not just a
1416        // fixed sentence identical for every publication.
1417        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}