rust_igraph/algorithms/constructors/
full_citation.rs1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
28
29pub fn full_citation(n: u32, directed: bool) -> IgraphResult<Graph> {
59 if n == 0 {
60 return Graph::new(0, directed);
61 }
62 if n == 1 {
63 return Graph::new(1, directed);
64 }
65
66 let n_us = usize::try_from(n).map_err(|_| {
67 IgraphError::InvalidArgument(format!("full_citation: n {n} cannot fit usize"))
68 })?;
69 let prod = n_us.checked_mul(n_us - 1).ok_or_else(|| {
72 IgraphError::InvalidArgument(format!("full_citation: n·(n−1) overflows usize (n = {n})"))
73 })?;
74 let ecount = prod / 2;
75
76 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
77 for i in 1..n {
80 for j in 0..i {
81 edges.push((i, j));
82 }
83 }
84 debug_assert_eq!(edges.len(), ecount);
85
86 let mut g = Graph::new(n, directed)?;
87 g.add_edges(edges)?;
88 Ok(g)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use std::collections::BTreeSet;
95
96 fn dump_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
97 let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
98 (0..ec)
99 .map(|e| g.edge(e).expect("edge id in range"))
100 .collect()
101 }
102
103 fn canonical_undirected(g: &Graph) -> BTreeSet<(VertexId, VertexId)> {
104 dump_edges(g)
105 .into_iter()
106 .map(|(u, v)| if u <= v { (u, v) } else { (v, u) })
107 .collect()
108 }
109
110 #[test]
111 fn null_graph_zero_vertices() {
112 let g = full_citation(0, true).expect("ok");
113 assert_eq!(g.vcount(), 0);
114 assert_eq!(g.ecount(), 0);
115 assert!(g.is_directed());
116 }
117
118 #[test]
119 fn singleton_no_edges() {
120 let g = full_citation(1, true).expect("ok");
121 assert_eq!(g.vcount(), 1);
122 assert_eq!(g.ecount(), 0);
123 }
124
125 #[test]
126 fn directed_4_emission_order_matches_upstream() {
127 let g = full_citation(4, true).expect("ok");
130 assert_eq!(g.vcount(), 4);
131 assert_eq!(g.ecount(), 6);
132 assert!(g.is_directed());
133 let want = vec![(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)];
134 assert_eq!(dump_edges(&g), want);
135 }
136
137 fn in_out_counts(g: &Graph) -> (Vec<usize>, Vec<usize>) {
138 let n = g.vcount() as usize;
139 let mut in_count = vec![0usize; n];
140 let mut out_count = vec![0usize; n];
141 for (u, v) in dump_edges(g) {
142 out_count[u as usize] += 1;
143 in_count[v as usize] += 1;
144 }
145 (in_count, out_count)
146 }
147
148 #[test]
149 fn directed_5_in_out_degree_profile() {
150 let n = 5u32;
154 let g = full_citation(n, true).expect("ok");
155 let (in_d, out_d) = in_out_counts(&g);
156 for k in 0..n {
157 let want_in = (n - 1 - k) as usize;
158 let want_out = k as usize;
159 assert_eq!(out_d[k as usize], want_out, "vertex {k} out-degree");
160 assert_eq!(in_d[k as usize], want_in, "vertex {k} in-degree");
161 }
162 }
163
164 #[test]
165 fn undirected_5_multiset_matches_k5() {
166 let g = full_citation(5, false).expect("ok");
167 assert_eq!(g.vcount(), 5);
168 assert_eq!(g.ecount(), 10);
169 let want: BTreeSet<(u32, u32)> = (0..5u32)
170 .flat_map(|i| ((i + 1)..5u32).map(move |j| (i, j)))
171 .collect();
172 assert_eq!(canonical_undirected(&g), want);
173 }
174
175 #[test]
176 fn undirected_10_matches_k10_count() {
177 let g = full_citation(10, false).expect("ok");
178 assert_eq!(g.ecount(), 45);
179 }
180
181 #[test]
182 fn no_self_loops_in_directed() {
183 let g = full_citation(8, true).expect("ok");
184 for (u, v) in dump_edges(&g) {
185 assert_ne!(u, v, "directed full_citation must be loop-free");
186 }
187 }
188
189 #[test]
190 fn no_self_loops_in_undirected() {
191 let g = full_citation(8, false).expect("ok");
192 for (u, v) in dump_edges(&g) {
193 assert_ne!(u, v, "undirected full_citation must be loop-free");
194 }
195 }
196
197 #[test]
198 fn directed_arcs_are_canonically_descending() {
199 let g = full_citation(7, true).expect("ok");
201 for (u, v) in dump_edges(&g) {
202 assert!(u > v, "arc ({u}, {v}) violates citation invariant");
203 }
204 }
205
206 #[test]
207 fn directed_ecount_closed_form() {
208 for n in 0..=12u32 {
209 let g = full_citation(n, true).expect("ok");
210 let want = if n == 0 {
211 0
212 } else {
213 (n as usize) * (n as usize - 1) / 2
214 };
215 assert_eq!(g.ecount(), want, "n = {n}");
216 }
217 }
218
219 #[test]
220 fn undirected_ecount_matches_directed() {
221 for n in 0..=12u32 {
222 let du = full_citation(n, false).expect("ok");
223 let dd = full_citation(n, true).expect("ok");
224 assert_eq!(du.ecount(), dd.ecount(), "n = {n}");
225 }
226 }
227
228 #[test]
229 fn directed_flag_round_trips() {
230 assert!(full_citation(5, true).expect("ok").is_directed());
231 assert!(!full_citation(5, false).expect("ok").is_directed());
232 }
233
234 #[cfg(all(test, feature = "proptest-harness"))]
235 mod prop {
236 use super::*;
237 use proptest::prelude::*;
238
239 proptest! {
240 #![proptest_config(ProptestConfig::with_cases(64))]
241
242 #[test]
243 fn ecount_is_triangular_for_directed(n in 0u32..=40) {
244 let g = full_citation(n, true).expect("ok");
245 let want = (n as usize) * (n as usize).saturating_sub(1) / 2;
246 prop_assert_eq!(g.ecount(), want);
247 }
248
249 #[test]
250 fn ecount_is_triangular_for_undirected(n in 0u32..=40) {
251 let g = full_citation(n, false).expect("ok");
252 let want = (n as usize) * (n as usize).saturating_sub(1) / 2;
253 prop_assert_eq!(g.ecount(), want);
254 }
255
256 #[test]
257 fn vcount_round_trips(n in 0u32..=80) {
258 let g = full_citation(n, true).expect("ok");
259 prop_assert_eq!(g.vcount(), n);
260 }
261
262 #[test]
263 fn directed_arcs_descending(n in 0u32..=40) {
264 let g = full_citation(n, true).expect("ok");
265 let ec = u32::try_from(g.ecount()).expect("ec");
266 for e in 0..ec {
267 let (u, v) = g.edge(e).expect("edge");
268 prop_assert!(u > v, "non-descending arc ({}, {})", u, v);
269 }
270 }
271
272 #[test]
273 fn in_degree_profile_directed(n in 1u32..=40) {
274 let g = full_citation(n, true).expect("ok");
275 let (in_d, out_d) = in_out_counts(&g);
276 for k in 0..n {
277 let want_in = (n - 1 - k) as usize;
278 let want_out = k as usize;
279 prop_assert_eq!(in_d[k as usize], want_in);
280 prop_assert_eq!(out_d[k as usize], want_out);
281 }
282 }
283
284 #[test]
285 fn undirected_is_simple_no_loops(n in 0u32..=40) {
286 let g = full_citation(n, false).expect("ok");
287 let ec = u32::try_from(g.ecount()).expect("ec");
288 let mut seen = std::collections::HashSet::new();
289 for e in 0..ec {
290 let (u, v) = g.edge(e).expect("edge");
291 prop_assert_ne!(u, v, "self loop ({}, {}) emitted", u, v);
292 let key = if u <= v { (u, v) } else { (v, u) };
293 prop_assert!(seen.insert(key), "parallel edge {:?}", key);
294 }
295 }
296 }
297 }
298}