1use crate::core::{Graph, IgraphError, IgraphResult};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum TriadType {
16 T003 = 0,
18 T012 = 1,
20 T102 = 2,
22 T021D = 3,
24 T021U = 4,
26 T021C = 5,
28 T111D = 6,
30 T111U = 7,
32 T030T = 8,
34 T030C = 9,
36 T201 = 10,
38 T120D = 11,
40 T120U = 12,
42 T120C = 13,
44 T210 = 14,
46 T300 = 15,
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct TriadCensus {
53 pub counts: [f64; 16],
55}
56
57impl TriadCensus {
58 pub fn get(&self, triad_type: TriadType) -> f64 {
73 self.counts[triad_type as usize]
74 }
75}
76
77pub fn triad_census(graph: &Graph) -> IgraphResult<TriadCensus> {
109 let n = graph.vcount();
110
111 if n < 3 {
112 return Ok(TriadCensus { counts: [0.0; 16] });
113 }
114
115 let adj = build_dyad_matrix(graph)?;
116 let mut counts = [0.0_f64; 16];
117
118 for i in 0..n {
119 for j in (i + 1)..n {
120 for k in (j + 1)..n {
121 let ab = adj[(i as usize) * (n as usize) + (j as usize)];
122 let ac = adj[(i as usize) * (n as usize) + (k as usize)];
123 let bc = adj[(j as usize) * (n as usize) + (k as usize)];
124 let idx = lookup_triad_type(ab, ac, bc);
125 counts[idx] += 1.0;
126 }
127 }
128 }
129
130 Ok(TriadCensus { counts })
131}
132
133fn build_dyad_matrix(graph: &Graph) -> IgraphResult<Vec<u8>> {
139 let n = graph.vcount();
140 let size = (n as usize)
141 .checked_mul(n as usize)
142 .ok_or_else(|| IgraphError::InvalidArgument("graph too large for triad census".into()))?;
143 let mut matrix = vec![0u8; size];
144 let nn = n as usize;
145 let ecount = graph.ecount();
146
147 for eid in 0..ecount {
148 #[allow(clippy::cast_possible_truncation)]
149 let (src, tgt) = graph.edge(eid as u32)?;
150 if src == tgt {
151 continue;
152 }
153 let idx_st = (src as usize) * nn + (tgt as usize);
154 let idx_ts = (tgt as usize) * nn + (src as usize);
155 matrix[idx_st] |= 1;
156 matrix[idx_ts] |= 2;
157 }
158
159 if !graph.is_directed() {
160 for cell in &mut matrix {
162 if *cell != 0 {
163 *cell = 3;
164 }
165 }
166 }
167
168 Ok(matrix)
169}
170
171fn lookup_triad_type(ab: u8, ac: u8, bc: u8) -> usize {
177 let mut m = 0u8;
179 let mut a = 0u8;
180 let mut n_count = 0u8;
181
182 for &d in &[ab, ac, bc] {
183 match d {
184 0 => n_count += 1,
185 3 => m += 1,
186 _ => a += 1,
187 }
188 }
189
190 match (m, a, n_count) {
191 (0, 1, 2) => 1, (1, 0, 2) => 2, (0, 2, 1) => classify_021(ab, ac, bc),
194 (1, 1, 1) => classify_111(ab, ac, bc),
195 (0, 3, 0) => classify_030(ab, ac, bc),
196 (2, 0, 1) => 10, (1, 2, 0) => classify_120(ab, ac, bc),
198 (2, 1, 0) => 14, (3, 0, 0) => 15, _ => 0, }
202}
203
204fn classify_021(ab: u8, ac: u8, bc: u8) -> usize {
209 let (from_center_1, from_center_2) = if bc == 0 {
212 (ab, ac)
214 } else if ac == 0 {
215 (flip_dyad(ab), bc)
217 } else {
218 (flip_dyad(ac), flip_dyad(bc))
220 };
221
222 match (from_center_1, from_center_2) {
224 (1, 1) => 3, (2, 2) => 4, _ => 5, }
228}
229
230fn classify_111(ab: u8, ac: u8, bc: u8) -> usize {
234 let asym_from_mutual_vertex = if ab == 3 {
237 if ac != 0 { ac } else { bc }
240 } else if ac == 3 {
241 if ab != 0 { ab } else { flip_dyad(bc) }
244 } else {
245 if ab != 0 {
248 flip_dyad(ab)
249 } else {
250 flip_dyad(ac)
251 }
252 };
253
254 if asym_from_mutual_vertex == 1 {
256 7 } else {
258 6 }
260}
261
262fn classify_030(ab: u8, ac: u8, bc: u8) -> usize {
266 let mut out_a = 0u8;
268 let mut out_b = 0u8;
269 let mut out_c = 0u8;
270
271 if ab == 1 {
272 out_a += 1;
273 } else {
274 out_b += 1;
275 }
276 if ac == 1 {
277 out_a += 1;
278 } else {
279 out_c += 1;
280 }
281 if bc == 1 {
282 out_b += 1;
283 } else {
284 out_c += 1;
285 }
286
287 if out_a == 2 || out_b == 2 || out_c == 2 {
288 8 } else {
290 9 }
292}
293
294fn classify_120(ab: u8, ac: u8, bc: u8) -> usize {
299 let (to_third_1, to_third_2) = if ab == 3 {
302 (ac, bc)
304 } else if ac == 3 {
305 (ab, flip_dyad(bc))
307 } else {
308 (flip_dyad(ab), flip_dyad(ac))
310 };
311
312 match (to_third_1, to_third_2) {
314 (2, 2) => 11, (1, 1) => 12, _ => 13, }
318}
319
320fn flip_dyad(d: u8) -> u8 {
322 match d {
323 1 => 2,
324 2 => 1,
325 other => other,
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_empty_graph() {
335 let g = Graph::new(0, true).unwrap();
336 let tc = triad_census(&g).unwrap();
337 assert!(tc.counts.iter().all(|&c| c.abs() < 1e-10));
338 }
339
340 #[test]
341 fn test_two_vertices() {
342 let g = Graph::new(2, true).unwrap();
343 let tc = triad_census(&g).unwrap();
344 assert!(tc.counts.iter().all(|&c| c.abs() < 1e-10));
345 }
346
347 #[test]
348 fn test_three_vertices_no_edges() {
349 let g = Graph::new(3, true).unwrap();
350 let tc = triad_census(&g).unwrap();
351 assert!((tc.get(TriadType::T003) - 1.0).abs() < 1e-10);
352 assert!(tc.counts[1..].iter().all(|&c| c.abs() < 1e-10));
353 }
354
355 #[test]
356 fn test_single_edge() {
357 let mut g = Graph::new(3, true).unwrap();
358 g.add_edge(0, 1).unwrap();
359 let tc = triad_census(&g).unwrap();
360 assert!((tc.get(TriadType::T012) - 1.0).abs() < 1e-10);
361 assert!((tc.get(TriadType::T003)).abs() < 1e-10);
362 }
363
364 #[test]
365 fn test_mutual_edge() {
366 let mut g = Graph::new(3, true).unwrap();
367 g.add_edge(0, 1).unwrap();
368 g.add_edge(1, 0).unwrap();
369 let tc = triad_census(&g).unwrap();
370 assert!((tc.get(TriadType::T102) - 1.0).abs() < 1e-10);
371 }
372
373 #[test]
374 fn test_directed_3_cycle() {
375 let mut g = Graph::new(3, true).unwrap();
376 g.add_edge(0, 1).unwrap();
377 g.add_edge(1, 2).unwrap();
378 g.add_edge(2, 0).unwrap();
379 let tc = triad_census(&g).unwrap();
380 assert!((tc.get(TriadType::T030C) - 1.0).abs() < 1e-10);
381 }
382
383 #[test]
384 fn test_transitive_triple() {
385 let mut g = Graph::new(3, true).unwrap();
386 g.add_edge(0, 1).unwrap();
387 g.add_edge(0, 2).unwrap();
388 g.add_edge(1, 2).unwrap();
389 let tc = triad_census(&g).unwrap();
390 assert!((tc.get(TriadType::T030T) - 1.0).abs() < 1e-10);
391 }
392
393 #[test]
394 fn test_complete_directed() {
395 let mut g = Graph::new(3, true).unwrap();
396 for i in 0..3u32 {
397 for j in 0..3u32 {
398 if i != j {
399 g.add_edge(i, j).unwrap();
400 }
401 }
402 }
403 let tc = triad_census(&g).unwrap();
404 assert!((tc.get(TriadType::T300) - 1.0).abs() < 1e-10);
405 }
406
407 #[test]
408 fn test_021d_out_star() {
409 let mut g = Graph::new(3, true).unwrap();
411 g.add_edge(1, 0).unwrap();
412 g.add_edge(1, 2).unwrap();
413 let tc = triad_census(&g).unwrap();
414 assert!((tc.get(TriadType::T021D) - 1.0).abs() < 1e-10);
415 }
416
417 #[test]
418 fn test_021u_in_star() {
419 let mut g = Graph::new(3, true).unwrap();
421 g.add_edge(0, 1).unwrap();
422 g.add_edge(2, 1).unwrap();
423 let tc = triad_census(&g).unwrap();
424 assert!((tc.get(TriadType::T021U) - 1.0).abs() < 1e-10);
425 }
426
427 #[test]
428 fn test_021c_chain() {
429 let mut g = Graph::new(3, true).unwrap();
431 g.add_edge(0, 1).unwrap();
432 g.add_edge(1, 2).unwrap();
433 let tc = triad_census(&g).unwrap();
434 assert!((tc.get(TriadType::T021C) - 1.0).abs() < 1e-10);
435 }
436
437 #[test]
438 fn test_four_vertices_sum() {
439 let mut g = Graph::new(4, true).unwrap();
441 g.add_edge(0, 1).unwrap();
442 g.add_edge(1, 2).unwrap();
443 g.add_edge(2, 0).unwrap();
444 let tc = triad_census(&g).unwrap();
445 let total: f64 = tc.counts.iter().sum();
446 assert!((total - 4.0).abs() < 1e-10);
447 }
448
449 #[test]
450 fn test_201_two_mutual() {
451 let mut g = Graph::new(3, true).unwrap();
453 g.add_edge(0, 1).unwrap();
454 g.add_edge(1, 0).unwrap();
455 g.add_edge(0, 2).unwrap();
456 g.add_edge(2, 0).unwrap();
457 let tc = triad_census(&g).unwrap();
458 assert!((tc.get(TriadType::T201) - 1.0).abs() < 1e-10);
459 }
460
461 #[test]
462 fn test_undirected_triangle() {
463 let mut g = Graph::with_vertices(3);
464 g.add_edge(0, 1).unwrap();
465 g.add_edge(1, 2).unwrap();
466 g.add_edge(0, 2).unwrap();
467 let tc = triad_census(&g).unwrap();
468 assert!((tc.get(TriadType::T300) - 1.0).abs() < 1e-10);
469 }
470
471 #[test]
472 fn test_undirected_path() {
473 let mut g = Graph::with_vertices(3);
475 g.add_edge(0, 1).unwrap();
476 g.add_edge(1, 2).unwrap();
477 let tc = triad_census(&g).unwrap();
478 assert!((tc.get(TriadType::T201) - 1.0).abs() < 1e-10);
479 }
480
481 #[test]
482 fn test_counts_sum_to_total() {
483 let mut g = Graph::new(5, true).unwrap();
485 g.add_edge(0, 1).unwrap();
486 g.add_edge(1, 2).unwrap();
487 g.add_edge(2, 3).unwrap();
488 g.add_edge(3, 4).unwrap();
489 g.add_edge(4, 0).unwrap();
490 let tc = triad_census(&g).unwrap();
491 let total: f64 = tc.counts.iter().sum();
492 assert!((total - 10.0).abs() < 1e-10);
493 }
494
495 #[test]
496 fn test_111d() {
497 let mut g = Graph::new(3, true).unwrap();
499 g.add_edge(0, 1).unwrap();
500 g.add_edge(1, 0).unwrap();
501 g.add_edge(2, 1).unwrap();
502 let tc = triad_census(&g).unwrap();
503 assert!((tc.get(TriadType::T111D) - 1.0).abs() < 1e-10);
504 }
505
506 #[test]
507 fn test_111u() {
508 let mut g = Graph::new(3, true).unwrap();
510 g.add_edge(0, 1).unwrap();
511 g.add_edge(1, 0).unwrap();
512 g.add_edge(1, 2).unwrap();
513 let tc = triad_census(&g).unwrap();
514 assert!((tc.get(TriadType::T111U) - 1.0).abs() < 1e-10);
515 }
516
517 #[test]
518 fn test_210() {
519 let mut g = Graph::new(3, true).unwrap();
522 g.add_edge(1, 2).unwrap();
523 g.add_edge(2, 1).unwrap();
524 g.add_edge(0, 2).unwrap();
525 g.add_edge(2, 0).unwrap();
526 g.add_edge(0, 1).unwrap();
527 let tc = triad_census(&g).unwrap();
528 assert!((tc.get(TriadType::T210) - 1.0).abs() < 1e-10);
529 }
530
531 #[test]
532 fn test_self_loops_ignored() {
533 let mut g = Graph::new(3, true).unwrap();
534 g.add_edge(0, 0).unwrap();
535 g.add_edge(0, 1).unwrap();
536 let tc = triad_census(&g).unwrap();
537 assert!((tc.get(TriadType::T012) - 1.0).abs() < 1e-10);
538 }
539
540 #[test]
541 fn test_120d() {
542 let mut g = Graph::new(3, true).unwrap();
546 g.add_edge(0, 2).unwrap();
547 g.add_edge(2, 0).unwrap();
548 g.add_edge(1, 0).unwrap();
549 g.add_edge(1, 2).unwrap();
550 let tc = triad_census(&g).unwrap();
551 assert!((tc.get(TriadType::T120D) - 1.0).abs() < 1e-10);
552 }
553
554 #[test]
555 fn test_120u() {
556 let mut g = Graph::new(3, true).unwrap();
559 g.add_edge(0, 2).unwrap();
560 g.add_edge(2, 0).unwrap();
561 g.add_edge(0, 1).unwrap();
562 g.add_edge(2, 1).unwrap();
563 let tc = triad_census(&g).unwrap();
564 assert!((tc.get(TriadType::T120U) - 1.0).abs() < 1e-10);
565 }
566
567 #[test]
568 fn test_120c() {
569 let mut g = Graph::new(3, true).unwrap();
572 g.add_edge(0, 2).unwrap();
573 g.add_edge(2, 0).unwrap();
574 g.add_edge(0, 1).unwrap();
575 g.add_edge(1, 2).unwrap();
576 let tc = triad_census(&g).unwrap();
577 assert!((tc.get(TriadType::T120C) - 1.0).abs() < 1e-10);
578 }
579}