use crate::algorithms::constructors::full_multipartite::{
FullMultipartite, MultipartiteMode, full_multipartite,
};
use crate::core::{IgraphError, IgraphResult};
pub fn turan(n: u32, r: u32) -> IgraphResult<FullMultipartite> {
if n == 0 {
return full_multipartite(&[], false, MultipartiteMode::All);
}
if r == 0 {
return Err(IgraphError::InvalidArgument(
"turan: number of partitions must be positive when n > 0".into(),
));
}
let r_effective = r.min(n);
let quotient = n / r_effective;
let remainder = n % r_effective;
let mut partitions: Vec<u32> = Vec::with_capacity(r_effective as usize);
let big = quotient.checked_add(1).ok_or_else(|| {
IgraphError::InvalidArgument("turan: partition size n/r + 1 overflows u32".into())
})?;
for i in 0..r_effective {
if i < remainder {
partitions.push(big);
} else {
partitions.push(quotient);
}
}
full_multipartite(&partitions, false, MultipartiteMode::All)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn canonical_edges(g: &crate::core::Graph) -> BTreeSet<(u32, u32)> {
let m = u32::try_from(g.ecount()).expect("ecount fits u32");
(0..m)
.map(|e| g.edge(e).expect("edge id in bounds"))
.map(|(u, v)| if u <= v { (u, v) } else { (v, u) })
.collect()
}
#[test]
fn empty_n_zero_returns_empty_graph() {
let r = turan(0, 10).expect("ok");
assert_eq!(r.graph.vcount(), 0);
assert_eq!(r.graph.ecount(), 0);
assert!(!r.graph.is_directed());
assert!(r.types.is_empty());
}
#[test]
fn empty_n_zero_with_r_one_also_empty() {
let r = turan(0, 1).expect("ok");
assert_eq!(r.graph.vcount(), 0);
assert!(r.types.is_empty());
}
#[test]
fn empty_n_zero_with_r_zero_still_empty_no_error() {
let r = turan(0, 0).expect("ok");
assert_eq!(r.graph.vcount(), 0);
assert!(r.types.is_empty());
}
#[test]
fn r_zero_with_positive_n_is_error() {
let err = turan(5, 0).expect_err("must reject r == 0 when n > 0");
match err {
IgraphError::InvalidArgument(_) => {}
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
#[test]
fn r_eq_one_is_n_isolated_vertices() {
let r = turan(10, 1).expect("ok");
assert_eq!(r.graph.vcount(), 10);
assert_eq!(r.graph.ecount(), 0);
assert_eq!(r.types, vec![0; 10]);
}
#[test]
fn r_exceeds_n_caps_at_n_yielding_kn() {
let r = turan(4, 6).expect("ok");
assert_eq!(r.graph.vcount(), 4);
assert_eq!(r.graph.ecount(), 6);
assert_eq!(r.types, vec![0, 1, 2, 3]);
let expected: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
.into_iter()
.collect();
assert_eq!(canonical_edges(&r.graph), expected);
}
#[test]
fn turan_13_4_matches_upstream() {
let r = turan(13, 4).expect("ok");
assert_eq!(r.graph.vcount(), 13);
assert_eq!(r.graph.ecount(), 63);
assert_eq!(r.types, vec![0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
}
#[test]
fn turan_13_4_ecount_matches_closed_form() {
let r = turan(13, 4).expect("ok");
let n: u32 = 13;
let sizes: [u32; 4] = [4, 3, 3, 3];
let twice_e: u32 = sizes.iter().map(|&s| s * (n - s)).sum();
let expected_e = (twice_e / 2) as usize;
assert_eq!(r.graph.ecount(), expected_e);
}
#[test]
fn turan_8_3_matches_upstream() {
let r = turan(8, 3).expect("ok");
assert_eq!(r.graph.vcount(), 8);
assert_eq!(r.types, vec![0, 0, 0, 1, 1, 1, 2, 2]);
assert_eq!(r.graph.ecount(), 21);
}
#[test]
fn turan_6_3_matches_upstream() {
let r = turan(6, 3).expect("ok");
assert_eq!(r.graph.vcount(), 6);
assert_eq!(r.graph.ecount(), 12);
assert_eq!(r.types, vec![0, 0, 1, 1, 2, 2]);
}
#[test]
fn turan_balanced_when_remainder_zero() {
let r = turan(12, 4).expect("ok");
assert_eq!(r.graph.vcount(), 12);
assert_eq!(r.graph.ecount(), 54);
assert_eq!(r.types, vec![0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
}
#[test]
fn turan_first_partitions_get_the_extras_when_remainder_nonzero() {
let r = turan(14, 4).expect("ok");
assert_eq!(r.graph.vcount(), 14);
assert_eq!(r.types, vec![0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
}
#[test]
fn turan_no_self_loops_no_intra_partition_edges() {
let r = turan(15, 5).expect("ok");
for (u, v) in canonical_edges(&r.graph) {
assert_ne!(u, v, "no self-loops");
assert_ne!(
r.types[u as usize], r.types[v as usize],
"edge ({u}, {v}) lives inside a single partition"
);
}
}
#[test]
fn turan_is_always_undirected() {
for (n, r) in [(0, 1), (5, 3), (10, 4), (20, 7)] {
let res = turan(n, r).expect("ok");
assert!(!res.graph.is_directed(), "T({n}, {r}) must be undirected");
}
}
#[test]
fn turan_isomorphic_to_full_multipartite_balanced() {
let t = turan(13, 4).expect("ok");
let mp = full_multipartite(&[4, 3, 3, 3], false, MultipartiteMode::All).expect("ok");
assert_eq!(t.graph.vcount(), mp.graph.vcount());
assert_eq!(t.graph.ecount(), mp.graph.ecount());
assert_eq!(t.types, mp.types);
assert_eq!(canonical_edges(&t.graph), canonical_edges(&mp.graph));
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptest_invariants {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn vcount_always_equals_n(n in 0u32..30, r in 1u32..10) {
let res = turan(n, r).expect("ok for valid inputs");
prop_assert_eq!(res.graph.vcount(), n);
prop_assert_eq!(u32::try_from(res.types.len()).unwrap(), n);
}
#[test]
fn always_undirected(n in 0u32..30, r in 1u32..10) {
let res = turan(n, r).expect("ok");
prop_assert!(!res.graph.is_directed());
}
#[test]
fn types_partition_major_monotone(n in 1u32..30, r in 1u32..10) {
let res = turan(n, r).expect("ok");
for w in res.types.windows(2) {
prop_assert!(w[0] <= w[1]);
}
}
#[test]
fn partition_size_differs_by_at_most_one(n in 1u32..30, r in 1u32..10) {
let res = turan(n, r).expect("ok");
let max_t = *res.types.iter().max().unwrap();
let mut sizes = vec![0u32; (max_t + 1) as usize];
for &t in &res.types {
sizes[t as usize] += 1;
}
let lo = *sizes.iter().min().unwrap();
let hi = *sizes.iter().max().unwrap();
prop_assert!(hi - lo <= 1, "partition sizes must differ by ≤ 1: {sizes:?}");
}
#[test]
fn no_intra_partition_edges(n in 0u32..20, r in 1u32..8) {
let res = turan(n, r).expect("ok");
let m = u32::try_from(res.graph.ecount()).unwrap();
for e in 0..m {
let (u, v) = res.graph.edge(e).unwrap();
prop_assert_ne!(res.types[u as usize], res.types[v as usize]);
}
}
#[test]
fn r_capped_at_n_when_oversized(n in 1u32..15, extra in 0u32..20) {
let big = turan(n, n + extra).expect("ok");
let cap = turan(n, n).expect("ok");
prop_assert_eq!(big.graph.vcount(), cap.graph.vcount());
prop_assert_eq!(big.graph.ecount(), cap.graph.ecount());
prop_assert_eq!(big.types, cap.types);
}
}
}