Skip to main content

lc_rag/graph_rag/
leiden.rs

1// src/retrieval/graph_rag/leiden.rs
2//! Deterministic weighted Leiden community detection.
3//!
4//! Implements the Leiden algorithm (Traag, Waltman & van Eck, 2019) with the
5//! **weighted modularity** quality function — the same quality function used
6//! by graspologic / Microsoft GraphRAG:
7//!
8//! ```text
9//! Q = Σ_c ( E_c/m − γ (Σ_c / 2m)² )
10//! ```
11//!
12//! where `E_c` is the internal edge weight of community `c` (each undirected
13//! edge counted once), `Σ_c` the sum of member degrees, `2m` total degree,
14//! and `γ` the resolution (higher → smaller communities). Each outer
15//! iteration runs the three canonical phases — fast local moving,
16//! guaranteed-refinement, and aggregation — on an ever coarser graph until
17//! no community can be merged any further.
18//!
19//! The implementation is dependency-free and **deterministic** for a given
20//! seed (a splitmix64 PRNG drives node visit order; ties break by community
21//! id). Graph construction aggregates multi-edges, so an entity pair
22//! extracted from many documents is connected more strongly.
23
24use std::collections::{HashMap, VecDeque};
25
26/// Moves must improve modularity by more than this (scaled by `2m`) to be
27/// accepted — guards against floating-point noise around zero.
28const EPS: f64 = 1e-9;
29
30/// Undirected weighted graph with self-loop weights.
31///
32/// `adj` holds each edge in **both** directions and never contains
33/// self entries; self-loop weight (internal edges after aggregation) lives
34/// separately in `loops`.
35#[derive(Debug, Clone)]
36pub struct WeightedGraph {
37    n: usize,
38    adj: Vec<Vec<(usize, f64)>>,
39    loops: Vec<f64>,
40}
41
42impl WeightedGraph {
43    /// Creates an empty graph with `n` isolated nodes.
44    pub fn new(n: usize) -> Self {
45        Self {
46            n,
47            adj: vec![Vec::new(); n],
48            loops: vec![0.0; n],
49        }
50    }
51
52    /// Number of nodes.
53    pub fn node_count(&self) -> usize {
54        self.n
55    }
56
57    /// Builds a graph from `(u, v, weight)` undirected edges, aggregating
58    /// parallel edges. Edges with non-finite or non-positive weight and edges
59    /// referencing unknown nodes are ignored. A self edge contributes to the
60    /// node's self-loop weight.
61    pub fn from_edges(n: usize, edges: impl IntoIterator<Item = (usize, usize, f64)>) -> Self {
62        let mut aggregated: HashMap<(usize, usize), f64> = HashMap::new();
63        for (u, v, w) in edges {
64            if u >= n || v >= n || !w.is_finite() || w <= 0.0 {
65                continue;
66            }
67            let key = if u <= v { (u, v) } else { (v, u) };
68            *aggregated.entry(key).or_insert(0.0) += w;
69        }
70
71        let mut graph = Self::new(n);
72        for ((u, v), w) in aggregated {
73            if u == v {
74                graph.loops[u] += w;
75            } else {
76                graph.adj[u].push((v, w));
77                graph.adj[v].push((u, w));
78            }
79        }
80        // Sorted neighbors make traversal order (and hence results) stable.
81        for neighbors in &mut graph.adj {
82            neighbors.sort_by(|a, b| a.0.cmp(&b.0));
83        }
84        graph
85    }
86
87    /// Degree of `node`: incident edge weight plus twice its self-loop weight.
88    fn degree(&self, node: usize) -> f64 {
89        let incident: f64 = self.adj[node].iter().map(|(_, w)| *w).sum();
90        incident + 2.0 * self.loops[node]
91    }
92
93    /// Total weight of edges from `node` to nodes in community `label`,
94    /// excluding the node itself (self loops are omitted — they are constant
95    /// across moves and cancel from every modularity delta).
96    fn weight_to(&self, node: usize, labels: &[i64], label: i64) -> f64 {
97        self.adj[node]
98            .iter()
99            .filter(|(nb, _)| labels[*nb] == label)
100            .map(|(_, w)| *w)
101            .sum()
102    }
103}
104
105/// A compacted partition: community labels in `0..k`.
106#[derive(Debug, Clone)]
107struct Partition {
108    /// Community label per node.
109    labels: Vec<i64>,
110    /// Sum of member degrees per community (indexed by label).
111    degree_sums: Vec<f64>,
112}
113
114impl Partition {
115    fn num_communities(&self) -> usize {
116        self.degree_sums.len()
117    }
118}
119
120/// Compacts arbitrary community ids (seed node ids, inherited parent labels)
121/// into the contiguous range `0..k`, ordered by first appearance in node
122/// order so labels are stable across runs.
123fn compact_labels(raw: &[i64], degrees: &[f64]) -> Partition {
124    let mut remap: HashMap<i64, i64> = HashMap::new();
125    let mut labels = vec![-1i64; raw.len()];
126    let mut degree_sums = Vec::new();
127    for (node, &raw_label) in raw.iter().enumerate() {
128        let next = remap.len() as i64;
129        let label = *remap.entry(raw_label).or_insert(next);
130        labels[node] = label;
131        if label as usize >= degree_sums.len() {
132            degree_sums.push(0.0);
133        }
134        degree_sums[label as usize] += degrees[node];
135    }
136    Partition {
137        labels,
138        degree_sums,
139    }
140}
141
142/// splitmix64 — a tiny deterministic PRNG (Sebastiano Vigna, public domain /
143/// CC0). We only need reproducible visit ordering, not cryptographic quality.
144struct Rng {
145    state: u64,
146}
147
148impl Rng {
149    fn new(seed: u64) -> Self {
150        Self { state: seed }
151    }
152
153    fn next_u64(&mut self) -> u64 {
154        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
155        let mut z = self.state;
156        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
157        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
158        z ^ (z >> 31)
159    }
160
161    /// Fisher–Yates shuffle.
162    fn shuffle<T>(&mut self, items: &mut [T]) {
163        if items.len() < 2 {
164            return;
165        }
166        for i in (1..items.len()).rev() {
167            let j = (self.next_u64() % (i as u64 + 1)) as usize;
168            items.swap(i, j);
169        }
170    }
171}
172
173/// Scaled modularity delta of moving `node` from its current community into
174/// community `to_label`; the common factor `2m` is omitted so only the sign
175/// matters.
176///
177/// ```text
178/// 2m·ΔQ = 2(w_to − w_from) + γ·deg(node)·(Σ_from − deg(node) − Σ_to) / m
179/// ```
180fn move_delta(
181    weight_to: f64,
182    weight_from: f64,
183    degree: f64,
184    sum_from_minus_node: f64,
185    sum_to: f64,
186    resolution: f64,
187    m: f64,
188) -> f64 {
189    2.0 * (weight_to - weight_from) + resolution * degree * (sum_from_minus_node - sum_to) / m
190}
191
192/// Fast local-moving phase.
193///
194/// Starts from `initial` and moves a node to the neighboring community with
195/// the largest strictly positive modularity delta until the queue drains.
196fn local_moving(
197    graph: &WeightedGraph,
198    initial: &[i64],
199    resolution: f64,
200    rng: &mut Rng,
201) -> Partition {
202    let degrees: Vec<f64> = (0..graph.n).map(|node| graph.degree(node)).collect();
203    let m2: f64 = degrees.iter().sum();
204
205    // Edgeless graph: nothing can ever improve modularity.
206    if m2 <= 0.0 {
207        return compact_labels(initial, &degrees);
208    }
209    let m = m2 / 2.0;
210
211    let partition = compact_labels(initial, &degrees);
212    let mut labels = partition.labels;
213    let mut degree_sums = partition.degree_sums;
214
215    let mut order: Vec<usize> = (0..graph.n).collect();
216    rng.shuffle(&mut order);
217
218    let mut queue: VecDeque<usize> = order.iter().copied().collect();
219    let mut in_queue = vec![true; graph.n];
220
221    while let Some(node) = queue.pop_front() {
222        in_queue[node] = false;
223
224        let current = labels[node];
225        let node_degree = degrees[node];
226        let sum_from_without_node = degree_sums[current as usize] - node_degree;
227        let weight_current = graph.weight_to(node, &labels, current);
228
229        // Aggregate edge weight from `node` into each neighboring community.
230        let mut candidate_weight: HashMap<i64, f64> = HashMap::new();
231        for &(nb, w) in &graph.adj[node] {
232            let label = labels[nb];
233            *candidate_weight.entry(label).or_insert(0.0) += w;
234        }
235
236        // Best strictly-positive delta; ties resolve to the smallest label.
237        let mut best_label = current;
238        let mut best_delta = 0.0_f64;
239        for (&label, &weight_to_label) in &candidate_weight {
240            if label == current {
241                continue;
242            }
243            let delta = move_delta(
244                weight_to_label,
245                weight_current,
246                node_degree,
247                sum_from_without_node,
248                degree_sums[label as usize],
249                resolution,
250                m,
251            );
252            if delta > best_delta + EPS
253                || (delta > EPS && (delta - best_delta).abs() <= EPS && label < best_label)
254            {
255                best_delta = delta;
256                best_label = label;
257            }
258        }
259
260        if best_label != current {
261            degree_sums[current as usize] -= node_degree;
262            degree_sums[best_label as usize] += node_degree;
263            labels[node] = best_label;
264
265            // Only neighbors outside the joined community may now want to move.
266            for &(nb, _) in &graph.adj[node] {
267                if labels[nb] != best_label && !in_queue[nb] {
268                    in_queue[nb] = true;
269                    queue.push_back(nb);
270                }
271            }
272        }
273    }
274
275    Partition {
276        labels,
277        degree_sums,
278    }
279}
280
281/// Refinement phase.
282///
283/// Splits every local-moving community into subsets, seeding each subset with
284/// one node and greedily absorbing neighbors on **non-negative** modularity
285/// deltas (zero-delta absorption is what lets refinement split a community
286/// while keeping every subset well connected). Members of different input
287/// communities never share a subset.
288fn refine(
289    graph: &WeightedGraph,
290    communities: &Partition,
291    resolution: f64,
292    rng: &mut Rng,
293) -> Partition {
294    let degrees: Vec<f64> = (0..graph.n).map(|node| graph.degree(node)).collect();
295    let m2: f64 = degrees.iter().sum();
296    let m = m2 / 2.0;
297
298    let mut subset = vec![-1i64; graph.n];
299    let mut subset_degree_sums: Vec<f64> = Vec::new();
300    let mut next_subset = 0i64;
301
302    let mut community_nodes: HashMap<i64, Vec<usize>> = HashMap::new();
303    for (node, &label) in communities.labels.iter().enumerate() {
304        community_nodes.entry(label).or_default().push(node);
305    }
306    let mut community_order: Vec<i64> = community_nodes.keys().copied().collect();
307    community_order.sort_unstable();
308
309    for community in community_order {
310        let mut nodes = community_nodes.remove(&community).unwrap_or_default();
311        rng.shuffle(&mut nodes);
312
313        for node in nodes {
314            // Weight from `node` into each existing subset of its own community.
315            let mut candidate_weight: HashMap<i64, f64> = HashMap::new();
316            for &(nb, w) in &graph.adj[node] {
317                let s = subset[nb];
318                if s >= 0 && communities.labels[nb] == community {
319                    *candidate_weight.entry(s).or_insert(0.0) += w;
320                }
321            }
322
323            // The node is currently an unassigned singleton: only the
324            // "join subset S" side contributes (2·w − γ·deg·Σ_S/m, scaled).
325            let mut best_subset = -1i64;
326            let mut best_delta = 0.0_f64;
327            for (&s, &weight) in &candidate_weight {
328                let delta =
329                    2.0 * weight - resolution * degrees[node] * subset_degree_sums[s as usize] / m;
330                if delta < -EPS {
331                    continue;
332                }
333                if best_subset < 0
334                    || delta > best_delta + EPS
335                    || ((delta - best_delta).abs() <= EPS && s < best_subset)
336                {
337                    best_delta = delta;
338                    best_subset = s;
339                }
340            }
341
342            if best_subset < 0 {
343                best_subset = next_subset;
344                next_subset += 1;
345                subset_degree_sums.push(0.0);
346            }
347            subset[node] = best_subset;
348            subset_degree_sums[best_subset as usize] += degrees[node];
349        }
350    }
351
352    compact_labels(&subset, &degrees)
353}
354
355/// An aggregated graph plus the original node indices represented by each
356/// aggregate node.
357struct Aggregated {
358    graph: WeightedGraph,
359    members: Vec<Vec<usize>>,
360}
361
362/// Aggregates `graph` by the refined partition: one node per subset, edge
363/// weights summed across subsets, internal edges becoming self loops.
364fn aggregate(graph: &WeightedGraph, refined: &Partition, members: &[Vec<usize>]) -> Aggregated {
365    let k = refined.num_communities();
366
367    let mut edges: HashMap<(usize, usize), f64> = HashMap::new();
368    for node in 0..graph.n {
369        let subset = refined.labels[node] as usize;
370        for &(nb, w) in &graph.adj[node] {
371            if nb > node {
372                let other = refined.labels[nb] as usize;
373                let key = if subset <= other {
374                    (subset, other)
375                } else {
376                    (other, subset)
377                };
378                *edges.entry(key).or_insert(0.0) += w;
379            }
380        }
381        if graph.loops[node] > 0.0 {
382            *edges.entry((subset, subset)).or_insert(0.0) += graph.loops[node];
383        }
384    }
385
386    let mut aggregated = WeightedGraph::new(k);
387    for ((u, v), w) in edges {
388        if u == v {
389            aggregated.loops[u] += w;
390        } else {
391            aggregated.adj[u].push((v, w));
392            aggregated.adj[v].push((u, w));
393        }
394    }
395    for neighbors in &mut aggregated.adj {
396        neighbors.sort_by(|a, b| a.0.cmp(&b.0));
397    }
398
399    let mut aggregated_members = vec![Vec::new(); k];
400    for (node, original) in members.iter().enumerate() {
401        aggregated_members[refined.labels[node] as usize].extend_from_slice(original);
402    }
403
404    Aggregated {
405        graph: aggregated,
406        members: aggregated_members,
407    }
408}
409
410/// Runs Leiden community detection with the weighted-modularity quality
411/// function.
412///
413/// Returns a community label per node in `0..k`. Singleton and disconnected
414/// nodes receive their own label; callers decide whether to keep them.
415///
416/// - `resolution` is γ (canonical default `1.0`; values above 1 favor smaller
417///   communities, below 1 favor larger ones).
418/// - `seed` makes the node visit order reproducible.
419pub fn leiden_modularity(graph: &WeightedGraph, resolution: f64, seed: u64) -> Vec<usize> {
420    let n = graph.n;
421    if n == 0 {
422        return Vec::new();
423    }
424    if n == 1 {
425        return vec![0];
426    }
427
428    let mut rng = Rng::new(seed);
429    let members: Vec<Vec<usize>> = (0..n).map(|node| vec![node]).collect();
430
431    // Level 0: local moving from the singleton partition.
432    let singleton: Vec<i64> = (0..n as i64).collect();
433    let mut partition = local_moving(graph, &singleton, resolution, &mut rng);
434    let mut current_graph = graph.clone();
435    let mut current_members = members;
436
437    loop {
438        // Every node its own community: nothing to refine or merge.
439        if partition.num_communities() == current_graph.n {
440            break;
441        }
442
443        let refined = refine(&current_graph, &partition, resolution, &mut rng);
444
445        // Refinement must be strict; if it did not subdivide anything, another
446        // round on the same graph could never change the result either.
447        if refined.num_communities() == partition.num_communities() {
448            break;
449        }
450
451        let aggregated = aggregate(&current_graph, &refined, &current_members);
452
453        // Initial partition on the aggregate graph: each refined subset
454        // inherits the community its members had after local moving.
455        let mut initial = vec![-1i64; aggregated.graph.n];
456        for node in 0..current_graph.n {
457            initial[refined.labels[node] as usize] = partition.labels[node];
458        }
459
460        partition = local_moving(&aggregated.graph, &initial, resolution, &mut rng);
461        current_graph = aggregated.graph;
462        current_members = aggregated.members;
463    }
464
465    // Expand labels on the coarsest aggregate graph back to original nodes.
466    let mut result = vec![0usize; n];
467    for (aggregate_node, originals) in current_members.iter().enumerate() {
468        for &original in originals {
469            result[original] = partition.labels[aggregate_node] as usize;
470        }
471    }
472
473    // Final compaction in first-appearance order.
474    let degrees: Vec<f64> = (0..n).map(|node| graph.degree(node)).collect();
475    let compact = compact_labels(
476        &result.iter().map(|&x| x as i64).collect::<Vec<_>>(),
477        &degrees,
478    );
479    compact.labels.iter().map(|&x| x as usize).collect()
480}
481
482/// Returns true if the subgraph induced by `nodes` is connected using only
483/// edges inside `nodes` (Leiden's well-connected-community guarantee).
484#[cfg(test)]
485pub(crate) fn is_connected_induced(graph: &WeightedGraph, nodes: &[usize]) -> bool {
486    if nodes.is_empty() {
487        return true;
488    }
489    let in_set: std::collections::HashSet<usize> = nodes.iter().copied().collect();
490    let mut seen = std::collections::HashSet::new();
491    let mut stack = vec![nodes[0]];
492    seen.insert(nodes[0]);
493    while let Some(node) = stack.pop() {
494        for &(nb, _) in &graph.adj[node] {
495            if in_set.contains(&nb) && seen.insert(nb) {
496                stack.push(nb);
497            }
498        }
499    }
500    seen.len() == nodes.len()
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    /// Builds a graph from (u, v) unit edges.
508    fn graph_from(n: usize, pairs: &[(usize, usize)]) -> WeightedGraph {
509        let edges = pairs.iter().map(|&(u, v)| (u, v, 1.0)).collect::<Vec<_>>();
510        WeightedGraph::from_edges(n, edges)
511    }
512
513    /// Adds a k-node clique to `pairs`.
514    fn push_clique(pairs: &mut Vec<(usize, usize)>, offset: usize, k: usize) {
515        for i in 0..k {
516            for j in (i + 1)..k {
517                pairs.push((offset + i, offset + j));
518            }
519        }
520    }
521
522    fn groups(labels: &[usize]) -> Vec<Vec<usize>> {
523        let k = labels.iter().copied().max().map(|m| m + 1).unwrap_or(0);
524        let mut groups = vec![Vec::new(); k];
525        for (node, &label) in labels.iter().enumerate() {
526            groups[label].push(node);
527        }
528        groups.retain(|g| !g.is_empty());
529        groups
530    }
531
532    #[test]
533    fn empty_and_single_node_graphs() {
534        let g = WeightedGraph::new(0);
535        assert!(leiden_modularity(&g, 1.0, 1).is_empty());
536        let g = WeightedGraph::new(1);
537        assert_eq!(leiden_modularity(&g, 1.0, 1), vec![0]);
538    }
539
540    #[test]
541    fn connected_pair_is_one_community() {
542        let g = graph_from(2, &[(0, 1)]);
543        let labels = leiden_modularity(&g, 1.0, 1);
544        assert_eq!(labels[0], labels[1]);
545    }
546
547    #[test]
548    fn two_disconnected_triangles_stay_separate() {
549        let mut pairs = Vec::new();
550        push_clique(&mut pairs, 0, 3);
551        push_clique(&mut pairs, 3, 3);
552        let g = graph_from(6, &pairs);
553        let labels = leiden_modularity(&g, 1.0, 1);
554        assert_ne!(labels[0], labels[3]);
555        assert_eq!(labels[0], labels[1]);
556        assert_eq!(labels[1], labels[2]);
557        assert_eq!(labels[3], labels[4]);
558        assert_eq!(labels[4], labels[5]);
559    }
560
561    #[test]
562    fn weak_bridge_does_not_merge_dense_cliques() {
563        // Two 4-cliques joined by a single bridge edge.
564        let mut pairs = Vec::new();
565        push_clique(&mut pairs, 0, 4);
566        push_clique(&mut pairs, 4, 4);
567        pairs.push((3, 4));
568        let g = graph_from(8, &pairs);
569        let labels = leiden_modularity(&g, 1.0, 7);
570        let groups = groups(&labels);
571        assert_eq!(groups.len(), 2, "expected two communities, got {groups:?}");
572        for group in &groups {
573            assert_eq!(group.len(), 4);
574            assert!(is_connected_induced(&g, group));
575        }
576    }
577
578    #[test]
579    fn hierarchy_fixture_flat_partition_is_four_groups() {
580        // Four dense groups: two small triangles linked by one bridge edge and
581        // two large 8-cliques linked the same way, with no edges between the
582        // pairs. Flat Leiden keeps all four separate (refinement produces no
583        // subdivision, so internal aggregation never runs). The coarser merge
584        // of the low-degree pair is exercised one level up via
585        // detect_hierarchy in community.rs.
586        let mut pairs = Vec::new();
587        push_clique(&mut pairs, 0, 3); // A: 0..3
588        push_clique(&mut pairs, 3, 3); // B: 3..6
589        push_clique(&mut pairs, 6, 8); // C: 6..14
590        push_clique(&mut pairs, 14, 8); // D: 14..22
591        pairs.push((2, 3));
592        pairs.push((13, 14));
593        let g = graph_from(22, &pairs);
594
595        let labels = leiden_modularity(&g, 1.0, 7);
596        let level0 = groups(&labels);
597        assert_eq!(level0.len(), 4, "level 0: {level0:?}");
598        // The two triangles stay in distinct level-0 communities...
599        assert_ne!(labels[0], labels[3]);
600        for group in &level0 {
601            assert!(is_connected_induced(&g, group));
602        }
603    }
604
605    #[test]
606    fn heavy_bridge_edge_merges_two_pairs() {
607        // Two disconnected pairs stay in two communities...
608        let weak = WeightedGraph::from_edges(4, vec![(0, 1, 1.0), (2, 3, 1.0)]);
609        assert_eq!(groups(&leiden_modularity(&weak, 1.0, 3)).len(), 2);
610
611        // ...while a weight-10 bridge between the inner nodes dominates the
612        // modularity penalty and pulls all four nodes together.
613        let strong = WeightedGraph::from_edges(4, vec![(0, 1, 1.0), (2, 3, 1.0), (1, 2, 10.0)]);
614        assert_eq!(groups(&leiden_modularity(&strong, 1.0, 3)).len(), 1);
615    }
616
617    #[test]
618    fn result_is_deterministic_for_a_seed() {
619        // A 32-node graph: four 8-node cliques with a few weak bridges.
620        let mut pairs = Vec::new();
621        for c in 0..4 {
622            push_clique(&mut pairs, c * 8, 8);
623        }
624        pairs.push((6, 10));
625        pairs.push((20, 26));
626        pairs.push((1, 17));
627        let g = graph_from(32, &pairs);
628
629        let a = leiden_modularity(&g, 1.0, 123_456);
630        let b = leiden_modularity(&g, 1.0, 123_456);
631        assert_eq!(a, b);
632
633        // Every detected community must induce a connected subgraph — the
634        // well-connectedness property that distinguishes Leiden from Louvain.
635        for group in groups(&a) {
636            assert!(
637                is_connected_induced(&g, &group),
638                "disconnected community {group:?}"
639            );
640        }
641    }
642
643    #[test]
644    fn isolated_nodes_remain_singletons() {
645        let g = graph_from(5, &[(0, 1), (1, 2)]);
646        let labels = leiden_modularity(&g, 1.0, 1);
647        let groups = groups(&labels);
648        // One connected community + two singletons.
649        assert_eq!(groups.len(), 3);
650        assert!(groups.iter().any(|g| g.len() == 3));
651        assert_eq!(groups.iter().filter(|g| g.len() == 1).count(), 2);
652    }
653
654    #[test]
655    fn malformed_edges_are_ignored() {
656        let g = WeightedGraph::from_edges(
657            3,
658            vec![
659                (0, 1, 1.0),
660                (0, 9, 1.0),  // unknown node
661                (1, 2, -1.0), // non-positive
662                (2, 2, 2.0),  // self loop only
663                (0, 2, f64::NAN),
664            ],
665        );
666        assert_eq!(g.node_count(), 3);
667        let labels = leiden_modularity(&g, 1.0, 1);
668        // 0-1 connected; 2 only has a self loop so it stays singleton.
669        assert_eq!(labels[0], labels[1]);
670        assert_ne!(labels[0], labels[2]);
671    }
672
673    #[test]
674    fn rng_shuffle_is_deterministic() {
675        let mut a = Rng::new(99);
676        let mut va: Vec<usize> = (0..20).collect();
677        a.shuffle(&mut va);
678        let mut b = Rng::new(99);
679        let mut vb: Vec<usize> = (0..20).collect();
680        b.shuffle(&mut vb);
681        assert_eq!(va, vb);
682        assert_ne!(va, (0..20).collect::<Vec<_>>());
683    }
684}