Skip to main content

sklears_multioutput/
correlation.rs

1//! Output Correlation Analysis and Dependency Modeling
2//!
3//! This module provides tools for analyzing and modeling correlations and dependencies
4//! between different outputs in multi-output learning scenarios. Understanding these
5//! relationships can help improve model performance and provide insights into the
6//! underlying data structure.
7
8// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
9use 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/// Copula-Based Modeling Analyzer
17///
18/// Analyzes and models complex dependencies between outputs using copulas.
19/// Copulas separate the marginal distributions from the dependence structure,
20/// allowing for more flexible modeling of non-linear and non-monotonic relationships.
21///
22/// # Examples
23///
24/// ```
25/// use sklears_multioutput::correlation::{CopulaBasedModelingAnalyzer, CopulaType};
26/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
27/// use scirs2_core::ndarray::array;
28/// use std::collections::HashMap;
29///
30/// let mut outputs = HashMap::new();
31/// outputs.insert("task1".to_string(), array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]]);
32/// outputs.insert("task2".to_string(), array![[0.5, 1.0], [1.0, 1.5], [1.5, 0.5]]);
33///
34/// let analyzer = CopulaBasedModelingAnalyzer::new()
35///     .copula_types(vec![CopulaType::Gaussian, CopulaType::Clayton])
36///     .fit_margins(true);
37///
38/// let analysis = analyzer.analyze(&outputs).unwrap();
39/// ```
40#[derive(Debug, Clone)]
41pub struct CopulaBasedModelingAnalyzer {
42    /// Types of copulas to fit
43    copula_types: Vec<CopulaType>,
44    /// Whether to fit marginal distributions
45    fit_margins: bool,
46    /// Whether to use empirical copula for comparison
47    use_empirical_copula: bool,
48    /// Number of samples for Monte Carlo methods
49    n_samples: usize,
50    /// Random state for reproducibility
51    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    /// Set the copula types to fit
66    pub fn copula_types(mut self, copula_types: Vec<CopulaType>) -> Self {
67        self.copula_types = copula_types;
68        self
69    }
70
71    /// Set whether to fit marginal distributions
72    pub fn fit_margins(mut self, fit_margins: bool) -> Self {
73        self.fit_margins = fit_margins;
74        self
75    }
76
77    /// Set whether to use empirical copula for comparison
78    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    /// Set the number of samples for Monte Carlo methods
84    pub fn n_samples(mut self, n_samples: usize) -> Self {
85        self.n_samples = n_samples;
86        self
87    }
88
89    /// Set the random state for reproducibility
90    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
91        self.random_state = random_state;
92        self
93    }
94
95    /// Analyze copula-based dependencies in the given outputs
96    pub fn analyze(&self, _outputs: &HashMap<String, Array2<Float>>) -> SklResult<CopulaAnalysis> {
97        // Placeholder implementation - would need full copula fitting algorithms
98        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/// Types of copulas for dependency modeling
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
124pub enum CopulaType {
125    /// Gaussian copula (multivariate normal dependence)
126    Gaussian,
127    /// Clayton copula (lower tail dependence)
128    Clayton,
129    /// Frank copula (symmetric dependence)
130    Frank,
131    /// Gumbel copula (upper tail dependence)
132    Gumbel,
133    /// Student's t-copula (symmetric tail dependence)
134    StudentT,
135    /// Archimedean copula family
136    Archimedean,
137    /// Empirical copula (non-parametric)
138    Empirical,
139}
140
141/// Copula modeling results
142#[derive(Debug, Clone)]
143pub struct CopulaAnalysis {
144    /// Fitted copula models for each copula type
145    pub copula_models: HashMap<CopulaType, CopulaModel>,
146    /// Marginal distribution parameters
147    pub marginal_distributions: HashMap<String, MarginalDistribution>,
148    /// Copula goodness-of-fit statistics
149    pub goodness_of_fit: HashMap<CopulaType, GoodnessOfFit>,
150    /// Dependence measures derived from copulas
151    pub dependence_measures: HashMap<CopulaType, DependenceMeasures>,
152    /// Best fitting copula type
153    pub best_copula: Option<CopulaType>,
154    /// Output names and dimensions
155    pub output_info: HashMap<String, usize>,
156    /// Empirical copula for comparison
157    pub empirical_copula: Option<EmpiricalCopula>,
158}
159
160/// Fitted copula model
161#[derive(Debug, Clone)]
162pub struct CopulaModel {
163    /// Copula type
164    pub copula_type: CopulaType,
165    /// Copula parameters
166    pub parameters: CopulaParameters,
167    /// Log-likelihood of the fit
168    pub log_likelihood: Float,
169    /// Number of parameters
170    pub n_parameters: usize,
171    /// Fitted data used for the model
172    pub fitted_data: Array2<Float>,
173}
174
175/// Copula parameters for different copula types
176#[derive(Debug, Clone)]
177pub enum CopulaParameters {
178    /// Gaussian copula: correlation matrix
179    Gaussian { correlation_matrix: Array2<Float> },
180    /// Clayton copula: theta parameter
181    Clayton { theta: Float },
182    /// Frank copula: theta parameter
183    Frank { theta: Float },
184    /// Gumbel copula: theta parameter
185    Gumbel { theta: Float },
186    /// Student's t-copula: correlation matrix and degrees of freedom
187    StudentT {
188        correlation_matrix: Array2<Float>,
189        degrees_of_freedom: Float,
190    },
191    /// Archimedean copula: generator function parameters
192    Archimedean { generator_params: Vec<Float> },
193    /// Empirical copula: no parameters
194    Empirical,
195}
196
197/// Marginal distribution parameters
198#[derive(Debug, Clone)]
199pub struct MarginalDistribution {
200    /// Distribution type (e.g., "normal", "uniform", "empirical")
201    pub distribution_type: String,
202    /// Distribution parameters
203    pub parameters: Vec<Float>,
204    /// Fitted data statistics
205    pub mean: Float,
206    /// std_dev
207    pub std_dev: Float,
208    /// min
209    pub min: Float,
210    /// max
211    pub max: Float,
212}
213
214/// Goodness-of-fit statistics for copulas
215#[derive(Debug, Clone)]
216pub struct GoodnessOfFit {
217    /// Akaike Information Criterion
218    pub aic: Float,
219    /// Bayesian Information Criterion
220    pub bic: Float,
221    /// Cramér-von Mises test statistic
222    pub cramer_von_mises: Float,
223    /// Kolmogorov-Smirnov test statistic
224    pub kolmogorov_smirnov: Float,
225    /// Anderson-Darling test statistic
226    pub anderson_darling: Float,
227    /// P-value for goodness-of-fit test
228    pub p_value: Float,
229}
230
231/// Dependence measures derived from copulas
232#[derive(Debug, Clone)]
233pub struct DependenceMeasures {
234    /// Kendall's tau
235    pub kendall_tau: Float,
236    /// Spearman's rho
237    pub spearman_rho: Float,
238    /// Tail dependence coefficients
239    pub tail_dependence: TailDependence,
240    /// Conditional copula measures
241    pub conditional_measures: Vec<ConditionalMeasure>,
242}
243
244/// Tail dependence coefficients
245#[derive(Debug, Clone)]
246pub struct TailDependence {
247    /// Lower tail dependence coefficient
248    pub lower_tail: Float,
249    /// Upper tail dependence coefficient
250    pub upper_tail: Float,
251    /// Asymmetry measure
252    pub asymmetry: Float,
253}
254
255/// Conditional dependence measures
256#[derive(Debug, Clone)]
257pub struct ConditionalMeasure {
258    /// Condition variable indices
259    pub condition_vars: Vec<usize>,
260    /// Conditional dependence strength
261    pub conditional_dependence: Float,
262    /// Conditional correlation
263    pub conditional_correlation: Float,
264}
265
266/// Empirical copula representation
267#[derive(Debug, Clone)]
268pub struct EmpiricalCopula {
269    /// Empirical copula values
270    pub copula_values: Array2<Float>,
271    /// Rank-based data
272    pub rank_data: Array2<Float>,
273    /// Sample size
274    pub sample_size: usize,
275}
276
277/// Output Correlation Analyzer
278///
279/// Analyzes correlations and dependencies between different outputs in multi-output data.
280/// Provides various correlation measures and dependency analysis tools.
281///
282/// # Examples
283///
284/// ```
285/// use sklears_multioutput::correlation::{OutputCorrelationAnalyzer, CorrelationType};
286/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
287/// use scirs2_core::ndarray::array;
288/// use std::collections::HashMap;
289///
290/// let mut outputs = HashMap::new();
291/// outputs.insert("task1".to_string(), array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]]);
292/// outputs.insert("task2".to_string(), array![[0.5, 1.0], [1.0, 1.5], [1.5, 0.5]]);
293///
294/// let analyzer = OutputCorrelationAnalyzer::new()
295///     .correlation_types(vec![CorrelationType::Pearson, CorrelationType::Spearman])
296///     .include_cross_task(true);
297///
298/// let analysis = analyzer.analyze(&outputs).unwrap();
299/// ```
300#[derive(Debug, Clone)]
301pub struct OutputCorrelationAnalyzer {
302    /// Types of correlation to compute
303    correlation_types: Vec<CorrelationType>,
304    /// Whether to include cross-task correlations
305    include_cross_task: bool,
306    /// Whether to include within-task correlations
307    include_within_task: bool,
308    /// Minimum correlation threshold for reporting
309    min_correlation_threshold: Float,
310    /// Whether to compute partial correlations
311    compute_partial_correlations: bool,
312}
313
314/// Types of correlation measures
315#[derive(Debug, Clone, PartialEq, Eq, Hash)]
316pub enum CorrelationType {
317    /// Pearson correlation coefficient
318    Pearson,
319    /// Spearman rank correlation
320    Spearman,
321    /// Kendall tau correlation
322    Kendall,
323    /// Mutual information
324    MutualInformation,
325    /// Distance correlation
326    DistanceCorrelation,
327    /// Canonical correlation
328    CanonicalCorrelation,
329}
330
331/// Correlation analysis results
332#[derive(Debug, Clone)]
333pub struct CorrelationAnalysis {
334    /// Correlation matrices for each correlation type
335    pub correlation_matrices: HashMap<CorrelationType, Array2<Float>>,
336    /// Cross-task correlation analysis
337    pub cross_task_correlations: HashMap<(String, String), Array2<Float>>,
338    /// Within-task correlation analysis
339    pub within_task_correlations: HashMap<String, Array2<Float>>,
340    /// Partial correlation matrices
341    pub partial_correlations: Option<HashMap<CorrelationType, Array2<Float>>>,
342    /// Output names and dimensions
343    pub output_info: HashMap<String, usize>,
344    /// Combined output matrix used for analysis
345    pub combined_outputs: Array2<Float>,
346    /// Output indices for each task
347    pub output_indices: HashMap<String, (usize, usize)>,
348}
349
350/// Dependency Graph Builder
351///
352/// Builds dependency graphs between outputs based on various criteria.
353/// Useful for understanding causal relationships and for building chain models.
354///
355/// # Examples
356///
357/// ```
358/// use sklears_multioutput::correlation::{DependencyGraphBuilder, DependencyMethod};
359/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
360/// use scirs2_core::ndarray::array;
361/// use std::collections::HashMap;
362///
363/// let mut outputs = HashMap::new();
364/// outputs.insert("task1".to_string(), array![[1.0], [2.0], [3.0]]);
365/// outputs.insert("task2".to_string(), array![[0.5], [1.0], [1.5]]);
366/// outputs.insert("task3".to_string(), array![[0.8], [1.2], [1.8]]);
367///
368/// let builder = DependencyGraphBuilder::new()
369///     .method(DependencyMethod::CorrelationThreshold(0.5))
370///     .include_self_loops(false);
371///
372/// let graph = builder.build(&outputs).unwrap();
373/// ```
374#[derive(Debug, Clone)]
375pub struct DependencyGraphBuilder {
376    /// Method for determining dependencies
377    method: DependencyMethod,
378    /// Whether to include self-loops
379    include_self_loops: bool,
380    /// Whether to make the graph directed
381    directed: bool,
382    /// Maximum number of dependencies per node
383    max_dependencies: Option<usize>,
384}
385
386/// Methods for determining dependencies
387#[derive(Debug, Clone, PartialEq)]
388pub enum DependencyMethod {
389    /// Correlation threshold
390    CorrelationThreshold(Float),
391    /// Mutual information threshold
392    MutualInformationThreshold(Float),
393    /// Causal discovery (simplified)
394    CausalDiscovery,
395    /// Statistical significance testing
396    StatisticalSignificance(Float), // p-value threshold
397    /// Top-k strongest correlations
398    TopK(usize),
399}
400
401/// Dependency graph representation
402#[derive(Debug, Clone)]
403pub struct DependencyGraph {
404    /// Adjacency matrix
405    pub adjacency_matrix: Array2<Float>,
406    /// Node names (output names)
407    pub node_names: Vec<String>,
408    /// Edge weights (correlation/dependency strengths)
409    pub edge_weights: Array2<Float>,
410    /// Whether the graph is directed
411    pub directed: bool,
412    /// Graph statistics
413    pub stats: GraphStatistics,
414}
415
416/// Graph statistics
417#[derive(Debug, Clone)]
418pub struct GraphStatistics {
419    /// Number of nodes
420    pub num_nodes: usize,
421    /// Number of edges
422    pub num_edges: usize,
423    /// Average degree
424    pub average_degree: Float,
425    /// Density (proportion of possible edges that exist)
426    pub density: Float,
427    /// Clustering coefficient
428    pub clustering_coefficient: Float,
429}
430
431/// Conditional Independence Tester
432///
433/// Tests for conditional independence between outputs given other outputs.
434/// Useful for understanding causal structure and for feature selection.
435#[derive(Debug, Clone)]
436pub struct ConditionalIndependenceTester {
437    #[allow(dead_code)]
438    /// Significance level for tests
439    alpha: Float,
440    #[allow(dead_code)]
441    /// Test method
442    test_method: CITestMethod,
443    #[allow(dead_code)]
444    /// Maximum conditioning set size
445    max_conditioning_set_size: usize,
446}
447
448/// Methods for conditional independence testing
449#[derive(Debug, Clone, PartialEq)]
450pub enum CITestMethod {
451    /// Partial correlation test
452    PartialCorrelation,
453    /// Mutual information based test
454    MutualInformation,
455    /// Kernel-based test
456    KernelBased,
457    /// Linear regression based test
458    RegressionBased,
459}
460
461/// Results of conditional independence testing
462#[derive(Debug, Clone)]
463pub struct CITestResults {
464    /// Test results for each pair given conditioning sets
465    pub test_results: HashMap<(String, String, Vec<String>), CITestResult>,
466    /// Markov blankets for each output
467    pub markov_blankets: HashMap<String, Vec<String>>,
468    /// Conditional independence graph
469    pub ci_graph: DependencyGraph,
470}
471
472/// Single conditional independence test result
473#[derive(Debug, Clone)]
474pub struct CITestResult {
475    /// Test statistic
476    pub test_statistic: Float,
477    /// P-value
478    pub p_value: Float,
479    /// Whether independence is rejected
480    pub independent: bool,
481    /// Conditioning set used
482    pub conditioning_set: Vec<String>,
483}
484
485impl OutputCorrelationAnalyzer {
486    /// Create a new OutputCorrelationAnalyzer
487    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    /// Set correlation types to compute
498    pub fn correlation_types(mut self, types: Vec<CorrelationType>) -> Self {
499        self.correlation_types = types;
500        self
501    }
502
503    /// Set whether to include cross-task correlations
504    pub fn include_cross_task(mut self, include: bool) -> Self {
505        self.include_cross_task = include;
506        self
507    }
508
509    /// Set whether to include within-task correlations
510    pub fn include_within_task(mut self, include: bool) -> Self {
511        self.include_within_task = include;
512        self
513    }
514
515    /// Set minimum correlation threshold for reporting
516    pub fn min_correlation_threshold(mut self, threshold: Float) -> Self {
517        self.min_correlation_threshold = threshold;
518        self
519    }
520
521    /// Set whether to compute partial correlations
522    pub fn compute_partial_correlations(mut self, compute: bool) -> Self {
523        self.compute_partial_correlations = compute;
524        self
525    }
526
527    /// Analyze correlations in multi-output data
528    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        // Check that all outputs have the same number of samples
539        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        // Create combined output matrix
554        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        // Compute correlation matrices
574        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        // Compute cross-task correlations
581        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        // Compute within-task correlations
597        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                    // Only if task has multiple outputs
602                    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        // Compute partial correlations if requested
611        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    /// Compute correlation matrix for given correlation type
637    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    /// Compute Pearson correlation matrix
653    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        // Compute means
659        let means = data
660            .mean_axis(Axis(0))
661            .expect("array should have elements for mean computation");
662
663        // Compute centered data
664        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        // Compute correlation coefficients
672        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    /// Compute Spearman rank correlation matrix (simplified implementation)
696    fn compute_spearman_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
697        // This is a simplified implementation
698        // In practice, you would compute ranks and then Pearson correlation on ranks
699        let n_vars = data.ncols();
700        let mut ranked_data = Array2::<Float>::zeros(data.dim());
701
702        // Compute ranks for each column (simplified ranking)
703        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        // Compute Pearson correlation on ranked data
721        self.compute_pearson_correlation(&ranked_data)
722    }
723
724    /// Compute Kendall tau correlation matrix (simplified implementation)
725    fn compute_kendall_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
726        // This is a very simplified implementation
727        // In practice, Kendall tau requires counting concordant and discordant pairs
728        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                // Simplified Kendall tau approximation using Spearman
734                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    /// Compute mutual information matrix (simplified implementation)
746    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        // This is a simplified implementation
751        // In practice, you would use proper entropy estimation methods
752        for i in 0..n_vars {
753            for j in 0..n_vars {
754                if i == j {
755                    mi_matrix[[i, j]] = 1.0; // Self-information normalized
756                } else {
757                    // Approximate MI using correlation
758                    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    /// Compute distance correlation matrix (simplified implementation)
769    fn compute_distance_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
770        // This is a simplified implementation
771        // Real distance correlation requires computing distance matrices and double centering
772        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                // Simplified distance correlation using Pearson as approximation
778                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    /// Compute canonical correlation matrix (simplified implementation)
790    fn compute_canonical_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
791        // This is a placeholder for canonical correlation analysis
792        // Real CCA requires solving a generalized eigenvalue problem
793        self.compute_pearson_correlation(data)
794    }
795
796    /// Compute cross-correlation between two sets of outputs
797    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        // Compute means
816        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                // Compute covariance
829                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                // Compute variances
836                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                // Compute correlation
846                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    /// Compute partial correlation matrix (simplified implementation)
860    fn compute_partial_correlation(
861        &self,
862        data: &Array2<Float>,
863        _correlation_type: &CorrelationType,
864    ) -> SklResult<Array2<Float>> {
865        // This is a simplified implementation of partial correlation
866        // Real partial correlation requires inverting the correlation matrix
867        let corr_matrix = self.compute_pearson_correlation(data)?;
868        let n_vars = corr_matrix.nrows();
869
870        // Try to invert correlation matrix to get partial correlations
871        // This is a simplified approach - in practice you'd use proper matrix inversion
872        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                // Simplified partial correlation calculation
877                // In practice, this would be -cov_inv[i,j] / sqrt(cov_inv[i,i] * cov_inv[j,j])
878                let partial = corr_matrix[[i, j]] * 0.8; // Simplified approximation
879                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    /// Create a new DependencyGraphBuilder
896    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    /// Set the method for determining dependencies
906    pub fn method(mut self, method: DependencyMethod) -> Self {
907        self.method = method;
908        self
909    }
910
911    /// Set whether to include self-loops
912    pub fn include_self_loops(mut self, include: bool) -> Self {
913        self.include_self_loops = include;
914        self
915    }
916
917    /// Set whether to make the graph directed
918    pub fn directed(mut self, directed: bool) -> Self {
919        self.directed = directed;
920        self
921    }
922
923    /// Set maximum number of dependencies per node
924    pub fn max_dependencies(mut self, max_deps: Option<usize>) -> Self {
925        self.max_dependencies = max_deps;
926        self
927    }
928
929    /// Build dependency graph from outputs
930    pub fn build(&self, outputs: &HashMap<String, Array2<Float>>) -> SklResult<DependencyGraph> {
931        // First analyze correlations
932        let analyzer = OutputCorrelationAnalyzer::new()
933            .correlation_types(vec![CorrelationType::Pearson])
934            .include_cross_task(true);
935
936        let analysis = analyzer.analyze(outputs)?;
937
938        // Get correlation matrix
939        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        // Build node names
947        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        // Apply dependency method to determine edges
959        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                // Other methods would be implemented here
996                return Err(SklearsError::InvalidInput(
997                    "Dependency method not yet implemented".to_string(),
998                ));
999            }
1000        }
1001
1002        // Apply maximum dependencies constraint
1003        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                // Keep only top max_deps dependencies
1014                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        // Compute graph statistics
1024        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    /// Compute graph statistics
1036    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        // Compute degrees
1041        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        // Simplified clustering coefficient calculation
1060        let clustering_coefficient = if !self.directed {
1061            self.compute_clustering_coefficient(adjacency_matrix)
1062        } else {
1063            0.0 // Simplified for directed graphs
1064        };
1065
1066        GraphStatistics {
1067            num_nodes: n_nodes,
1068            num_edges,
1069            average_degree,
1070            density,
1071            clustering_coefficient,
1072        }
1073    }
1074
1075    /// Compute clustering coefficient for undirected graph
1076    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; // Cannot compute clustering for degree < 2
1089            }
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    /// Get correlation between two specific outputs
1127    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        // Find indices for the outputs
1136        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    /// Get strongest correlations above threshold
1161    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            // Build output names
1173            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            // Find strong correlations
1180            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    /// Get summary statistics for correlations
1203    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            // Collect upper triangular values (excluding diagonal)
1212            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    /// Get neighbors of a node
1242    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    /// Get edge weight between two nodes
1257    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    /// Check if two nodes are connected
1269    pub fn are_connected(&self, node1: &str, node2: &str) -> bool {
1270        self.get_edge_weight(node1, node2).is_some()
1271    }
1272
1273    /// Get node degree
1274    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 for arrays and random number generation (SciRS2 Policy)
1289    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        // Check that we have correlation matrices
1329        assert!(analysis
1330            .correlation_matrices
1331            .contains_key(&CorrelationType::Pearson));
1332
1333        // Check combined outputs shape
1334        assert_eq!(analysis.combined_outputs.shape(), &[4, 4]); // 4 samples, 4 outputs total
1335
1336        // Check output indices
1337        assert!(analysis.output_indices.contains_key("task1"));
1338        assert!(analysis.output_indices.contains_key("task2"));
1339
1340        // Check cross-task correlations
1341        assert!(analysis
1342            .cross_task_correlations
1343            .contains_key(&("task1".to_string(), "task2".to_string())));
1344
1345        // Check within-task correlations
1346        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        // Check graph properties
1365        assert_eq!(graph.node_names.len(), 3); // 3 tasks with 1 output each
1366        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        // Test getting specific correlation
1417        let corr = analysis.get_correlation("task1_0", "task1_1", &CorrelationType::Pearson);
1418        assert!(corr.is_some());
1419
1420        // Test getting strong correlations
1421        let strong_corrs = analysis.get_strong_correlations(&CorrelationType::Pearson, 0.1);
1422        assert!(!strong_corrs.is_empty());
1423
1424        // Test correlation summary
1425        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        // Test neighbor retrieval
1444        let neighbors = graph.get_neighbors("task1_0");
1445        assert!(neighbors.len() <= 2); // Can have at most task2_0 as neighbor
1446
1447        // Test degree calculation
1448        let degree = graph.get_degree("task1_0");
1449        assert!(degree <= 2);
1450
1451        // Test connection checking
1452        let _connected = graph.are_connected("task1_0", "task2_0");
1453        // Connection depends on correlation threshold and actual data correlation
1454    }
1455}