Skip to main content

rust_igraph/algorithms/games/
chung_lu.rs

1//! Chung–Lu expected-degree random graph generator (ALGO-GN-012).
2//!
3//! Counterpart of `igraph_chung_lu_game()` from
4//! `references/igraph/src/games/chung_lu.c:193`.
5//!
6//! Given a per-vertex weight vector `w` (and, in the directed case,
7//! a separate in-weight vector with the same total mass), every pair
8//! `(i, j)` is connected independently with probability `p_ij`
9//! depending on the chosen variant. All three variants share the
10//! "expected-degree base form" `q_ij = w_i · w_j / S` where
11//! `S = Σ w_k`.
12//!
13//! ## Variants
14//!
15//! * [`ChungLuVariant::Original`] — `p_ij = min(q_ij, 1)`. The
16//!   original Chung–Lu (2002) construction. When `q_ij > 1` a single
17//!   warning is emitted (via `tracing`-style `eprintln!` if the
18//!   `verbose-warnings` feature ever lands; today the variant is
19//!   silently clamped) and the pair is still sampled — but in that
20//!   regime expected degrees no longer match the input weights.
21//! * [`ChungLuVariant::Maxent`] — `p_ij = q_ij / (1 + q_ij)`. The
22//!   "generalised random graph" / maximum-entropy model with fixed
23//!   expected degrees (Park & Newman 2004, Britton–Deijfen–Martin-Löf
24//!   2006).
25//! * [`ChungLuVariant::Nr`] — `p_ij = 1 - exp(-q_ij)`. The simple-graph
26//!   projection of Norros & Reittu's (2006) conditionally Poissonian
27//!   multigraph model.
28//!
29//! All three are equivalent in the sparse-graph limit `q_ij → 0`.
30//!
31//! ## Algorithm
32//!
33//! Implements the Miller–Hagberg (2011) `O(|V| + |E|)` sampler used by
34//! upstream igraph. The vertices are sorted by **in-weight descending**
35//! to obtain a permutation `idx`, then for each origin `i`:
36//!
37//! 1. Walk the target index `j` (starting at `0` for directed, at `i`
38//!    for undirected).
39//! 2. Maintain a running upper-bound probability `p` (starts at `1`).
40//!    Draw `gap ~ Geom(p)` and skip past it; the geometric skip is the
41//!    same primitive used by the Batagelj–Brandes ER samplers.
42//! 3. Compute the true probability `q` for the pair, accept the edge
43//!    with probability `q / p`, then set `p ← q`.
44//! 4. Because the sort is descending and `w_i` is fixed, `q` is
45//!    monotonically non-increasing as `j` grows, so `q / p ≤ 1`.
46//!
47//! The geometric-skip + accept/reject combination is mathematically
48//! identical to Bernoulli sampling each pair independently with the
49//! variant-specific probability, but only the accepted edges incur work.
50//!
51//! ## Determinism
52//!
53//! Reproducible given `(out_weights, in_weights, loops, variant, seed)`
54//! against the shared `SplitMix64` PRNG.
55//!
56//! ## References
57//!
58//! * Chung F, Lu L: *Connected components in a random graph with given
59//!   degree sequences*. Annals of Combinatorics **6**, 125–145 (2002).
60//! * Miller JC, Hagberg A: *Efficient generation of networks with given
61//!   expected degrees* (2011).
62//! * Park J, Newman MEJ: *Statistical mechanics of networks*.
63//!   Phys. Rev. E **70**, 066117 (2004).
64//! * Norros I, Reittu H: *On a conditionally Poissonian graph process*.
65//!   Adv. Appl. Probab. **38**, 59–75 (2006).
66
67#![allow(
68    unknown_lints,
69    clippy::cast_possible_truncation,
70    clippy::cast_precision_loss,
71    clippy::cast_sign_loss,
72    clippy::float_cmp,
73    clippy::similar_names,
74    clippy::many_single_char_names,
75    clippy::unnecessary_cast
76)]
77
78use crate::core::rng::SplitMix64;
79use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
80
81/// Variant of the Chung–Lu connection-probability formula.
82///
83/// All three share the base `q_ij = w_i · w_j / S` with
84/// `S = Σ out_weights`.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ChungLuVariant {
87    /// `p_ij = min(q_ij, 1)`. The original Chung–Lu (2002) formula.
88    /// Saturates to `1` when `q_ij > 1`.
89    Original,
90    /// `p_ij = q_ij / (1 + q_ij)`. The maximum-entropy / generalised
91    /// random-graph variant (Park & Newman 2004).
92    Maxent,
93    /// `p_ij = 1 - exp(-q_ij)`. The Norros–Reittu (2006) simple-graph
94    /// projection.
95    Nr,
96}
97
98/// Largest weight-vector length we support. Upstream igraph rejects
99/// at `IGRAPH_MAX_EXACT_REAL` (= `2^53`), which is the largest integer
100/// exactly representable as `f64`. On 64-bit targets we pick the same
101/// bound; on 32-bit targets `usize::MAX < 2^53`, so the cap is
102/// effectively the address-space limit and the const stays valid.
103const MAX_NODES: usize = if usize::BITS >= 64 {
104    1usize.wrapping_shl(53)
105} else {
106    usize::MAX
107};
108
109fn check_weights(label: &str, weights: &[f64]) -> IgraphResult<()> {
110    let mut max_w = f64::NEG_INFINITY;
111    let mut min_w = f64::INFINITY;
112    for (i, &w) in weights.iter().enumerate() {
113        if w.is_nan() {
114            return Err(IgraphError::InvalidArgument(format!(
115                "{label}[{i}] is NaN; Chung–Lu weights must be finite"
116            )));
117        }
118        if w < min_w {
119            min_w = w;
120        }
121        if w > max_w {
122            max_w = w;
123        }
124    }
125    if weights.is_empty() {
126        return Ok(());
127    }
128    if min_w < 0.0 {
129        return Err(IgraphError::InvalidArgument(format!(
130            "{label} must be non-negative; got minimum {min_w}"
131        )));
132    }
133    if !max_w.is_finite() {
134        return Err(IgraphError::InvalidArgument(format!(
135            "{label} must be finite; got maximum {max_w}"
136        )));
137    }
138    Ok(())
139}
140
141/// Sort indices `0..n` descending by `key[idx[k]]`. Ties broken by the
142/// natural index order to keep the output deterministic across runs of
143/// the same input.
144fn sort_indices_desc(key: &[f64]) -> Vec<usize> {
145    let mut idx: Vec<usize> = (0..key.len()).collect();
146    idx.sort_by(|&a, &b| {
147        key[b]
148            .partial_cmp(&key[a])
149            .unwrap_or(std::cmp::Ordering::Equal)
150            .then(a.cmp(&b))
151    });
152    idx
153}
154
155#[inline]
156fn apply_variant(q: f64, variant: ChungLuVariant) -> f64 {
157    match variant {
158        ChungLuVariant::Original => {
159            if q > 1.0 {
160                1.0
161            } else {
162                q
163            }
164        }
165        ChungLuVariant::Maxent => q / (1.0 + q),
166        ChungLuVariant::Nr => 1.0 - (-q).exp(),
167    }
168}
169
170/// Sample a random graph from the **Chung–Lu** expected-degree model.
171///
172/// * `out_weights` — non-negative, finite per-vertex weights. Length is
173///   the resulting graph's vertex count `n`. In the sparse-graph limit
174///   each `out_weights[i]` is approximately the expected (out-)degree of
175///   vertex `i`.
176/// * `in_weights` — when `Some`, generates a **directed** graph; must
177///   have the same length as `out_weights` and the same total mass
178///   (within `f64` exact equality, per upstream igraph). When `None`,
179///   generates an **undirected** graph with `in_weights = out_weights`.
180/// * `loops` — when `true`, self-loop edges are allowed (sampled
181///   independently with `p_ii`).
182/// * `variant` — which connection-probability formula to use; see
183///   [`ChungLuVariant`].
184/// * `seed` — initialises an internal `SplitMix64` PRNG so output is
185///   reproducible given the inputs.
186///
187/// # Errors
188///
189/// Returns [`IgraphError::InvalidArgument`] if:
190/// * any weight is negative, NaN, or non-finite (`±∞`);
191/// * `in_weights` has a different length than `out_weights`;
192/// * `in_weights` and `out_weights` have a different sum (directed
193///   case);
194/// * `out_weights.len()` exceeds `2^53` (the largest integer exactly
195///   representable as `f64`, matching upstream igraph's
196///   `IGRAPH_MAX_EXACT_REAL` guard).
197///
198/// # Complexity
199///
200/// `O(|V| log |V| + |E|)` — the `log |V|` factor comes from the
201/// descending in-weight sort; the sampler itself is linear in the
202/// realised edge count.
203///
204/// # Examples
205///
206/// ```
207/// use rust_igraph::{chung_lu_game, ChungLuVariant};
208///
209/// // Power-law-style weights: a handful of "hubs" plus many low-degree
210/// // vertices. The expected degree of vertex i ≈ out_weights[i].
211/// let weights: Vec<f64> = (0..50).map(|i| 1.0 + (i as f64) * 0.2).collect();
212/// let g = chung_lu_game(&weights, None, false, ChungLuVariant::Maxent, 42).unwrap();
213/// assert_eq!(g.vcount(), 50);
214/// assert!(!g.is_directed());
215/// ```
216pub fn chung_lu_game(
217    out_weights: &[f64],
218    in_weights: Option<&[f64]>,
219    loops: bool,
220    variant: ChungLuVariant,
221    seed: u64,
222) -> IgraphResult<Graph> {
223    let n = out_weights.len();
224    let directed = in_weights.is_some();
225
226    #[allow(clippy::absurd_extreme_comparisons)]
227    if n > MAX_NODES {
228        return Err(IgraphError::InvalidArgument(format!(
229            "Chung–Lu vertex count {n} exceeds the largest exactly representable f64 integer (2^53)"
230        )));
231    }
232
233    if let Some(inw) = in_weights {
234        if inw.len() != n {
235            return Err(IgraphError::InvalidArgument(format!(
236                "in_weights length {} does not match out_weights length {n}",
237                inw.len()
238            )));
239        }
240    }
241
242    let n_u32 = u32::try_from(n).map_err(|_| {
243        IgraphError::InvalidArgument(format!("Chung–Lu vertex count {n} exceeds u32::MAX"))
244    })?;
245
246    if n == 0 {
247        return Graph::new(0, directed);
248    }
249
250    check_weights("out_weights", out_weights)?;
251    let wsum: f64 = out_weights.iter().sum();
252
253    let in_view: &[f64] = match in_weights {
254        Some(inw) => {
255            check_weights("in_weights", inw)?;
256            let in_sum: f64 = inw.iter().sum();
257            if in_sum != wsum {
258                return Err(IgraphError::InvalidArgument(format!(
259                    "Sum of out- and in-weights must be the same; got out_sum = {wsum} and in_sum = {in_sum}"
260                )));
261            }
262            inw
263        }
264        None => out_weights,
265    };
266
267    // S == 0 means every q_ij is 0 ⇒ empty graph regardless of variant.
268    if wsum == 0.0 {
269        return Graph::new(n_u32, directed);
270    }
271
272    let idx = sort_indices_desc(in_view);
273    let mut rng = SplitMix64::new(seed);
274    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
275
276    for i in 0..n {
277        let vi = idx[i];
278        let wi = out_weights[vi];
279        if wi == 0.0 {
280            if directed {
281                continue;
282            }
283            break;
284        }
285
286        let mut j = if directed { 0 } else { i };
287        let mut p: f64 = 1.0;
288
289        loop {
290            let remaining = n - j;
291            let gap = rng.gen_geom(p);
292            // Terminate when the skip overshoots; also protects against
293            // overflow when `p` is tiny.
294            if !gap.is_finite() || gap >= remaining as f64 {
295                break;
296            }
297            // gen_geom returns a non-negative float; safe to cast.
298            j += gap as usize;
299            if j >= n {
300                break;
301            }
302
303            let vj = idx[j];
304            let wj = in_view[vj];
305
306            // q = w_i · w_j / S with the variant-specific transform.
307            let q_raw = wi * wj / wsum;
308            let q = apply_variant(q_raw, variant);
309
310            if q == 0.0 {
311                break;
312            }
313
314            // p is the running upper-bound probability used for the
315            // geometric skip; q / p is always in [0, 1] because q is
316            // monotonically non-increasing along the descending-weight
317            // sort and the previous step set p to its previous q value.
318            let ratio = if p > 0.0 { q / p } else { 0.0 };
319            let accept = rng.gen_unit() < ratio;
320            if accept && (loops || vi != vj) {
321                let from = u32::try_from(vi).map_err(|_| {
322                    IgraphError::InvalidArgument(format!(
323                        "Chung–Lu source vertex index {vi} overflows u32"
324                    ))
325                })?;
326                let to = u32::try_from(vj).map_err(|_| {
327                    IgraphError::InvalidArgument(format!(
328                        "Chung–Lu target vertex index {vj} overflows u32"
329                    ))
330                })?;
331                edges.push((from, to));
332            }
333
334            p = q;
335            j += 1;
336        }
337    }
338
339    let mut g = Graph::new(n_u32, directed)?;
340    g.add_edges(edges)?;
341    Ok(g)
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    fn ecount(g: &Graph) -> usize {
349        g.ecount() as usize
350    }
351
352    #[test]
353    fn empty_weights_undirected() {
354        let g = chung_lu_game(&[], None, false, ChungLuVariant::Original, 0).unwrap();
355        assert_eq!(g.vcount(), 0);
356        assert!(!g.is_directed());
357        assert_eq!(g.ecount(), 0);
358    }
359
360    #[test]
361    fn empty_weights_directed() {
362        let in_w: [f64; 0] = [];
363        let g = chung_lu_game(&[], Some(&in_w), true, ChungLuVariant::Maxent, 0).unwrap();
364        assert_eq!(g.vcount(), 0);
365        assert!(g.is_directed());
366    }
367
368    #[test]
369    fn zero_weights_yields_empty_graph() {
370        let w = vec![0.0; 8];
371        for variant in [
372            ChungLuVariant::Original,
373            ChungLuVariant::Maxent,
374            ChungLuVariant::Nr,
375        ] {
376            let g = chung_lu_game(&w, None, true, variant, 1).unwrap();
377            assert_eq!(g.vcount(), 8);
378            assert_eq!(g.ecount(), 0, "variant {variant:?} should give empty graph");
379        }
380    }
381
382    #[test]
383    fn single_vertex_no_loops_is_empty() {
384        let g = chung_lu_game(&[5.0], None, false, ChungLuVariant::Maxent, 7).unwrap();
385        assert_eq!(g.vcount(), 1);
386        assert_eq!(g.ecount(), 0);
387    }
388
389    #[test]
390    fn rejects_negative_weight() {
391        let w = vec![1.0, -0.5, 2.0];
392        let err = chung_lu_game(&w, None, false, ChungLuVariant::Original, 0).unwrap_err();
393        match err {
394            IgraphError::InvalidArgument(msg) => {
395                assert!(msg.contains("non-negative"), "msg = {msg}");
396            }
397            other => panic!("unexpected error: {other:?}"),
398        }
399    }
400
401    #[test]
402    fn rejects_non_finite_weight() {
403        let w = vec![1.0, f64::INFINITY];
404        let err = chung_lu_game(&w, None, false, ChungLuVariant::Original, 0).unwrap_err();
405        assert!(matches!(err, IgraphError::InvalidArgument(_)));
406
407        let w_nan = vec![1.0, f64::NAN];
408        let err = chung_lu_game(&w_nan, None, false, ChungLuVariant::Maxent, 0).unwrap_err();
409        assert!(matches!(err, IgraphError::InvalidArgument(_)));
410    }
411
412    #[test]
413    fn rejects_mismatched_in_out_lengths() {
414        let out_w = vec![1.0, 2.0, 3.0];
415        let in_w = vec![1.0, 5.0];
416        let err =
417            chung_lu_game(&out_w, Some(&in_w), false, ChungLuVariant::Original, 0).unwrap_err();
418        match err {
419            IgraphError::InvalidArgument(msg) => assert!(msg.contains("length"), "msg = {msg}"),
420            other => panic!("{other:?}"),
421        }
422    }
423
424    #[test]
425    fn rejects_mismatched_in_out_sums() {
426        let out_w = vec![1.0, 2.0, 3.0];
427        let in_w = vec![1.0, 1.0, 1.0];
428        let err = chung_lu_game(&out_w, Some(&in_w), false, ChungLuVariant::Maxent, 0).unwrap_err();
429        match err {
430            IgraphError::InvalidArgument(msg) => assert!(msg.contains("sum"), "msg = {msg}"),
431            other => panic!("{other:?}"),
432        }
433    }
434
435    #[test]
436    fn deterministic_for_fixed_seed() {
437        let w: Vec<f64> = (0..40).map(|i| 1.0 + f64::from(i) * 0.1).collect();
438        let g1 = chung_lu_game(&w, None, false, ChungLuVariant::Maxent, 0xABC).unwrap();
439        let g2 = chung_lu_game(&w, None, false, ChungLuVariant::Maxent, 0xABC).unwrap();
440        assert_eq!(ecount(&g1), ecount(&g2));
441        let m = ecount(&g1);
442        for eid in 0..m as u32 {
443            assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
444        }
445    }
446
447    #[test]
448    fn different_seeds_differ_or_match_size_only() {
449        let w: Vec<f64> = (0..40).map(|i| 1.0 + f64::from(i) * 0.1).collect();
450        let g1 = chung_lu_game(&w, None, false, ChungLuVariant::Maxent, 1).unwrap();
451        let g2 = chung_lu_game(&w, None, false, ChungLuVariant::Maxent, 2).unwrap();
452        // Vertex counts always match.
453        assert_eq!(g1.vcount(), g2.vcount());
454        // Probability that 40-vertex outputs share edge lists exactly is
455        // astronomically low; assert difference.
456        let m1 = ecount(&g1);
457        let m2 = ecount(&g2);
458        if m1 == m2 && m1 > 0 {
459            let mut any_diff = false;
460            for eid in 0..m1 as u32 {
461                if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
462                    any_diff = true;
463                    break;
464                }
465            }
466            assert!(
467                any_diff,
468                "two seeds happened to produce identical edge sequences"
469            );
470        }
471    }
472
473    #[test]
474    fn no_loops_when_loops_false() {
475        let w = vec![5.0; 20];
476        let g = chung_lu_game(&w, None, false, ChungLuVariant::Maxent, 9).unwrap();
477        let m = ecount(&g) as u32;
478        for eid in 0..m {
479            let (u, v) = g.edge(eid).unwrap();
480            assert!(u != v, "found self-loop at edge {eid}: ({u},{v})");
481        }
482    }
483
484    #[test]
485    fn directed_graph_has_directed_edges() {
486        let w = vec![3.0; 15];
487        let g = chung_lu_game(&w, Some(&w), false, ChungLuVariant::Maxent, 11).unwrap();
488        assert!(g.is_directed());
489        assert_eq!(g.vcount(), 15);
490    }
491
492    #[test]
493    fn maxent_mean_degree_matches_weight_in_sparse_limit() {
494        // E[deg(i)] ≈ w_i (S - w_i) / S for undirected, no loops; with
495        // identical weights w_i = w, this is w (n-1) w / (n w) = w (n-1) / n.
496        let n = 200_usize;
497        let w_val = 4.0_f64;
498        let w = vec![w_val; n];
499        let g = chung_lu_game(&w, None, false, ChungLuVariant::Maxent, 0x5EE_D001).unwrap();
500        // Sum of degrees = 2|E| (undirected).
501        let total_deg = 2.0 * ecount(&g) as f64;
502        let mean_deg = total_deg / n as f64;
503        // For Maxent: p = q / (1 + q) with q = w²/(n·w) = w/n.
504        // So mean degree ≈ (n-1) · (w/n) / (1 + w/n)
505        //                ≈ w · (n-1)/(n + w)
506        let expected = w_val * (n as f64 - 1.0) / (n as f64 + w_val);
507        let tol = 1.0_f64; // very generous; only sanity that it's near.
508        assert!(
509            (mean_deg - expected).abs() < tol,
510            "mean_deg = {mean_deg}, expected ≈ {expected}"
511        );
512    }
513
514    #[test]
515    fn nr_and_original_all_zero_probability_yields_no_edges() {
516        // Very low weights mean q ≈ 0 for all pairs → tiny graph.
517        let w = vec![1e-3; 50];
518        for variant in [
519            ChungLuVariant::Original,
520            ChungLuVariant::Maxent,
521            ChungLuVariant::Nr,
522        ] {
523            let g = chung_lu_game(&w, None, false, variant, 13).unwrap();
524            // Expected edges = C(50,2) · q with q = (1e-3)² / (50·1e-3) = 2e-5
525            // => about 0.02 in expectation; almost certainly 0 in any draw.
526            assert!(
527                ecount(&g) <= 3,
528                "variant {variant:?} produced too many edges"
529            );
530        }
531    }
532
533    #[test]
534    fn vertex_count_is_input_length() {
535        for n in [1, 2, 5, 20] {
536            let w = vec![2.0; n];
537            let g = chung_lu_game(&w, None, true, ChungLuVariant::Maxent, 1).unwrap();
538            assert_eq!(g.vcount() as usize, n);
539        }
540    }
541
542    #[test]
543    fn original_with_very_large_weights_is_dense() {
544        // Pick weights so that q_ij >> 1 for most pairs in the original
545        // variant. p_ij gets clamped to 1, so we should approach the
546        // complete graph.
547        let n = 30;
548        let w = vec![100.0; n];
549        let g = chung_lu_game(&w, None, false, ChungLuVariant::Original, 19).unwrap();
550        let m = ecount(&g);
551        // Complete graph has n·(n-1)/2 = 435 edges; we should be ≥ 95%
552        // of that thanks to clamp-to-1.
553        let cmax = n * (n - 1) / 2;
554        assert!(
555            m as f64 >= 0.95 * cmax as f64,
556            "expected near-complete graph, got {m}/{cmax}"
557        );
558    }
559}
560
561#[cfg(all(test, feature = "proptest-harness"))]
562mod proptests {
563    use super::*;
564    use proptest::prelude::*;
565
566    proptest! {
567        #![proptest_config(ProptestConfig::with_cases(48))]
568
569        /// Vertex count is always exactly the weight-vector length.
570        #[test]
571        fn vcount_matches_weight_len(
572            n in 1usize..40,
573            w_max in 0.0_f64..5.0,
574            seed: u64,
575            variant_pick in 0u8..3,
576        ) {
577            let weights: Vec<f64> = (0..n).map(|i| w_max * (i as f64 + 1.0) / n as f64).collect();
578            let variant = match variant_pick {
579                0 => ChungLuVariant::Original,
580                1 => ChungLuVariant::Maxent,
581                _ => ChungLuVariant::Nr,
582            };
583            let g = chung_lu_game(&weights, None, false, variant, seed).unwrap();
584            prop_assert_eq!(g.vcount() as usize, n);
585            prop_assert!(!g.is_directed());
586        }
587
588        /// Determinism: same seed + same inputs ⇒ identical edge sequence.
589        #[test]
590        fn determinism(
591            n in 1usize..40,
592            w_max in 0.0_f64..3.0,
593            seed: u64,
594            loops: bool,
595        ) {
596            let weights: Vec<f64> = (0..n).map(|i| w_max * (i as f64 + 1.0) / n as f64).collect();
597            let g1 = chung_lu_game(&weights, None, loops, ChungLuVariant::Maxent, seed).unwrap();
598            let g2 = chung_lu_game(&weights, None, loops, ChungLuVariant::Maxent, seed).unwrap();
599            prop_assert_eq!(g1.ecount(), g2.ecount());
600            let m = g1.ecount();
601            for eid in 0..m as u32 {
602                prop_assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
603            }
604        }
605
606        /// `loops = false` ⇒ no self-loop edges in the output.
607        #[test]
608        fn no_self_loops_when_disabled(
609            n in 2usize..30,
610            w_max in 0.5_f64..4.0,
611            seed: u64,
612        ) {
613            let weights: Vec<f64> = (0..n).map(|i| w_max * (i as f64 + 1.0) / n as f64).collect();
614            let g = chung_lu_game(&weights, None, false, ChungLuVariant::Maxent, seed).unwrap();
615            let m = g.ecount() as u32;
616            for eid in 0..m {
617                let (u, v) = g.edge(eid).unwrap();
618                prop_assert_ne!(u, v);
619            }
620        }
621    }
622}