1use crate::Vector;
7use anyhow::Result;
8use chrono::{DateTime, Utc};
9use scirs2_core::random::{Random, RngExt};
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12use std::sync::{Arc, RwLock};
13use tokio::task::JoinHandle;
14use tracing::{debug, info};
15
16pub struct ResearchNetworkAnalyzer {
18 author_embeddings: Arc<RwLock<HashMap<String, AuthorEmbedding>>>,
20 publication_embeddings: Arc<RwLock<HashMap<String, PublicationEmbedding>>>,
22 citation_network: Arc<RwLock<CitationNetwork>>,
24 collaboration_network: Arc<RwLock<CollaborationNetwork>>,
26 topic_models: Arc<RwLock<HashMap<String, TopicModel>>>,
28 author_profiles: Arc<RwLock<HashMap<String, AuthorProfile>>>,
35 publication_metadata: Arc<RwLock<HashMap<String, PublicationMetadataInput>>>,
42 config: ResearchNetworkConfig,
44 analysis_tasks: Vec<JoinHandle<()>>,
46}
47
48#[derive(Debug, Clone)]
51pub struct AuthorProfile {
52 pub name: String,
54 pub affiliations: Vec<String>,
56}
57
58#[derive(Debug, Clone)]
61pub struct PublicationMetadataInput {
62 pub title: String,
64 pub abstract_text: String,
66 pub authors: Vec<String>,
69 pub venue: String,
71 pub year: u32,
73 pub doi: Option<String>,
75}
76
77#[derive(Debug, Clone)]
79pub struct ResearchNetworkConfig {
80 pub max_authors: usize,
82 pub max_publications: usize,
84 pub citation_update_interval_hours: u64,
86 pub collaboration_analysis_interval_hours: u64,
88 pub impact_prediction_refresh_hours: u64,
90 pub enable_real_time_citation_tracking: bool,
92 pub min_citation_threshold: u32,
94 pub topic_config: TopicModelingConfig,
96 pub embedding_dimension: usize,
98}
99
100impl Default for ResearchNetworkConfig {
101 fn default() -> Self {
102 Self {
103 max_authors: 100_000,
104 max_publications: 1_000_000,
105 citation_update_interval_hours: 24,
106 collaboration_analysis_interval_hours: 12,
107 impact_prediction_refresh_hours: 48,
108 enable_real_time_citation_tracking: true,
109 min_citation_threshold: 5,
110 topic_config: TopicModelingConfig::default(),
111 embedding_dimension: 512,
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct TopicModelingConfig {
119 pub num_topics: usize,
121 pub min_word_freq: u32,
123 pub max_doc_freq_ratio: f64,
125 pub lda_iterations: u32,
127 pub coherence_threshold: f64,
129}
130
131impl Default for TopicModelingConfig {
132 fn default() -> Self {
133 Self {
134 num_topics: 50,
135 min_word_freq: 5,
136 max_doc_freq_ratio: 0.8,
137 lda_iterations: 1000,
138 coherence_threshold: 0.4,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct AuthorEmbedding {
146 pub author_id: String,
148 pub name: String,
150 pub affiliations: Vec<String>,
152 pub research_topics: Vec<String>,
154 pub h_index: f64,
156 pub citation_count: u64,
158 pub publication_count: u64,
160 pub embedding: Vector,
162 pub collaboration_score: f64,
164 pub impact_score: f64,
166 pub career_stage: CareerStage,
168 pub last_updated: DateTime<Utc>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct PublicationEmbedding {
175 pub publication_id: String,
177 pub title: String,
179 pub abstract_text: String,
181 pub authors: Vec<String>,
183 pub venue: String,
185 pub year: u32,
187 pub citation_count: u64,
189 pub topic_distribution: Vec<f64>,
191 pub embedding: Vector,
193 pub predicted_impact: f64,
195 pub publication_type: PublicationType,
197 pub doi: Option<String>,
199 pub last_updated: DateTime<Utc>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub enum CareerStage {
206 EarlyCareer,
207 MidCareer,
208 SeniorCareer,
209 Emeritus,
210 Unknown,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub enum PublicationType {
216 JournalArticle,
217 ConferencePaper,
218 BookChapter,
219 Book,
220 Preprint,
221 Thesis,
222 TechnicalReport,
223 Other,
224}
225
226#[derive(Debug, Clone)]
228pub struct CitationNetwork {
229 pub citations: HashMap<String, Vec<Citation>>,
231 pub co_citations: HashMap<String, Vec<CoCitation>>,
233 pub bibliographic_coupling: HashMap<String, Vec<BibliographicCoupling>>,
235 pub temporal_patterns: HashMap<String, Vec<TemporalCitation>>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct Citation {
242 pub citing_paper: String,
244 pub cited_paper: String,
246 pub context: String,
248 pub citation_type: CitationType,
250 pub section: PaperSection,
252 pub timestamp: DateTime<Utc>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub enum CitationType {
259 Supportive,
260 Contrasting,
261 Neutral,
262 Background,
263 Methodological,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub enum PaperSection {
269 Introduction,
270 RelatedWork,
271 Methods,
272 Results,
273 Discussion,
274 Conclusion,
275 Other,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct CoCitation {
281 pub paper1: String,
283 pub paper2: String,
285 pub co_citation_count: u32,
287 pub similarity_score: f64,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct BibliographicCoupling {
294 pub paper1: String,
296 pub paper2: String,
298 pub shared_references: u32,
300 pub coupling_strength: f64,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct TemporalCitation {
307 pub paper_id: String,
309 pub timestamp: DateTime<Utc>,
311 pub citation_count: u64,
313 pub citation_velocity: f64,
315}
316
317#[derive(Debug, Clone)]
319pub struct CollaborationNetwork {
320 pub collaborations: HashMap<String, Vec<Collaboration>>,
322 pub research_communities: Vec<ResearchCommunity>,
324 pub temporal_collaborations: HashMap<String, Vec<TemporalCollaboration>>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct Collaboration {
331 pub author1: String,
333 pub author2: String,
335 pub joint_publications: u32,
337 pub strength: f64,
339 pub shared_topics: Vec<String>,
341 pub first_collaboration: DateTime<Utc>,
343 pub last_collaboration: DateTime<Utc>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct ResearchCommunity {
350 pub community_id: String,
352 pub members: Vec<String>,
354 pub topics: Vec<String>,
356 pub central_members: Vec<String>,
358 pub coherence_score: f64,
360 pub size: usize,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct TemporalCollaboration {
367 pub author_id: String,
369 pub timestamp: DateTime<Utc>,
371 pub active_collaborations: u32,
373 pub new_collaborations: u32,
375}
376
377#[derive(Debug, Clone)]
379pub struct TopicModel {
380 pub topic_id: String,
382 pub topic_name: String,
384 pub topic_words: Vec<(String, f64)>,
386 pub document_topics: HashMap<String, f64>,
388 pub coherence_score: f64,
390 pub temporal_trend: Vec<TopicTrend>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct TopicTrend {
397 pub timestamp: DateTime<Utc>,
399 pub popularity: f64,
401 pub publication_count: u64,
403 pub growth_rate: f64,
405}
406
407#[derive(Debug, Clone)]
409pub struct ImpactPredictor {
410 pub feature_weights: HashMap<String, f64>,
412 pub performance_metrics: PredictionMetrics,
414 pub last_update: DateTime<Utc>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct PredictionMetrics {
421 pub mae: f64,
423 pub rmse: f64,
425 pub r2_score: f64,
427 pub precision_at_k: HashMap<u32, f64>,
429}
430
431impl ResearchNetworkAnalyzer {
432 pub fn new(config: ResearchNetworkConfig) -> Self {
434 Self {
435 author_embeddings: Arc::new(RwLock::new(HashMap::new())),
436 publication_embeddings: Arc::new(RwLock::new(HashMap::new())),
437 citation_network: Arc::new(RwLock::new(CitationNetwork {
438 citations: HashMap::new(),
439 co_citations: HashMap::new(),
440 bibliographic_coupling: HashMap::new(),
441 temporal_patterns: HashMap::new(),
442 })),
443 collaboration_network: Arc::new(RwLock::new(CollaborationNetwork {
444 collaborations: HashMap::new(),
445 research_communities: Vec::new(),
446 temporal_collaborations: HashMap::new(),
447 })),
448 topic_models: Arc::new(RwLock::new(HashMap::new())),
449 author_profiles: Arc::new(RwLock::new(HashMap::new())),
450 publication_metadata: Arc::new(RwLock::new(HashMap::new())),
451 config,
452 analysis_tasks: Vec::new(),
453 }
454 }
455
456 pub fn register_author_profile(&self, author_id: impl Into<String>, profile: AuthorProfile) {
463 self.author_profiles
464 .write()
465 .expect("rwlock should not be poisoned")
466 .insert(author_id.into(), profile);
467 }
468
469 pub fn register_publication_metadata(
479 &self,
480 publication_id: impl Into<String>,
481 metadata: PublicationMetadataInput,
482 ) {
483 self.publication_metadata
484 .write()
485 .expect("rwlock should not be poisoned")
486 .insert(publication_id.into(), metadata);
487 }
488
489 pub async fn add_collaboration(&self, collaboration: Collaboration) -> Result<()> {
498 let mut network = self
499 .collaboration_network
500 .write()
501 .expect("rwlock should not be poisoned");
502 network
503 .collaborations
504 .entry(collaboration.author1.clone())
505 .or_default()
506 .push(collaboration.clone());
507 if collaboration.author2 != collaboration.author1 {
508 network
509 .collaborations
510 .entry(collaboration.author2.clone())
511 .or_default()
512 .push(collaboration);
513 }
514 Ok(())
515 }
516
517 pub async fn start(&mut self) -> Result<()> {
519 info!("Starting research network analysis system");
520
521 let citation_task = self.start_citation_analysis().await;
523 self.analysis_tasks.push(citation_task);
524
525 let collaboration_task = self.start_collaboration_analysis().await;
527 self.analysis_tasks.push(collaboration_task);
528
529 let impact_task = self.start_impact_prediction().await;
531 self.analysis_tasks.push(impact_task);
532
533 let topic_task = self.start_topic_modeling().await;
535 self.analysis_tasks.push(topic_task);
536
537 info!("Research network analysis system started successfully");
538 Ok(())
539 }
540
541 pub async fn stop(&mut self) {
543 info!("Stopping research network analysis system");
544
545 for task in self.analysis_tasks.drain(..) {
546 task.abort();
547 }
548
549 info!("Research network analysis system stopped");
550 }
551
552 pub async fn generate_author_embedding(&self, author_id: &str) -> Result<AuthorEmbedding> {
554 {
556 let embeddings = self
557 .author_embeddings
558 .read()
559 .expect("rwlock should not be poisoned");
560 if let Some(existing) = embeddings.get(author_id) {
561 return Ok(existing.clone());
562 }
563 }
564
565 info!("Generating author embedding for: {}", author_id);
566
567 let profile = {
572 let profiles = self
573 .author_profiles
574 .read()
575 .expect("rwlock should not be poisoned");
576 profiles.get(author_id).cloned()
577 }
578 .ok_or_else(|| {
579 anyhow::anyhow!(
580 "No author profile registered for '{author_id}': \
581 ResearchNetworkAnalyzer has no external author database to resolve \
582 name/affiliations from. Call register_author_profile({author_id}, ...) \
583 before generating an embedding."
584 )
585 })?;
586
587 let author_publications = self.get_author_publications(author_id).await?;
589
590 let collaborations = self.get_author_collaborations(author_id).await?;
592
593 let research_topics = self
595 .extract_author_topics(author_id, &author_publications)
596 .await?;
597
598 let h_index = self.calculate_h_index(&author_publications).await?;
600 let citation_count = author_publications.iter().map(|p| p.citation_count).sum();
601 let collaboration_score = self.calculate_collaboration_score(&collaborations).await?;
602 let impact_score = self.calculate_author_impact_score(author_id).await?;
603
604 let embedding = self
606 .compute_author_embedding_vector(
607 &author_publications,
608 &collaborations,
609 &research_topics,
610 )
611 .await?;
612
613 let career_stage = self
615 .classify_career_stage(citation_count, author_publications.len() as u64, h_index)
616 .await?;
617
618 let author_embedding = AuthorEmbedding {
619 author_id: author_id.to_string(),
620 name: profile.name,
621 affiliations: profile.affiliations,
622 research_topics,
623 h_index,
624 citation_count,
625 publication_count: author_publications.len() as u64,
626 embedding,
627 collaboration_score,
628 impact_score,
629 career_stage,
630 last_updated: Utc::now(),
631 };
632
633 {
635 let mut embeddings = self
636 .author_embeddings
637 .write()
638 .expect("rwlock should not be poisoned");
639 embeddings.insert(author_id.to_string(), author_embedding.clone());
640 }
641
642 info!(
643 "Generated author embedding for {} with h-index: {:.2}",
644 author_id, h_index
645 );
646 Ok(author_embedding)
647 }
648
649 pub async fn generate_publication_embedding(
651 &self,
652 publication_id: &str,
653 ) -> Result<PublicationEmbedding> {
654 {
656 let embeddings = self
657 .publication_embeddings
658 .read()
659 .expect("rwlock should not be poisoned");
660 if let Some(existing) = embeddings.get(publication_id) {
661 return Ok(existing.clone());
662 }
663 }
664
665 info!("Generating publication embedding for: {}", publication_id);
666
667 let metadata = {
673 let records = self
674 .publication_metadata
675 .read()
676 .expect("rwlock should not be poisoned");
677 records.get(publication_id).cloned()
678 }
679 .ok_or_else(|| {
680 anyhow::anyhow!(
681 "No metadata registered for publication '{publication_id}': \
682 ResearchNetworkAnalyzer has no external publication database to resolve \
683 title/venue/year from. Call register_publication_metadata({publication_id}, ...) \
684 before generating an embedding."
685 )
686 })?;
687 let PublicationMetadataInput {
688 title,
689 abstract_text,
690 authors,
691 venue,
692 year,
693 doi,
694 } = metadata;
695
696 let citation_count = self.get_publication_citation_count(publication_id).await?;
698
699 let topic_distribution = self
701 .extract_publication_topics(publication_id, &abstract_text)
702 .await?;
703
704 let embedding = self
706 .compute_publication_embedding_vector(&title, &abstract_text, &topic_distribution)
707 .await?;
708
709 let predicted_impact = self
711 .predict_publication_impact(citation_count, &topic_distribution, &embedding)
712 .await?;
713
714 let publication_embedding = PublicationEmbedding {
715 publication_id: publication_id.to_string(),
716 title,
717 abstract_text,
718 authors,
719 venue,
720 year,
721 citation_count,
722 topic_distribution,
723 embedding,
724 predicted_impact,
725 publication_type: PublicationType::JournalArticle, doi,
727 last_updated: Utc::now(),
728 };
729
730 {
732 let mut embeddings = self
733 .publication_embeddings
734 .write()
735 .expect("rwlock should not be poisoned");
736 embeddings.insert(publication_id.to_string(), publication_embedding.clone());
737 }
738
739 info!(
740 "Generated publication embedding for {} with predicted impact: {:.3}",
741 publication_id, predicted_impact
742 );
743 Ok(publication_embedding)
744 }
745
746 pub async fn analyze_citation_patterns(&self, publication_id: &str) -> Result<Vec<Citation>> {
748 let network = self
749 .citation_network
750 .read()
751 .expect("rwlock should not be poisoned");
752
753 if let Some(citations) = network.citations.get(publication_id) {
754 Ok(citations.clone())
755 } else {
756 Ok(Vec::new())
757 }
758 }
759
760 pub async fn find_similar_authors(
762 &self,
763 author_id: &str,
764 k: usize,
765 ) -> Result<Vec<(String, f64)>> {
766 let target_embedding = self.generate_author_embedding(author_id).await?;
767 let embeddings_data: Vec<(String, AuthorEmbedding)> = {
768 let embeddings = self
769 .author_embeddings
770 .read()
771 .expect("rwlock should not be poisoned");
772 embeddings
773 .iter()
774 .filter(|(other_id, _)| *other_id != author_id)
775 .map(|(id, emb)| (id.clone(), emb.clone()))
776 .collect()
777 };
778
779 let mut similarities = Vec::new();
780
781 for (other_id, other_embedding) in embeddings_data {
782 let similarity = self
783 .calculate_author_similarity(&target_embedding, &other_embedding)
784 .await?;
785 similarities.push((other_id, similarity));
786 }
787
788 similarities.sort_by(|a, b| {
790 b.1.partial_cmp(&a.1)
791 .expect("similarity scores should be comparable")
792 });
793 similarities.truncate(k);
794
795 Ok(similarities)
796 }
797
798 pub async fn predict_research_impact(&self, publication_id: &str) -> Result<f64> {
800 let publication = self.generate_publication_embedding(publication_id).await?;
801 Ok(publication.predicted_impact)
802 }
803
804 pub async fn analyze_research_trends(
806 &self,
807 topic: &str,
808 years: u32,
809 ) -> Result<Vec<TopicTrend>> {
810 let topics = self
811 .topic_models
812 .read()
813 .expect("rwlock should not be poisoned");
814
815 if let Some(topic_model) = topics.get(topic) {
816 let cutoff_date = Utc::now() - chrono::Duration::days((years * 365) as i64);
818 let recent_trends: Vec<TopicTrend> = topic_model
819 .temporal_trend
820 .iter()
821 .filter(|trend| trend.timestamp > cutoff_date)
822 .cloned()
823 .collect();
824
825 Ok(recent_trends)
826 } else {
827 Ok(Vec::new())
828 }
829 }
830
831 pub async fn get_research_communities(&self) -> Result<Vec<ResearchCommunity>> {
833 let network = self
834 .collaboration_network
835 .read()
836 .expect("rwlock should not be poisoned");
837 Ok(network.research_communities.clone())
838 }
839
840 pub async fn add_citation(&self, citation: Citation) -> Result<()> {
842 let mut network = self
843 .citation_network
844 .write()
845 .expect("rwlock should not be poisoned");
846
847 network
848 .citations
849 .entry(citation.citing_paper.clone())
850 .or_default()
851 .push(citation);
852
853 info!("Added new citation to network");
854 Ok(())
855 }
856
857 async fn get_author_publications(&self, author_id: &str) -> Result<Vec<PublicationEmbedding>> {
865 let publication_ids: Vec<String> = {
866 let records = self
867 .publication_metadata
868 .read()
869 .expect("rwlock should not be poisoned");
870 records
871 .iter()
872 .filter(|(_, metadata)| metadata.authors.iter().any(|a| a == author_id))
873 .map(|(id, _)| id.clone())
874 .collect()
875 };
876
877 let mut publications = Vec::with_capacity(publication_ids.len());
878 for publication_id in publication_ids {
879 publications.push(self.generate_publication_embedding(&publication_id).await?);
880 }
881 Ok(publications)
882 }
883
884 async fn get_author_collaborations(&self, author_id: &str) -> Result<Vec<Collaboration>> {
889 let network = self
890 .collaboration_network
891 .read()
892 .expect("rwlock should not be poisoned");
893 Ok(network
894 .collaborations
895 .get(author_id)
896 .cloned()
897 .unwrap_or_default())
898 }
899
900 async fn extract_author_topics(
901 &self,
902 _author_id: &str,
903 _publications: &[PublicationEmbedding],
904 ) -> Result<Vec<String>> {
905 Ok(vec![
907 "machine_learning".to_string(),
908 "natural_language_processing".to_string(),
909 ])
910 }
911
912 async fn calculate_h_index(&self, publications: &[PublicationEmbedding]) -> Result<f64> {
913 let mut citation_counts: Vec<u64> = publications.iter().map(|p| p.citation_count).collect();
914
915 citation_counts.sort_by(|a, b| b.cmp(a));
916
917 let mut h_index = 0;
918 for (i, &citations) in citation_counts.iter().enumerate() {
919 if citations >= (i + 1) as u64 {
920 h_index = i + 1;
921 } else {
922 break;
923 }
924 }
925
926 Ok(h_index as f64)
927 }
928
929 async fn calculate_collaboration_score(&self, collaborations: &[Collaboration]) -> Result<f64> {
930 if collaborations.is_empty() {
931 return Ok(0.0);
932 }
933
934 let total_strength: f64 = collaborations.iter().map(|c| c.strength).sum();
935 Ok(total_strength / collaborations.len() as f64)
936 }
937
938 async fn calculate_author_impact_score(&self, _author_id: &str) -> Result<f64> {
939 Ok(0.75)
941 }
942
943 async fn compute_author_embedding_vector(
944 &self,
945 _publications: &[PublicationEmbedding],
946 _collaborations: &[Collaboration],
947 _topics: &[String],
948 ) -> Result<Vector> {
949 let values = (0..self.config.embedding_dimension)
951 .map(|_| {
952 let mut random = Random::default();
953 random.random::<f32>()
954 })
955 .collect();
956 Ok(Vector::new(values))
957 }
958
959 async fn classify_career_stage(
960 &self,
961 citation_count: u64,
962 publication_count: u64,
963 h_index: f64,
964 ) -> Result<CareerStage> {
965 if citation_count < 100 && publication_count < 10 && h_index < 5.0 {
966 Ok(CareerStage::EarlyCareer)
967 } else if citation_count < 1000 && publication_count < 50 && h_index < 20.0 {
968 Ok(CareerStage::MidCareer)
969 } else if citation_count >= 1000 || publication_count >= 50 || h_index >= 20.0 {
970 Ok(CareerStage::SeniorCareer)
971 } else {
972 Ok(CareerStage::Unknown)
973 }
974 }
975
976 async fn get_publication_citation_count(&self, _publication_id: &str) -> Result<u64> {
977 let mut random = Random::default();
979 Ok(random.random::<u64>() % 100)
980 }
981
982 async fn extract_publication_topics(
983 &self,
984 _publication_id: &str,
985 _abstract_text: &str,
986 ) -> Result<Vec<f64>> {
987 let num_topics = self.config.topic_config.num_topics;
989 let mut distribution = vec![0.0; num_topics];
990
991 let total: f64 = (0..num_topics)
993 .map(|_| {
994 let mut random = Random::default();
995 random.random::<f64>()
996 })
997 .sum();
998 for item in distribution.iter_mut().take(num_topics) {
999 let mut random = Random::default();
1000 *item = random.random::<f64>() / total;
1001 }
1002
1003 Ok(distribution)
1004 }
1005
1006 async fn compute_publication_embedding_vector(
1007 &self,
1008 _title: &str,
1009 _abstract_text: &str,
1010 _topic_distribution: &[f64],
1011 ) -> Result<Vector> {
1012 let values = (0..self.config.embedding_dimension)
1014 .map(|_| {
1015 let mut random = Random::default();
1016 random.random::<f32>()
1017 })
1018 .collect();
1019 Ok(Vector::new(values))
1020 }
1021
1022 async fn predict_publication_impact(
1023 &self,
1024 citation_count: u64,
1025 _topic_distribution: &[f64],
1026 _embedding: &Vector,
1027 ) -> Result<f64> {
1028 let base_impact = (citation_count as f64).ln() / 10.0;
1030 Ok(base_impact.clamp(0.0, 1.0))
1031 }
1032
1033 async fn calculate_author_similarity(
1034 &self,
1035 author1: &AuthorEmbedding,
1036 author2: &AuthorEmbedding,
1037 ) -> Result<f64> {
1038 let embedding1 = &author1.embedding.values;
1040 let embedding2 = &author2.embedding.values;
1041
1042 let dot_product: f32 = embedding1
1043 .iter()
1044 .zip(embedding2.iter())
1045 .map(|(a, b)| a * b)
1046 .sum();
1047 let norm1: f32 = embedding1.iter().map(|x| x * x).sum::<f32>().sqrt();
1048 let norm2: f32 = embedding2.iter().map(|x| x * x).sum::<f32>().sqrt();
1049
1050 let cosine_similarity = if norm1 > 0.0 && norm2 > 0.0 {
1051 dot_product / (norm1 * norm2)
1052 } else {
1053 0.0
1054 };
1055
1056 let topic_similarity = self
1058 .calculate_topic_similarity(&author1.research_topics, &author2.research_topics)
1059 .await?;
1060
1061 let final_similarity = 0.7 * cosine_similarity as f64 + 0.3 * topic_similarity;
1063
1064 Ok(final_similarity)
1065 }
1066
1067 async fn calculate_topic_similarity(
1068 &self,
1069 topics1: &[String],
1070 topics2: &[String],
1071 ) -> Result<f64> {
1072 let set1: HashSet<_> = topics1.iter().collect();
1073 let set2: HashSet<_> = topics2.iter().collect();
1074
1075 let intersection = set1.intersection(&set2).count();
1076 let union = set1.union(&set2).count();
1077
1078 if union > 0 {
1079 Ok(intersection as f64 / union as f64)
1080 } else {
1081 Ok(0.0)
1082 }
1083 }
1084
1085 async fn start_citation_analysis(&self) -> JoinHandle<()> {
1088 let _citation_network = Arc::clone(&self.citation_network);
1089 let interval =
1090 std::time::Duration::from_secs(self.config.citation_update_interval_hours * 3600);
1091
1092 tokio::spawn(async move {
1093 let mut interval_timer = tokio::time::interval(interval);
1094
1095 loop {
1096 interval_timer.tick().await;
1097
1098 info!("Performing citation network analysis");
1100
1101 debug!("Citation network analysis completed");
1105 }
1106 })
1107 }
1108
1109 async fn start_collaboration_analysis(&self) -> JoinHandle<()> {
1110 let _collaboration_network = Arc::clone(&self.collaboration_network);
1111 let interval = std::time::Duration::from_secs(
1112 self.config.collaboration_analysis_interval_hours * 3600,
1113 );
1114
1115 tokio::spawn(async move {
1116 let mut interval_timer = tokio::time::interval(interval);
1117
1118 loop {
1119 interval_timer.tick().await;
1120
1121 info!("Performing collaboration network analysis");
1123
1124 debug!("Collaboration network analysis completed");
1128 }
1129 })
1130 }
1131
1132 async fn start_impact_prediction(&self) -> JoinHandle<()> {
1133 let interval =
1134 std::time::Duration::from_secs(self.config.impact_prediction_refresh_hours * 3600);
1135
1136 tokio::spawn(async move {
1137 let mut interval_timer = tokio::time::interval(interval);
1138
1139 loop {
1140 interval_timer.tick().await;
1141
1142 info!("Refreshing impact prediction models");
1144
1145 debug!("Impact prediction models refreshed");
1149 }
1150 })
1151 }
1152
1153 async fn start_topic_modeling(&self) -> JoinHandle<()> {
1154 let topic_models = Arc::clone(&self.topic_models);
1155 let _config = self.config.clone();
1156 let interval = std::time::Duration::from_secs(24 * 3600); tokio::spawn(async move {
1159 let mut interval_timer = tokio::time::interval(interval);
1160
1161 loop {
1162 interval_timer.tick().await;
1163
1164 info!("Updating topic models");
1166
1167 let topic_model = TopicModel {
1169 topic_id: "machine_learning".to_string(),
1170 topic_name: "Machine Learning".to_string(),
1171 topic_words: vec![
1172 ("neural".to_string(), 0.1),
1173 ("network".to_string(), 0.09),
1174 ("learning".to_string(), 0.08),
1175 ("algorithm".to_string(), 0.07),
1176 ("model".to_string(), 0.06),
1177 ],
1178 document_topics: HashMap::new(),
1179 coherence_score: 0.75,
1180 temporal_trend: vec![
1181 TopicTrend {
1182 timestamp: Utc::now() - chrono::Duration::days(365),
1183 popularity: 0.6,
1184 publication_count: 1000,
1185 growth_rate: 0.15,
1186 },
1187 TopicTrend {
1188 timestamp: Utc::now(),
1189 popularity: 0.8,
1190 publication_count: 1500,
1191 growth_rate: 0.25,
1192 },
1193 ],
1194 };
1195
1196 {
1197 let mut models = topic_models.write().expect("rwlock should not be poisoned");
1198 models.insert("machine_learning".to_string(), topic_model);
1199 }
1200
1201 debug!("Topic models updated");
1202 }
1203 })
1204 }
1205}
1206
1207#[derive(Debug, Clone, Serialize, Deserialize)]
1209pub struct NetworkMetrics {
1210 pub total_authors: usize,
1212 pub total_publications: usize,
1214 pub total_citations: u64,
1216 pub avg_citations_per_paper: f64,
1218 pub network_density: f64,
1220 pub clustering_coefficient: f64,
1222 pub average_path_length: f64,
1224 pub top_authors: Vec<String>,
1226 pub trending_topics: Vec<String>,
1228}
1229
1230impl ResearchNetworkAnalyzer {
1231 pub async fn get_network_metrics(&self) -> Result<NetworkMetrics> {
1233 let author_embeddings = self
1234 .author_embeddings
1235 .read()
1236 .expect("rwlock should not be poisoned");
1237 let publication_embeddings = self
1238 .publication_embeddings
1239 .read()
1240 .expect("rwlock should not be poisoned");
1241
1242 let total_authors = author_embeddings.len();
1243 let total_publications = publication_embeddings.len();
1244 let total_citations = publication_embeddings
1245 .values()
1246 .map(|p| p.citation_count)
1247 .sum();
1248
1249 let avg_citations_per_paper = if total_publications > 0 {
1250 total_citations as f64 / total_publications as f64
1251 } else {
1252 0.0
1253 };
1254
1255 let mut author_scores: Vec<_> = author_embeddings
1257 .iter()
1258 .map(|(id, embedding)| (id.clone(), embedding.impact_score))
1259 .collect();
1260 author_scores.sort_by(|a, b| {
1261 b.1.partial_cmp(&a.1)
1262 .expect("similarity scores should be comparable")
1263 });
1264 let top_authors: Vec<String> = author_scores
1265 .into_iter()
1266 .take(10)
1267 .map(|(id, _)| id)
1268 .collect();
1269
1270 let (network_density, clustering_coefficient, average_path_length) =
1271 Self::compute_coauthorship_graph_metrics(&publication_embeddings);
1272
1273 Ok(NetworkMetrics {
1274 total_authors,
1275 total_publications,
1276 total_citations,
1277 avg_citations_per_paper,
1278 network_density,
1279 clustering_coefficient,
1280 average_path_length,
1281 top_authors,
1282 trending_topics: vec!["machine_learning".to_string(), "deep_learning".to_string()],
1283 })
1284 }
1285
1286 fn compute_coauthorship_graph_metrics(
1291 publication_embeddings: &HashMap<String, PublicationEmbedding>,
1292 ) -> (f64, f64, f64) {
1293 use std::collections::VecDeque;
1294
1295 let mut adjacency: HashMap<String, HashSet<String>> = HashMap::new();
1296 for publication in publication_embeddings.values() {
1297 for i in 0..publication.authors.len() {
1298 for j in (i + 1)..publication.authors.len() {
1299 let a = &publication.authors[i];
1300 let b = &publication.authors[j];
1301 if a == b {
1302 continue;
1303 }
1304 adjacency.entry(a.clone()).or_default().insert(b.clone());
1305 adjacency.entry(b.clone()).or_default().insert(a.clone());
1306 }
1307 }
1308 }
1309
1310 let node_count = adjacency.len();
1311 if node_count < 2 {
1312 return (0.0, 0.0, 0.0);
1313 }
1314
1315 let edge_count: usize = adjacency
1316 .values()
1317 .map(|neighbors| neighbors.len())
1318 .sum::<usize>()
1319 / 2;
1320 let max_edges = (node_count * (node_count - 1)) as f64 / 2.0;
1321 let network_density = if max_edges > 0.0 {
1322 edge_count as f64 / max_edges
1323 } else {
1324 0.0
1325 };
1326
1327 let mut clustering_sum = 0.0;
1330 let mut clustering_count = 0usize;
1331 for neighbors in adjacency.values() {
1332 let degree = neighbors.len();
1333 if degree < 2 {
1334 continue;
1335 }
1336 let neighbor_list: Vec<&String> = neighbors.iter().collect();
1337 let mut links = 0usize;
1338 for i in 0..neighbor_list.len() {
1339 for j in (i + 1)..neighbor_list.len() {
1340 if adjacency
1341 .get(neighbor_list[i].as_str())
1342 .is_some_and(|n| n.contains(neighbor_list[j].as_str()))
1343 {
1344 links += 1;
1345 }
1346 }
1347 }
1348 let possible = (degree * (degree - 1)) / 2;
1349 clustering_sum += links as f64 / possible as f64;
1350 clustering_count += 1;
1351 }
1352 let clustering_coefficient = if clustering_count > 0 {
1353 clustering_sum / clustering_count as f64
1354 } else {
1355 0.0
1356 };
1357
1358 const MAX_BFS_SOURCES: usize = 200;
1361 let mut node_ids: Vec<&String> = adjacency.keys().collect();
1362 node_ids.sort();
1363 node_ids.truncate(MAX_BFS_SOURCES);
1364
1365 let mut total_distance = 0.0f64;
1366 let mut total_pairs = 0usize;
1367 for source in &node_ids {
1368 let mut visited: HashSet<&String> = HashSet::new();
1369 visited.insert(source);
1370 let mut queue: VecDeque<(&String, usize)> = VecDeque::new();
1371 queue.push_back((source, 0));
1372 while let Some((node, dist)) = queue.pop_front() {
1373 if dist > 0 {
1374 total_distance += dist as f64;
1375 total_pairs += 1;
1376 }
1377 if let Some(neighbors) = adjacency.get(node.as_str()) {
1378 for neighbor in neighbors {
1379 if visited.insert(neighbor) {
1380 queue.push_back((neighbor, dist + 1));
1381 }
1382 }
1383 }
1384 }
1385 }
1386 let average_path_length = if total_pairs > 0 {
1387 total_distance / total_pairs as f64
1388 } else {
1389 0.0
1390 };
1391
1392 (network_density, clustering_coefficient, average_path_length)
1393 }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398 use super::*;
1399
1400 fn make_publication(id: &str, authors: &[&str]) -> PublicationEmbedding {
1401 PublicationEmbedding {
1402 publication_id: id.to_string(),
1403 title: format!("Title {id}"),
1404 abstract_text: String::new(),
1405 authors: authors.iter().map(|a| a.to_string()).collect(),
1406 venue: "Venue".to_string(),
1407 year: 2024,
1408 citation_count: 0,
1409 topic_distribution: vec![],
1410 embedding: Vector::new(vec![0.0; 4]),
1411 predicted_impact: 0.0,
1412 publication_type: PublicationType::JournalArticle,
1413 doi: None,
1414 last_updated: Utc::now(),
1415 }
1416 }
1417
1418 #[test]
1422 fn test_compute_coauthorship_graph_metrics_triangle() {
1423 let mut publications = HashMap::new();
1426 publications.insert("p1".to_string(), make_publication("p1", &["A", "B", "C"]));
1427
1428 let (density, clustering, avg_path) =
1429 ResearchNetworkAnalyzer::compute_coauthorship_graph_metrics(&publications);
1430
1431 assert!((density - 1.0).abs() < 1e-9, "density = {density}");
1432 assert!((clustering - 1.0).abs() < 1e-9, "clustering = {clustering}");
1433 assert!((avg_path - 1.0).abs() < 1e-9, "avg_path = {avg_path}");
1434 }
1435
1436 #[test]
1437 fn test_compute_coauthorship_graph_metrics_empty() {
1438 let publications = HashMap::new();
1439 let (density, clustering, avg_path) =
1440 ResearchNetworkAnalyzer::compute_coauthorship_graph_metrics(&publications);
1441 assert_eq!(density, 0.0);
1442 assert_eq!(clustering, 0.0);
1443 assert_eq!(avg_path, 0.0);
1444 }
1445
1446 #[tokio::test]
1447 async fn test_research_network_analyzer_creation() {
1448 let config = ResearchNetworkConfig::default();
1449 let analyzer = ResearchNetworkAnalyzer::new(config);
1450
1451 assert_eq!(
1453 analyzer
1454 .author_embeddings
1455 .read()
1456 .expect("rwlock should not be poisoned")
1457 .len(),
1458 0
1459 );
1460 assert_eq!(
1461 analyzer
1462 .publication_embeddings
1463 .read()
1464 .expect("rwlock should not be poisoned")
1465 .len(),
1466 0
1467 );
1468 }
1469
1470 #[tokio::test]
1474 async fn test_author_embedding_requires_registered_profile() {
1475 let config = ResearchNetworkConfig::default();
1476 let analyzer = ResearchNetworkAnalyzer::new(config);
1477
1478 let result = analyzer
1479 .generate_author_embedding("unregistered_author")
1480 .await;
1481 assert!(result.is_err());
1482 let msg = result.expect_err("must fail loudly").to_string();
1483 assert!(msg.contains("unregistered_author"));
1484 assert!(msg.contains("register_author_profile"));
1485 }
1486
1487 #[tokio::test]
1488 async fn test_author_embedding_generation() {
1489 let config = ResearchNetworkConfig::default();
1490 let analyzer = ResearchNetworkAnalyzer::new(config);
1491 analyzer.register_author_profile(
1492 "test_author",
1493 AuthorProfile {
1494 name: "Dr. Ada Example".to_string(),
1495 affiliations: vec!["Example University".to_string()],
1496 },
1497 );
1498
1499 let result = analyzer.generate_author_embedding("test_author").await;
1500 assert!(result.is_ok());
1501
1502 let embedding = result.expect("should succeed");
1503 assert_eq!(embedding.author_id, "test_author");
1504 assert_eq!(embedding.name, "Dr. Ada Example");
1506 assert_eq!(
1507 embedding.affiliations,
1508 vec!["Example University".to_string()]
1509 );
1510 assert!(embedding.h_index >= 0.0);
1511 assert_eq!(embedding.embedding.values.len(), 512); }
1513
1514 #[tokio::test]
1519 async fn test_publication_embedding_requires_registered_metadata() {
1520 let config = ResearchNetworkConfig::default();
1521 let analyzer = ResearchNetworkAnalyzer::new(config);
1522
1523 let result = analyzer
1524 .generate_publication_embedding("unregistered_publication")
1525 .await;
1526 assert!(result.is_err());
1527 let msg = result.expect_err("must fail loudly").to_string();
1528 assert!(msg.contains("unregistered_publication"));
1529 assert!(msg.contains("register_publication_metadata"));
1530 }
1531
1532 #[tokio::test]
1533 async fn test_publication_embedding_generation() {
1534 let config = ResearchNetworkConfig::default();
1535 let analyzer = ResearchNetworkAnalyzer::new(config);
1536 analyzer.register_publication_metadata(
1537 "test_publication",
1538 PublicationMetadataInput {
1539 title: "A Study of RDF Embeddings".to_string(),
1540 abstract_text: "We study embeddings of RDF graphs.".to_string(),
1541 authors: vec!["test_author".to_string()],
1542 venue: "Journal of Semantic Web".to_string(),
1543 year: 2025,
1544 doi: Some("10.1234/example".to_string()),
1545 },
1546 );
1547
1548 let result = analyzer
1549 .generate_publication_embedding("test_publication")
1550 .await;
1551 assert!(result.is_ok());
1552
1553 let embedding = result.expect("should succeed");
1554 assert_eq!(embedding.publication_id, "test_publication");
1555 assert_eq!(embedding.title, "A Study of RDF Embeddings");
1557 assert_eq!(embedding.venue, "Journal of Semantic Web");
1558 assert_eq!(embedding.year, 2025);
1559 assert!(embedding.predicted_impact >= 0.0);
1560 assert!(embedding.predicted_impact <= 1.0);
1561 }
1562
1563 #[tokio::test]
1566 async fn test_author_publications_and_collaborations_are_real() {
1567 let config = ResearchNetworkConfig::default();
1568 let analyzer = ResearchNetworkAnalyzer::new(config);
1569
1570 analyzer.register_author_profile(
1571 "author_a",
1572 AuthorProfile {
1573 name: "Author A".to_string(),
1574 affiliations: vec!["Uni A".to_string()],
1575 },
1576 );
1577 analyzer.register_publication_metadata(
1578 "pub1",
1579 PublicationMetadataInput {
1580 title: "Paper One".to_string(),
1581 abstract_text: String::new(),
1582 authors: vec!["author_a".to_string()],
1583 venue: "Venue".to_string(),
1584 year: 2024,
1585 doi: None,
1586 },
1587 );
1588 analyzer
1589 .add_collaboration(Collaboration {
1590 author1: "author_a".to_string(),
1591 author2: "author_b".to_string(),
1592 joint_publications: 1,
1593 strength: 0.8,
1594 shared_topics: vec![],
1595 first_collaboration: Utc::now(),
1596 last_collaboration: Utc::now(),
1597 })
1598 .await
1599 .expect("should succeed");
1600
1601 let publications = analyzer
1602 .get_author_publications("author_a")
1603 .await
1604 .expect("should succeed");
1605 assert_eq!(publications.len(), 1);
1606 assert_eq!(publications[0].title, "Paper One");
1607
1608 let collaborations = analyzer
1609 .get_author_collaborations("author_a")
1610 .await
1611 .expect("should succeed");
1612 assert_eq!(collaborations.len(), 1);
1613 assert_eq!(collaborations[0].author2, "author_b");
1614
1615 let collaborations_b = analyzer
1617 .get_author_collaborations("author_b")
1618 .await
1619 .expect("should succeed");
1620 assert_eq!(collaborations_b.len(), 1);
1621 assert_eq!(collaborations_b[0].author1, "author_a");
1622 }
1623
1624 #[tokio::test]
1625 async fn test_h_index_calculation() {
1626 let config = ResearchNetworkConfig::default();
1627 let analyzer = ResearchNetworkAnalyzer::new(config);
1628
1629 let publications = vec![
1631 PublicationEmbedding {
1632 publication_id: "p1".to_string(),
1633 title: "Test 1".to_string(),
1634 abstract_text: "Abstract 1".to_string(),
1635 authors: vec!["author1".to_string()],
1636 venue: "Venue 1".to_string(),
1637 year: 2023,
1638 citation_count: 10,
1639 topic_distribution: vec![],
1640 embedding: Vector::new(vec![]),
1641 predicted_impact: 0.5,
1642 publication_type: PublicationType::JournalArticle,
1643 doi: None,
1644 last_updated: Utc::now(),
1645 },
1646 PublicationEmbedding {
1647 publication_id: "p2".to_string(),
1648 title: "Test 2".to_string(),
1649 abstract_text: "Abstract 2".to_string(),
1650 authors: vec!["author1".to_string()],
1651 venue: "Venue 2".to_string(),
1652 year: 2023,
1653 citation_count: 5,
1654 topic_distribution: vec![],
1655 embedding: Vector::new(vec![]),
1656 predicted_impact: 0.3,
1657 publication_type: PublicationType::JournalArticle,
1658 doi: None,
1659 last_updated: Utc::now(),
1660 },
1661 ];
1662
1663 let h_index = analyzer
1664 .calculate_h_index(&publications)
1665 .await
1666 .expect("should succeed");
1667 assert_eq!(h_index, 2.0); }
1669
1670 #[test]
1671 fn test_career_stage_classification() {
1672 let rt = tokio::runtime::Runtime::new().expect("should succeed");
1674 let config = ResearchNetworkConfig::default();
1675 let analyzer = ResearchNetworkAnalyzer::new(config);
1676
1677 let stage = rt
1678 .block_on(analyzer.classify_career_stage(50, 5, 3.0))
1679 .expect("should succeed");
1680 assert!(matches!(stage, CareerStage::EarlyCareer));
1681
1682 let stage = rt
1684 .block_on(analyzer.classify_career_stage(2000, 100, 25.0))
1685 .expect("should succeed");
1686 assert!(matches!(stage, CareerStage::SeniorCareer));
1687 }
1688
1689 #[tokio::test]
1690 async fn test_network_metrics() {
1691 let config = ResearchNetworkConfig::default();
1692 let analyzer = ResearchNetworkAnalyzer::new(config);
1693
1694 analyzer.register_author_profile(
1696 "test_author",
1697 AuthorProfile {
1698 name: "Test Author".to_string(),
1699 affiliations: vec!["Test University".to_string()],
1700 },
1701 );
1702 analyzer.register_publication_metadata(
1703 "test_publication",
1704 PublicationMetadataInput {
1705 title: "Test Publication".to_string(),
1706 abstract_text: String::new(),
1707 authors: vec!["test_author".to_string()],
1708 venue: "Test Venue".to_string(),
1709 year: 2025,
1710 doi: None,
1711 },
1712 );
1713 let _author_embedding = analyzer
1714 .generate_author_embedding("test_author")
1715 .await
1716 .expect("should succeed");
1717 let _publication_embedding = analyzer
1718 .generate_publication_embedding("test_publication")
1719 .await
1720 .expect("should succeed");
1721
1722 let metrics = analyzer
1723 .get_network_metrics()
1724 .await
1725 .expect("should succeed");
1726 assert_eq!(metrics.total_authors, 1);
1727 assert_eq!(metrics.total_publications, 1);
1728 }
1729}