Skip to main content

rust_igraph/algorithms/constructors/
full_multipartite.rs

1//! Full multipartite graph constructor (ALGO-CN-026).
2//!
3//! Counterpart of `igraph_full_multipartite()` in
4//! `references/igraph/src/constructors/full.c:154-247`.
5//!
6//! A *complete (full) multipartite graph* on partitions of sizes
7//! `n_0, n_1, …, n_{k-1}` is the graph whose vertex set is the disjoint
8//! union of the `k` partitions, with an edge between every pair of
9//! vertices that belong to **different** partitions and no edge between
10//! vertices that share a partition.
11//!
12//! Vertices are laid out in partition-major order:
13//!
14//! ```text
15//! partition 0: vertices [0,                 n_0)
16//! partition 1: vertices [n_0,         n_0 + n_1)
17//! …
18//! partition k: vertices [Σ n_i (i < k), Σ n_i (i ≤ k))
19//! ```
20//!
21//! Edge orientation is governed by [`MultipartiteMode`]:
22//!
23//! * [`MultipartiteMode::Out`] (directed only) — emit `(from, to)` where
24//!   `from` lives in the earlier partition (lower partition index).
25//! * [`MultipartiteMode::In`]  (directed only) — emit `(to, from)`,
26//!   reversing every arc.
27//! * [`MultipartiteMode::All`] — for directed graphs, emit **both**
28//!   `(from, to)` and `(to, from)` per pair (mutual). For undirected
29//!   graphs this is the only meaningful setting; `Out` and `In` collapse
30//!   to the same edge multiset, so we accept `All` as the canonical
31//!   choice and treat `Out`/`In` as equivalent.
32//!
33//! Edge counts:
34//!
35//! * Empty partition list (`k = 0`) — empty graph, no vertices.
36//! * `E_undirected = Σ_{i < j} n_i · n_j`, equivalently
37//!   `½ · Σ_i n_i · (N − n_i)` where `N = Σ_i n_i`.
38//! * Directed `Out`/`In` arc count equals `E_undirected`.
39//! * Directed `All` arc count equals `2 · E_undirected`.
40//!
41//! The returned [`FullMultipartite`] bundles the constructed [`Graph`]
42//! with a `types` vector that records the partition index of every
43//! vertex (`types[v]` is the partition `v` lives in), matching the
44//! optional `types` out-parameter of the upstream C entry point.
45//!
46//! Degenerate cases:
47//!
48//! * `partitions == &[]` → empty graph (`vcount = 0`, `types == []`).
49//! * Any individual `n_i == 0` → that partition contributes no vertices
50//!   and no edges; later partitions are still numbered consecutively in
51//!   the natural way.
52//! * `partitions == &[n]` (single partition) → graph with `n` isolated
53//!   vertices, no edges, `types == [0; n]`.
54//!
55//! Time complexity: `O(|V| + |E|)`.
56
57use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
58
59/// Direction policy for [`full_multipartite`].
60///
61/// Mirrors `igraph_neimode_t` in the C reference, restricted to the
62/// three settings the upstream `igraph_full_multipartite` handles.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum MultipartiteMode {
65    /// Emit both arcs per pair (mutual). The only legal choice for
66    /// undirected graphs; for directed graphs the result has exactly
67    /// `2 · E_undirected` arcs.
68    All,
69    /// Directed: arcs flow **from** the earlier-indexed partition to
70    /// the later-indexed partition. Ignored for undirected graphs.
71    Out,
72    /// Directed: arcs flow **from** the later-indexed partition back
73    /// to the earlier-indexed partition. Ignored for undirected graphs.
74    In,
75}
76
77/// Bundled return type of [`full_multipartite`].
78///
79/// `graph` is the constructed complete multipartite graph; `types[v]`
80/// is the partition index (0-based) of vertex `v`. `types` always has
81/// length equal to `graph.vcount()`.
82#[derive(Debug, Clone)]
83pub struct FullMultipartite {
84    /// The constructed graph.
85    pub graph: Graph,
86    /// Per-vertex partition index.
87    pub types: Vec<u32>,
88}
89
90/// Build the complete (full) multipartite graph on the given partitions.
91///
92/// `partitions[i]` gives the number of vertices in partition `i`.
93/// The empty slice yields an empty graph and an empty `types` vector.
94///
95/// See the module documentation for the precise semantics of
96/// [`MultipartiteMode`] and the vertex / edge layout.
97///
98/// # Errors
99///
100/// * [`IgraphError::InvalidArgument`] — the total vertex count
101///   `Σ partitions[i]` overflows `u32`, or the (intermediate) edge
102///   count overflows `usize`. The overflow checks mirror the upstream
103///   `IGRAPH_SAFE_ADD` / `IGRAPH_SAFE_MULT` guards.
104///
105/// # Examples
106///
107/// ```
108/// use rust_igraph::{full_multipartite, MultipartiteMode};
109///
110/// // Complete bipartite K_{2,3} on 5 vertices, undirected → 6 edges.
111/// let result = full_multipartite(&[2, 3], false, MultipartiteMode::All).unwrap();
112/// assert_eq!(result.graph.vcount(), 5);
113/// assert_eq!(result.graph.ecount(), 6);
114/// assert_eq!(result.types, vec![0, 0, 1, 1, 1]);
115///
116/// // Same partitions, directed mutual → 12 arcs.
117/// let dir = full_multipartite(&[2, 3], true, MultipartiteMode::All).unwrap();
118/// assert_eq!(dir.graph.ecount(), 12);
119/// ```
120pub fn full_multipartite(
121    partitions: &[u32],
122    directed: bool,
123    mode: MultipartiteMode,
124) -> IgraphResult<FullMultipartite> {
125    let no_of_types = partitions.len();
126
127    if no_of_types == 0 {
128        let graph = Graph::new(0, directed)?;
129        return Ok(FullMultipartite {
130            graph,
131            types: Vec::new(),
132        });
133    }
134
135    // n_acc[i] = sum_{k < i} partitions[k]. Length = no_of_types + 1.
136    // We keep it in u64 during accumulation so we can detect overflow
137    // against u32::MAX, mirroring the upstream IGRAPH_SAFE_ADD chain.
138    let mut n_acc: Vec<u64> = Vec::with_capacity(no_of_types + 1);
139    n_acc.push(0);
140    for &n_i in partitions {
141        let next = n_acc[n_acc.len() - 1]
142            .checked_add(u64::from(n_i))
143            .ok_or_else(|| {
144                IgraphError::InvalidArgument(
145                    "full_multipartite: cumulative partition size overflows u64".to_string(),
146                )
147            })?;
148        n_acc.push(next);
149    }
150    let total_v_u64 = n_acc[no_of_types];
151    let total_v = u32::try_from(total_v_u64).map_err(|_| {
152        IgraphError::InvalidArgument(format!(
153            "full_multipartite: total vertex count {total_v_u64} cannot fit u32"
154        ))
155    })?;
156
157    // Undirected edge count: sum_{i < j} n_i * n_j.
158    // We compute it via the upstream formula:
159    //   no_of_edges = (1/2) * sum_i n_i * (N - n_i)
160    // For "All on directed", we keep the unhalved value (= 2·undirected).
161    // We track in usize with checked arithmetic so allocation is safe.
162    let total_v_usize = usize::try_from(total_v).map_err(|_| {
163        IgraphError::InvalidArgument(format!(
164            "full_multipartite: total vertex count {total_v} cannot fit usize"
165        ))
166    })?;
167    let mut sum_2e: usize = 0;
168    for &n_i in partitions {
169        let n_i_us = n_i as usize;
170        // partial = (total_v - n_i) * n_i
171        let partial = total_v_usize
172            .checked_sub(n_i_us)
173            .and_then(|d| d.checked_mul(n_i_us))
174            .ok_or_else(|| {
175                IgraphError::InvalidArgument(
176                    "full_multipartite: partition product overflows usize".to_string(),
177                )
178            })?;
179        sum_2e = sum_2e.checked_add(partial).ok_or_else(|| {
180            IgraphError::InvalidArgument("full_multipartite: edge sum overflows usize".to_string())
181        })?;
182    }
183    // sum_2e is exactly 2 · E_undirected because every unordered pair
184    // {i,j} contributes n_i·n_j (from i's iteration) plus n_j·n_i (from
185    // j's iteration). Undirected edge count is half of that.
186    let e_undirected = sum_2e / 2;
187    let edge_count = if directed && mode == MultipartiteMode::All {
188        sum_2e
189    } else {
190        e_undirected
191    };
192
193    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_count);
194
195    // Emission walk mirrors full.c:201-225 verbatim.
196    //
197    //   for from_type in 0..no_of_types-1:
198    //     edge_from = n_acc[from_type]
199    //     for i in 0..n[from_type]:
200    //       for to_type in from_type+1..no_of_types:
201    //         edge_to = n_acc[to_type]
202    //         for j in 0..n[to_type]:
203    //           emit per (directed, mode)
204    //           edge_to++
205    //       edge_from++
206    if no_of_types >= 2 {
207        for from_type in 0..(no_of_types - 1) {
208            // edge_from starts at n_acc[from_type] and increments per i.
209            // The cast to VertexId is safe: n_acc[from_type] < total_v ≤ u32::MAX.
210            #[allow(clippy::cast_possible_truncation)]
211            let from_base = n_acc[from_type] as VertexId;
212            for i in 0..partitions[from_type] {
213                #[allow(clippy::cast_possible_truncation)]
214                let edge_from = from_base + i as VertexId;
215                for to_type in (from_type + 1)..no_of_types {
216                    #[allow(clippy::cast_possible_truncation)]
217                    let to_base = n_acc[to_type] as VertexId;
218                    for j in 0..partitions[to_type] {
219                        #[allow(clippy::cast_possible_truncation)]
220                        let edge_to = to_base + j as VertexId;
221                        if !directed || mode == MultipartiteMode::Out {
222                            edges.push((edge_from, edge_to));
223                        } else if mode == MultipartiteMode::In {
224                            edges.push((edge_to, edge_from));
225                        } else {
226                            // directed && mode == All: emit both arcs.
227                            edges.push((edge_from, edge_to));
228                            edges.push((edge_to, edge_from));
229                        }
230                    }
231                }
232            }
233        }
234    }
235    debug_assert_eq!(edges.len(), edge_count);
236
237    // types[v] = partition index of v.
238    // Mirrors the C loop: v=1; for i in 0..N: if i == n_acc[v] { v++; } types[i] = v-1.
239    let mut types: Vec<u32> = Vec::with_capacity(total_v_usize);
240    if total_v_usize > 0 {
241        let mut v: usize = 1;
242        for i in 0..total_v_usize {
243            // Skip any empty partitions (n_acc[v-1] == n_acc[v] means partition v-1
244            // is empty, and now i has caught up with the next partition boundary).
245            while v < no_of_types && (i as u64) == n_acc[v] {
246                v += 1;
247            }
248            // v - 1 is the partition index for vertex i; bounded by no_of_types - 1.
249            let part_idx = u32::try_from(v - 1).map_err(|_| {
250                IgraphError::InvalidArgument(
251                    "full_multipartite: partition index overflows u32".to_string(),
252                )
253            })?;
254            types.push(part_idx);
255        }
256    }
257    debug_assert_eq!(types.len(), total_v_usize);
258
259    let mut graph = Graph::new(total_v, directed)?;
260    graph.add_edges(edges)?;
261    Ok(FullMultipartite { graph, types })
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use std::collections::BTreeSet;
268
269    fn dump_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
270        let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
271        (0..ec)
272            .map(|e| g.edge(e).expect("edge id in range"))
273            .collect()
274    }
275
276    fn canonical_undirected(g: &Graph) -> BTreeSet<(VertexId, VertexId)> {
277        dump_edges(g)
278            .into_iter()
279            .map(|(u, v)| if u <= v { (u, v) } else { (v, u) })
280            .collect()
281    }
282
283    fn partition_of(types: &[u32], v: VertexId) -> u32 {
284        types[v as usize]
285    }
286
287    // -------- degenerate cases (matching .out fixtures 1, 2, 6) --------
288
289    #[test]
290    fn empty_partitions_directed_all() {
291        let r = full_multipartite(&[], true, MultipartiteMode::All).expect("ok");
292        assert_eq!(r.graph.vcount(), 0);
293        assert_eq!(r.graph.ecount(), 0);
294        assert!(r.graph.is_directed());
295        assert!(r.types.is_empty());
296    }
297
298    #[test]
299    fn empty_partitions_undirected_all() {
300        let r = full_multipartite(&[], false, MultipartiteMode::All).expect("ok");
301        assert_eq!(r.graph.vcount(), 0);
302        assert_eq!(r.graph.ecount(), 0);
303        assert!(!r.graph.is_directed());
304        assert!(r.types.is_empty());
305    }
306
307    #[test]
308    fn single_partition_n4_directed_all() {
309        // .out fixture 2: directed n=[4] ALL → vcount=4, 0 edges, types=[0,0,0,0]
310        let r = full_multipartite(&[4], true, MultipartiteMode::All).expect("ok");
311        assert_eq!(r.graph.vcount(), 4);
312        assert_eq!(r.graph.ecount(), 0);
313        assert!(r.graph.is_directed());
314        assert_eq!(r.types, vec![0, 0, 0, 0]);
315    }
316
317    #[test]
318    fn all_zero_partitions_directed_all() {
319        // .out fixture 6: directed n=[0,0,0] ALL → vcount=0, 0 edges, types=[]
320        let r = full_multipartite(&[0, 0, 0], true, MultipartiteMode::All).expect("ok");
321        assert_eq!(r.graph.vcount(), 0);
322        assert_eq!(r.graph.ecount(), 0);
323        assert!(r.types.is_empty());
324    }
325
326    // -------- canonical .out fixtures (3, 4, 5, 7) --------
327
328    #[test]
329    fn directed_three_partitions_all_2_3_3() {
330        // .out fixture 3: directed n=[2,3,3] ALL → vcount=8, 42 arcs
331        let r = full_multipartite(&[2, 3, 3], true, MultipartiteMode::All).expect("ok");
332        assert_eq!(r.graph.vcount(), 8);
333        assert_eq!(r.graph.ecount(), 42);
334        assert_eq!(r.types, vec![0, 0, 1, 1, 1, 2, 2, 2]);
335        // Byte-stable emission order must match upstream.
336        let expected: Vec<(u32, u32)> = vec![
337            (0, 2),
338            (2, 0),
339            (0, 3),
340            (3, 0),
341            (0, 4),
342            (4, 0),
343            (0, 5),
344            (5, 0),
345            (0, 6),
346            (6, 0),
347            (0, 7),
348            (7, 0),
349            (1, 2),
350            (2, 1),
351            (1, 3),
352            (3, 1),
353            (1, 4),
354            (4, 1),
355            (1, 5),
356            (5, 1),
357            (1, 6),
358            (6, 1),
359            (1, 7),
360            (7, 1),
361            (2, 5),
362            (5, 2),
363            (2, 6),
364            (6, 2),
365            (2, 7),
366            (7, 2),
367            (3, 5),
368            (5, 3),
369            (3, 6),
370            (6, 3),
371            (3, 7),
372            (7, 3),
373            (4, 5),
374            (5, 4),
375            (4, 6),
376            (6, 4),
377            (4, 7),
378            (7, 4),
379        ];
380        assert_eq!(dump_edges(&r.graph), expected);
381    }
382
383    #[test]
384    fn directed_four_partitions_in_2_3_4_2() {
385        // .out fixture 4: directed n=[2,3,4,2] IN → vcount=11, 44 arcs
386        let r = full_multipartite(&[2, 3, 4, 2], true, MultipartiteMode::In).expect("ok");
387        assert_eq!(r.graph.vcount(), 11);
388        assert_eq!(r.graph.ecount(), 44);
389        assert_eq!(r.types, vec![0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3]);
390        let expected: Vec<(u32, u32)> = vec![
391            (2, 0),
392            (3, 0),
393            (4, 0),
394            (5, 0),
395            (6, 0),
396            (7, 0),
397            (8, 0),
398            (9, 0),
399            (10, 0),
400            (2, 1),
401            (3, 1),
402            (4, 1),
403            (5, 1),
404            (6, 1),
405            (7, 1),
406            (8, 1),
407            (9, 1),
408            (10, 1),
409            (5, 2),
410            (6, 2),
411            (7, 2),
412            (8, 2),
413            (9, 2),
414            (10, 2),
415            (5, 3),
416            (6, 3),
417            (7, 3),
418            (8, 3),
419            (9, 3),
420            (10, 3),
421            (5, 4),
422            (6, 4),
423            (7, 4),
424            (8, 4),
425            (9, 4),
426            (10, 4),
427            (9, 5),
428            (10, 5),
429            (9, 6),
430            (10, 6),
431            (9, 7),
432            (10, 7),
433            (9, 8),
434            (10, 8),
435        ];
436        assert_eq!(dump_edges(&r.graph), expected);
437    }
438
439    #[test]
440    fn undirected_four_partitions_all_2_3_4_2() {
441        // .out fixture 5: undirected n=[2,3,4,2] ALL → vcount=11, 44 edges
442        let r = full_multipartite(&[2, 3, 4, 2], false, MultipartiteMode::All).expect("ok");
443        assert_eq!(r.graph.vcount(), 11);
444        assert_eq!(r.graph.ecount(), 44);
445        assert!(!r.graph.is_directed());
446        assert_eq!(r.types, vec![0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3]);
447    }
448
449    #[test]
450    fn directed_one_empty_partition_2_0_3() {
451        // .out fixture 7: directed n=[2,0,3] ALL → vcount=5, 12 arcs
452        let r = full_multipartite(&[2, 0, 3], true, MultipartiteMode::All).expect("ok");
453        assert_eq!(r.graph.vcount(), 5);
454        assert_eq!(r.graph.ecount(), 12);
455        // Vertices 0,1 belong to partition 0; vertices 2,3,4 to partition 2.
456        // Partition 1 is empty.
457        assert_eq!(r.types, vec![0, 0, 2, 2, 2]);
458    }
459
460    // -------- additional cross-mode consistency tests --------
461
462    #[test]
463    fn directed_out_count_matches_undirected_for_same_partitions() {
464        let parts = [3u32, 4, 2];
465        let undir = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
466        let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
467        let in_ = full_multipartite(&parts, true, MultipartiteMode::In).expect("ok");
468        let all = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
469        assert_eq!(undir.graph.ecount(), out.graph.ecount());
470        assert_eq!(undir.graph.ecount(), in_.graph.ecount());
471        assert_eq!(all.graph.ecount(), 2 * undir.graph.ecount());
472    }
473
474    #[test]
475    fn out_and_in_have_reversed_arcs() {
476        let parts = [2u32, 3, 1];
477        let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
478        let in_ = full_multipartite(&parts, true, MultipartiteMode::In).expect("ok");
479        let out_edges: BTreeSet<(VertexId, VertexId)> =
480            dump_edges(&out.graph).into_iter().collect();
481        let in_reversed: BTreeSet<(VertexId, VertexId)> = dump_edges(&in_.graph)
482            .into_iter()
483            .map(|(a, b)| (b, a))
484            .collect();
485        assert_eq!(out_edges, in_reversed);
486    }
487
488    #[test]
489    fn no_intra_partition_edges() {
490        let parts = [2u32, 3, 4, 2];
491        let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
492        for (u, v) in dump_edges(&r.graph) {
493            assert_ne!(
494                partition_of(&r.types, u),
495                partition_of(&r.types, v),
496                "edge ({u}, {v}) connects same-partition vertices"
497            );
498        }
499    }
500
501    #[test]
502    fn types_are_monotone_non_decreasing() {
503        // Layout is partition-major so types must be non-decreasing.
504        let r = full_multipartite(&[3, 0, 2, 1, 4], false, MultipartiteMode::All).expect("ok");
505        for w in r.types.windows(2) {
506            assert!(w[0] <= w[1], "types {:?} must be monotone", r.types);
507        }
508    }
509
510    #[test]
511    fn vcount_equals_partition_sum() {
512        let parts = [5u32, 7, 0, 3];
513        let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
514        let want: u64 = parts.iter().map(|&x| u64::from(x)).sum();
515        assert_eq!(u64::from(r.graph.vcount()), want);
516    }
517
518    #[test]
519    fn directed_out_undirected_share_canonical_multiset() {
520        let parts = [3u32, 4, 2];
521        let undir = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
522        let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
523        assert_eq!(
524            canonical_undirected(&undir.graph),
525            canonical_undirected(&out.graph),
526        );
527    }
528}
529
530#[cfg(all(test, feature = "proptest-harness"))]
531mod proptests {
532    use super::*;
533    use proptest::prelude::*;
534
535    fn arb_partitions() -> impl Strategy<Value = Vec<u32>> {
536        // Up to 6 partitions, each up to 8 vertices.
537        prop::collection::vec(0u32..=8, 0..=6)
538    }
539
540    proptest! {
541        #[test]
542        fn pp_vcount_equals_partition_sum(parts in arb_partitions()) {
543            let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
544            let want: u64 = parts.iter().map(|&x| u64::from(x)).sum();
545            prop_assert_eq!(r.graph.vcount() as u64, want);
546            prop_assert_eq!(r.types.len() as u64, want);
547        }
548
549        #[test]
550        fn pp_no_intra_partition_edges(parts in arb_partitions()) {
551            let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
552            let ec = u32::try_from(r.graph.ecount()).unwrap();
553            for e in 0..ec {
554                let (u, v) = r.graph.edge(e).expect("edge in range");
555                prop_assert_ne!(r.types[u as usize], r.types[v as usize]);
556            }
557        }
558
559        #[test]
560        fn pp_all_doubles_out_ecount(parts in arb_partitions()) {
561            let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
562            let all = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
563            prop_assert_eq!(all.graph.ecount(), 2 * out.graph.ecount());
564        }
565
566        #[test]
567        fn pp_out_undirected_same_canonical_multiset(parts in arb_partitions()) {
568            let und = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
569            let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
570            let canon = |g: &Graph| -> std::collections::BTreeSet<(VertexId, VertexId)> {
571                let ec = u32::try_from(g.ecount()).unwrap();
572                (0..ec)
573                    .map(|e| g.edge(e).expect("ok"))
574                    .map(|(a, b)| if a <= b { (a, b) } else { (b, a) })
575                    .collect()
576            };
577            prop_assert_eq!(canon(&und.graph), canon(&out.graph));
578        }
579
580        #[test]
581        fn pp_types_monotone_non_decreasing(parts in arb_partitions()) {
582            let r = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
583            for w in r.types.windows(2) {
584                prop_assert!(w[0] <= w[1]);
585            }
586        }
587
588        #[test]
589        fn pp_in_reverses_out(parts in arb_partitions()) {
590            let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
591            let in_ = full_multipartite(&parts, true, MultipartiteMode::In).expect("ok");
592            prop_assert_eq!(out.graph.ecount(), in_.graph.ecount());
593            let out_set: std::collections::BTreeSet<(VertexId, VertexId)> = {
594                let ec = u32::try_from(out.graph.ecount()).unwrap();
595                (0..ec).map(|e| out.graph.edge(e).expect("ok")).collect()
596            };
597            let in_reversed: std::collections::BTreeSet<(VertexId, VertexId)> = {
598                let ec = u32::try_from(in_.graph.ecount()).unwrap();
599                (0..ec)
600                    .map(|e| in_.graph.edge(e).expect("ok"))
601                    .map(|(a, b)| (b, a))
602                    .collect()
603            };
604            prop_assert_eq!(out_set, in_reversed);
605        }
606    }
607}