Skip to main content

rust_igraph/algorithms/properties/
graphicality.rs

1//! Degree sequence realizability tests (ALGO-PR-034).
2//!
3//! Counterpart of `igraph_is_graphical` + `igraph_is_bigraphical` from
4//! `references/igraph/src/misc/graphicality.c` (928 lines).
5//!
6//! Tests whether a given degree sequence can be realized as a graph
7//! of the specified type (simple, multi-edges, loops, etc.).
8
9/// What kinds of edges are allowed when testing graphicality.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum EdgeTypeFilter {
12    /// Simple graph: no self-loops, no multi-edges.
13    Simple,
14    /// At most one self-loop per vertex, no multi-edges.
15    LoopsSimple,
16    /// No self-loops, multi-edges allowed.
17    Multi,
18    /// Self-loops and multi-edges both allowed.
19    LoopsMulti,
20}
21
22/// Test whether a degree sequence is graphical (realizable as some graph).
23///
24/// For **undirected** graphs, pass `in_degrees = None`.
25/// For **directed** graphs, pass `out_degrees` and `Some(in_degrees)`.
26///
27/// # Algorithms
28///
29/// - Simple undirected: Erdős–Gallai theorem (Cloteaux 2015 linear-time).
30/// - Loopy-simple undirected: Cairns–Mendan modification of Erdős–Gallai.
31/// - Loopless multi undirected: even sum + sum ≥ 2·max.
32/// - Loopy multi undirected: even sum only.
33/// - Simple directed: Fulkerson–Chen–Anstee theorem (Berger 2014 relaxation).
34/// - Loopy-simple directed: reduces to bigraphical simple (Gale–Ryser).
35/// - Loopless multi directed: `sum_in` == `sum_out` + `sum_out` >= `max_total`.
36/// - Loopy multi directed: `sum_in` == `sum_out` only.
37///
38/// # Examples
39///
40/// ```
41/// use rust_igraph::{EdgeTypeFilter, is_graphical};
42///
43/// // Star K_{1,4}: center has degree 4, leaves have degree 1
44/// assert!(is_graphical(&[4, 1, 1, 1, 1], None, EdgeTypeFilter::Simple));
45///
46/// // Odd sum → not graphical
47/// assert!(!is_graphical(&[3, 1, 1], None, EdgeTypeFilter::Simple));
48/// ```
49pub fn is_graphical(
50    out_degrees: &[u32],
51    in_degrees: Option<&[u32]>,
52    edge_types: EdgeTypeFilter,
53) -> bool {
54    match in_degrees {
55        None => is_graphical_undirected(out_degrees, edge_types),
56        Some(in_deg) => is_graphical_directed(out_degrees, in_deg, edge_types),
57    }
58}
59
60/// Test whether a bi-degree-sequence is bigraphical (realizable as a
61/// bipartite graph).
62///
63/// `degrees1` and `degrees2` are the degree sequences of the two
64/// partitions. Self-loops are impossible in bipartite graphs, so the
65/// `edge_types` parameter only distinguishes `Simple` vs `Multi`.
66///
67/// # Algorithms
68///
69/// - Simple: Gale–Ryser theorem (Berger 2014 relaxation), O(n).
70/// - Multi: sum equality check, O(n).
71///
72/// # Examples
73///
74/// ```
75/// use rust_igraph::{EdgeTypeFilter, is_bigraphical};
76///
77/// // Complete bipartite K_{2,3}: [3,3] and [2,2,2]
78/// assert!(is_bigraphical(&[3, 3], &[2, 2, 2], EdgeTypeFilter::Simple));
79///
80/// // Unequal sums → not bigraphical
81/// assert!(!is_bigraphical(&[3, 3], &[2, 2, 1], EdgeTypeFilter::Multi));
82/// ```
83pub fn is_bigraphical(degrees1: &[u32], degrees2: &[u32], edge_types: EdgeTypeFilter) -> bool {
84    match edge_types {
85        EdgeTypeFilter::Multi | EdgeTypeFilter::LoopsMulti => bigraphical_multi(degrees1, degrees2),
86        _ => bigraphical_simple(degrees1, degrees2),
87    }
88}
89
90fn is_graphical_undirected(degrees: &[u32], edge_types: EdgeTypeFilter) -> bool {
91    match edge_types {
92        EdgeTypeFilter::LoopsMulti => undirected_multi_loops(degrees),
93        EdgeTypeFilter::Multi => undirected_loopless_multi(degrees),
94        EdgeTypeFilter::LoopsSimple => undirected_loopy_simple(degrees),
95        EdgeTypeFilter::Simple => undirected_simple(degrees),
96    }
97}
98
99fn is_graphical_directed(
100    out_degrees: &[u32],
101    in_degrees: &[u32],
102    edge_types: EdgeTypeFilter,
103) -> bool {
104    if out_degrees.len() != in_degrees.len() {
105        return false;
106    }
107    match edge_types {
108        EdgeTypeFilter::LoopsMulti => directed_loopy_multi(out_degrees, in_degrees),
109        EdgeTypeFilter::Multi => directed_loopless_multi(out_degrees, in_degrees),
110        EdgeTypeFilter::LoopsSimple => bigraphical_simple(out_degrees, in_degrees),
111        EdgeTypeFilter::Simple => directed_simple(out_degrees, in_degrees),
112    }
113}
114
115// --- Undirected variants ---
116
117fn undirected_multi_loops(degrees: &[u32]) -> bool {
118    let mut sum_parity: u64 = 0;
119    for &d in degrees {
120        sum_parity = (sum_parity + u64::from(d)) & 1;
121    }
122    sum_parity == 0
123}
124
125fn undirected_loopless_multi(degrees: &[u32]) -> bool {
126    if degrees.is_empty() {
127        return true;
128    }
129    let mut dsum: u64 = 0;
130    let mut dmax: u64 = 0;
131    for &d in degrees {
132        let d64 = u64::from(d);
133        dsum += d64;
134        if d64 > dmax {
135            dmax = d64;
136        }
137    }
138    dsum % 2 == 0 && dsum >= 2 * dmax
139}
140
141#[allow(
142    clippy::cast_possible_truncation,
143    clippy::cast_sign_loss,
144    clippy::cast_possible_wrap,
145    clippy::many_single_char_names
146)]
147fn undirected_loopy_simple(degrees: &[u32]) -> bool {
148    let n = degrees.len();
149    if n == 0 {
150        return true;
151    }
152
153    if !undirected_multi_loops(degrees) {
154        return false;
155    }
156
157    let n_i64 = n as i64;
158    let mut num_degs = vec![0i64; n + 2];
159    for &d in degrees {
160        let d_usize = d as usize;
161        if d_usize > n + 1 {
162            return false;
163        }
164        num_degs[d_usize] += 1;
165    }
166
167    for d in (0..=n).rev() {
168        num_degs[d] += num_degs[d + 1];
169    }
170
171    let mut wd: i64 = 0;
172    let mut kd: i64 = n_i64 + 1;
173    let mut w: i64 = n_i64 - 1;
174    let mut b: i64 = 0;
175    let mut s: i64 = 0;
176    let mut c: i64 = 0;
177
178    for k in 0..n_i64 {
179        while k >= num_degs[kd as usize] {
180            kd -= 1;
181        }
182        b += kd;
183        c += w;
184        while w > k {
185            while wd <= n_i64 && w < num_degs[(wd + 1) as usize] {
186                wd += 1;
187            }
188            if wd > k + 1 {
189                break;
190            }
191            s += wd;
192            c -= k + 1;
193            w -= 1;
194        }
195        if b > c + s + 2 * (k + 1) {
196            return false;
197        }
198        if w == k {
199            break;
200        }
201    }
202    true
203}
204
205#[allow(
206    clippy::cast_possible_truncation,
207    clippy::cast_possible_wrap,
208    clippy::cast_sign_loss,
209    clippy::similar_names
210)]
211fn undirected_simple(degrees: &[u32]) -> bool {
212    let p = degrees.len();
213    if p == 0 {
214        return true;
215    }
216
217    let mut num_degs = vec![0i64; p];
218    let mut deg_min: u64 = u64::from(u32::MAX);
219    let mut deg_max: u64 = 0;
220    let mut dsum: u64 = 0;
221    let mut nonzero_count: i64 = 0;
222
223    for &d in degrees {
224        let d64 = u64::from(d);
225        if d64 >= p as u64 {
226            return false;
227        }
228        if d > 0 {
229            if d64 > deg_max {
230                deg_max = d64;
231            }
232            if d64 < deg_min {
233                deg_min = d64;
234            }
235            dsum += d64;
236            nonzero_count += 1;
237            num_degs[d as usize] += 1;
238        }
239    }
240
241    if dsum % 2 != 0 {
242        return false;
243    }
244    if nonzero_count == 0 {
245        return true;
246    }
247
248    // Zverovich bound — sufficient condition for graphicality
249    let zv_arg = (deg_max + deg_min + 1) as i64;
250    let mut zverovich_bound = (zv_arg * zv_arg) / 4;
251    if deg_min % 2 == 1 || (deg_max + deg_min) % 4 == 1 {
252        zverovich_bound -= 1;
253    }
254    if (deg_min as i64) * nonzero_count >= zverovich_bound {
255        return true;
256    }
257
258    // Erdős–Gallai via Cloteaux's linear-time algorithm
259    let mut k: i64 = 0;
260    let mut sum_deg: i64 = 0;
261    let mut sum_freq: i64 = 0;
262    let mut sum_ifreq: i64 = 0;
263
264    let deg_min_i = deg_min as i64;
265    let deg_max_i = deg_max as i64;
266
267    let mut dk = deg_max_i;
268    while dk >= deg_min_i {
269        if dk < k + 1 {
270            return true;
271        }
272
273        let mut run_size = num_degs[dk as usize];
274        if run_size > 0 {
275            if dk < k + run_size {
276                run_size = dk - k;
277            }
278            sum_deg += run_size * dk;
279            for v in 0..run_size {
280                sum_freq += num_degs[(k + v) as usize];
281                sum_ifreq += (k + v) * num_degs[(k + v) as usize];
282            }
283            k += run_size;
284            if sum_deg > k * (nonzero_count - 1) - k * sum_freq + sum_ifreq {
285                return false;
286            }
287        }
288        dk -= 1;
289    }
290
291    true
292}
293
294// --- Directed variants ---
295
296fn directed_loopy_multi(out_degrees: &[u32], in_degrees: &[u32]) -> bool {
297    let mut sumdiff: i64 = 0;
298    for (&dout, &din) in out_degrees.iter().zip(in_degrees.iter()) {
299        sumdiff += i64::from(din) - i64::from(dout);
300    }
301    sumdiff == 0
302}
303
304fn directed_loopless_multi(out_degrees: &[u32], in_degrees: &[u32]) -> bool {
305    let mut sumin: u64 = 0;
306    let mut sumout: u64 = 0;
307    let mut dmax: u64 = 0;
308    for (&dout, &din) in out_degrees.iter().zip(in_degrees.iter()) {
309        let d = u64::from(dout) + u64::from(din);
310        sumin += u64::from(din);
311        sumout += u64::from(dout);
312        if d > dmax {
313            dmax = d;
314        }
315    }
316    sumin == sumout && sumout >= dmax
317}
318
319#[allow(
320    clippy::cast_possible_truncation,
321    clippy::cast_possible_wrap,
322    clippy::cast_sign_loss
323)]
324fn directed_simple(out_degrees: &[u32], in_degrees: &[u32]) -> bool {
325    if !directed_loopy_multi(out_degrees, in_degrees) {
326        return false;
327    }
328
329    let vcount = out_degrees.len();
330    if vcount == 0 {
331        return true;
332    }
333    let vc_i64 = vcount as i64;
334
335    // Counting sort by in-degree (descending) with stable ordering
336    let mut in_degree_cumcounts = vec![0usize; vcount + 1];
337    for idx in 0..vcount {
338        let indeg = in_degrees[idx] as usize;
339        let outdeg = out_degrees[idx] as usize;
340        if indeg >= vcount || outdeg >= vcount {
341            return false;
342        }
343        in_degree_cumcounts[indeg + 1] += 1;
344    }
345
346    for indeg in 0..vcount {
347        in_degree_cumcounts[indeg + 1] += in_degree_cumcounts[indeg];
348    }
349
350    let mut sorted_out = vec![0i64; vcount];
351    let mut sorted_in = vec![0i64; vcount];
352    let mut in_degree_counts = vec![0usize; vcount];
353
354    for idx in 0..vcount {
355        let outdeg = out_degrees[idx] as usize;
356        let indeg = in_degrees[idx] as usize;
357        let pos = in_degree_cumcounts[indeg] + in_degree_counts[indeg];
358        sorted_out[vcount - pos - 1] = outdeg as i64;
359        sorted_in[vcount - pos - 1] = indeg as i64;
360        in_degree_counts[indeg] += 1;
361    }
362
363    // Fulkerson–Chen–Anstee check
364    let mut left_pq = vec![0i64; vcount];
365    let mut right_pq = vec![0i64; vcount];
366    let mut left_sum: i64 = 0;
367    let mut right_sum: i64 = 0;
368    let mut left_pq_size: i64 = 0;
369    let mut right_pq_size: i64 = vc_i64;
370    let mut left_cursor: i64 = 0;
371    let mut right_cursor: i64 = 0;
372
373    for idx in 0..vcount {
374        right_pq[sorted_out[idx] as usize] += 1;
375    }
376
377    let mut lhs: i64 = 0;
378    for step in 0..vc_i64 {
379        let step_u = step as usize;
380        lhs += sorted_in[step_u];
381
382        if sorted_out[step_u] < step {
383            left_sum += sorted_out[step_u];
384        } else {
385            left_pq[sorted_out[step_u] as usize] += 1;
386            left_pq_size += 1;
387        }
388        while left_cursor < step {
389            let lc = left_cursor as usize;
390            while left_pq[lc] > 0 {
391                left_pq[lc] -= 1;
392                left_pq_size -= 1;
393                left_sum += left_cursor;
394            }
395            left_cursor += 1;
396        }
397
398        while right_cursor < step + 1 {
399            let rc = right_cursor as usize;
400            while right_pq[rc] > 0 {
401                right_pq[rc] -= 1;
402                right_pq_size -= 1;
403                right_sum += right_cursor;
404            }
405            right_cursor += 1;
406        }
407        if sorted_out[step_u] < step + 1 {
408            right_sum -= sorted_out[step_u];
409        } else {
410            right_pq[sorted_out[step_u] as usize] -= 1;
411            right_pq_size -= 1;
412        }
413
414        let rhs = left_sum + step * left_pq_size + right_sum + (step + 1) * right_pq_size;
415        if lhs > rhs {
416            return false;
417        }
418    }
419    true
420}
421
422// --- Bipartite variants ---
423
424fn bigraphical_multi(degrees1: &[u32], degrees2: &[u32]) -> bool {
425    let sum1: u64 = degrees1.iter().map(|&d| u64::from(d)).sum();
426    let sum2: u64 = degrees2.iter().map(|&d| u64::from(d)).sum();
427    sum1 == sum2
428}
429
430#[allow(
431    clippy::cast_possible_truncation,
432    clippy::cast_possible_wrap,
433    clippy::cast_sign_loss
434)]
435fn bigraphical_simple(degrees1: &[u32], degrees2: &[u32]) -> bool {
436    let (mut d1, mut d2) = (degrees1, degrees2);
437    let (mut n1, mut n2) = (d1.len(), d2.len());
438
439    if n1 == 0 && n2 == 0 {
440        return true;
441    }
442
443    if !bigraphical_multi(d1, d2) {
444        return false;
445    }
446
447    // Ensure d1 is the shorter vector
448    if n2 < n1 {
449        std::mem::swap(&mut d1, &mut d2);
450        std::mem::swap(&mut n1, &mut n2);
451    }
452
453    let mut deg_freq1 = vec![0i64; n2 + 1];
454    let mut deg_freq2 = vec![0i64; n1 + 1];
455
456    for &d in d1 {
457        let du = d as usize;
458        if du > n2 {
459            return false;
460        }
461        deg_freq1[du] += 1;
462    }
463    for &d in d2 {
464        let du = d as usize;
465        if du > n1 {
466            return false;
467        }
468        deg_freq2[du] += 1;
469    }
470
471    // Gale–Ryser theorem (Berger 2014 relaxation)
472    let mut lhs_sum: i64 = 0;
473    let mut partial_rhs_sum: i64 = 0;
474    let mut partial_rhs_count: i64 = 0;
475    let mut b: i64 = 0;
476    let mut k: i64 = -1;
477    let n2_i64 = n2 as i64;
478
479    let mut a = n2 as i64;
480    while a >= 0 {
481        let acount = deg_freq1[a as usize];
482        lhs_sum += a * acount;
483        k += acount;
484
485        while b <= k + 1 {
486            let bu = b as usize;
487            let bcount = deg_freq2[bu];
488            partial_rhs_sum += b * bcount;
489            partial_rhs_count += bcount;
490            b += 1;
491        }
492
493        if lhs_sum > partial_rhs_sum + (n2_i64 - partial_rhs_count) * (k + 1) {
494            return false;
495        }
496        a -= 1;
497    }
498
499    true
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    // --- is_graphical undirected simple ---
507
508    #[test]
509    fn empty_sequence_is_graphical() {
510        assert!(is_graphical(&[], None, EdgeTypeFilter::Simple));
511    }
512
513    #[test]
514    fn single_zero_degree() {
515        assert!(is_graphical(&[0], None, EdgeTypeFilter::Simple));
516    }
517
518    #[test]
519    fn all_zeros() {
520        assert!(is_graphical(&[0, 0, 0], None, EdgeTypeFilter::Simple));
521    }
522
523    #[test]
524    fn star_k14_simple() {
525        assert!(is_graphical(&[4, 1, 1, 1, 1], None, EdgeTypeFilter::Simple));
526    }
527
528    #[test]
529    fn complete_k4_simple() {
530        assert!(is_graphical(&[3, 3, 3, 3], None, EdgeTypeFilter::Simple));
531    }
532
533    #[test]
534    fn cycle_c5_simple() {
535        assert!(is_graphical(&[2, 2, 2, 2, 2], None, EdgeTypeFilter::Simple));
536    }
537
538    #[test]
539    fn odd_sum_not_graphical() {
540        assert!(!is_graphical(&[3, 1, 1], None, EdgeTypeFilter::Simple));
541    }
542
543    #[test]
544    fn degree_too_large_simple() {
545        assert!(!is_graphical(
546            &[5, 1, 1, 1, 1],
547            None,
548            EdgeTypeFilter::Simple
549        ));
550    }
551
552    #[test]
553    fn erdos_gallai_failure() {
554        // [4, 4, 1, 1]: sum=10 even, but EG fails at k=2
555        assert!(!is_graphical(&[4, 4, 1, 1], None, EdgeTypeFilter::Simple));
556    }
557
558    #[test]
559    fn path_p3_simple() {
560        assert!(is_graphical(&[1, 2, 1], None, EdgeTypeFilter::Simple));
561    }
562
563    // --- is_graphical undirected with loops ---
564
565    #[test]
566    fn odd_sum_with_loops_multi() {
567        assert!(!is_graphical(&[3, 2], None, EdgeTypeFilter::LoopsMulti));
568    }
569
570    #[test]
571    fn even_sum_with_loops_multi() {
572        assert!(is_graphical(&[3, 3], None, EdgeTypeFilter::LoopsMulti));
573    }
574
575    // --- is_graphical undirected loopless multi ---
576
577    #[test]
578    fn loopless_multi_basic() {
579        assert!(is_graphical(&[4, 2, 2], None, EdgeTypeFilter::Multi));
580    }
581
582    #[test]
583    fn loopless_multi_max_too_large() {
584        // sum=6 even, but max=5 > sum/2=3 → 2*max=10 > sum=6
585        assert!(!is_graphical(&[5, 1, 0], None, EdgeTypeFilter::Multi));
586    }
587
588    // --- is_graphical undirected loopy simple ---
589
590    #[test]
591    fn loopy_simple_basic() {
592        // Each vertex can have at most one self-loop
593        assert!(is_graphical(
594            &[3, 3, 2, 2],
595            None,
596            EdgeTypeFilter::LoopsSimple
597        ));
598    }
599
600    #[test]
601    fn loopy_simple_degree_too_large() {
602        // d > n+1 is not realizable
603        assert!(!is_graphical(
604            &[10, 1, 1],
605            None,
606            EdgeTypeFilter::LoopsSimple
607        ));
608    }
609
610    // --- is_graphical directed ---
611
612    #[test]
613    fn directed_simple_basic() {
614        // Directed cycle: out=[1,1,1], in=[1,1,1]
615        assert!(is_graphical(
616            &[1, 1, 1],
617            Some(&[1, 1, 1]),
618            EdgeTypeFilter::Simple
619        ));
620    }
621
622    #[test]
623    fn directed_simple_star() {
624        // Directed star: center out=3, leaves in=1
625        assert!(is_graphical(
626            &[3, 0, 0, 0],
627            Some(&[0, 1, 1, 1]),
628            EdgeTypeFilter::Simple
629        ));
630    }
631
632    #[test]
633    fn directed_sum_mismatch() {
634        assert!(!is_graphical(
635            &[2, 1],
636            Some(&[1, 1]),
637            EdgeTypeFilter::Simple
638        ));
639    }
640
641    #[test]
642    fn directed_length_mismatch() {
643        assert!(!is_graphical(&[1, 1], Some(&[1]), EdgeTypeFilter::Simple));
644    }
645
646    #[test]
647    fn directed_loopy_multi_basic() {
648        assert!(is_graphical(
649            &[2, 1],
650            Some(&[1, 2]),
651            EdgeTypeFilter::LoopsMulti
652        ));
653    }
654
655    #[test]
656    fn directed_loopless_multi_basic() {
657        assert!(is_graphical(&[1, 1], Some(&[1, 1]), EdgeTypeFilter::Multi));
658    }
659
660    #[test]
661    fn directed_loopless_multi_max_too_large() {
662        // out=[3,0], in=[0,3] → sum_out=3, max_total=3 → 3>=3 OK
663        assert!(is_graphical(&[3, 0], Some(&[0, 3]), EdgeTypeFilter::Multi));
664        // out=[4,0], in=[0,4] → max_total=4, sum_out=4 → 4>=4 OK
665        assert!(is_graphical(&[4, 0], Some(&[0, 4]), EdgeTypeFilter::Multi));
666    }
667
668    #[test]
669    fn directed_degree_too_large_simple() {
670        // out=[3,0,0], in=[0,0,3] — each degree >= n=3, not simple
671        assert!(!is_graphical(
672            &[3, 0, 0],
673            Some(&[0, 0, 3]),
674            EdgeTypeFilter::Simple
675        ));
676    }
677
678    // --- is_bigraphical ---
679
680    #[test]
681    fn bigraphical_empty() {
682        assert!(is_bigraphical(&[], &[], EdgeTypeFilter::Simple));
683    }
684
685    #[test]
686    fn bigraphical_k23_simple() {
687        assert!(is_bigraphical(&[3, 3], &[2, 2, 2], EdgeTypeFilter::Simple));
688    }
689
690    #[test]
691    fn bigraphical_sum_mismatch() {
692        assert!(!is_bigraphical(&[3, 3], &[2, 2, 1], EdgeTypeFilter::Multi));
693    }
694
695    #[test]
696    fn bigraphical_degree_exceeds_partition() {
697        // degrees1 vertex has degree 4 but partition 2 has only 2 vertices
698        assert!(!is_bigraphical(&[4, 0], &[2, 2], EdgeTypeFilter::Simple));
699    }
700
701    #[test]
702    fn bigraphical_multi_basic() {
703        assert!(is_bigraphical(&[5, 5], &[5, 5], EdgeTypeFilter::Multi));
704    }
705
706    #[test]
707    fn bigraphical_simple_asymmetric() {
708        // K_{1,3}: [3] and [1,1,1]
709        assert!(is_bigraphical(&[3], &[1, 1, 1], EdgeTypeFilter::Simple));
710    }
711
712    #[test]
713    fn bigraphical_gale_ryser_failure() {
714        // [3, 3] and [1, 1, 1] → sum matches (6=3), no sum=6 vs sum=3 → fail
715        assert!(!is_bigraphical(&[3, 3], &[1, 1, 1], EdgeTypeFilter::Simple));
716    }
717
718    // --- Known graphical sequences from igraph C tests ---
719
720    #[test]
721    fn igraph_test_simple_sequences() {
722        // Sequences from igraph's test suite
723        assert!(is_graphical(&[0], None, EdgeTypeFilter::Simple));
724        assert!(is_graphical(&[0, 0], None, EdgeTypeFilter::Simple));
725        assert!(is_graphical(&[1, 1], None, EdgeTypeFilter::Simple));
726        assert!(is_graphical(
727            &[1, 3, 2, 4, 3, 3, 2, 2],
728            None,
729            EdgeTypeFilter::Simple
730        ));
731    }
732
733    #[test]
734    fn igraph_test_non_graphical_simple() {
735        assert!(!is_graphical(&[1], None, EdgeTypeFilter::Simple));
736        assert!(!is_graphical(&[1, 1, 1], None, EdgeTypeFilter::Simple));
737        assert!(!is_graphical(
738            &[1, 3, 2, 4, 3, 3, 2, 3],
739            None,
740            EdgeTypeFilter::Simple
741        ));
742    }
743
744    #[test]
745    fn directed_fulkerson_complete() {
746        // Complete directed graph K_4 (no self-loops): each vertex has out=3, in=3
747        assert!(is_graphical(
748            &[3, 3, 3, 3],
749            Some(&[3, 3, 3, 3]),
750            EdgeTypeFilter::Simple
751        ));
752    }
753
754    #[test]
755    fn directed_fulkerson_tournament() {
756        // Tournament on 4 vertices: out=[3,2,1,0], in=[0,1,2,3]
757        assert!(is_graphical(
758            &[3, 2, 1, 0],
759            Some(&[0, 1, 2, 3]),
760            EdgeTypeFilter::Simple
761        ));
762    }
763}
764
765#[cfg(all(test, feature = "proptest-harness"))]
766mod proptests {
767    use super::*;
768    use proptest::prelude::*;
769
770    proptest! {
771        #[test]
772        fn all_zeros_always_graphical(n in 0usize..50) {
773            let degrees = vec![0u32; n];
774            prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::Simple));
775            prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::Multi));
776            prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::LoopsSimple));
777            prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::LoopsMulti));
778        }
779
780        #[test]
781        fn regular_even_is_graphical(d in 1u32..10, n in 2usize..20) {
782            // d-regular graph on n vertices: sum = d*n must be even
783            let degrees = vec![d; n];
784            let sum = d as u64 * n as u64;
785            if sum % 2 == 0 && d < n as u32 {
786                // Multi always accepts even sum
787                prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::Multi));
788                // LoopsMulti always accepts even sum
789                prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::LoopsMulti));
790            }
791        }
792
793        #[test]
794        fn simple_implies_multi(
795            degrees in proptest::collection::vec(0u32..20, 0..20)
796        ) {
797            // If realizable as simple, must be realizable as multi (more permissive)
798            if is_graphical(&degrees, None, EdgeTypeFilter::Simple) {
799                prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::Multi));
800            }
801        }
802
803        #[test]
804        fn multi_implies_loops_multi(
805            degrees in proptest::collection::vec(0u32..20, 0..20)
806        ) {
807            if is_graphical(&degrees, None, EdgeTypeFilter::Multi) {
808                prop_assert!(is_graphical(&degrees, None, EdgeTypeFilter::LoopsMulti));
809            }
810        }
811
812        #[test]
813        fn directed_sum_parity(
814            out_deg in proptest::collection::vec(0u32..10, 1..15),
815            in_deg in proptest::collection::vec(0u32..10, 1..15),
816        ) {
817            let n = out_deg.len().min(in_deg.len());
818            let out_d = &out_deg[..n];
819            let in_d = &in_deg[..n];
820            let sum_out: u64 = out_d.iter().map(|&x| u64::from(x)).sum();
821            let sum_in: u64 = in_d.iter().map(|&x| u64::from(x)).sum();
822            if sum_out != sum_in {
823                prop_assert!(!is_graphical(out_d, Some(in_d), EdgeTypeFilter::LoopsMulti));
824            }
825        }
826
827        #[test]
828        fn bigraphical_simple_implies_multi(
829            d1 in proptest::collection::vec(0u32..10, 0..15),
830            d2 in proptest::collection::vec(0u32..10, 0..15),
831        ) {
832            if is_bigraphical(&d1, &d2, EdgeTypeFilter::Simple) {
833                prop_assert!(is_bigraphical(&d1, &d2, EdgeTypeFilter::Multi));
834            }
835        }
836    }
837}