use crate::error::OptimError;
use crate::error::Result;
use crate::research::experiments::{Experiment, RunStatus};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Publication {
pub id: String,
pub title: String,
pub abstracttext: String,
pub authors: Vec<Author>,
pub publication_type: PublicationType,
pub venue: Option<Venue>,
pub status: PublicationStatus,
pub keywords: Vec<String>,
pub sections: Vec<ManuscriptSection>,
pub bibliography: Bibliography,
pub experiment_ids: Vec<String>,
pub submission_history: Vec<SubmissionRecord>,
pub reviews: Vec<Review>,
pub metadata: PublicationMetadata,
pub created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Author {
pub name: String,
pub email: String,
pub affiliations: Vec<Affiliation>,
pub orcid: Option<String>,
pub position: AuthorPosition,
pub contributions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Affiliation {
pub institution: String,
pub department: Option<String>,
pub address: String,
pub country: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuthorPosition {
First,
Corresponding,
Senior,
EqualContribution,
Regular,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum PublicationType {
ConferencePaper,
JournalArticle,
WorkshopPaper,
TechnicalReport,
Preprint,
Thesis,
BookChapter,
Patent,
SoftwarePaper,
DatasetPaper,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Venue {
pub name: String,
pub venue_type: VenueType,
pub abbreviation: Option<String>,
pub publisher: Option<String>,
pub impact_factor: Option<f64>,
pub h_index: Option<u32>,
pub acceptance_rate: Option<f64>,
pub ranking: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum VenueType {
Conference,
Journal,
Workshop,
Symposium,
PreprintServer,
Repository,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PublicationStatus {
Draft,
ReadyForSubmission,
Submitted,
UnderReview,
RevisionRequested,
Accepted,
Published,
Rejected,
Withdrawn,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManuscriptSection {
pub title: String,
pub content: String,
pub order: usize,
pub section_type: SectionType,
pub word_count: usize,
pub figures: Vec<Figure>,
pub tables: Vec<Table>,
pub references: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SectionType {
Abstract,
Introduction,
RelatedWork,
Methodology,
Experiments,
Results,
Discussion,
Conclusion,
Acknowledgments,
References,
Appendix,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Figure {
pub caption: String,
pub file_path: PathBuf,
pub figure_type: FigureType,
pub number: usize,
pub width: Option<f64>,
pub height: Option<f64>,
pub experiment_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum FigureType {
Plot,
Diagram,
Flowchart,
Architecture,
Screenshot,
Photo,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Table {
pub caption: String,
pub data: Vec<Vec<String>>,
pub headers: Vec<String>,
pub number: usize,
pub experiment_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bibliography {
pub entries: HashMap<String, BibTeXEntry>,
pub citation_style: CitationStyle,
pub file_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BibTeXEntry {
pub key: String,
pub entry_type: String,
pub fields: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum CitationStyle {
APA,
IEEE,
ACM,
Nature,
Science,
Chicago,
MLA,
Harvard,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmissionRecord {
pub submitted_at: DateTime<Utc>,
pub venue: Venue,
pub submission_id: Option<String>,
pub status: SubmissionStatus,
pub decision_date: Option<DateTime<Utc>>,
pub decision: Option<Decision>,
pub editor_comments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SubmissionStatus {
Submitted,
UnderReview,
Decided,
Withdrawn,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Decision {
Accept,
AcceptMinorRevisions,
AcceptMajorRevisions,
RejectAndResubmit,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Review {
pub id: String,
pub reviewer: ReviewerInfo,
pub overall_score: Option<f64>,
pub confidence_score: Option<f64>,
pub detailed_scores: HashMap<String, f64>,
pub reviewtext: String,
pub strengths: Vec<String>,
pub weaknesses: Vec<String>,
pub questions: Vec<String>,
pub recommendation: ReviewRecommendation,
pub submitted_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewerInfo {
pub anonymous_id: String,
pub expertise_level: ExpertiseLevel,
pub research_areas: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExpertiseLevel {
Expert,
Knowledgeable,
SomeKnowledge,
LimitedKnowledge,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReviewRecommendation {
StrongAccept,
Accept,
WeakAccept,
Borderline,
WeakReject,
Reject,
StrongReject,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PublicationMetadata {
pub doi: Option<String>,
pub arxiv_id: Option<String>,
pub pages: Option<String>,
pub volume: Option<String>,
pub issue: Option<String>,
pub year: Option<u32>,
pub month: Option<u32>,
pub isbn_issn: Option<String>,
pub license: Option<String>,
pub open_access: bool,
}
#[derive(Debug)]
pub struct PublicationGenerator {
templates: HashMap<PublicationType, PublicationTemplate>,
default_citation_style: CitationStyle,
output_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicationTemplate {
pub name: String,
pub sections: Vec<SectionTemplate>,
pub formatting: FormattingOptions,
pub venue_constraints: VenueConstraints,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SectionTemplate {
pub section_type: SectionType,
pub template: String,
pub required_fields: Vec<String>,
pub target_word_count: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormattingOptions {
pub format: DocumentFormat,
pub font_size: u32,
pub line_spacing: f64,
pub margins: Margins,
pub citation_format: CitationFormat,
pub figure_numbering: NumberingStyle,
pub table_numbering: NumberingStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DocumentFormat {
LaTeX,
Markdown,
HTML,
Word,
PDF,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Margins {
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CitationFormat {
Numbered,
AuthorYear,
Superscript,
Footnote,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum NumberingStyle {
Arabic,
Roman,
Letters,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VenueConstraints {
pub max_word_count: Option<usize>,
pub max_page_count: Option<usize>,
pub required_sections: Vec<SectionType>,
pub forbidden_sections: Vec<SectionType>,
pub max_figures: Option<usize>,
pub max_tables: Option<usize>,
pub max_references: Option<usize>,
}
impl Publication {
pub fn new(title: &str) -> Self {
let now = Utc::now();
Self {
id: uuid::Uuid::new_v4().to_string(),
title: title.to_string(),
abstracttext: String::new(),
authors: Vec::new(),
publication_type: PublicationType::ConferencePaper,
venue: None,
status: PublicationStatus::Draft,
keywords: Vec::new(),
sections: Vec::new(),
bibliography: Bibliography::new(),
experiment_ids: Vec::new(),
submission_history: Vec::new(),
reviews: Vec::new(),
metadata: PublicationMetadata::default(),
created_at: now,
modified_at: now,
}
}
pub fn abstracttext(mut self, abstracttext: &str) -> Self {
self.abstracttext = abstracttext.to_string();
self.modified_at = Utc::now();
self
}
pub fn add_author(mut self, author: Author) -> Self {
self.authors.push(author);
self.modified_at = Utc::now();
self
}
pub fn publication_type(mut self, pubtype: PublicationType) -> Self {
self.publication_type = pubtype;
self.modified_at = Utc::now();
self
}
pub fn venue(mut self, venue: Venue) -> Self {
self.venue = Some(venue);
self.modified_at = Utc::now();
self
}
pub fn keywords(mut self, keywords: Vec<String>) -> Self {
self.keywords = keywords;
self.modified_at = Utc::now();
self
}
pub fn add_experiment(&mut self, experiment_id: &str) {
self.experiment_ids.push(experiment_id.to_string());
self.modified_at = Utc::now();
}
pub fn add_section(&mut self, section: ManuscriptSection) {
self.sections.push(section);
self.modified_at = Utc::now();
}
pub fn generate_latex(&self) -> Result<String> {
let mut latex = String::new();
latex.push_str("\\documentclass[conference]{IEEEtran}\n");
latex.push_str("\\usepackage{graphicx}\n");
latex.push_str("\\usepackage{booktabs}\n");
latex.push_str("\\usepackage{amsmath}\n");
latex.push_str("\\usepackage{url}\n\n");
latex.push_str("\\begin{document}\n\n");
latex.push_str(&format!("\\title{{{}}}\n\n", self.title));
latex.push_str("\\author{\n");
for (i, author) in self.authors.iter().enumerate() {
if i > 0 {
latex.push_str("\\and\n");
}
latex.push_str(&format!("\\IEEEauthorblockN{{{}}}\n", author.name));
if !author.affiliations.is_empty() {
latex.push_str(&format!(
"\\IEEEauthorblockA{{{}}}\n",
author.affiliations[0].institution
));
}
}
latex.push_str("}\n\n");
latex.push_str("\\maketitle\n\n");
if !self.abstracttext.is_empty() {
latex.push_str("\\begin{abstract}\n");
latex.push_str(&self.abstracttext);
latex.push_str("\n\\end{abstract}\n\n");
}
if !self.keywords.is_empty() {
latex.push_str("\\begin{IEEEkeywords}\n");
latex.push_str(&self.keywords.join(", "));
latex.push_str("\n\\end{IEEEkeywords}\n\n");
}
let mut sorted_sections = self.sections.clone();
sorted_sections.sort_by_key(|s| s.order);
for section in sorted_sections {
match section.section_type {
SectionType::Abstract => continue, SectionType::References => continue, _ => {
latex.push_str(&format!("\\section{{{}}}\n", section.title));
latex.push_str(§ion.content);
latex.push_str("\n\n");
}
}
}
if !self.bibliography.entries.is_empty() {
latex.push_str("\\begin{thebibliography}{99}\n");
for entry in self.bibliography.entries.values() {
latex.push_str(&self.format_bibtex_entry_latex(entry));
}
latex.push_str("\\end{thebibliography}\n\n");
}
latex.push_str("\\end{document}\n");
Ok(latex)
}
fn format_bibtex_entry_latex(&self, entry: &BibTeXEntry) -> String {
format!(
"\\bibitem{{{}}}\n{}\n\n",
entry.key,
self.format_bibtex_fields(&entry.fields)
)
}
fn format_bibtex_fields(&self, fields: &HashMap<String, String>) -> String {
let mut result = String::new();
if let Some(author) = fields.get("author") {
result.push_str(author);
}
if let Some(title) = fields.get("title") {
result.push_str(&format!(", ``{}'', ", title));
}
if let Some(journal) = fields.get("journal") {
result.push_str(&format!("\\emph{{{}}}, ", journal));
} else if let Some(booktitle) = fields.get("booktitle") {
result.push_str(&format!("in \\emph{{{}}}, ", booktitle));
}
if let Some(year) = fields.get("year") {
result.push_str(year);
}
result
}
pub fn generate_markdown(&self) -> Result<String> {
let mut markdown = String::new();
markdown.push_str(&format!("# {}\n\n", self.title));
if !self.authors.is_empty() {
markdown.push_str("**Authors**: ");
let author_names: Vec<String> = self.authors.iter().map(|a| a.name.clone()).collect();
markdown.push_str(&author_names.join(", "));
markdown.push_str("\n\n");
}
if !self.abstracttext.is_empty() {
markdown.push_str("## Abstract\n\n");
markdown.push_str(&self.abstracttext);
markdown.push_str("\n\n");
}
if !self.keywords.is_empty() {
markdown.push_str("**Keywords**: ");
markdown.push_str(&self.keywords.join(", "));
markdown.push_str("\n\n");
}
let mut sorted_sections = self.sections.clone();
sorted_sections.sort_by_key(|s| s.order);
for section in sorted_sections {
match section.section_type {
SectionType::Abstract => continue, _ => {
markdown.push_str(&format!("## {}\n\n", section.title));
markdown.push_str(§ion.content);
markdown.push_str("\n\n");
}
}
}
if !self.bibliography.entries.is_empty() {
markdown.push_str("## References\n\n");
for (i, entry) in self.bibliography.entries.values().enumerate() {
markdown.push_str(&format!(
"{}. {}\n",
i + 1,
self.format_bibtex_entry_markdown(entry)
));
}
}
Ok(markdown)
}
fn format_bibtex_entry_markdown(&self, entry: &BibTeXEntry) -> String {
let mut result = String::new();
if let Some(author) = entry.fields.get("author") {
result.push_str(author);
}
if let Some(title) = entry.fields.get("title") {
result.push_str(&format!(". \"{}\". ", title));
}
if let Some(journal) = entry.fields.get("journal") {
result.push_str(&format!("*{}*. ", journal));
} else if let Some(booktitle) = entry.fields.get("booktitle") {
result.push_str(&format!("In *{}*. ", booktitle));
}
if let Some(year) = entry.fields.get("year") {
result.push_str(year);
}
result
}
pub fn submission_statistics(&self) -> SubmissionStatistics {
let total_submissions = self.submission_history.len();
let accepted = self
.submission_history
.iter()
.filter(|s| {
matches!(
s.decision,
Some(Decision::Accept)
| Some(Decision::AcceptMinorRevisions)
| Some(Decision::AcceptMajorRevisions)
)
})
.count();
let rejected = self
.submission_history
.iter()
.filter(|s| matches!(s.decision, Some(Decision::Reject)))
.count();
let avg_review_time = if !self.submission_history.is_empty() {
let total_days: i64 = self
.submission_history
.iter()
.filter_map(|s| {
s.decision_date
.map(|decision_date| (decision_date - s.submitted_at).num_days())
})
.sum();
total_days as f64 / self.submission_history.len() as f64
} else {
0.0
};
SubmissionStatistics {
total_submissions,
accepted,
rejected,
pending: total_submissions - accepted - rejected,
acceptance_rate: if total_submissions > 0 {
accepted as f64 / total_submissions as f64
} else {
0.0
},
avg_review_time_days: avg_review_time,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmissionStatistics {
pub total_submissions: usize,
pub accepted: usize,
pub rejected: usize,
pub pending: usize,
pub acceptance_rate: f64,
pub avg_review_time_days: f64,
}
impl Default for Bibliography {
fn default() -> Self {
Self::new()
}
}
impl Bibliography {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
citation_style: CitationStyle::IEEE,
file_path: None,
}
}
pub fn add_entry(&mut self, entry: BibTeXEntry) {
self.entries.insert(entry.key.clone(), entry);
}
pub fn load_bibtex_file(&mut self, filepath: &PathBuf) -> Result<()> {
let content = std::fs::read_to_string(filepath)?;
self.parse_bibtex(&content)?;
self.file_path = Some(filepath.clone());
Ok(())
}
pub fn parse_bibtex(&mut self, content: &str) -> Result<()> {
for (entry_type, key, fields) in crate::research::citations::parse_bibtex_entries(content) {
let entry = BibTeXEntry {
key: key.clone(),
entry_type,
fields,
};
self.entries.insert(key, entry);
}
Ok(())
}
}
impl PublicationGenerator {
pub fn new(output_dir: PathBuf) -> Self {
Self {
templates: HashMap::new(),
default_citation_style: CitationStyle::IEEE,
output_dir,
}
}
pub fn output_dir(&self) -> &Path {
&self.output_dir
}
pub fn default_citation_style(&self) -> &CitationStyle {
&self.default_citation_style
}
pub fn set_default_citation_style(&mut self, style: CitationStyle) {
self.default_citation_style = style;
}
pub fn register_template(
&mut self,
publication_type: PublicationType,
template: PublicationTemplate,
) {
self.templates.insert(publication_type, template);
}
pub fn template(&self, publication_type: &PublicationType) -> Option<&PublicationTemplate> {
self.templates.get(publication_type)
}
pub fn generate_registered(
&self,
experiments: &[Experiment],
publication_type: PublicationType,
) -> Result<Publication> {
let template = self.templates.get(&publication_type).ok_or_else(|| {
OptimError::InvalidConfig(format!(
"no template is registered for {publication_type:?}; call register_template first"
))
})?;
let mut publication = self.generate_from_experiments(experiments, template)?;
publication.publication_type = publication_type;
Ok(publication)
}
pub fn write_markdown(&self, publication: &Publication) -> Result<PathBuf> {
std::fs::create_dir_all(&self.output_dir)?;
let file_name = publication
.title
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect::<String>();
let path = self.output_dir.join(format!("{file_name}.md"));
std::fs::write(&path, publication.generate_markdown()?)?;
Ok(path)
}
pub fn generate_from_experiments(
&self,
experiments: &[Experiment],
template: &PublicationTemplate,
) -> Result<Publication> {
let mut publication = Publication::new("Generated Publication");
let abstracttext = self.generate_abstract(experiments)?;
publication.abstracttext = abstracttext;
for (index, section_template) in template.sections.iter().enumerate() {
let mut section = self.generate_section(section_template, experiments)?;
section.order = index;
section.word_count = section.content.split_whitespace().count();
publication.add_section(section);
}
Ok(publication)
}
fn generate_abstract(&self, experiments: &[Experiment]) -> Result<String> {
let mut abstracttext = String::new();
abstracttext.push_str(
"This paper presents experimental results comparing various optimization algorithms. ",
);
if !experiments.is_empty() {
abstracttext.push_str(&format!(
"We conducted {} experiments evaluating the performance of different optimizers. ",
experiments.len()
));
}
abstracttext.push_str("Our results demonstrate significant differences in convergence behavior and final performance across different optimization methods.");
Ok(abstracttext)
}
fn generate_section(
&self,
template: &SectionTemplate,
experiments: &[Experiment],
) -> Result<ManuscriptSection> {
let content = match template.section_type {
SectionType::Introduction => self.generate_introduction(experiments)?,
SectionType::Methodology => self.generate_methodology(experiments)?,
SectionType::Experiments => self.generate_experiments_section(experiments)?,
SectionType::Results => self.generate_results(experiments)?,
SectionType::Conclusion => self.generate_conclusion(experiments)?,
_ => template.template.clone(),
};
Ok(ManuscriptSection {
title: match template.section_type {
SectionType::Abstract => "Abstract".to_string(),
SectionType::Introduction => "Introduction".to_string(),
SectionType::RelatedWork => "Related Work".to_string(),
SectionType::Methodology => "Methodology".to_string(),
SectionType::Experiments => "Experiments".to_string(),
SectionType::Results => "Results".to_string(),
SectionType::Discussion => "Discussion".to_string(),
SectionType::Conclusion => "Conclusion".to_string(),
SectionType::Acknowledgments => "Acknowledgments".to_string(),
SectionType::References => "References".to_string(),
SectionType::Appendix => "Appendix".to_string(),
SectionType::Custom(ref name) => name.clone(),
},
content,
order: 0,
section_type: template.section_type.clone(),
word_count: 0,
figures: Vec::new(),
tables: Vec::new(),
references: Vec::new(),
})
}
fn generate_introduction(&self, experiments: &[Experiment]) -> Result<String> {
let mut content = String::from(
"This section introduces the research problem and motivation for comparing optimization algorithms.",
);
let hypotheses: Vec<&str> = experiments
.iter()
.map(|e| e.hypothesis.as_str())
.filter(|h| !h.is_empty())
.collect();
if !hypotheses.is_empty() {
content.push_str("\n\nThis work investigates the following hypotheses:\n\n");
for hypothesis in hypotheses {
content.push_str(&format!("- {hypothesis}\n"));
}
}
let questions: Vec<&str> = experiments
.iter()
.map(|e| e.metadata.research_question.as_str())
.filter(|q| !q.is_empty())
.collect();
if !questions.is_empty() {
content.push_str("\nThe research questions addressed are:\n\n");
for question in questions {
content.push_str(&format!("- {question}\n"));
}
}
Ok(content)
}
fn generate_methodology(&self, experiments: &[Experiment]) -> Result<String> {
let mut content = String::new();
content.push_str("We evaluate the following optimization algorithms:\n\n");
for experiment in experiments {
for optimizer_name in experiment.optimizer_configs.keys() {
content.push_str(&format!("- {}\n", optimizer_name));
}
}
Ok(content)
}
fn generate_experiments_section(&self, experiments: &[Experiment]) -> Result<String> {
let mut content = String::new();
content.push_str("We conducted the following experiments:\n\n");
for experiment in experiments {
content.push_str(&format!(
"**{}**: {}\n\n",
experiment.name, experiment.description
));
}
Ok(content)
}
fn generate_results(&self, experiments: &[Experiment]) -> Result<String> {
let mut content = String::new();
content.push_str("The experimental results are summarized below:\n\n");
for experiment in experiments {
if !experiment.results.is_empty() {
content.push_str(&format!("### {}\n\n", experiment.name));
content.push_str(&format!("Number of runs: {}\n\n", experiment.results.len()));
}
}
Ok(content)
}
fn generate_conclusion(&self, experiments: &[Experiment]) -> Result<String> {
let mut content = String::from(
"This section summarizes the key findings and implications of the experimental results.",
);
let total_runs: usize = experiments.iter().map(|e| e.results.len()).sum();
if total_runs > 0 {
let successful_runs = experiments
.iter()
.flat_map(|e| e.results.iter())
.filter(|r| r.status == RunStatus::Success)
.count();
let optimizer_names: std::collections::BTreeSet<&str> = experiments
.iter()
.flat_map(|e| e.optimizer_configs.keys())
.map(String::as_str)
.collect();
content.push_str(&format!(
"\n\nAcross {} experiment(s) and {} run(s), {} completed successfully ({:.1}%).",
experiments.len(),
total_runs,
successful_runs,
100.0 * successful_runs as f64 / total_runs as f64
));
if !optimizer_names.is_empty() {
content.push_str(&format!(
" Optimizers evaluated: {}.",
optimizer_names.into_iter().collect::<Vec<_>>().join(", ")
));
}
}
Ok(content)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::research::experiments::{ExperimentResult, ResourceUsage, TrainingHistory};
#[test]
fn test_publication_creation() {
let publication = Publication::new("Test Publication")
.abstracttext("Test abstract")
.publication_type(PublicationType::ConferencePaper)
.keywords(vec![
"optimization".to_string(),
"machine learning".to_string(),
]);
assert_eq!(publication.title, "Test Publication");
assert_eq!(publication.abstracttext, "Test abstract");
assert_eq!(
publication.publication_type,
PublicationType::ConferencePaper
);
assert_eq!(publication.keywords.len(), 2);
}
#[test]
fn test_bibliography() {
let mut bibliography = Bibliography::new();
let entry = BibTeXEntry {
key: "test2023".to_string(),
entry_type: "article".to_string(),
fields: {
let mut fields = HashMap::new();
fields.insert("author".to_string(), "Test Author".to_string());
fields.insert("title".to_string(), "Test Title".to_string());
fields.insert("year".to_string(), "2023".to_string());
fields
},
};
bibliography.add_entry(entry);
assert_eq!(bibliography.entries.len(), 1);
assert!(bibliography.entries.contains_key("test2023"));
}
#[test]
fn test_markdown_generation() {
let mut publication = Publication::new("Test Paper");
publication.abstracttext = "This is a test abstract.".to_string();
publication.keywords = vec!["test".to_string(), "paper".to_string()];
let markdown = publication.generate_markdown().expect("unwrap failed");
assert!(markdown.contains("# Test Paper"));
assert!(markdown.contains("## Abstract"));
assert!(markdown.contains("This is a test abstract."));
assert!(markdown.contains("**Keywords**: test, paper"));
}
#[test]
fn test_bibliography_parse_bibtex_handles_multiline_values() {
let mut bibliography = Bibliography::new();
let bibtex =
"@article{multi2024,\n title = {Spans\nmultiple\nlines},\n year = {2024},\n}\n";
bibliography
.parse_bibtex(bibtex)
.expect("parse should succeed");
assert_eq!(bibliography.entries.len(), 1);
let entry = bibliography
.entries
.get("multi2024")
.expect("entry should be present");
assert_eq!(entry.entry_type, "article");
assert_eq!(
entry.fields.get("title").map(String::as_str),
Some("Spans multiple lines")
);
assert_eq!(entry.fields.get("year").map(String::as_str), Some("2024"));
}
fn minimal_template(section_types: Vec<SectionType>) -> PublicationTemplate {
PublicationTemplate {
name: "Test Template".to_string(),
sections: section_types
.into_iter()
.map(|section_type| SectionTemplate {
section_type,
template: String::new(),
required_fields: Vec::new(),
target_word_count: None,
})
.collect(),
formatting: FormattingOptions {
format: DocumentFormat::Markdown,
font_size: 12,
line_spacing: 1.0,
margins: Margins {
top: 2.5,
bottom: 2.5,
left: 2.5,
right: 2.5,
},
citation_format: CitationFormat::Numbered,
figure_numbering: NumberingStyle::Arabic,
table_numbering: NumberingStyle::Arabic,
},
venue_constraints: VenueConstraints {
max_word_count: None,
max_page_count: None,
required_sections: Vec::new(),
forbidden_sections: Vec::new(),
max_figures: None,
max_tables: None,
max_references: None,
},
}
}
#[test]
fn test_generate_from_experiments_sets_real_order_and_word_count() {
let generator = PublicationGenerator::new(PathBuf::from("."));
let template = minimal_template(vec![
SectionType::Introduction,
SectionType::Methodology,
SectionType::Conclusion,
]);
let mut experiment = Experiment::new("Adam vs SGD");
experiment.hypothesis = "Adam converges faster than SGD on this benchmark".to_string();
let publication = generator
.generate_from_experiments(&[experiment], &template)
.expect("generation should succeed");
assert_eq!(publication.sections.len(), 3);
let orders: Vec<usize> = publication.sections.iter().map(|s| s.order).collect();
assert_eq!(orders, vec![0, 1, 2], "sections should keep template order");
for section in &publication.sections {
let expected_word_count = section.content.split_whitespace().count();
assert_eq!(section.word_count, expected_word_count);
assert!(
expected_word_count > 0,
"generated section content should be non-empty"
);
}
let introduction = &publication.sections[0];
assert_eq!(introduction.section_type, SectionType::Introduction);
assert!(
introduction
.content
.contains("Adam converges faster than SGD on this benchmark"),
"introduction should quote the real hypothesis: {:?}",
introduction.content
);
}
#[test]
fn test_generate_conclusion_reports_real_success_rate() {
let generator = PublicationGenerator::new(PathBuf::from("."));
let mut experiment = Experiment::new("Conclusion Test");
experiment.optimizer_configs.insert(
"adam".to_string(),
crate::unified_api::OptimizerConfig::default(),
);
experiment.results.push(ExperimentResult {
run_id: "run-1".to_string(),
optimizer_name: "adam".to_string(),
start_time: Utc::now(),
end_time: None,
status: RunStatus::Success,
final_metrics: HashMap::new(),
training_history: TrainingHistory {
epochs: vec![],
train_metrics: HashMap::new(),
val_metrics: HashMap::new(),
learning_rates: vec![],
gradient_norms: vec![],
parameter_norms: vec![],
step_times: vec![],
},
resource_usage: ResourceUsage::default(),
error_info: None,
metadata: HashMap::new(),
});
let conclusion = generator
.generate_conclusion(std::slice::from_ref(&experiment))
.expect("conclusion generation should succeed");
assert!(conclusion.contains("1 run(s)"));
assert!(conclusion.contains("100.0%"));
assert!(conclusion.contains("adam"));
}
}