Skip to main content

scirs2_graph/alignment/
types.rs

1//! Types for network alignment algorithms.
2//!
3//! This module provides core data structures for configuring and storing
4//! results of network alignment operations, including similarity matrices
5//! between nodes of two graphs.
6
7use scirs2_core::ndarray::Array2;
8
9use crate::error::{GraphError, Result};
10
11/// Configuration for network alignment algorithms.
12///
13/// Controls the trade-off between topology and prior knowledge,
14/// convergence criteria, and GRASP meta-heuristic parameters.
15#[derive(Debug, Clone)]
16pub struct AlignmentConfig {
17    /// Weight for topology vs. prior similarity (0.0 = only prior, 1.0 = only topology).
18    /// Default: 0.6
19    pub alpha: f64,
20    /// Maximum number of power iterations for IsoRank. Default: 100
21    pub max_iter: usize,
22    /// Convergence tolerance for power iteration. Default: 1e-8
23    pub tolerance: f64,
24    /// Size of the restricted candidate list for GRASP construction. Default: 5
25    pub greedy_candidates: usize,
26    /// Maximum number of local search iterations in GRASP. Default: 50
27    pub local_search_depth: usize,
28}
29
30impl Default for AlignmentConfig {
31    fn default() -> Self {
32        Self {
33            alpha: 0.6,
34            max_iter: 100,
35            tolerance: 1e-8,
36            greedy_candidates: 5,
37            local_search_depth: 50,
38        }
39    }
40}
41
42/// Result of a network alignment computation.
43///
44/// Contains the node mapping between two graphs along with quality metrics.
45#[derive(Debug, Clone)]
46pub struct AlignmentResult {
47    /// Pairs of aligned nodes: `(node_in_g1, node_in_g2)`.
48    pub mapping: Vec<(usize, usize)>,
49    /// Overall alignment quality score (higher is better).
50    pub score: f64,
51    /// Fraction of edges in G1 that are preserved in the alignment to G2.
52    pub edge_conservation: f64,
53    /// Whether the algorithm converged within the specified tolerance.
54    pub converged: bool,
55    /// Number of iterations performed.
56    pub iterations: usize,
57}
58
59/// Similarity matrix between nodes of two graphs.
60///
61/// Stores an `[n1 x n2]` matrix where entry `(i, j)` represents the similarity
62/// between node `i` in graph 1 and node `j` in graph 2.
63#[derive(Debug, Clone)]
64pub struct SimilarityMatrix {
65    data: Array2<f64>,
66    n1: usize,
67    n2: usize,
68}
69
70impl SimilarityMatrix {
71    /// Create a new similarity matrix initialized to uniform values `1 / (n1 * n2)`.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if either dimension is zero.
76    pub fn new(n1: usize, n2: usize) -> Result<Self> {
77        if n1 == 0 || n2 == 0 {
78            return Err(GraphError::InvalidParameter {
79                param: "dimensions".to_string(),
80                value: format!("({}, {})", n1, n2),
81                expected: "both dimensions > 0".to_string(),
82                context: "SimilarityMatrix::new".to_string(),
83            });
84        }
85        let val = 1.0 / (n1 as f64 * n2 as f64);
86        let data = Array2::from_elem((n1, n2), val);
87        Ok(Self { data, n1, n2 })
88    }
89
90    /// Create a similarity matrix from a prior similarity array.
91    ///
92    /// The prior is normalized so that all entries sum to 1.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the prior has zero dimensions or all-zero entries.
97    pub fn from_prior(prior: Array2<f64>) -> Result<Self> {
98        let shape = prior.shape();
99        let n1 = shape[0];
100        let n2 = shape[1];
101        if n1 == 0 || n2 == 0 {
102            return Err(GraphError::InvalidParameter {
103                param: "prior dimensions".to_string(),
104                value: format!("({}, {})", n1, n2),
105                expected: "both dimensions > 0".to_string(),
106                context: "SimilarityMatrix::from_prior".to_string(),
107            });
108        }
109        let mut sm = Self {
110            data: prior,
111            n1,
112            n2,
113        };
114        sm.normalize();
115        Ok(sm)
116    }
117
118    /// Get the similarity between node `i` in G1 and node `j` in G2.
119    ///
120    /// Returns 0.0 if indices are out of bounds.
121    pub fn get(&self, i: usize, j: usize) -> f64 {
122        if i < self.n1 && j < self.n2 {
123            self.data[[i, j]]
124        } else {
125            0.0
126        }
127    }
128
129    /// Set the similarity between node `i` in G1 and node `j` in G2.
130    ///
131    /// Does nothing if indices are out of bounds.
132    pub fn set(&mut self, i: usize, j: usize, value: f64) {
133        if i < self.n1 && j < self.n2 {
134            self.data[[i, j]] = value;
135        }
136    }
137
138    /// Normalize the matrix so that all entries sum to 1.
139    ///
140    /// If the sum is zero, sets all entries to uniform `1 / (n1 * n2)`.
141    pub fn normalize(&mut self) {
142        let sum: f64 = self.data.iter().sum();
143        if sum.abs() < f64::EPSILON {
144            let val = 1.0 / (self.n1 as f64 * self.n2 as f64);
145            self.data.fill(val);
146        } else {
147            self.data /= sum;
148        }
149    }
150
151    /// Return a reference to the underlying array.
152    pub fn as_array(&self) -> &Array2<f64> {
153        &self.data
154    }
155
156    /// Return the number of rows (nodes in G1).
157    pub fn n1(&self) -> usize {
158        self.n1
159    }
160
161    /// Return the number of columns (nodes in G2).
162    pub fn n2(&self) -> usize {
163        self.n2
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use scirs2_core::ndarray::array;
171
172    #[test]
173    fn test_alignment_config_default() {
174        let cfg = AlignmentConfig::default();
175        assert!((cfg.alpha - 0.6).abs() < f64::EPSILON);
176        assert_eq!(cfg.max_iter, 100);
177        assert!((cfg.tolerance - 1e-8).abs() < f64::EPSILON);
178        assert_eq!(cfg.greedy_candidates, 5);
179        assert_eq!(cfg.local_search_depth, 50);
180    }
181
182    #[test]
183    fn test_similarity_matrix_new() {
184        let sm = SimilarityMatrix::new(3, 4).expect("should create matrix");
185        let expected = 1.0 / 12.0;
186        for i in 0..3 {
187            for j in 0..4 {
188                assert!((sm.get(i, j) - expected).abs() < f64::EPSILON);
189            }
190        }
191    }
192
193    #[test]
194    fn test_similarity_matrix_zero_dim() {
195        assert!(SimilarityMatrix::new(0, 5).is_err());
196        assert!(SimilarityMatrix::new(5, 0).is_err());
197    }
198
199    #[test]
200    fn test_similarity_matrix_from_prior() {
201        let prior = array![[1.0, 2.0], [3.0, 4.0]];
202        let sm = SimilarityMatrix::from_prior(prior).expect("should create from prior");
203        let sum: f64 = sm.as_array().iter().sum();
204        assert!((sum - 1.0).abs() < 1e-12);
205    }
206
207    #[test]
208    fn test_similarity_matrix_set_get() {
209        let mut sm = SimilarityMatrix::new(2, 2).expect("should create matrix");
210        sm.set(0, 1, 0.99);
211        assert!((sm.get(0, 1) - 0.99).abs() < f64::EPSILON);
212        // Out-of-bounds returns 0.0
213        assert!((sm.get(10, 10)).abs() < f64::EPSILON);
214    }
215
216    #[test]
217    fn test_similarity_matrix_normalize_zero() {
218        let prior = Array2::zeros((3, 3));
219        let sm = SimilarityMatrix::from_prior(prior).expect("should create from zero prior");
220        let expected = 1.0 / 9.0;
221        assert!((sm.get(0, 0) - expected).abs() < 1e-12);
222    }
223}