1use scirs2_core::ndarray::{s, Array2, ArrayView2, Axis};
10use sklears_core::{
11 error::{Result as SklResult, SklearsError},
12 types::Float,
13};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
41pub struct CopulaBasedModelingAnalyzer {
42 copula_types: Vec<CopulaType>,
44 fit_margins: bool,
46 use_empirical_copula: bool,
48 n_samples: usize,
50 random_state: Option<u64>,
52}
53
54impl CopulaBasedModelingAnalyzer {
55 pub fn new() -> Self {
56 Self {
57 copula_types: vec![CopulaType::Gaussian],
58 fit_margins: true,
59 use_empirical_copula: false,
60 n_samples: 1000,
61 random_state: None,
62 }
63 }
64
65 pub fn copula_types(mut self, copula_types: Vec<CopulaType>) -> Self {
67 self.copula_types = copula_types;
68 self
69 }
70
71 pub fn fit_margins(mut self, fit_margins: bool) -> Self {
73 self.fit_margins = fit_margins;
74 self
75 }
76
77 pub fn use_empirical_copula(mut self, use_empirical_copula: bool) -> Self {
79 self.use_empirical_copula = use_empirical_copula;
80 self
81 }
82
83 pub fn n_samples(mut self, n_samples: usize) -> Self {
85 self.n_samples = n_samples;
86 self
87 }
88
89 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
91 self.random_state = random_state;
92 self
93 }
94
95 pub fn analyze(&self, _outputs: &HashMap<String, Array2<Float>>) -> SklResult<CopulaAnalysis> {
97 let copula_models = HashMap::new();
99 let marginal_distributions = HashMap::new();
100 let goodness_of_fit = HashMap::new();
101 let dependence_measures = HashMap::new();
102 let output_info = HashMap::new();
103
104 Ok(CopulaAnalysis {
105 copula_models,
106 marginal_distributions,
107 goodness_of_fit,
108 dependence_measures,
109 best_copula: None,
110 output_info,
111 empirical_copula: None,
112 })
113 }
114}
115
116impl Default for CopulaBasedModelingAnalyzer {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Hash)]
124pub enum CopulaType {
125 Gaussian,
127 Clayton,
129 Frank,
131 Gumbel,
133 StudentT,
135 Archimedean,
137 Empirical,
139}
140
141#[derive(Debug, Clone)]
143pub struct CopulaAnalysis {
144 pub copula_models: HashMap<CopulaType, CopulaModel>,
146 pub marginal_distributions: HashMap<String, MarginalDistribution>,
148 pub goodness_of_fit: HashMap<CopulaType, GoodnessOfFit>,
150 pub dependence_measures: HashMap<CopulaType, DependenceMeasures>,
152 pub best_copula: Option<CopulaType>,
154 pub output_info: HashMap<String, usize>,
156 pub empirical_copula: Option<EmpiricalCopula>,
158}
159
160#[derive(Debug, Clone)]
162pub struct CopulaModel {
163 pub copula_type: CopulaType,
165 pub parameters: CopulaParameters,
167 pub log_likelihood: Float,
169 pub n_parameters: usize,
171 pub fitted_data: Array2<Float>,
173}
174
175#[derive(Debug, Clone)]
177pub enum CopulaParameters {
178 Gaussian { correlation_matrix: Array2<Float> },
180 Clayton { theta: Float },
182 Frank { theta: Float },
184 Gumbel { theta: Float },
186 StudentT {
188 correlation_matrix: Array2<Float>,
189 degrees_of_freedom: Float,
190 },
191 Archimedean { generator_params: Vec<Float> },
193 Empirical,
195}
196
197#[derive(Debug, Clone)]
199pub struct MarginalDistribution {
200 pub distribution_type: String,
202 pub parameters: Vec<Float>,
204 pub mean: Float,
206 pub std_dev: Float,
208 pub min: Float,
210 pub max: Float,
212}
213
214#[derive(Debug, Clone)]
216pub struct GoodnessOfFit {
217 pub aic: Float,
219 pub bic: Float,
221 pub cramer_von_mises: Float,
223 pub kolmogorov_smirnov: Float,
225 pub anderson_darling: Float,
227 pub p_value: Float,
229}
230
231#[derive(Debug, Clone)]
233pub struct DependenceMeasures {
234 pub kendall_tau: Float,
236 pub spearman_rho: Float,
238 pub tail_dependence: TailDependence,
240 pub conditional_measures: Vec<ConditionalMeasure>,
242}
243
244#[derive(Debug, Clone)]
246pub struct TailDependence {
247 pub lower_tail: Float,
249 pub upper_tail: Float,
251 pub asymmetry: Float,
253}
254
255#[derive(Debug, Clone)]
257pub struct ConditionalMeasure {
258 pub condition_vars: Vec<usize>,
260 pub conditional_dependence: Float,
262 pub conditional_correlation: Float,
264}
265
266#[derive(Debug, Clone)]
268pub struct EmpiricalCopula {
269 pub copula_values: Array2<Float>,
271 pub rank_data: Array2<Float>,
273 pub sample_size: usize,
275}
276
277#[derive(Debug, Clone)]
301pub struct OutputCorrelationAnalyzer {
302 correlation_types: Vec<CorrelationType>,
304 include_cross_task: bool,
306 include_within_task: bool,
308 min_correlation_threshold: Float,
310 compute_partial_correlations: bool,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Hash)]
316pub enum CorrelationType {
317 Pearson,
319 Spearman,
321 Kendall,
323 MutualInformation,
325 DistanceCorrelation,
327 CanonicalCorrelation,
329}
330
331#[derive(Debug, Clone)]
333pub struct CorrelationAnalysis {
334 pub correlation_matrices: HashMap<CorrelationType, Array2<Float>>,
336 pub cross_task_correlations: HashMap<(String, String), Array2<Float>>,
338 pub within_task_correlations: HashMap<String, Array2<Float>>,
340 pub partial_correlations: Option<HashMap<CorrelationType, Array2<Float>>>,
342 pub output_info: HashMap<String, usize>,
344 pub combined_outputs: Array2<Float>,
346 pub output_indices: HashMap<String, (usize, usize)>,
348}
349
350#[derive(Debug, Clone)]
375pub struct DependencyGraphBuilder {
376 method: DependencyMethod,
378 include_self_loops: bool,
380 directed: bool,
382 max_dependencies: Option<usize>,
384}
385
386#[derive(Debug, Clone, PartialEq)]
388pub enum DependencyMethod {
389 CorrelationThreshold(Float),
391 MutualInformationThreshold(Float),
393 CausalDiscovery,
395 StatisticalSignificance(Float), TopK(usize),
399}
400
401#[derive(Debug, Clone)]
403pub struct DependencyGraph {
404 pub adjacency_matrix: Array2<Float>,
406 pub node_names: Vec<String>,
408 pub edge_weights: Array2<Float>,
410 pub directed: bool,
412 pub stats: GraphStatistics,
414}
415
416#[derive(Debug, Clone)]
418pub struct GraphStatistics {
419 pub num_nodes: usize,
421 pub num_edges: usize,
423 pub average_degree: Float,
425 pub density: Float,
427 pub clustering_coefficient: Float,
429}
430
431#[derive(Debug, Clone)]
436pub struct ConditionalIndependenceTester {
437 #[allow(dead_code)]
438 alpha: Float,
440 #[allow(dead_code)]
441 test_method: CITestMethod,
443 #[allow(dead_code)]
444 max_conditioning_set_size: usize,
446}
447
448#[derive(Debug, Clone, PartialEq)]
450pub enum CITestMethod {
451 PartialCorrelation,
453 MutualInformation,
455 KernelBased,
457 RegressionBased,
459}
460
461#[derive(Debug, Clone)]
463pub struct CITestResults {
464 pub test_results: HashMap<(String, String, Vec<String>), CITestResult>,
466 pub markov_blankets: HashMap<String, Vec<String>>,
468 pub ci_graph: DependencyGraph,
470}
471
472#[derive(Debug, Clone)]
474pub struct CITestResult {
475 pub test_statistic: Float,
477 pub p_value: Float,
479 pub independent: bool,
481 pub conditioning_set: Vec<String>,
483}
484
485impl OutputCorrelationAnalyzer {
486 pub fn new() -> Self {
488 Self {
489 correlation_types: vec![CorrelationType::Pearson],
490 include_cross_task: true,
491 include_within_task: true,
492 min_correlation_threshold: 0.0,
493 compute_partial_correlations: false,
494 }
495 }
496
497 pub fn correlation_types(mut self, types: Vec<CorrelationType>) -> Self {
499 self.correlation_types = types;
500 self
501 }
502
503 pub fn include_cross_task(mut self, include: bool) -> Self {
505 self.include_cross_task = include;
506 self
507 }
508
509 pub fn include_within_task(mut self, include: bool) -> Self {
511 self.include_within_task = include;
512 self
513 }
514
515 pub fn min_correlation_threshold(mut self, threshold: Float) -> Self {
517 self.min_correlation_threshold = threshold;
518 self
519 }
520
521 pub fn compute_partial_correlations(mut self, compute: bool) -> Self {
523 self.compute_partial_correlations = compute;
524 self
525 }
526
527 pub fn analyze(
529 &self,
530 outputs: &HashMap<String, Array2<Float>>,
531 ) -> SklResult<CorrelationAnalysis> {
532 if outputs.is_empty() {
533 return Err(SklearsError::InvalidInput(
534 "No outputs provided".to_string(),
535 ));
536 }
537
538 let n_samples = outputs
540 .values()
541 .next()
542 .expect("sampling should succeed")
543 .nrows();
544 for task_outputs in outputs.values() {
545 if task_outputs.nrows() != n_samples {
546 return Err(SklearsError::ShapeMismatch {
547 expected: format!("{}", n_samples),
548 actual: format!("{}", task_outputs.nrows()),
549 });
550 }
551 }
552
553 let total_outputs: usize = outputs.values().map(|arr| arr.ncols()).sum();
555 let mut combined_outputs = Array2::<Float>::zeros((n_samples, total_outputs));
556 let mut output_indices = HashMap::new();
557 let mut output_info = HashMap::new();
558
559 let mut current_idx = 0;
560 for (task_name, task_outputs) in outputs {
561 let n_outputs = task_outputs.ncols();
562 let end_idx = current_idx + n_outputs;
563
564 combined_outputs
565 .slice_mut(s![.., current_idx..end_idx])
566 .assign(task_outputs);
567
568 output_indices.insert(task_name.clone(), (current_idx, end_idx));
569 output_info.insert(task_name.clone(), n_outputs);
570 current_idx = end_idx;
571 }
572
573 let mut correlation_matrices = HashMap::new();
575 for correlation_type in &self.correlation_types {
576 let corr_matrix = self.compute_correlation(&combined_outputs, correlation_type)?;
577 correlation_matrices.insert(correlation_type.clone(), corr_matrix);
578 }
579
580 let mut cross_task_correlations = HashMap::new();
582 if self.include_cross_task {
583 for (task1, &(start1, end1)) in &output_indices {
584 for (task2, &(start2, end2)) in &output_indices {
585 if task1 != task2 {
586 let task1_outputs = combined_outputs.slice(s![.., start1..end1]);
587 let task2_outputs = combined_outputs.slice(s![.., start2..end2]);
588 let cross_corr =
589 self.compute_cross_correlation(&task1_outputs, &task2_outputs)?;
590 cross_task_correlations.insert((task1.clone(), task2.clone()), cross_corr);
591 }
592 }
593 }
594 }
595
596 let mut within_task_correlations = HashMap::new();
598 if self.include_within_task {
599 for (task_name, &(start_idx, end_idx)) in &output_indices {
600 if end_idx - start_idx > 1 {
601 let task_outputs = combined_outputs.slice(s![.., start_idx..end_idx]);
603 let within_corr = self
604 .compute_correlation(&task_outputs.to_owned(), &CorrelationType::Pearson)?;
605 within_task_correlations.insert(task_name.clone(), within_corr);
606 }
607 }
608 }
609
610 let partial_correlations = if self.compute_partial_correlations {
612 let mut partial_corrs = HashMap::new();
613 for correlation_type in &self.correlation_types {
614 if let Ok(partial_corr) =
615 self.compute_partial_correlation(&combined_outputs, correlation_type)
616 {
617 partial_corrs.insert(correlation_type.clone(), partial_corr);
618 }
619 }
620 Some(partial_corrs)
621 } else {
622 None
623 };
624
625 Ok(CorrelationAnalysis {
626 correlation_matrices,
627 cross_task_correlations,
628 within_task_correlations,
629 partial_correlations,
630 output_info,
631 combined_outputs,
632 output_indices,
633 })
634 }
635
636 fn compute_correlation(
638 &self,
639 data: &Array2<Float>,
640 correlation_type: &CorrelationType,
641 ) -> SklResult<Array2<Float>> {
642 match correlation_type {
643 CorrelationType::Pearson => self.compute_pearson_correlation(data),
644 CorrelationType::Spearman => self.compute_spearman_correlation(data),
645 CorrelationType::Kendall => self.compute_kendall_correlation(data),
646 CorrelationType::MutualInformation => self.compute_mutual_information_matrix(data),
647 CorrelationType::DistanceCorrelation => self.compute_distance_correlation(data),
648 CorrelationType::CanonicalCorrelation => self.compute_canonical_correlation(data),
649 }
650 }
651
652 fn compute_pearson_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
654 let n_vars = data.ncols();
655 let n_samples = data.nrows();
656 let mut corr_matrix = Array2::eye(n_vars);
657
658 let means = data
660 .mean_axis(Axis(0))
661 .expect("array should have elements for mean computation");
662
663 let mut centered_data = data.clone();
665 for i in 0..n_samples {
666 for j in 0..n_vars {
667 centered_data[[i, j]] -= means[j];
668 }
669 }
670
671 for i in 0..n_vars {
673 for j in (i + 1)..n_vars {
674 let col_i = centered_data.column(i);
675 let col_j = centered_data.column(j);
676
677 let covariance = col_i.dot(&col_j) / (n_samples as Float - 1.0);
678 let var_i = col_i.dot(&col_i) / (n_samples as Float - 1.0);
679 let var_j = col_j.dot(&col_j) / (n_samples as Float - 1.0);
680
681 let correlation = if var_i > 0.0 && var_j > 0.0 {
682 covariance / (var_i.sqrt() * var_j.sqrt())
683 } else {
684 0.0
685 };
686
687 corr_matrix[[i, j]] = correlation;
688 corr_matrix[[j, i]] = correlation;
689 }
690 }
691
692 Ok(corr_matrix)
693 }
694
695 fn compute_spearman_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
697 let n_vars = data.ncols();
700 let mut ranked_data = Array2::<Float>::zeros(data.dim());
701
702 for j in 0..n_vars {
704 let mut column_data: Vec<(Float, usize)> = data
705 .column(j)
706 .iter()
707 .enumerate()
708 .map(|(i, &val)| (val, i))
709 .collect();
710 column_data.sort_by(|a, b| {
711 a.0.partial_cmp(&b.0)
712 .expect("matrix indexing should be valid")
713 });
714
715 for (rank, (_, original_idx)) in column_data.iter().enumerate() {
716 ranked_data[[*original_idx, j]] = rank as Float;
717 }
718 }
719
720 self.compute_pearson_correlation(&ranked_data)
722 }
723
724 fn compute_kendall_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
726 let n_vars = data.ncols();
729 let mut corr_matrix = Array2::eye(n_vars);
730
731 for i in 0..n_vars {
732 for j in (i + 1)..n_vars {
733 let spearman_corr = self.compute_spearman_correlation(data)?;
735 let kendall_approx = (2.0 / std::f64::consts::PI) * spearman_corr[[i, j]].asin();
736
737 corr_matrix[[i, j]] = kendall_approx;
738 corr_matrix[[j, i]] = kendall_approx;
739 }
740 }
741
742 Ok(corr_matrix)
743 }
744
745 fn compute_mutual_information_matrix(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
747 let n_vars = data.ncols();
748 let mut mi_matrix = Array2::<Float>::zeros((n_vars, n_vars));
749
750 for i in 0..n_vars {
753 for j in 0..n_vars {
754 if i == j {
755 mi_matrix[[i, j]] = 1.0; } else {
757 let pearson_corr = self.compute_pearson_correlation(data)?;
759 let mi_approx = -0.5 * (1.0 - pearson_corr[[i, j]].powi(2)).ln();
760 mi_matrix[[i, j]] = mi_approx.max(0.0);
761 }
762 }
763 }
764
765 Ok(mi_matrix)
766 }
767
768 fn compute_distance_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
770 let n_vars = data.ncols();
773 let mut dcorr_matrix = Array2::eye(n_vars);
774
775 for i in 0..n_vars {
776 for j in (i + 1)..n_vars {
777 let pearson_corr = self.compute_pearson_correlation(data)?;
779 let dcorr_approx = pearson_corr[[i, j]].abs();
780
781 dcorr_matrix[[i, j]] = dcorr_approx;
782 dcorr_matrix[[j, i]] = dcorr_approx;
783 }
784 }
785
786 Ok(dcorr_matrix)
787 }
788
789 fn compute_canonical_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
791 self.compute_pearson_correlation(data)
794 }
795
796 fn compute_cross_correlation(
798 &self,
799 data1: &ArrayView2<Float>,
800 data2: &ArrayView2<Float>,
801 ) -> SklResult<Array2<Float>> {
802 let n_outputs1 = data1.ncols();
803 let n_outputs2 = data2.ncols();
804 let n_samples = data1.nrows();
805
806 if data2.nrows() != n_samples {
807 return Err(SklearsError::ShapeMismatch {
808 expected: format!("{}", n_samples),
809 actual: format!("{}", data2.nrows()),
810 });
811 }
812
813 let mut cross_corr = Array2::<Float>::zeros((n_outputs1, n_outputs2));
814
815 let means1 = data1
817 .mean_axis(Axis(0))
818 .expect("array should have elements for mean computation");
819 let means2 = data2
820 .mean_axis(Axis(0))
821 .expect("array should have elements for mean computation");
822
823 for i in 0..n_outputs1 {
824 for j in 0..n_outputs2 {
825 let col1 = data1.column(i);
826 let col2 = data2.column(j);
827
828 let mut covariance = 0.0;
830 for k in 0..n_samples {
831 covariance += (col1[k] - means1[i]) * (col2[k] - means2[j]);
832 }
833 covariance /= n_samples as Float - 1.0;
834
835 let mut var1 = 0.0;
837 let mut var2 = 0.0;
838 for k in 0..n_samples {
839 var1 += (col1[k] - means1[i]).powi(2);
840 var2 += (col2[k] - means2[j]).powi(2);
841 }
842 var1 /= n_samples as Float - 1.0;
843 var2 /= n_samples as Float - 1.0;
844
845 let correlation = if var1 > 0.0 && var2 > 0.0 {
847 covariance / (var1.sqrt() * var2.sqrt())
848 } else {
849 0.0
850 };
851
852 cross_corr[[i, j]] = correlation;
853 }
854 }
855
856 Ok(cross_corr)
857 }
858
859 fn compute_partial_correlation(
861 &self,
862 data: &Array2<Float>,
863 _correlation_type: &CorrelationType,
864 ) -> SklResult<Array2<Float>> {
865 let corr_matrix = self.compute_pearson_correlation(data)?;
868 let n_vars = corr_matrix.nrows();
869
870 let mut partial_corr = Array2::eye(n_vars);
873
874 for i in 0..n_vars {
875 for j in (i + 1)..n_vars {
876 let partial = corr_matrix[[i, j]] * 0.8; partial_corr[[i, j]] = partial;
880 partial_corr[[j, i]] = partial;
881 }
882 }
883
884 Ok(partial_corr)
885 }
886}
887
888impl Default for OutputCorrelationAnalyzer {
889 fn default() -> Self {
890 Self::new()
891 }
892}
893
894impl DependencyGraphBuilder {
895 pub fn new() -> Self {
897 Self {
898 method: DependencyMethod::CorrelationThreshold(0.5),
899 include_self_loops: false,
900 directed: false,
901 max_dependencies: None,
902 }
903 }
904
905 pub fn method(mut self, method: DependencyMethod) -> Self {
907 self.method = method;
908 self
909 }
910
911 pub fn include_self_loops(mut self, include: bool) -> Self {
913 self.include_self_loops = include;
914 self
915 }
916
917 pub fn directed(mut self, directed: bool) -> Self {
919 self.directed = directed;
920 self
921 }
922
923 pub fn max_dependencies(mut self, max_deps: Option<usize>) -> Self {
925 self.max_dependencies = max_deps;
926 self
927 }
928
929 pub fn build(&self, outputs: &HashMap<String, Array2<Float>>) -> SklResult<DependencyGraph> {
931 let analyzer = OutputCorrelationAnalyzer::new()
933 .correlation_types(vec![CorrelationType::Pearson])
934 .include_cross_task(true);
935
936 let analysis = analyzer.analyze(outputs)?;
937
938 let correlation_matrix = analysis
940 .correlation_matrices
941 .get(&CorrelationType::Pearson)
942 .ok_or_else(|| {
943 SklearsError::InvalidInput("Failed to compute correlations".to_string())
944 })?;
945
946 let mut node_names = Vec::new();
948 for (task_name, &(start_idx, end_idx)) in &analysis.output_indices {
949 for i in start_idx..end_idx {
950 node_names.push(format!("{}_{}", task_name, i - start_idx));
951 }
952 }
953
954 let n_nodes = node_names.len();
955 let mut adjacency_matrix = Array2::<Float>::zeros((n_nodes, n_nodes));
956 let mut edge_weights = Array2::<Float>::zeros((n_nodes, n_nodes));
957
958 match &self.method {
960 DependencyMethod::CorrelationThreshold(threshold) => {
961 for i in 0..n_nodes {
962 for j in 0..n_nodes {
963 if i != j || self.include_self_loops {
964 let corr_strength = correlation_matrix[[i, j]].abs();
965 if corr_strength >= *threshold {
966 adjacency_matrix[[i, j]] = 1.0;
967 edge_weights[[i, j]] = corr_strength;
968
969 if !self.directed {
970 adjacency_matrix[[j, i]] = 1.0;
971 edge_weights[[j, i]] = corr_strength;
972 }
973 }
974 }
975 }
976 }
977 }
978 DependencyMethod::TopK(k) => {
979 for i in 0..n_nodes {
980 let mut correlations: Vec<(usize, Float)> = (0..n_nodes)
981 .filter(|&j| i != j || self.include_self_loops)
982 .map(|j| (j, correlation_matrix[[i, j]].abs()))
983 .collect();
984
985 correlations
986 .sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
987
988 for (j, corr_strength) in correlations.iter().take(*k) {
989 adjacency_matrix[[i, *j]] = 1.0;
990 edge_weights[[i, *j]] = *corr_strength;
991 }
992 }
993 }
994 _ => {
995 return Err(SklearsError::InvalidInput(
997 "Dependency method not yet implemented".to_string(),
998 ));
999 }
1000 }
1001
1002 if let Some(max_deps) = self.max_dependencies {
1004 for i in 0..n_nodes {
1005 let mut dependencies: Vec<(usize, Float)> = (0..n_nodes)
1006 .filter(|&j| adjacency_matrix[[i, j]] > 0.0)
1007 .map(|j| (j, edge_weights[[i, j]]))
1008 .collect();
1009
1010 dependencies
1011 .sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
1012
1013 for (idx, (j, _)) in dependencies.iter().enumerate() {
1015 if idx >= max_deps {
1016 adjacency_matrix[[i, *j]] = 0.0;
1017 edge_weights[[i, *j]] = 0.0;
1018 }
1019 }
1020 }
1021 }
1022
1023 let stats = self.compute_graph_statistics(&adjacency_matrix);
1025
1026 Ok(DependencyGraph {
1027 adjacency_matrix,
1028 node_names,
1029 edge_weights,
1030 directed: self.directed,
1031 stats,
1032 })
1033 }
1034
1035 fn compute_graph_statistics(&self, adjacency_matrix: &Array2<Float>) -> GraphStatistics {
1037 let n_nodes = adjacency_matrix.nrows();
1038 let num_edges = adjacency_matrix.sum() as usize;
1039
1040 let degrees: Vec<Float> = (0..n_nodes)
1042 .map(|i| adjacency_matrix.row(i).sum())
1043 .collect();
1044
1045 let average_degree = degrees.iter().sum::<Float>() / (n_nodes as Float);
1046
1047 let max_possible_edges = if self.directed {
1048 n_nodes * (n_nodes - 1)
1049 } else {
1050 n_nodes * (n_nodes - 1) / 2
1051 };
1052
1053 let density = if max_possible_edges > 0 {
1054 num_edges as Float / max_possible_edges as Float
1055 } else {
1056 0.0
1057 };
1058
1059 let clustering_coefficient = if !self.directed {
1061 self.compute_clustering_coefficient(adjacency_matrix)
1062 } else {
1063 0.0 };
1065
1066 GraphStatistics {
1067 num_nodes: n_nodes,
1068 num_edges,
1069 average_degree,
1070 density,
1071 clustering_coefficient,
1072 }
1073 }
1074
1075 fn compute_clustering_coefficient(&self, adjacency_matrix: &Array2<Float>) -> Float {
1077 let n_nodes = adjacency_matrix.nrows();
1078 let mut total_clustering = 0.0;
1079 let mut valid_nodes = 0;
1080
1081 for i in 0..n_nodes {
1082 let neighbors: Vec<usize> = (0..n_nodes)
1083 .filter(|&j| adjacency_matrix[[i, j]] > 0.0)
1084 .collect();
1085
1086 let degree = neighbors.len();
1087 if degree < 2 {
1088 continue; }
1090
1091 let mut triangles = 0;
1092 for &j in &neighbors {
1093 for &k in &neighbors {
1094 if j < k && adjacency_matrix[[j, k]] > 0.0 {
1095 triangles += 1;
1096 }
1097 }
1098 }
1099
1100 let possible_triangles = degree * (degree - 1) / 2;
1101 let clustering = if possible_triangles > 0 {
1102 triangles as Float / possible_triangles as Float
1103 } else {
1104 0.0
1105 };
1106
1107 total_clustering += clustering;
1108 valid_nodes += 1;
1109 }
1110
1111 if valid_nodes > 0 {
1112 total_clustering / valid_nodes as Float
1113 } else {
1114 0.0
1115 }
1116 }
1117}
1118
1119impl Default for DependencyGraphBuilder {
1120 fn default() -> Self {
1121 Self::new()
1122 }
1123}
1124
1125impl CorrelationAnalysis {
1126 pub fn get_correlation(
1128 &self,
1129 output1: &str,
1130 output2: &str,
1131 correlation_type: &CorrelationType,
1132 ) -> Option<Float> {
1133 let corr_matrix = self.correlation_matrices.get(correlation_type)?;
1134
1135 let mut output1_idx = None;
1137 let mut output2_idx = None;
1138 let mut current_idx = 0;
1139
1140 for (task_name, &(start_idx, end_idx)) in &self.output_indices {
1141 for i in start_idx..end_idx {
1142 let output_name = format!("{}_{}", task_name, i - start_idx);
1143 if output_name == output1 {
1144 output1_idx = Some(current_idx);
1145 }
1146 if output_name == output2 {
1147 output2_idx = Some(current_idx);
1148 }
1149 current_idx += 1;
1150 }
1151 }
1152
1153 if let (Some(idx1), Some(idx2)) = (output1_idx, output2_idx) {
1154 Some(corr_matrix[[idx1, idx2]])
1155 } else {
1156 None
1157 }
1158 }
1159
1160 pub fn get_strong_correlations(
1162 &self,
1163 correlation_type: &CorrelationType,
1164 threshold: Float,
1165 ) -> Vec<(String, String, Float)> {
1166 let mut strong_correlations = Vec::new();
1167
1168 if let Some(corr_matrix) = self.correlation_matrices.get(correlation_type) {
1169 let _current_idx = 0;
1170 let mut output_names = Vec::new();
1171
1172 for (task_name, &(start_idx, end_idx)) in &self.output_indices {
1174 for i in start_idx..end_idx {
1175 output_names.push(format!("{}_{}", task_name, i - start_idx));
1176 }
1177 }
1178
1179 for i in 0..output_names.len() {
1181 for j in (i + 1)..output_names.len() {
1182 let corr_value = corr_matrix[[i, j]];
1183 if corr_value.abs() >= threshold {
1184 strong_correlations.push((
1185 output_names[i].clone(),
1186 output_names[j].clone(),
1187 corr_value,
1188 ));
1189 }
1190 }
1191 }
1192 }
1193
1194 strong_correlations.sort_by(|a, b| {
1195 b.2.abs()
1196 .partial_cmp(&a.2.abs())
1197 .expect("operation should succeed")
1198 });
1199 strong_correlations
1200 }
1201
1202 pub fn correlation_summary(
1204 &self,
1205 correlation_type: &CorrelationType,
1206 ) -> Option<(Float, Float, Float, Float)> {
1207 if let Some(corr_matrix) = self.correlation_matrices.get(correlation_type) {
1208 let n = corr_matrix.nrows();
1209 let mut values = Vec::new();
1210
1211 for i in 0..n {
1213 for j in (i + 1)..n {
1214 values.push(corr_matrix[[i, j]]);
1215 }
1216 }
1217
1218 if values.is_empty() {
1219 return Some((0.0, 0.0, 0.0, 0.0));
1220 }
1221
1222 values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
1223
1224 let mean = values.iter().sum::<Float>() / values.len() as Float;
1225 let median = if values.len() % 2 == 0 {
1226 (values[values.len() / 2 - 1] + values[values.len() / 2]) / 2.0
1227 } else {
1228 values[values.len() / 2]
1229 };
1230 let min = values[0];
1231 let max = values[values.len() - 1];
1232
1233 Some((mean, median, min, max))
1234 } else {
1235 None
1236 }
1237 }
1238}
1239
1240impl DependencyGraph {
1241 pub fn get_neighbors(&self, node_name: &str) -> Vec<String> {
1243 if let Some(node_idx) = self.node_names.iter().position(|name| name == node_name) {
1244 let mut neighbors = Vec::new();
1245 for j in 0..self.node_names.len() {
1246 if self.adjacency_matrix[[node_idx, j]] > 0.0 {
1247 neighbors.push(self.node_names[j].clone());
1248 }
1249 }
1250 neighbors
1251 } else {
1252 Vec::new()
1253 }
1254 }
1255
1256 pub fn get_edge_weight(&self, node1: &str, node2: &str) -> Option<Float> {
1258 let idx1 = self.node_names.iter().position(|name| name == node1)?;
1259 let idx2 = self.node_names.iter().position(|name| name == node2)?;
1260
1261 if self.adjacency_matrix[[idx1, idx2]] > 0.0 {
1262 Some(self.edge_weights[[idx1, idx2]])
1263 } else {
1264 None
1265 }
1266 }
1267
1268 pub fn are_connected(&self, node1: &str, node2: &str) -> bool {
1270 self.get_edge_weight(node1, node2).is_some()
1271 }
1272
1273 pub fn get_degree(&self, node_name: &str) -> usize {
1275 if let Some(node_idx) = self.node_names.iter().position(|name| name == node_name) {
1276 self.adjacency_matrix.row(node_idx).sum() as usize
1277 } else {
1278 0
1279 }
1280 }
1281}
1282
1283#[allow(non_snake_case)]
1284#[cfg(test)]
1285mod correlation_tests {
1286 use super::*;
1287 use approx::assert_abs_diff_eq;
1288 use scirs2_core::ndarray::array;
1290
1291 #[test]
1292 fn test_correlation_analyzer_creation() {
1293 let analyzer = OutputCorrelationAnalyzer::new()
1294 .correlation_types(vec![CorrelationType::Pearson, CorrelationType::Spearman])
1295 .include_cross_task(true)
1296 .include_within_task(true)
1297 .min_correlation_threshold(0.1)
1298 .compute_partial_correlations(true);
1299
1300 assert_eq!(analyzer.correlation_types.len(), 2);
1301 assert!(analyzer.include_cross_task);
1302 assert!(analyzer.include_within_task);
1303 assert_abs_diff_eq!(analyzer.min_correlation_threshold, 0.1);
1304 assert!(analyzer.compute_partial_correlations);
1305 }
1306
1307 #[test]
1308 fn test_correlation_analysis() {
1309 let mut outputs = HashMap::new();
1310 outputs.insert(
1311 "task1".to_string(),
1312 array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 2.0]],
1313 );
1314 outputs.insert(
1315 "task2".to_string(),
1316 array![[0.5, 1.0], [1.0, 1.5], [1.5, 0.5], [2.0, 1.0]],
1317 );
1318
1319 let analyzer = OutputCorrelationAnalyzer::new()
1320 .correlation_types(vec![CorrelationType::Pearson])
1321 .include_cross_task(true)
1322 .include_within_task(true);
1323
1324 let analysis = analyzer
1325 .analyze(&outputs)
1326 .expect("operation should succeed");
1327
1328 assert!(analysis
1330 .correlation_matrices
1331 .contains_key(&CorrelationType::Pearson));
1332
1333 assert_eq!(analysis.combined_outputs.shape(), &[4, 4]); assert!(analysis.output_indices.contains_key("task1"));
1338 assert!(analysis.output_indices.contains_key("task2"));
1339
1340 assert!(analysis
1342 .cross_task_correlations
1343 .contains_key(&("task1".to_string(), "task2".to_string())));
1344
1345 assert!(analysis.within_task_correlations.contains_key("task1"));
1347 assert!(analysis.within_task_correlations.contains_key("task2"));
1348 }
1349
1350 #[test]
1351 fn test_dependency_graph_builder() {
1352 let mut outputs = HashMap::new();
1353 outputs.insert("task1".to_string(), array![[1.0], [2.0], [3.0], [4.0]]);
1354 outputs.insert("task2".to_string(), array![[0.5], [1.0], [1.5], [2.0]]);
1355 outputs.insert("task3".to_string(), array![[0.8], [1.2], [1.8], [2.4]]);
1356
1357 let builder = DependencyGraphBuilder::new()
1358 .method(DependencyMethod::CorrelationThreshold(0.5))
1359 .include_self_loops(false)
1360 .directed(false);
1361
1362 let graph = builder.build(&outputs).expect("operation should succeed");
1363
1364 assert_eq!(graph.node_names.len(), 3); assert!(!graph.directed);
1367 assert_eq!(graph.stats.num_nodes, 3);
1368 }
1369
1370 #[test]
1371 fn test_correlation_types() {
1372 let types = [
1373 CorrelationType::Pearson,
1374 CorrelationType::Spearman,
1375 CorrelationType::Kendall,
1376 CorrelationType::MutualInformation,
1377 CorrelationType::DistanceCorrelation,
1378 CorrelationType::CanonicalCorrelation,
1379 ];
1380
1381 assert_eq!(types.len(), 6);
1382 assert_eq!(types[0], CorrelationType::Pearson);
1383 }
1384
1385 #[test]
1386 fn test_dependency_methods() {
1387 let methods = [
1388 DependencyMethod::CorrelationThreshold(0.5),
1389 DependencyMethod::MutualInformationThreshold(0.3),
1390 DependencyMethod::CausalDiscovery,
1391 DependencyMethod::StatisticalSignificance(0.05),
1392 DependencyMethod::TopK(3),
1393 ];
1394
1395 assert_eq!(methods.len(), 5);
1396 assert_eq!(methods[0], DependencyMethod::CorrelationThreshold(0.5));
1397 }
1398
1399 #[test]
1400 fn test_correlation_analysis_accessors() {
1401 let mut outputs = HashMap::new();
1402 outputs.insert(
1403 "task1".to_string(),
1404 array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]],
1405 );
1406 outputs.insert(
1407 "task2".to_string(),
1408 array![[0.5, 1.0], [1.0, 1.5], [1.5, 0.5]],
1409 );
1410
1411 let analyzer = OutputCorrelationAnalyzer::new();
1412 let analysis = analyzer
1413 .analyze(&outputs)
1414 .expect("operation should succeed");
1415
1416 let corr = analysis.get_correlation("task1_0", "task1_1", &CorrelationType::Pearson);
1418 assert!(corr.is_some());
1419
1420 let strong_corrs = analysis.get_strong_correlations(&CorrelationType::Pearson, 0.1);
1422 assert!(!strong_corrs.is_empty());
1423
1424 let summary = analysis.correlation_summary(&CorrelationType::Pearson);
1426 assert!(summary.is_some());
1427 let (_mean, median, min, max) = summary.expect("operation should succeed");
1428 assert!(min <= median);
1429 assert!(median <= max);
1430 }
1431
1432 #[test]
1433 fn test_dependency_graph_accessors() {
1434 let mut outputs = HashMap::new();
1435 outputs.insert("task1".to_string(), array![[1.0], [2.0], [3.0]]);
1436 outputs.insert("task2".to_string(), array![[0.5], [1.0], [1.5]]);
1437
1438 let builder =
1439 DependencyGraphBuilder::new().method(DependencyMethod::CorrelationThreshold(0.1));
1440
1441 let graph = builder.build(&outputs).expect("operation should succeed");
1442
1443 let neighbors = graph.get_neighbors("task1_0");
1445 assert!(neighbors.len() <= 2); let degree = graph.get_degree("task1_0");
1449 assert!(degree <= 2);
1450
1451 let _connected = graph.are_connected("task1_0", "task2_0");
1453 }
1455}