1use crate::algorithms::constructors::full_multipartite::{
35 FullMultipartite, MultipartiteMode, full_multipartite,
36};
37use crate::core::{IgraphError, IgraphResult};
38
39pub fn turan(n: u32, r: u32) -> IgraphResult<FullMultipartite> {
84 if n == 0 {
85 return full_multipartite(&[], false, MultipartiteMode::All);
86 }
87
88 if r == 0 {
89 return Err(IgraphError::InvalidArgument(
90 "turan: number of partitions must be positive when n > 0".into(),
91 ));
92 }
93
94 let r_effective = r.min(n);
95 let quotient = n / r_effective;
96 let remainder = n % r_effective;
97
98 let mut partitions: Vec<u32> = Vec::with_capacity(r_effective as usize);
103 let big = quotient.checked_add(1).ok_or_else(|| {
104 IgraphError::InvalidArgument("turan: partition size n/r + 1 overflows u32".into())
105 })?;
106 for i in 0..r_effective {
107 if i < remainder {
108 partitions.push(big);
109 } else {
110 partitions.push(quotient);
111 }
112 }
113
114 full_multipartite(&partitions, false, MultipartiteMode::All)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::collections::BTreeSet;
121
122 fn canonical_edges(g: &crate::core::Graph) -> BTreeSet<(u32, u32)> {
123 let m = u32::try_from(g.ecount()).expect("ecount fits u32");
124 (0..m)
125 .map(|e| g.edge(e).expect("edge id in bounds"))
126 .map(|(u, v)| if u <= v { (u, v) } else { (v, u) })
127 .collect()
128 }
129
130 #[test]
131 fn empty_n_zero_returns_empty_graph() {
132 let r = turan(0, 10).expect("ok");
134 assert_eq!(r.graph.vcount(), 0);
135 assert_eq!(r.graph.ecount(), 0);
136 assert!(!r.graph.is_directed());
137 assert!(r.types.is_empty());
138 }
139
140 #[test]
141 fn empty_n_zero_with_r_one_also_empty() {
142 let r = turan(0, 1).expect("ok");
144 assert_eq!(r.graph.vcount(), 0);
145 assert!(r.types.is_empty());
146 }
147
148 #[test]
149 fn empty_n_zero_with_r_zero_still_empty_no_error() {
150 let r = turan(0, 0).expect("ok");
153 assert_eq!(r.graph.vcount(), 0);
154 assert!(r.types.is_empty());
155 }
156
157 #[test]
158 fn r_zero_with_positive_n_is_error() {
159 let err = turan(5, 0).expect_err("must reject r == 0 when n > 0");
160 match err {
161 IgraphError::InvalidArgument(_) => {}
162 other => panic!("expected InvalidArgument, got {other:?}"),
163 }
164 }
165
166 #[test]
167 fn r_eq_one_is_n_isolated_vertices() {
168 let r = turan(10, 1).expect("ok");
170 assert_eq!(r.graph.vcount(), 10);
171 assert_eq!(r.graph.ecount(), 0);
172 assert_eq!(r.types, vec![0; 10]);
173 }
174
175 #[test]
176 fn r_exceeds_n_caps_at_n_yielding_kn() {
177 let r = turan(4, 6).expect("ok");
179 assert_eq!(r.graph.vcount(), 4);
180 assert_eq!(r.graph.ecount(), 6);
181 assert_eq!(r.types, vec![0, 1, 2, 3]);
182 let expected: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
184 .into_iter()
185 .collect();
186 assert_eq!(canonical_edges(&r.graph), expected);
187 }
188
189 #[test]
190 fn turan_13_4_matches_upstream() {
191 let r = turan(13, 4).expect("ok");
195 assert_eq!(r.graph.vcount(), 13);
196 assert_eq!(r.graph.ecount(), 63);
198 assert_eq!(r.types, vec![0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
199 }
200
201 #[test]
202 fn turan_13_4_ecount_matches_closed_form() {
203 let r = turan(13, 4).expect("ok");
206 let n: u32 = 13;
207 let sizes: [u32; 4] = [4, 3, 3, 3];
208 let twice_e: u32 = sizes.iter().map(|&s| s * (n - s)).sum();
209 let expected_e = (twice_e / 2) as usize;
210 assert_eq!(r.graph.ecount(), expected_e);
211 }
212
213 #[test]
214 fn turan_8_3_matches_upstream() {
215 let r = turan(8, 3).expect("ok");
219 assert_eq!(r.graph.vcount(), 8);
220 assert_eq!(r.types, vec![0, 0, 0, 1, 1, 1, 2, 2]);
221 assert_eq!(r.graph.ecount(), 21);
223 }
224
225 #[test]
226 fn turan_6_3_matches_upstream() {
227 let r = turan(6, 3).expect("ok");
231 assert_eq!(r.graph.vcount(), 6);
232 assert_eq!(r.graph.ecount(), 12);
233 assert_eq!(r.types, vec![0, 0, 1, 1, 2, 2]);
234 }
235
236 #[test]
237 fn turan_balanced_when_remainder_zero() {
238 let r = turan(12, 4).expect("ok");
240 assert_eq!(r.graph.vcount(), 12);
241 assert_eq!(r.graph.ecount(), 54);
243 assert_eq!(r.types, vec![0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
244 }
245
246 #[test]
247 fn turan_first_partitions_get_the_extras_when_remainder_nonzero() {
248 let r = turan(14, 4).expect("ok");
250 assert_eq!(r.graph.vcount(), 14);
251 assert_eq!(r.types, vec![0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
253 }
254
255 #[test]
256 fn turan_no_self_loops_no_intra_partition_edges() {
257 let r = turan(15, 5).expect("ok");
258 for (u, v) in canonical_edges(&r.graph) {
259 assert_ne!(u, v, "no self-loops");
260 assert_ne!(
261 r.types[u as usize], r.types[v as usize],
262 "edge ({u}, {v}) lives inside a single partition"
263 );
264 }
265 }
266
267 #[test]
268 fn turan_is_always_undirected() {
269 for (n, r) in [(0, 1), (5, 3), (10, 4), (20, 7)] {
270 let res = turan(n, r).expect("ok");
271 assert!(!res.graph.is_directed(), "T({n}, {r}) must be undirected");
272 }
273 }
274
275 #[test]
276 fn turan_isomorphic_to_full_multipartite_balanced() {
277 let t = turan(13, 4).expect("ok");
282 let mp = full_multipartite(&[4, 3, 3, 3], false, MultipartiteMode::All).expect("ok");
283 assert_eq!(t.graph.vcount(), mp.graph.vcount());
284 assert_eq!(t.graph.ecount(), mp.graph.ecount());
285 assert_eq!(t.types, mp.types);
286 assert_eq!(canonical_edges(&t.graph), canonical_edges(&mp.graph));
287 }
288}
289
290#[cfg(all(test, feature = "proptest-harness"))]
291mod proptest_invariants {
292 use super::*;
293 use proptest::prelude::*;
294
295 proptest! {
296 #![proptest_config(ProptestConfig::with_cases(64))]
297
298 #[test]
299 fn vcount_always_equals_n(n in 0u32..30, r in 1u32..10) {
300 let res = turan(n, r).expect("ok for valid inputs");
301 prop_assert_eq!(res.graph.vcount(), n);
302 prop_assert_eq!(u32::try_from(res.types.len()).unwrap(), n);
303 }
304
305 #[test]
306 fn always_undirected(n in 0u32..30, r in 1u32..10) {
307 let res = turan(n, r).expect("ok");
308 prop_assert!(!res.graph.is_directed());
309 }
310
311 #[test]
312 fn types_partition_major_monotone(n in 1u32..30, r in 1u32..10) {
313 let res = turan(n, r).expect("ok");
314 for w in res.types.windows(2) {
315 prop_assert!(w[0] <= w[1]);
316 }
317 }
318
319 #[test]
320 fn partition_size_differs_by_at_most_one(n in 1u32..30, r in 1u32..10) {
321 let res = turan(n, r).expect("ok");
322 let max_t = *res.types.iter().max().unwrap();
324 let mut sizes = vec![0u32; (max_t + 1) as usize];
325 for &t in &res.types {
326 sizes[t as usize] += 1;
327 }
328 let lo = *sizes.iter().min().unwrap();
329 let hi = *sizes.iter().max().unwrap();
330 prop_assert!(hi - lo <= 1, "partition sizes must differ by ≤ 1: {sizes:?}");
331 }
332
333 #[test]
334 fn no_intra_partition_edges(n in 0u32..20, r in 1u32..8) {
335 let res = turan(n, r).expect("ok");
336 let m = u32::try_from(res.graph.ecount()).unwrap();
337 for e in 0..m {
338 let (u, v) = res.graph.edge(e).unwrap();
339 prop_assert_ne!(res.types[u as usize], res.types[v as usize]);
340 }
341 }
342
343 #[test]
344 fn r_capped_at_n_when_oversized(n in 1u32..15, extra in 0u32..20) {
345 let big = turan(n, n + extra).expect("ok");
347 let cap = turan(n, n).expect("ok");
348 prop_assert_eq!(big.graph.vcount(), cap.graph.vcount());
349 prop_assert_eq!(big.graph.ecount(), cap.graph.ecount());
350 prop_assert_eq!(big.types, cap.types);
351 }
352 }
353}