rust_igraph/algorithms/motifs/
mod.rs1pub mod graph_count;
9pub mod isoclass;
10pub mod motifs_randesu;
11pub mod triad_census;
12
13pub use graph_count::graph_count;
14pub use isoclass::{isoclass, isoclass_create, isoclass_subgraph};
15pub use motifs_randesu::{
16 motifs_randesu, motifs_randesu_callback, motifs_randesu_estimate, motifs_randesu_no,
17};
18pub use triad_census::{TriadCensus, TriadType, triad_census};
19
20use crate::core::{Graph, IgraphResult, VertexId};
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct DyadCensus {
25 pub mutual: f64,
27 pub asymmetric: f64,
29 pub null: f64,
31}
32
33pub fn dyad_census(graph: &Graph) -> IgraphResult<DyadCensus> {
64 let n = graph.vcount();
65
66 if n < 2 {
67 return Ok(DyadCensus {
68 mutual: 0.0,
69 asymmetric: 0.0,
70 null: 0.0,
71 });
72 }
73
74 if !graph.is_directed() {
75 return dyad_census_undirected(graph);
76 }
77
78 let (in_adj, out_adj) = build_directed_adj(graph)?;
79 let mut rec: f64 = 0.0;
80 let mut nonrec: f64 = 0.0;
81
82 for v in 0..n {
83 let in_neis = &in_adj[v as usize];
84 let out_neis = &out_adj[v as usize];
85
86 let mut ip = 0;
87 let mut op = 0;
88
89 while ip < in_neis.len() && op < out_neis.len() {
90 match in_neis[ip].cmp(&out_neis[op]) {
91 std::cmp::Ordering::Less => {
92 nonrec += 1.0;
93 ip += 1;
94 }
95 std::cmp::Ordering::Greater => {
96 nonrec += 1.0;
97 op += 1;
98 }
99 std::cmp::Ordering::Equal => {
100 rec += 1.0;
101 ip += 1;
102 op += 1;
103 }
104 }
105 }
106 #[allow(clippy::cast_precision_loss)]
107 {
108 nonrec += (in_neis.len() - ip) as f64 + (out_neis.len() - op) as f64;
109 }
110 }
111
112 let mutual = rec / 2.0;
113 let asymmetric = nonrec / 2.0;
114 #[allow(clippy::cast_precision_loss)]
115 let total_dyads = 0.5 * (f64::from(n)) * ((f64::from(n)) - 1.0);
116 let null = total_dyads - mutual - asymmetric;
117
118 Ok(DyadCensus {
119 mutual,
120 asymmetric,
121 null: if null == 0.0 { 0.0 } else { null },
122 })
123}
124
125fn dyad_census_undirected(graph: &Graph) -> IgraphResult<DyadCensus> {
126 let n = graph.vcount();
127 let ecount = graph.ecount();
128
129 let mut edge_set = std::collections::HashSet::new();
131 for eid in 0..ecount {
132 #[allow(clippy::cast_possible_truncation)]
133 let (src, tgt) = graph.edge(eid as u32)?;
134 if src != tgt {
135 let key = if src < tgt { (src, tgt) } else { (tgt, src) };
136 edge_set.insert(key);
137 }
138 }
139
140 #[allow(clippy::cast_precision_loss)]
141 let mutual = edge_set.len() as f64;
142 #[allow(clippy::cast_precision_loss)]
143 let total_dyads = 0.5 * (f64::from(n)) * ((f64::from(n)) - 1.0);
144 let null = total_dyads - mutual;
145
146 Ok(DyadCensus {
147 mutual,
148 asymmetric: 0.0,
149 null: if null == 0.0 { 0.0 } else { null },
150 })
151}
152
153type AdjPair = (Vec<Vec<VertexId>>, Vec<Vec<VertexId>>);
154
155fn build_directed_adj(graph: &Graph) -> IgraphResult<AdjPair> {
157 let n = graph.vcount() as usize;
158 let ecount = graph.ecount();
159 let mut in_adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
160 let mut out_adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
161
162 for eid in 0..ecount {
163 #[allow(clippy::cast_possible_truncation)]
164 let (src, tgt) = graph.edge(eid as u32)?;
165 if src != tgt {
166 out_adj[src as usize].push(tgt);
167 in_adj[tgt as usize].push(src);
168 }
169 }
170
171 for neighbors in &mut in_adj {
172 neighbors.sort_unstable();
173 neighbors.dedup();
174 }
175 for neighbors in &mut out_adj {
176 neighbors.sort_unstable();
177 neighbors.dedup();
178 }
179
180 Ok((in_adj, out_adj))
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_empty_graph() {
189 let g = Graph::with_vertices(0);
190 let dc = dyad_census(&g).unwrap();
191 assert!((dc.mutual).abs() < 1e-10);
192 assert!((dc.asymmetric).abs() < 1e-10);
193 assert!((dc.null).abs() < 1e-10);
194 }
195
196 #[test]
197 fn test_single_vertex() {
198 let g = Graph::new(1, true).unwrap();
199 let dc = dyad_census(&g).unwrap();
200 assert!((dc.mutual).abs() < 1e-10);
201 assert!((dc.asymmetric).abs() < 1e-10);
202 assert!((dc.null).abs() < 1e-10);
203 }
204
205 #[test]
206 fn test_two_vertices_no_edges() {
207 let g = Graph::new(2, true).unwrap();
208 let dc = dyad_census(&g).unwrap();
209 assert!((dc.mutual).abs() < 1e-10);
210 assert!((dc.asymmetric).abs() < 1e-10);
211 assert!((dc.null - 1.0).abs() < 1e-10);
212 }
213
214 #[test]
215 fn test_two_vertices_one_edge() {
216 let mut g = Graph::new(2, true).unwrap();
217 g.add_edge(0, 1).unwrap();
218 let dc = dyad_census(&g).unwrap();
219 assert!((dc.mutual).abs() < 1e-10);
220 assert!((dc.asymmetric - 1.0).abs() < 1e-10);
221 assert!((dc.null).abs() < 1e-10);
222 }
223
224 #[test]
225 fn test_two_vertices_mutual() {
226 let mut g = Graph::new(2, true).unwrap();
227 g.add_edge(0, 1).unwrap();
228 g.add_edge(1, 0).unwrap();
229 let dc = dyad_census(&g).unwrap();
230 assert!((dc.mutual - 1.0).abs() < 1e-10);
231 assert!((dc.asymmetric).abs() < 1e-10);
232 assert!((dc.null).abs() < 1e-10);
233 }
234
235 #[test]
236 fn test_three_vertices_cycle() {
237 let mut g = Graph::new(3, true).unwrap();
239 g.add_edge(0, 1).unwrap();
240 g.add_edge(1, 2).unwrap();
241 g.add_edge(2, 0).unwrap();
242 let dc = dyad_census(&g).unwrap();
243 assert!((dc.mutual).abs() < 1e-10);
244 assert!((dc.asymmetric - 3.0).abs() < 1e-10);
245 assert!((dc.null).abs() < 1e-10);
246 }
247
248 #[test]
249 fn test_complete_directed_mutual() {
250 let mut g = Graph::new(4, true).unwrap();
252 for i in 0..4u32 {
253 for j in 0..4 {
254 if i != j {
255 g.add_edge(i, j).unwrap();
256 }
257 }
258 }
259 let dc = dyad_census(&g).unwrap();
260 assert!((dc.mutual - 6.0).abs() < 1e-10);
261 assert!((dc.asymmetric).abs() < 1e-10);
262 assert!((dc.null).abs() < 1e-10);
263 }
264
265 #[test]
266 fn test_mixed() {
267 let mut g = Graph::new(4, true).unwrap();
272 g.add_edge(0, 1).unwrap();
273 g.add_edge(1, 0).unwrap();
274 g.add_edge(2, 3).unwrap();
275 let dc = dyad_census(&g).unwrap();
276 assert!((dc.mutual - 1.0).abs() < 1e-10);
277 assert!((dc.asymmetric - 1.0).abs() < 1e-10);
278 assert!((dc.null - 4.0).abs() < 1e-10);
279 }
280
281 #[test]
282 fn test_self_loops_ignored() {
283 let mut g = Graph::new(3, true).unwrap();
284 g.add_edge(0, 0).unwrap();
285 g.add_edge(0, 1).unwrap();
286 g.add_edge(1, 1).unwrap();
287 let dc = dyad_census(&g).unwrap();
288 assert!((dc.mutual).abs() < 1e-10);
290 assert!((dc.asymmetric - 1.0).abs() < 1e-10);
291 assert!((dc.null - 2.0).abs() < 1e-10);
292 }
293
294 #[test]
295 fn test_multi_edges_deduplicated() {
296 let mut g = Graph::new(2, true).unwrap();
297 g.add_edge(0, 1).unwrap();
298 g.add_edge(0, 1).unwrap();
299 g.add_edge(0, 1).unwrap();
300 let dc = dyad_census(&g).unwrap();
301 assert!((dc.mutual).abs() < 1e-10);
303 assert!((dc.asymmetric - 1.0).abs() < 1e-10);
304 assert!((dc.null).abs() < 1e-10);
305 }
306
307 #[test]
308 fn test_undirected_all_mutual() {
309 let mut g = Graph::with_vertices(4);
311 g.add_edge(0, 1).unwrap();
312 g.add_edge(1, 2).unwrap();
313 g.add_edge(2, 3).unwrap();
314 let dc = dyad_census(&g).unwrap();
315 assert!((dc.mutual - 3.0).abs() < 1e-10);
316 assert!((dc.asymmetric).abs() < 1e-10);
317 assert!((dc.null - 3.0).abs() < 1e-10);
318 }
319
320 #[test]
321 fn test_undirected_complete() {
322 let mut g = Graph::with_vertices(5);
323 for i in 0..5u32 {
324 for j in (i + 1)..5 {
325 g.add_edge(i, j).unwrap();
326 }
327 }
328 let dc = dyad_census(&g).unwrap();
329 assert!((dc.mutual - 10.0).abs() < 1e-10);
330 assert!((dc.asymmetric).abs() < 1e-10);
331 assert!((dc.null).abs() < 1e-10);
332 }
333
334 #[test]
335 fn test_counts_sum_to_total() {
336 let mut g = Graph::new(5, true).unwrap();
337 g.add_edge(0, 1).unwrap();
338 g.add_edge(1, 0).unwrap();
339 g.add_edge(2, 3).unwrap();
340 g.add_edge(3, 4).unwrap();
341 g.add_edge(4, 2).unwrap();
342 let dc = dyad_census(&g).unwrap();
343 let total = dc.mutual + dc.asymmetric + dc.null;
344 assert!((total - 10.0).abs() < 1e-10);
346 }
347}