Skip to main content

optirs_core/research/
conferences.rs

1// Academic conference integration and submission tools
2//
3// This module provides tools for managing conference submissions,
4// tracking deadlines, and preparing submission materials.
5
6use crate::error::{OptimError, Result};
7use chrono::{DateTime, Datelike, TimeZone, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Conference database and submission manager
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ConferenceManager {
14    /// Known conferences
15    pub conferences: HashMap<String, Conference>,
16    /// Submission tracking
17    pub submissions: Vec<Submission>,
18    /// Deadline alerts
19    pub alerts: Vec<DeadlineAlert>,
20}
21
22/// Academic conference information
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Conference {
25    /// Conference identifier
26    pub id: String,
27    /// Conference name
28    pub name: String,
29    /// Conference abbreviation
30    pub abbreviation: String,
31    /// Conference description
32    pub description: String,
33    /// Conference URL
34    pub url: String,
35    /// Conference ranking/tier
36    pub ranking: ConferenceRanking,
37    /// Research areas
38    pub research_areas: Vec<String>,
39    /// Annual occurrence
40    pub annual: bool,
41    /// Conference series information
42    pub series_info: SeriesInfo,
43    /// Important dates
44    pub dates: ConferenceDates,
45    /// Submission requirements
46    pub requirements: SubmissionRequirements,
47    /// Review process
48    pub review_process: ReviewProcess,
49}
50
51/// Conference ranking/tier
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub enum ConferenceRanking {
54    /// Top-tier (A*)
55    TopTier,
56    /// High-quality (A)
57    HighQuality,
58    /// Good (B)
59    Good,
60    /// Acceptable (C)
61    Acceptable,
62    /// Emerging
63    Emerging,
64    /// Workshop
65    Workshop,
66    /// Unranked
67    Unranked,
68}
69
70/// Conference series information
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct SeriesInfo {
73    /// Series number (e.g., 35th)
74    pub series_number: u32,
75    /// Year
76    pub year: u32,
77    /// Location
78    pub location: String,
79    /// Country
80    pub country: String,
81    /// Conference format
82    pub format: ConferenceFormat,
83}
84
85/// Conference format
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87pub enum ConferenceFormat {
88    /// In-person conference
89    InPerson,
90    /// Virtual conference
91    Virtual,
92    /// Hybrid conference
93    Hybrid,
94}
95
96/// Important conference dates
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ConferenceDates {
99    /// Abstract submission deadline
100    pub abstract_deadline: Option<DateTime<Utc>>,
101    /// Paper submission deadline
102    pub paper_deadline: DateTime<Utc>,
103    /// Notification date
104    pub notification_date: DateTime<Utc>,
105    /// Camera-ready deadline
106    pub camera_ready_deadline: DateTime<Utc>,
107    /// Conference start date
108    pub conference_start: DateTime<Utc>,
109    /// Conference end date
110    pub conference_end: DateTime<Utc>,
111}
112
113/// Submission requirements
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SubmissionRequirements {
116    /// Page limit
117    pub page_limit: u32,
118    /// Word limit
119    pub word_limit: Option<u32>,
120    /// Format requirements
121    pub format: FormatRequirements,
122    /// Required sections
123    pub required_sections: Vec<String>,
124    /// Supplementary material allowed
125    pub supplementary_allowed: bool,
126    /// Anonymous submission required
127    pub anonymous_submission: bool,
128    /// Double-blind review
129    pub double_blind: bool,
130}
131
132/// Format requirements
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct FormatRequirements {
135    /// Document template
136    pub template: String,
137    /// Font size
138    pub font_size: u32,
139    /// Line spacing
140    pub line_spacing: f64,
141    /// Margins
142    pub margins: String,
143    /// Citation style
144    pub citation_style: String,
145    /// File format
146    pub file_format: Vec<String>,
147}
148
149/// Review process information
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ReviewProcess {
152    /// Number of reviewers per paper
153    pub reviewers_per_paper: u32,
154    /// Review criteria
155    pub review_criteria: Vec<String>,
156    /// Rebuttal allowed
157    pub rebuttal_allowed: bool,
158    /// Acceptance rate (if known)
159    pub acceptance_rate: Option<f64>,
160    /// Review format
161    pub review_format: ReviewFormat,
162}
163
164/// Review format
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166pub enum ReviewFormat {
167    /// Numerical scores
168    NumericalScores,
169    /// Written reviews only
170    WrittenOnly,
171    /// Mixed format
172    Mixed,
173}
174
175/// Conference submission
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct Submission {
178    /// Submission ID
179    pub id: String,
180    /// Conference ID
181    pub conference_id: String,
182    /// Paper/publication ID
183    pub paper_id: String,
184    /// Submission status
185    pub status: SubmissionStatus,
186    /// Submission date
187    pub submitted_at: DateTime<Utc>,
188    /// Track/category
189    pub track: Option<String>,
190    /// Submission materials
191    pub materials: SubmissionMaterials,
192    /// Review information
193    pub reviews: Vec<Review>,
194    /// Decision information
195    pub decision: Option<Decision>,
196}
197
198/// Submission status
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200pub enum SubmissionStatus {
201    /// Draft
202    Draft,
203    /// Submitted
204    Submitted,
205    /// Under review
206    UnderReview,
207    /// Rebuttal period
208    Rebuttal,
209    /// Decision made
210    Decided,
211    /// Camera-ready submitted
212    CameraReady,
213    /// Withdrawn
214    Withdrawn,
215}
216
217/// Submission materials
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SubmissionMaterials {
220    /// Main paper file
221    pub paper_file: String,
222    /// Supplementary materials
223    pub supplementary_files: Vec<String>,
224    /// Abstract
225    pub abstracttext: String,
226    /// Keywords
227    pub keywords: Vec<String>,
228    /// Author information (if not anonymous)
229    pub authors: Option<Vec<String>>,
230}
231
232/// Review information
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Review {
235    /// Review ID
236    pub id: String,
237    /// Reviewer (anonymous)
238    pub reviewer: String,
239    /// Overall score
240    pub overall_score: Option<f64>,
241    /// Detailed scores
242    pub detailed_scores: HashMap<String, f64>,
243    /// Written review
244    pub reviewtext: String,
245    /// Recommendation
246    pub recommendation: ReviewRecommendation,
247    /// Review date
248    pub reviewed_at: DateTime<Utc>,
249}
250
251/// Review recommendations
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253pub enum ReviewRecommendation {
254    /// Strong accept
255    StrongAccept,
256    /// Accept
257    Accept,
258    /// Weak accept
259    WeakAccept,
260    /// Borderline
261    Borderline,
262    /// Weak reject
263    WeakReject,
264    /// Reject
265    Reject,
266    /// Strong reject
267    StrongReject,
268}
269
270/// Conference decision
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Decision {
273    /// Decision outcome
274    pub outcome: DecisionOutcome,
275    /// Decision date
276    pub decided_at: DateTime<Utc>,
277    /// Editor comments
278    pub editor_comments: Option<String>,
279    /// Required revisions
280    pub required_revisions: Vec<String>,
281}
282
283/// Decision outcomes
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
285pub enum DecisionOutcome {
286    /// Accept
287    Accept,
288    /// Accept with minor revisions
289    AcceptMinorRevisions,
290    /// Accept with major revisions
291    AcceptMajorRevisions,
292    /// Conditional accept
293    ConditionalAccept,
294    /// Reject
295    Reject,
296    /// Reject and resubmit
297    RejectAndResubmit,
298}
299
300/// Deadline alert
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DeadlineAlert {
303    /// Alert ID
304    pub id: String,
305    /// Conference ID
306    pub conference_id: String,
307    /// Deadline type
308    pub deadline_type: DeadlineType,
309    /// Alert date
310    pub alert_date: DateTime<Utc>,
311    /// Days before deadline
312    pub days_before: u32,
313    /// Alert message
314    pub message: String,
315    /// Alert sent
316    pub sent: bool,
317}
318
319/// Types of deadlines
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321pub enum DeadlineType {
322    /// Abstract submission
323    AbstractSubmission,
324    /// Paper submission
325    PaperSubmission,
326    /// Notification
327    Notification,
328    /// Camera-ready
329    CameraReady,
330    /// Conference start
331    ConferenceStart,
332}
333
334impl Default for ConferenceManager {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340impl ConferenceManager {
341    /// Create a new conference manager
342    pub fn new() -> Self {
343        Self {
344            conferences: HashMap::new(),
345            submissions: Vec::new(),
346            alerts: Vec::new(),
347        }
348    }
349
350    /// Add a conference to the database
351    pub fn add_conference(&mut self, conference: Conference) {
352        self.conferences.insert(conference.id.clone(), conference);
353    }
354
355    /// Submit a paper to a conference
356    pub fn submit_paper(
357        &mut self,
358        conference_id: &str,
359        paper_id: &str,
360        materials: SubmissionMaterials,
361    ) -> Result<String> {
362        if !self.conferences.contains_key(conference_id) {
363            return Err(OptimError::InvalidConfig(format!(
364                "Conference '{}' not found",
365                conference_id
366            )));
367        }
368
369        let submission_id = uuid::Uuid::new_v4().to_string();
370        let submission = Submission {
371            id: submission_id.clone(),
372            conference_id: conference_id.to_string(),
373            paper_id: paper_id.to_string(),
374            status: SubmissionStatus::Submitted,
375            submitted_at: Utc::now(),
376            track: None,
377            materials,
378            reviews: Vec::new(),
379            decision: None,
380        };
381
382        self.submissions.push(submission);
383        Ok(submission_id)
384    }
385
386    /// Get upcoming deadlines
387    pub fn get_upcoming_deadlines(
388        &self,
389        days_ahead: u32,
390    ) -> Vec<(&Conference, DeadlineType, DateTime<Utc>)> {
391        let mut deadlines = Vec::new();
392        let now = Utc::now();
393        let future_limit = now + chrono::Duration::days(days_ahead as i64);
394
395        for conference in self.conferences.values() {
396            let dates = &conference.dates;
397
398            if let Some(abstract_deadline) = dates.abstract_deadline {
399                if abstract_deadline > now && abstract_deadline <= future_limit {
400                    deadlines.push((
401                        conference,
402                        DeadlineType::AbstractSubmission,
403                        abstract_deadline,
404                    ));
405                }
406            }
407
408            if dates.paper_deadline > now && dates.paper_deadline <= future_limit {
409                deadlines.push((
410                    conference,
411                    DeadlineType::PaperSubmission,
412                    dates.paper_deadline,
413                ));
414            }
415
416            if dates.notification_date > now && dates.notification_date <= future_limit {
417                deadlines.push((
418                    conference,
419                    DeadlineType::Notification,
420                    dates.notification_date,
421                ));
422            }
423
424            if dates.camera_ready_deadline > now && dates.camera_ready_deadline <= future_limit {
425                deadlines.push((
426                    conference,
427                    DeadlineType::CameraReady,
428                    dates.camera_ready_deadline,
429                ));
430            }
431
432            if dates.conference_start > now && dates.conference_start <= future_limit {
433                deadlines.push((
434                    conference,
435                    DeadlineType::ConferenceStart,
436                    dates.conference_start,
437                ));
438            }
439        }
440
441        // Sort by deadline date
442        deadlines.sort_by_key(|a| a.2);
443        deadlines
444    }
445
446    /// Scan all known conferences and create [`DeadlineAlert`]s for any
447    /// still-future deadline that falls within one of the `days_before`
448    /// thresholds (e.g. `&[30, 7, 1]` for month/week/day-out reminders),
449    /// appending them to `self.alerts`. Idempotent: calling this repeatedly
450    /// (e.g. once a day) will not create duplicate alerts for the same
451    /// (conference, deadline type, threshold) combination. Returns the
452    /// alerts newly created by this call.
453    pub fn generate_deadline_alerts(&mut self, days_before: &[u32]) -> Vec<DeadlineAlert> {
454        let now = Utc::now();
455
456        // Snapshot deadlines up front so we are not borrowing
457        // `self.conferences` while mutating `self.alerts` below.
458        let mut deadline_entries: Vec<(String, DeadlineType, DateTime<Utc>)> = Vec::new();
459        for conference in self.conferences.values() {
460            let dates = &conference.dates;
461            if let Some(abstract_deadline) = dates.abstract_deadline {
462                deadline_entries.push((
463                    conference.id.clone(),
464                    DeadlineType::AbstractSubmission,
465                    abstract_deadline,
466                ));
467            }
468            deadline_entries.push((
469                conference.id.clone(),
470                DeadlineType::PaperSubmission,
471                dates.paper_deadline,
472            ));
473            deadline_entries.push((
474                conference.id.clone(),
475                DeadlineType::Notification,
476                dates.notification_date,
477            ));
478            deadline_entries.push((
479                conference.id.clone(),
480                DeadlineType::CameraReady,
481                dates.camera_ready_deadline,
482            ));
483            deadline_entries.push((
484                conference.id.clone(),
485                DeadlineType::ConferenceStart,
486                dates.conference_start,
487            ));
488        }
489
490        let mut new_alerts = Vec::new();
491        for (conference_id, deadline_type, deadline) in deadline_entries {
492            if deadline <= now {
493                continue;
494            }
495            let days_remaining = (deadline - now).num_days().max(0) as u32;
496
497            for &threshold in days_before {
498                if days_remaining > threshold {
499                    continue;
500                }
501
502                let already_exists = self.alerts.iter().any(|alert| {
503                    alert.conference_id == conference_id
504                        && alert.deadline_type == deadline_type
505                        && alert.days_before == threshold
506                });
507                if already_exists {
508                    continue;
509                }
510
511                let alert = DeadlineAlert {
512                    id: uuid::Uuid::new_v4().to_string(),
513                    conference_id: conference_id.clone(),
514                    deadline_type: deadline_type.clone(),
515                    alert_date: now,
516                    days_before: threshold,
517                    message: format!(
518                        "{deadline_type:?} deadline for conference '{conference_id}' is in \
519                         {days_remaining} day(s) ({deadline})"
520                    ),
521                    sent: false,
522                };
523                self.alerts.push(alert.clone());
524                new_alerts.push(alert);
525            }
526        }
527
528        new_alerts
529    }
530
531    /// Alerts that have not yet been marked as sent, most recent first.
532    pub fn pending_alerts(&self) -> Vec<&DeadlineAlert> {
533        self.alerts.iter().filter(|alert| !alert.sent).collect()
534    }
535
536    /// Mark an alert as sent (e.g. after successfully notifying a user).
537    pub fn mark_alert_sent(&mut self, alert_id: &str) -> Result<()> {
538        let alert = self
539            .alerts
540            .iter_mut()
541            .find(|alert| alert.id == alert_id)
542            .ok_or_else(|| OptimError::InvalidConfig(format!("alert '{alert_id}' not found")))?;
543        alert.sent = true;
544        Ok(())
545    }
546
547    /// Search conferences by research area
548    pub fn search_conferences(&self, research_area: &str) -> Vec<&Conference> {
549        self.conferences
550            .values()
551            .filter(|conf| {
552                conf.research_areas
553                    .iter()
554                    .any(|area| area.to_lowercase().contains(&research_area.to_lowercase()))
555            })
556            .collect()
557    }
558
559    /// Get conferences by ranking
560    pub fn get_conferences_by_ranking(&self, ranking: ConferenceRanking) -> Vec<&Conference> {
561        self.conferences
562            .values()
563            .filter(|conf| conf.ranking == ranking)
564            .collect()
565    }
566
567    /// Create standard ML/AI conferences, using each conference's next
568    /// upcoming edition (by real submission deadline) rather than a fixed
569    /// calendar year, so [`Self::get_upcoming_deadlines`] can actually find
570    /// them no matter when this is called.
571    pub fn load_standard_conferences(&mut self) {
572        self.add_conference(Self::next_upcoming_edition(Self::create_neurips_conference));
573        self.add_conference(Self::next_upcoming_edition(Self::create_icml_conference));
574        self.add_conference(Self::next_upcoming_edition(Self::create_iclr_conference));
575        self.add_conference(Self::next_upcoming_edition(Self::create_aaai_conference));
576        self.add_conference(Self::next_upcoming_edition(Self::create_ijcai_conference));
577    }
578
579    /// Build successive editions of a conference (via `build`, which takes
580    /// the edition's label year, e.g. 2027 for "NeurIPS 2027") until one is
581    /// found whose paper submission deadline has not yet passed, and return
582    /// that edition.
583    ///
584    /// Conference templates below are seasonal (same month/day pattern every
585    /// year), so trying a handful of consecutive label years is always
586    /// sufficient; this makes the loaded deadlines self-correcting as real
587    /// time passes, instead of frozen at whatever year they were written in.
588    fn next_upcoming_edition(build: fn(i32) -> Conference) -> Conference {
589        let now = Utc::now();
590        let start_year = now.year();
591        let last_candidate = start_year + 3;
592        for candidate_year in start_year..=last_candidate {
593            let conference = build(candidate_year);
594            if conference.dates.paper_deadline > now {
595                return conference;
596            }
597        }
598        // Unreachable in practice for an annual conference (deadlines cannot
599        // trail 3+ years behind "now" for every candidate), but stay honest
600        // and return a well-formed, self-consistent edition rather than
601        // panicking.
602        build(last_candidate)
603    }
604
605    fn create_neurips_conference(edition_year: i32) -> Conference {
606        Conference {
607            id: format!("neurips{edition_year}"),
608            name: "Conference on Neural Information Processing Systems".to_string(),
609            abbreviation: "NeurIPS".to_string(),
610            description: "Premier conference on neural information processing systems".to_string(),
611            url: "https://neurips.cc/".to_string(),
612            ranking: ConferenceRanking::TopTier,
613            research_areas: vec![
614                "Machine Learning".to_string(),
615                "Deep Learning".to_string(),
616                "Neural Networks".to_string(),
617                "Optimization".to_string(),
618            ],
619            annual: true,
620            series_info: SeriesInfo {
621                series_number: 38,
622                year: edition_year as u32,
623                location: "Vancouver".to_string(),
624                country: "Canada".to_string(),
625                format: ConferenceFormat::Hybrid,
626            },
627            dates: ConferenceDates {
628                abstract_deadline: Some(
629                    chrono::Utc
630                        .with_ymd_and_hms(edition_year, 5, 15, 23, 59, 59)
631                        .single()
632                        .expect("invalid datetime"),
633                ),
634                paper_deadline: chrono::Utc
635                    .with_ymd_and_hms(edition_year, 5, 22, 23, 59, 59)
636                    .single()
637                    .expect("invalid datetime"),
638                notification_date: chrono::Utc
639                    .with_ymd_and_hms(edition_year, 9, 25, 12, 0, 0)
640                    .single()
641                    .expect("invalid datetime"),
642                camera_ready_deadline: chrono::Utc
643                    .with_ymd_and_hms(edition_year, 10, 30, 23, 59, 59)
644                    .single()
645                    .expect("invalid datetime"),
646                conference_start: chrono::Utc
647                    .with_ymd_and_hms(edition_year, 12, 10, 9, 0, 0)
648                    .single()
649                    .expect("invalid datetime"),
650                conference_end: chrono::Utc
651                    .with_ymd_and_hms(edition_year, 12, 16, 18, 0, 0)
652                    .single()
653                    .expect("invalid datetime"),
654            },
655            requirements: SubmissionRequirements {
656                page_limit: 9,
657                word_limit: None,
658                format: FormatRequirements {
659                    template: format!("NeurIPS {edition_year} LaTeX template"),
660                    font_size: 10,
661                    line_spacing: 1.0,
662                    margins: "1 inch".to_string(),
663                    citation_style: "NeurIPS".to_string(),
664                    file_format: vec!["PDF".to_string()],
665                },
666                required_sections: vec![
667                    "Abstract".to_string(),
668                    "Introduction".to_string(),
669                    "Related Work".to_string(),
670                    "Method".to_string(),
671                    "Experiments".to_string(),
672                    "Conclusion".to_string(),
673                ],
674                supplementary_allowed: true,
675                anonymous_submission: true,
676                double_blind: true,
677            },
678            review_process: ReviewProcess {
679                reviewers_per_paper: 3,
680                review_criteria: vec![
681                    "Technical Quality".to_string(),
682                    "Novelty".to_string(),
683                    "Significance".to_string(),
684                    "Clarity".to_string(),
685                ],
686                rebuttal_allowed: true,
687                acceptance_rate: Some(0.26), // Approximately 26%
688                review_format: ReviewFormat::Mixed,
689            },
690        }
691    }
692
693    fn create_icml_conference(edition_year: i32) -> Conference {
694        Conference {
695            id: format!("icml{edition_year}"),
696            name: "International Conference on Machine Learning".to_string(),
697            abbreviation: "ICML".to_string(),
698            description: "Premier international conference on machine learning".to_string(),
699            url: "https://icml.cc/".to_string(),
700            ranking: ConferenceRanking::TopTier,
701            research_areas: vec![
702                "Machine Learning".to_string(),
703                "Optimization".to_string(),
704                "Statistical Learning".to_string(),
705                "Deep Learning".to_string(),
706            ],
707            annual: true,
708            series_info: SeriesInfo {
709                series_number: 41,
710                year: edition_year as u32,
711                location: "Vienna".to_string(),
712                country: "Austria".to_string(),
713                format: ConferenceFormat::Hybrid,
714            },
715            dates: ConferenceDates {
716                abstract_deadline: None,
717                paper_deadline: chrono::Utc
718                    .with_ymd_and_hms(edition_year, 2, 1, 23, 59, 59)
719                    .single()
720                    .expect("invalid datetime"),
721                notification_date: chrono::Utc
722                    .with_ymd_and_hms(edition_year, 5, 1, 12, 0, 0)
723                    .single()
724                    .expect("invalid datetime"),
725                camera_ready_deadline: chrono::Utc
726                    .with_ymd_and_hms(edition_year, 6, 1, 23, 59, 59)
727                    .single()
728                    .expect("invalid datetime"),
729                conference_start: chrono::Utc
730                    .with_ymd_and_hms(edition_year, 7, 21, 9, 0, 0)
731                    .single()
732                    .expect("invalid datetime"),
733                conference_end: chrono::Utc
734                    .with_ymd_and_hms(edition_year, 7, 27, 18, 0, 0)
735                    .single()
736                    .expect("invalid datetime"),
737            },
738            requirements: SubmissionRequirements {
739                page_limit: 8,
740                word_limit: None,
741                format: FormatRequirements {
742                    template: format!("ICML {edition_year} LaTeX template"),
743                    font_size: 10,
744                    line_spacing: 1.0,
745                    margins: "1 inch".to_string(),
746                    citation_style: "ICML".to_string(),
747                    file_format: vec!["PDF".to_string()],
748                },
749                required_sections: vec![
750                    "Abstract".to_string(),
751                    "Introduction".to_string(),
752                    "Methods".to_string(),
753                    "Results".to_string(),
754                    "Conclusion".to_string(),
755                ],
756                supplementary_allowed: true,
757                anonymous_submission: true,
758                double_blind: true,
759            },
760            review_process: ReviewProcess {
761                reviewers_per_paper: 3,
762                review_criteria: vec![
763                    "Technical Quality".to_string(),
764                    "Clarity".to_string(),
765                    "Originality".to_string(),
766                    "Significance".to_string(),
767                ],
768                rebuttal_allowed: true,
769                acceptance_rate: Some(0.23), // Approximately 23%
770                review_format: ReviewFormat::Mixed,
771            },
772        }
773    }
774
775    fn create_iclr_conference(edition_year: i32) -> Conference {
776        let prior_year = edition_year - 1;
777        Conference {
778            id: format!("iclr{edition_year}"),
779            name: "International Conference on Learning Representations".to_string(),
780            abbreviation: "ICLR".to_string(),
781            description: "Conference focused on learning representations".to_string(),
782            url: "https://iclr.cc/".to_string(),
783            ranking: ConferenceRanking::TopTier,
784            research_areas: vec![
785                "Deep Learning".to_string(),
786                "Representation Learning".to_string(),
787                "Neural Networks".to_string(),
788                "Optimization".to_string(),
789            ],
790            annual: true,
791            series_info: SeriesInfo {
792                series_number: 12,
793                year: edition_year as u32,
794                location: "Vienna".to_string(),
795                country: "Austria".to_string(),
796                format: ConferenceFormat::Hybrid,
797            },
798            dates: ConferenceDates {
799                abstract_deadline: Some(
800                    chrono::Utc
801                        .with_ymd_and_hms(prior_year, 9, 28, 23, 59, 59)
802                        .single()
803                        .expect("invalid datetime"),
804                ),
805                paper_deadline: chrono::Utc
806                    .with_ymd_and_hms(prior_year, 10, 2, 23, 59, 59)
807                    .single()
808                    .expect("invalid datetime"),
809                notification_date: chrono::Utc
810                    .with_ymd_and_hms(edition_year, 1, 15, 12, 0, 0)
811                    .single()
812                    .expect("invalid datetime"),
813                // Feb 28 rather than 29: this is a synthetic template date
814                // (not a specific historical deadline), and `edition_year`
815                // varies at runtime, so it must stay valid on non-leap years.
816                camera_ready_deadline: chrono::Utc
817                    .with_ymd_and_hms(edition_year, 2, 28, 23, 59, 59)
818                    .single()
819                    .expect("invalid datetime"),
820                conference_start: chrono::Utc
821                    .with_ymd_and_hms(edition_year, 5, 7, 9, 0, 0)
822                    .single()
823                    .expect("invalid datetime"),
824                conference_end: chrono::Utc
825                    .with_ymd_and_hms(edition_year, 5, 11, 18, 0, 0)
826                    .single()
827                    .expect("invalid datetime"),
828            },
829            requirements: SubmissionRequirements {
830                page_limit: 9,
831                word_limit: None,
832                format: FormatRequirements {
833                    template: format!("ICLR {edition_year} LaTeX template"),
834                    font_size: 10,
835                    line_spacing: 1.0,
836                    margins: "1 inch".to_string(),
837                    citation_style: "ICLR".to_string(),
838                    file_format: vec!["PDF".to_string()],
839                },
840                required_sections: vec![
841                    "Abstract".to_string(),
842                    "Introduction".to_string(),
843                    "Related Work".to_string(),
844                    "Method".to_string(),
845                    "Experiments".to_string(),
846                    "Conclusion".to_string(),
847                ],
848                supplementary_allowed: true,
849                anonymous_submission: true,
850                double_blind: true,
851            },
852            review_process: ReviewProcess {
853                reviewers_per_paper: 3,
854                review_criteria: vec![
855                    "Technical Quality".to_string(),
856                    "Clarity".to_string(),
857                    "Originality".to_string(),
858                    "Significance".to_string(),
859                ],
860                rebuttal_allowed: true,
861                acceptance_rate: Some(0.31), // Approximately 31%
862                review_format: ReviewFormat::Mixed,
863            },
864        }
865    }
866
867    fn create_aaai_conference(edition_year: i32) -> Conference {
868        let prior_year = edition_year - 1;
869        Conference {
870            id: format!("aaai{edition_year}"),
871            name: "AAAI Conference on Artificial Intelligence".to_string(),
872            abbreviation: "AAAI".to_string(),
873            description: "Conference on artificial intelligence".to_string(),
874            url: "https://aaai.org/".to_string(),
875            ranking: ConferenceRanking::TopTier,
876            research_areas: vec![
877                "Artificial Intelligence".to_string(),
878                "Machine Learning".to_string(),
879                "Knowledge Representation".to_string(),
880                "Planning".to_string(),
881            ],
882            annual: true,
883            series_info: SeriesInfo {
884                series_number: 38,
885                year: edition_year as u32,
886                location: "Vancouver".to_string(),
887                country: "Canada".to_string(),
888                format: ConferenceFormat::Hybrid,
889            },
890            dates: ConferenceDates {
891                abstract_deadline: Some(
892                    chrono::Utc
893                        .with_ymd_and_hms(prior_year, 8, 15, 23, 59, 59)
894                        .single()
895                        .expect("invalid datetime"),
896                ),
897                paper_deadline: chrono::Utc
898                    .with_ymd_and_hms(prior_year, 8, 19, 23, 59, 59)
899                    .single()
900                    .expect("invalid datetime"),
901                notification_date: chrono::Utc
902                    .with_ymd_and_hms(prior_year, 12, 9, 12, 0, 0)
903                    .single()
904                    .expect("invalid datetime"),
905                camera_ready_deadline: chrono::Utc
906                    .with_ymd_and_hms(edition_year, 1, 15, 23, 59, 59)
907                    .single()
908                    .expect("invalid datetime"),
909                conference_start: chrono::Utc
910                    .with_ymd_and_hms(edition_year, 2, 20, 9, 0, 0)
911                    .single()
912                    .expect("invalid datetime"),
913                conference_end: chrono::Utc
914                    .with_ymd_and_hms(edition_year, 2, 27, 18, 0, 0)
915                    .single()
916                    .expect("invalid datetime"),
917            },
918            requirements: SubmissionRequirements {
919                page_limit: 7,
920                word_limit: None,
921                format: FormatRequirements {
922                    template: format!("AAAI {edition_year} LaTeX template"),
923                    font_size: 10,
924                    line_spacing: 1.0,
925                    margins: "0.75 inch".to_string(),
926                    citation_style: "AAAI".to_string(),
927                    file_format: vec!["PDF".to_string()],
928                },
929                required_sections: vec![
930                    "Abstract".to_string(),
931                    "Introduction".to_string(),
932                    "Related Work".to_string(),
933                    "Approach".to_string(),
934                    "Experiments".to_string(),
935                    "Conclusion".to_string(),
936                ],
937                supplementary_allowed: false,
938                anonymous_submission: true,
939                double_blind: true,
940            },
941            review_process: ReviewProcess {
942                reviewers_per_paper: 3,
943                review_criteria: vec![
944                    "Technical Quality".to_string(),
945                    "Novelty".to_string(),
946                    "Significance".to_string(),
947                    "Clarity".to_string(),
948                ],
949                rebuttal_allowed: false,
950                acceptance_rate: Some(0.23), // Approximately 23%
951                review_format: ReviewFormat::NumericalScores,
952            },
953        }
954    }
955
956    fn create_ijcai_conference(edition_year: i32) -> Conference {
957        Conference {
958            id: format!("ijcai{edition_year}"),
959            name: "International Joint Conference on Artificial Intelligence".to_string(),
960            abbreviation: "IJCAI".to_string(),
961            description: "International conference on artificial intelligence".to_string(),
962            url: "https://ijcai.org/".to_string(),
963            ranking: ConferenceRanking::TopTier,
964            research_areas: vec![
965                "Artificial Intelligence".to_string(),
966                "Machine Learning".to_string(),
967                "Automated Reasoning".to_string(),
968                "Multi-agent Systems".to_string(),
969            ],
970            annual: true,
971            series_info: SeriesInfo {
972                series_number: 33,
973                year: edition_year as u32,
974                location: "Jeju".to_string(),
975                country: "South Korea".to_string(),
976                format: ConferenceFormat::Hybrid,
977            },
978            dates: ConferenceDates {
979                abstract_deadline: Some(
980                    chrono::Utc
981                        .with_ymd_and_hms(edition_year, 1, 17, 23, 59, 59)
982                        .single()
983                        .expect("invalid datetime"),
984                ),
985                paper_deadline: chrono::Utc
986                    .with_ymd_and_hms(edition_year, 1, 24, 23, 59, 59)
987                    .single()
988                    .expect("invalid datetime"),
989                notification_date: chrono::Utc
990                    .with_ymd_and_hms(edition_year, 4, 16, 12, 0, 0)
991                    .single()
992                    .expect("invalid datetime"),
993                camera_ready_deadline: chrono::Utc
994                    .with_ymd_and_hms(edition_year, 5, 15, 23, 59, 59)
995                    .single()
996                    .expect("invalid datetime"),
997                conference_start: chrono::Utc
998                    .with_ymd_and_hms(edition_year, 8, 3, 9, 0, 0)
999                    .single()
1000                    .expect("invalid datetime"),
1001                conference_end: chrono::Utc
1002                    .with_ymd_and_hms(edition_year, 8, 9, 18, 0, 0)
1003                    .single()
1004                    .expect("invalid datetime"),
1005            },
1006            requirements: SubmissionRequirements {
1007                page_limit: 7,
1008                word_limit: None,
1009                format: FormatRequirements {
1010                    template: format!("IJCAI {edition_year} LaTeX template"),
1011                    font_size: 10,
1012                    line_spacing: 1.0,
1013                    margins: "0.75 inch".to_string(),
1014                    citation_style: "IJCAI".to_string(),
1015                    file_format: vec!["PDF".to_string()],
1016                },
1017                required_sections: vec![
1018                    "Abstract".to_string(),
1019                    "Introduction".to_string(),
1020                    "Background".to_string(),
1021                    "Approach".to_string(),
1022                    "Experiments".to_string(),
1023                    "Conclusion".to_string(),
1024                ],
1025                supplementary_allowed: false,
1026                anonymous_submission: true,
1027                double_blind: true,
1028            },
1029            review_process: ReviewProcess {
1030                reviewers_per_paper: 3,
1031                review_criteria: vec![
1032                    "Technical Quality".to_string(),
1033                    "Novelty".to_string(),
1034                    "Significance".to_string(),
1035                    "Clarity".to_string(),
1036                ],
1037                rebuttal_allowed: true,
1038                acceptance_rate: Some(0.15), // Approximately 15%
1039                review_format: ReviewFormat::Mixed,
1040            },
1041        }
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048
1049    #[test]
1050    fn test_conference_manager_creation() {
1051        let manager = ConferenceManager::new();
1052        assert!(manager.conferences.is_empty());
1053        assert!(manager.submissions.is_empty());
1054    }
1055
1056    #[test]
1057    fn test_load_standard_conferences() {
1058        let mut manager = ConferenceManager::new();
1059        manager.load_standard_conferences();
1060
1061        assert_eq!(manager.conferences.len(), 5);
1062        let abbreviations: Vec<&str> = manager
1063            .conferences
1064            .values()
1065            .map(|c| c.abbreviation.as_str())
1066            .collect();
1067        for expected in ["NeurIPS", "ICML", "ICLR", "AAAI", "IJCAI"] {
1068            assert!(
1069                abbreviations.contains(&expected),
1070                "missing {expected} in {abbreviations:?}"
1071            );
1072        }
1073    }
1074
1075    // Regression test for F78: the standard conference templates had every
1076    // deadline hardcoded to a fixed calendar year (2023/2024). Once that
1077    // year passed, `get_upcoming_deadlines` could never return anything for
1078    // them again. `load_standard_conferences` must now always select an
1079    // edition whose deadlines lie in the future, however far "now" is from
1080    // when this code was written.
1081    #[test]
1082    fn test_load_standard_conferences_deadlines_are_in_the_future() {
1083        let mut manager = ConferenceManager::new();
1084        manager.load_standard_conferences();
1085        let now = Utc::now();
1086
1087        for conference in manager.conferences.values() {
1088            assert!(
1089                conference.dates.paper_deadline > now,
1090                "{}'s paper deadline {} is not in the future (now = {now})",
1091                conference.abbreviation,
1092                conference.dates.paper_deadline
1093            );
1094        }
1095
1096        // With a wide enough horizon, every loaded conference must surface
1097        // at least one upcoming deadline -- this was unconditionally empty
1098        // before the fix.
1099        let upcoming = manager.get_upcoming_deadlines(400);
1100        assert!(
1101            !upcoming.is_empty(),
1102            "expected at least one upcoming deadline within 400 days"
1103        );
1104    }
1105
1106    // Regression test for F78: `alerts` was declared on `ConferenceManager`
1107    // but no code path ever wrote to it.
1108    #[test]
1109    fn test_generate_deadline_alerts_populates_alerts() {
1110        let mut manager = ConferenceManager::new();
1111        manager.load_standard_conferences();
1112        assert!(manager.alerts.is_empty());
1113
1114        // A very wide threshold guarantees at least the paper deadlines
1115        // (already asserted to be in the future) fall inside the window.
1116        let created = manager.generate_deadline_alerts(&[400]);
1117        assert!(!created.is_empty());
1118        assert_eq!(manager.alerts.len(), created.len());
1119        assert!(manager.alerts.iter().all(|a| !a.sent));
1120        assert_eq!(manager.pending_alerts().len(), manager.alerts.len());
1121
1122        // Idempotent: calling again with the same thresholds must not
1123        // duplicate alerts for the same (conference, type, threshold).
1124        let created_again = manager.generate_deadline_alerts(&[400]);
1125        assert!(created_again.is_empty());
1126        assert_eq!(manager.alerts.len(), created.len());
1127
1128        let alert_id = manager.alerts[0].id.clone();
1129        manager
1130            .mark_alert_sent(&alert_id)
1131            .expect("mark should succeed");
1132        assert!(
1133            manager
1134                .alerts
1135                .iter()
1136                .find(|a| a.id == alert_id)
1137                .unwrap()
1138                .sent
1139        );
1140        assert_eq!(manager.pending_alerts().len(), manager.alerts.len() - 1);
1141    }
1142
1143    #[test]
1144    fn test_search_conferences() {
1145        let mut manager = ConferenceManager::new();
1146        manager.load_standard_conferences();
1147
1148        let ml_conferences = manager.search_conferences("Machine Learning");
1149        assert!(!ml_conferences.is_empty());
1150
1151        let top_tier = manager.get_conferences_by_ranking(ConferenceRanking::TopTier);
1152        assert!(!top_tier.is_empty());
1153    }
1154}