1use scirs2_core::ndarray::Array2;
8
9pub fn edge_conservation(
18 mapping: &[(usize, usize)],
19 adj1: &Array2<f64>,
20 adj2: &Array2<f64>,
21) -> f64 {
22 if mapping.is_empty() {
23 return 0.0;
24 }
25
26 let n1 = adj1.nrows();
27
28 let mut g1_to_g2 = vec![usize::MAX; n1];
30 for &(i, j) in mapping {
31 if i < n1 {
32 g1_to_g2[i] = j;
33 }
34 }
35
36 let mut total_edges = 0u64;
37 let mut preserved_edges = 0u64;
38
39 for &(u, _) in mapping {
41 for &(v, _) in mapping {
42 if u >= v {
43 continue; }
45 if u < adj1.nrows() && v < adj1.nrows() && adj1[[u, v]].abs() > f64::EPSILON {
46 total_edges += 1;
47 let fu = g1_to_g2[u];
48 let fv = g1_to_g2[v];
49 if fu < adj2.nrows() && fv < adj2.ncols() && adj2[[fu, fv]].abs() > f64::EPSILON {
50 preserved_edges += 1;
51 }
52 }
53 }
54 }
55
56 if total_edges == 0 {
57 return 0.0;
58 }
59
60 preserved_edges as f64 / total_edges as f64
61}
62
63pub fn symmetric_substructure_score(
75 mapping: &[(usize, usize)],
76 adj1: &Array2<f64>,
77 adj2: &Array2<f64>,
78) -> f64 {
79 if mapping.is_empty() {
80 return 0.0;
81 }
82
83 let n1 = adj1.nrows();
84 let n2 = adj2.nrows();
85
86 let mut g1_to_g2 = vec![usize::MAX; n1];
88 for &(i, j) in mapping {
89 if i < n1 {
90 g1_to_g2[i] = j;
91 }
92 }
93
94 let mut mapped_g2: Vec<bool> = vec![false; n2];
96 for &(_, j) in mapping {
97 if j < n2 {
98 mapped_g2[j] = true;
99 }
100 }
101
102 let mut edges_g1 = 0u64;
104 for &(u, _) in mapping {
105 for &(v, _) in mapping {
106 if u < v && u < adj1.nrows() && v < adj1.nrows() && adj1[[u, v]].abs() > f64::EPSILON {
107 edges_g1 += 1;
108 }
109 }
110 }
111
112 let mut edges_g2 = 0u64;
114 for &(_, ju) in mapping {
115 for &(_, jv) in mapping {
116 if ju < jv
117 && ju < adj2.nrows()
118 && jv < adj2.nrows()
119 && adj2[[ju, jv]].abs() > f64::EPSILON
120 {
121 edges_g2 += 1;
122 }
123 }
124 }
125
126 let mut preserved = 0u64;
128 for &(u, _) in mapping {
129 for &(v, _) in mapping {
130 if u < v && u < adj1.nrows() && v < adj1.nrows() && adj1[[u, v]].abs() > f64::EPSILON {
131 let fu = g1_to_g2[u];
132 let fv = g1_to_g2[v];
133 if fu < adj2.nrows() && fv < adj2.nrows() && adj2[[fu, fv]].abs() > f64::EPSILON {
134 preserved += 1;
135 }
136 }
137 }
138 }
139
140 let denominator = edges_g1 + edges_g2 - preserved;
141 if denominator == 0 {
142 return 0.0;
143 }
144
145 preserved as f64 / denominator as f64
146}
147
148pub fn node_correctness(mapping: &[(usize, usize)], ground_truth: &[(usize, usize)]) -> f64 {
158 if ground_truth.is_empty() {
159 return 0.0;
160 }
161
162 let max_node = ground_truth.iter().map(|&(i, _)| i).max().unwrap_or(0);
164 let mut gt_map = vec![usize::MAX; max_node + 1];
165 for &(i, j) in ground_truth {
166 if i <= max_node {
167 gt_map[i] = j;
168 }
169 }
170
171 let mut correct = 0usize;
172 for &(i, j) in mapping {
173 if i < gt_map.len() && gt_map[i] == j {
174 correct += 1;
175 }
176 }
177
178 correct as f64 / ground_truth.len() as f64
179}
180
181pub fn induced_conserved_structure(
193 mapping: &[(usize, usize)],
194 adj1: &Array2<f64>,
195 adj2: &Array2<f64>,
196) -> f64 {
197 if mapping.is_empty() {
198 return 0.0;
199 }
200
201 let n1 = adj1.nrows();
202
203 let mut g1_to_g2 = vec![usize::MAX; n1];
205 for &(i, j) in mapping {
206 if i < n1 {
207 g1_to_g2[i] = j;
208 }
209 }
210
211 let mut edges_g2 = 0u64;
213 for &(_, ju) in mapping {
214 for &(_, jv) in mapping {
215 if ju < jv
216 && ju < adj2.nrows()
217 && jv < adj2.nrows()
218 && adj2[[ju, jv]].abs() > f64::EPSILON
219 {
220 edges_g2 += 1;
221 }
222 }
223 }
224
225 if edges_g2 == 0 {
226 return 0.0;
227 }
228
229 let mut preserved = 0u64;
231 for &(u, _) in mapping {
232 for &(v, _) in mapping {
233 if u < v && u < adj1.nrows() && v < adj1.nrows() && adj1[[u, v]].abs() > f64::EPSILON {
234 let fu = g1_to_g2[u];
235 let fv = g1_to_g2[v];
236 if fu < adj2.nrows() && fv < adj2.nrows() && adj2[[fu, fv]].abs() > f64::EPSILON {
237 preserved += 1;
238 }
239 }
240 }
241 }
242
243 preserved as f64 / edges_g2 as f64
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use scirs2_core::ndarray::Array2;
250
251 fn path_graph(n: usize) -> Array2<f64> {
252 let mut adj = Array2::zeros((n, n));
253 for i in 0..n.saturating_sub(1) {
254 adj[[i, i + 1]] = 1.0;
255 adj[[i + 1, i]] = 1.0;
256 }
257 adj
258 }
259
260 #[test]
261 fn test_ec_identity_mapping() {
262 let adj = path_graph(5);
263 let identity: Vec<(usize, usize)> = (0..5).map(|i| (i, i)).collect();
264 let ec = edge_conservation(&identity, &adj, &adj);
265 assert!(
266 (ec - 1.0).abs() < 1e-10,
267 "Identity mapping on same graph should have EC=1.0, got {}",
268 ec
269 );
270 }
271
272 #[test]
273 fn test_ec_random_worse_than_identity() {
274 let adj = path_graph(5);
275 let identity: Vec<(usize, usize)> = (0..5).map(|i| (i, i)).collect();
276 let reversed: Vec<(usize, usize)> = (0..5).map(|i| (i, 4 - i)).collect();
278
279 let ec_id = edge_conservation(&identity, &adj, &adj);
280 let ec_rev = edge_conservation(&reversed, &adj, &adj);
281
282 assert!(ec_id >= 0.0);
286 assert!(ec_rev >= 0.0);
287 }
288
289 #[test]
290 fn test_ec_empty_mapping() {
291 let adj = path_graph(3);
292 let ec = edge_conservation(&[], &adj, &adj);
293 assert!((ec).abs() < f64::EPSILON);
294 }
295
296 #[test]
297 fn test_ec_no_edges() {
298 let adj = Array2::zeros((3, 3));
299 let mapping: Vec<(usize, usize)> = vec![(0, 0), (1, 1), (2, 2)];
300 let ec = edge_conservation(&mapping, &adj, &adj);
301 assert!((ec).abs() < f64::EPSILON);
302 }
303
304 #[test]
305 fn test_s3_range() {
306 let adj = path_graph(5);
307 let mapping: Vec<(usize, usize)> = (0..5).map(|i| (i, i)).collect();
308 let s3 = symmetric_substructure_score(&mapping, &adj, &adj);
309 assert!(
310 (0.0..=1.0).contains(&s3),
311 "S3 should be in [0, 1], got {}",
312 s3
313 );
314 }
315
316 #[test]
317 fn test_s3_identity() {
318 let adj = path_graph(4);
319 let identity: Vec<(usize, usize)> = (0..4).map(|i| (i, i)).collect();
320 let s3 = symmetric_substructure_score(&identity, &adj, &adj);
321 assert!(
322 (s3 - 1.0).abs() < 1e-10,
323 "S3 of identity on same graph should be 1.0, got {}",
324 s3
325 );
326 }
327
328 #[test]
329 fn test_s3_empty() {
330 let adj = path_graph(3);
331 let s3 = symmetric_substructure_score(&[], &adj, &adj);
332 assert!((s3).abs() < f64::EPSILON);
333 }
334
335 #[test]
336 fn test_node_correctness_identity() {
337 let gt: Vec<(usize, usize)> = (0..5).map(|i| (i, i)).collect();
338 let mapping = gt.clone();
339 let nc = node_correctness(&mapping, >);
340 assert!((nc - 1.0).abs() < 1e-10);
341 }
342
343 #[test]
344 fn test_node_correctness_none_correct() {
345 let gt: Vec<(usize, usize)> = vec![(0, 0), (1, 1), (2, 2)];
346 let mapping: Vec<(usize, usize)> = vec![(0, 2), (1, 0), (2, 1)];
347 let nc = node_correctness(&mapping, >);
348 assert!((nc).abs() < f64::EPSILON);
349 }
350
351 #[test]
352 fn test_node_correctness_partial() {
353 let gt: Vec<(usize, usize)> = vec![(0, 0), (1, 1), (2, 2), (3, 3)];
354 let mapping: Vec<(usize, usize)> = vec![(0, 0), (1, 2), (2, 2), (3, 1)];
355 let nc = node_correctness(&mapping, >);
356 assert!((nc - 0.5).abs() < 1e-10); }
358
359 #[test]
360 fn test_node_correctness_empty_gt() {
361 let nc = node_correctness(&[(0, 0)], &[]);
362 assert!((nc).abs() < f64::EPSILON);
363 }
364
365 #[test]
366 fn test_ics_range() {
367 let adj = path_graph(5);
368 let mapping: Vec<(usize, usize)> = (0..5).map(|i| (i, i)).collect();
369 let ics = induced_conserved_structure(&mapping, &adj, &adj);
370 assert!(
371 (0.0..=1.0).contains(&ics),
372 "ICS should be in [0, 1], got {}",
373 ics
374 );
375 }
376
377 #[test]
378 fn test_ics_identity() {
379 let adj = path_graph(4);
380 let identity: Vec<(usize, usize)> = (0..4).map(|i| (i, i)).collect();
381 let ics = induced_conserved_structure(&identity, &adj, &adj);
382 assert!(
383 (ics - 1.0).abs() < 1e-10,
384 "ICS of identity on same graph should be 1.0, got {}",
385 ics
386 );
387 }
388
389 #[test]
390 fn test_ics_empty() {
391 let adj = path_graph(3);
392 let ics = induced_conserved_structure(&[], &adj, &adj);
393 assert!((ics).abs() < f64::EPSILON);
394 }
395}