Skip to main content

yo_graph/
bisect.rs

1//! Numbering a graph by recursive bisection, which is where the bits are.
2//!
3//! [`csr`](crate::csr) says it plainly: on soc-LiveJournal1 the encoder is 1.18
4//! bits over the entropy of the gaps it is given, so nothing left in the code is
5//! worth having. What is left is in the numbering. A gap between two neighbours
6//! is the distance between two node ids, and node ids are ours to choose, so the
7//! question is which numbering makes the neighbours of a node land next to each
8//! other. [`order_by_degree`](crate::csr::order_by_degree) answers it with one
9//! sort and gets 0.73 bits on that graph. This answers it properly.
10//!
11//! # What this is
12//!
13//! Recursive graph bisection, from Dhulipala, Kabiljo, Karrer, Ottaviano and
14//! Pupyrev, "Compressing Graphs and Indexes with Recursive Graph Bisection",
15//! KDD 2016. The graph is read as a bipartite one: every node is a document to
16//! be numbered, and the lists it appears in are its terms. Split the documents
17//! in half, then repeatedly swap documents between the halves whenever the swap
18//! lowers a cost function that stands in for the size of the compressed output,
19//! then recurse into each half. The order the documents end up in is the
20//! numbering.
21//!
22//! The cost of one term split across two halves of sizes `n1` and `n2`, holding
23//! `d1` and `d2` of that term's occurrences, is
24//!
25//! ```text
26//! d1 * log2(n1 / (d1 + 1)) + d2 * log2(n2 / (d2 + 1))
27//! ```
28//!
29//! which is the paper's, and is what a list of `d` ids drawn out of a range of
30//! `n` costs under a log gap code. Minimising the sum of that over every term is
31//! minimising an estimate of the whole encoded size, and the estimate is good
32//! enough that the real number moves with it.
33//!
34//! # What it is worth
35//!
36//! On the two public graphs, bits an edge through the cold form, against the ids
37//! as they arrive and against [`order_by_degree`](crate::csr::order_by_degree):
38//!
39//! ```text
40//!                    as they came   degree ordered   bisected
41//! soc-LiveJournal1          17.99            19.00      15.00
42//! web-Google                23.19            20.21      15.04
43//! ```
44//!
45//! Twelve minutes on eight cores of server3 for the LiveJournal one, which is
46//! sixty nine million edges, and eight seconds for web-Google.
47//!
48//! On R-MAT it wins nothing, 9.72 against degree ordering's 9.38, and that is
49//! the useful control rather than a disappointment. R-MAT's structure is its
50//! hubs: every list contains some of the same few thousand nodes, and giving
51//! those the small ids is already close to the best numbering there is. A graph
52//! with real communities has a different structure and this finds it. The test
53//! below builds one out of sixty four groups of sixty four nodes, all of the
54//! same degree so that degree ordering has nothing to sort on, and shuffles the
55//! ids so nothing but the edges says where the groups are: 12.34 bits an edge as
56//! they came, 12.33 degree ordered, 9.21 bisected.
57//!
58//! # Why this and not layered label propagation
59//!
60//! LLP is what WebGraph uses and it is what the note in `csr.rs` said was
61//! coming. It is not here, and the reason is that the paper above is newer than
62//! it, beats it on exactly the kind of graph we are stuck on, and is simpler to
63//! be sure of. LLP runs label propagation at a sweep of resolution parameters
64//! and keeps the clustering that codes best, so it has a parameter list, a
65//! random restart and a quality criterion. Bisection has a leaf size and an
66//! iteration cap, its objective is written down above, and every swap it makes
67//! lowers that objective by an amount it can print. Facebook reported it beating
68//! LLP on social graphs and being several times quicker; the later work on it
69//! (Mackenzie and others, 2019 through 2021) is about faster convergence rather
70//! than about a better objective, and Zuckerli, which is the strongest published
71//! result on these graphs, keeps this ordering and improves the code that runs
72//! after it.
73//!
74//! # What it is not
75//!
76//! It is not fast. It is `O(m log n)` with an iteration count on the front of
77//! it, and on a graph with seventy million edges it is minutes rather than the
78//! second [`order_by_degree`](crate::csr::order_by_degree) takes. That is the
79//! trade the cold form is for: numbering happens once when a graph settles and
80//! the encoded bytes are then read forever.
81//!
82//! It is not a clustering. The output is an order and nothing else. Two nodes
83//! ending up adjacent means the encoder charges less for the pair, not that
84//! anything here believes they are related.
85//!
86//! It is not adaptive. A graph that has changed since it was numbered stays
87//! numbered the way it was, and the new edges go into the hot form. Renumbering
88//! is a rebuild.
89
90use yo_common::Rng;
91
92/// The knobs, all three of them.
93///
94/// The defaults are the paper's, and both of the first two buy less than they
95/// look like they should: doubling the iteration cap is worth hundredths of a
96/// bit an edge because the swap loop stops early on almost every split, and
97/// halving the leaf is worth about as much because sixteen ids in a group of
98/// five hundred and twelve are already adjacent.
99#[derive(Debug, Clone, Copy)]
100pub struct Tuning {
101    /// How many swap rounds one split gets before it moves on.
102    ///
103    /// A round that swaps nothing ends the split early, which is what usually
104    /// happens well before this, so the cap is a bound on the worst case rather
105    /// than a target.
106    pub iterations: u32,
107    /// The smallest partition worth splitting.
108    pub leaf: u32,
109    /// How many threads the recursion may spread over.
110    ///
111    /// One by default, because a numbering that depends on how many cores the
112    /// machine had would be a numbering nobody can reproduce. This one does not:
113    /// the split of a partition is decided before either half is recursed into,
114    /// so the halves are independent and the answer is the same at any thread
115    /// count. The cost of a thread is one scratch set, which is eight bytes a
116    /// node.
117    pub threads: usize,
118}
119
120impl Default for Tuning {
121    fn default() -> Self {
122        Tuning {
123            iterations: 20,
124            leaf: 16,
125            threads: 1,
126        }
127    }
128}
129
130/// A partition below this many documents is recursed into on the same thread
131/// whatever the budget says, because a thread costs a scratch set and a join and
132/// a small partition is not worth either.
133const SPLIT_OFF: usize = 1 << 16;
134
135/// Number the nodes by recursive bisection, with the defaults.
136///
137/// Returns the new id of every old id, so `out[old]` is `new`, which is the
138/// same shape [`order_by_degree`](crate::csr::order_by_degree) hands back and
139/// takes the same [`renumber`](crate::csr::renumber) to apply.
140#[must_use]
141pub fn order(nodes: u32, edges: &[(u32, u32)]) -> Vec<u32> {
142    order_with(nodes, edges, &Tuning::default())
143}
144
145/// The same with the knobs exposed.
146///
147/// # Panics
148///
149/// If a worker thread panics, which it does not, and the join is what would
150/// otherwise swallow it.
151#[must_use]
152pub fn order_with(nodes: u32, edges: &[(u32, u32)], tuning: &Tuning) -> Vec<u32> {
153    let mut to = vec![0u32; nodes as usize];
154    if nodes == 0 {
155        return to;
156    }
157    let lists = Lists::build(nodes, edges);
158    // The starting order is the one the caller handed over, which is what the
159    // paper does. Starting from a shuffle instead was measured and is a tenth
160    // of a bit worse on R-MAT: the first split has to do all the work either
161    // way, and starting from an order that already means something gives it a
162    // better half to improve rather than a random one.
163    let mut docs: Vec<u32> = (0..nodes).collect();
164    let mut scratch = Scratch::new(nodes, lists.widest());
165    descend(
166        &lists,
167        &mut docs,
168        tuning,
169        &mut scratch,
170        tuning.threads.max(1),
171    );
172    for (new, old) in docs.iter().enumerate() {
173        to[*old as usize] = new as u32;
174    }
175    to
176}
177
178/// The lists every document appears in.
179///
180/// A document is a node and its terms are the adjacency lists that hold it,
181/// which for the out lists the cold form encodes means its in neighbours: two
182/// nodes share a term when something points at both of them, and those are
183/// exactly the pairs whose ids want to be close. Numbering for both directions
184/// at once would take the union of the in and out neighbours here and nothing
185/// else, and would be a worse answer for either direction on its own.
186struct Lists {
187    /// Where each document's terms start, with a final entry for the end.
188    start: Vec<u64>,
189    /// The terms, document by document.
190    items: Vec<u32>,
191}
192
193impl Lists {
194    fn build(nodes: u32, edges: &[(u32, u32)]) -> Lists {
195        let n = nodes as usize;
196        let mut start = vec![0u64; n + 1];
197        for (_, d) in edges {
198            start[*d as usize + 1] += 1;
199        }
200        for i in 0..n {
201            start[i + 1] += start[i];
202        }
203        let mut items = vec![0u32; edges.len()];
204        let mut at = start.clone();
205        for (s, d) in edges {
206            items[at[*d as usize] as usize] = *s;
207            at[*d as usize] += 1;
208        }
209        Lists { start, items }
210    }
211
212    #[inline]
213    fn of(&self, doc: u32) -> &[u32] {
214        let from = self.start[doc as usize] as usize;
215        let to = self.start[doc as usize + 1] as usize;
216        &self.items[from..to]
217    }
218
219    /// The largest number of documents any one term holds, which is how far the
220    /// logarithm table has to go.
221    fn widest(&self) -> u32 {
222        let mut deg = vec![0u32; self.start.len() - 1];
223        for t in &self.items {
224            deg[*t as usize] += 1;
225        }
226        deg.into_iter().max().unwrap_or(0)
227    }
228}
229
230/// The arrays one thread reuses down its whole recursion.
231struct Scratch {
232    /// How much of each term is in the left half, over the partition being
233    /// split, and zero everywhere else.
234    left_deg: Vec<u32>,
235    /// The same for the right half.
236    right_deg: Vec<u32>,
237    /// The terms that are not zero, so the two above can be cleared without
238    /// walking the whole graph at every one of the two million splits.
239    touched: Vec<u32>,
240    /// The left half as gain and document, sorted by gain.
241    left: Vec<(f32, u32)>,
242    /// The right half, the same way.
243    right: Vec<(f32, u32)>,
244    /// `log2(k)` for every `k` a degree can reach, plus one.
245    ///
246    /// The inner loop asks for four of these per edge it looks at, and the
247    /// alternative is four calls to `log2` per edge, which is the difference
248    /// between minutes and an afternoon on a real graph.
249    log: Vec<f32>,
250}
251
252impl Scratch {
253    fn new(nodes: u32, widest: u32) -> Scratch {
254        let mut log = vec![0.0f32; widest as usize + 3];
255        for (k, v) in log.iter_mut().enumerate() {
256            *v = (k as f32).max(1.0).log2();
257        }
258        Scratch {
259            left_deg: vec![0u32; nodes as usize],
260            right_deg: vec![0u32; nodes as usize],
261            touched: Vec::new(),
262            left: Vec::new(),
263            right: Vec::new(),
264            log,
265        }
266    }
267}
268
269/// Split one partition, then each of its halves.
270fn descend(lists: &Lists, docs: &mut [u32], tuning: &Tuning, sc: &mut Scratch, budget: usize) {
271    if docs.len() <= tuning.leaf.max(2) as usize {
272        return;
273    }
274    let mid = docs.len() / 2;
275    split(lists, docs, tuning, sc, mid);
276
277    let (left, right) = docs.split_at_mut(mid);
278    // The two halves share nothing that is still being read, so this is the
279    // whole of the parallelism and it needs no locks. It also changes no
280    // answer: the split above is finished before either side is looked at.
281    if budget > 1 && right.len() >= SPLIT_OFF {
282        let half = budget / 2;
283        let widest = sc.log.len() as u32;
284        std::thread::scope(|s| {
285            let worker = s.spawn(|| {
286                let mut own = Scratch::new(lists.start.len() as u32 - 1, widest);
287                descend(lists, right, tuning, &mut own, budget - half);
288            });
289            descend(lists, left, tuning, sc, half);
290            worker.join().expect("the recursion does not panic");
291        });
292    } else {
293        descend(lists, left, tuning, sc, budget);
294        descend(lists, right, tuning, sc, budget);
295    }
296}
297
298/// Move documents across the middle for as long as it pays.
299fn split(lists: &Lists, docs: &mut [u32], tuning: &Tuning, sc: &mut Scratch, mid: usize) {
300    let Scratch {
301        left_deg,
302        right_deg,
303        touched,
304        left,
305        right,
306        log,
307    } = sc;
308
309    touched.clear();
310    for (i, doc) in docs.iter().enumerate() {
311        let left_side = i < mid;
312        for term in lists.of(*doc) {
313            let t = *term as usize;
314            // A term nobody in this partition has touched yet is one that has
315            // to be put back to zero on the way out, and this is the only place
316            // that knows which those are.
317            if left_deg[t] == 0 && right_deg[t] == 0 {
318                touched.push(*term);
319            }
320            if left_side {
321                left_deg[t] += 1;
322            } else {
323                right_deg[t] += 1;
324            }
325        }
326    }
327
328    let logn1 = (mid as f32).log2();
329    let logn2 = ((docs.len() - mid) as f32).log2();
330    for _ in 0..tuning.iterations {
331        left.clear();
332        right.clear();
333        for doc in &docs[..mid] {
334            left.push((
335                gain(lists.of(*doc), left_deg, right_deg, logn1, logn2, log),
336                *doc,
337            ));
338        }
339        for doc in &docs[mid..] {
340            right.push((
341                gain(lists.of(*doc), right_deg, left_deg, logn2, logn1, log),
342                *doc,
343            ));
344        }
345        // Best first down both lists, with the document id breaking ties so a
346        // graph always numbers the same way whatever the sort did with equal
347        // keys.
348        let by_gain = |a: &(f32, u32), b: &(f32, u32)| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1));
349        left.sort_unstable_by(by_gain);
350        right.sort_unstable_by(by_gain);
351
352        // The pair at the top of both lists is the one with the most to gain by
353        // trading places, and once a pair is not worth trading neither is any
354        // pair below it, because both lists only go down from here.
355        let mut swaps = 0usize;
356        for i in 0..mid.min(docs.len() - mid) {
357            if left[i].0 + right[i].0 <= 0.0 {
358                break;
359            }
360            let (a, b) = (left[i].1, right[i].1);
361            for term in lists.of(a) {
362                let t = *term as usize;
363                left_deg[t] -= 1;
364                right_deg[t] += 1;
365            }
366            for term in lists.of(b) {
367                let t = *term as usize;
368                right_deg[t] -= 1;
369                left_deg[t] += 1;
370            }
371            left[i].1 = b;
372            right[i].1 = a;
373            swaps += 1;
374        }
375
376        for (k, e) in left.iter().enumerate() {
377            docs[k] = e.1;
378        }
379        for (k, e) in right.iter().enumerate() {
380            docs[mid + k] = e.1;
381        }
382        if swaps == 0 {
383            break;
384        }
385    }
386
387    for term in touched.iter() {
388        left_deg[*term as usize] = 0;
389        right_deg[*term as usize] = 0;
390    }
391}
392
393/// What the objective drops by if this document changes sides.
394///
395/// `here` and `logn_here` are the half it is on now. A term that has this
396/// document and nothing else on this side leaves an empty half behind, which the
397/// cost function prices at zero, and that is the term that pays for most of what
398/// this pass achieves.
399#[inline]
400fn gain(
401    terms: &[u32],
402    here: &[u32],
403    there: &[u32],
404    logn_here: f32,
405    logn_there: f32,
406    log: &[f32],
407) -> f32 {
408    let mut total = 0.0f32;
409    for term in terms {
410        let t = *term as usize;
411        let (h, o) = (here[t], there[t]);
412        let before = charge(h, logn_here, log) + charge(o, logn_there, log);
413        let after = charge(h - 1, logn_here, log) + charge(o + 1, logn_there, log);
414        total += before - after;
415    }
416    total
417}
418
419/// What one half of one term costs: `d * log2(n / (d + 1))`.
420#[inline]
421fn charge(d: u32, logn: f32, log: &[f32]) -> f32 {
422    d as f32 * (logn - log[d as usize + 1])
423}
424
425/// A numbering that is worth nothing, for the control the tests need.
426///
427/// Bisection is only interesting if the graph has structure, and the way to
428/// show that is to run it against a graph that has none and see it do nothing.
429/// A shuffle is the other end of the same argument: a numbering this bad makes
430/// the encoder pay what a structureless graph pays.
431#[must_use]
432pub fn shuffled(nodes: u32, seed: u64) -> Vec<u32> {
433    let mut rng = Rng::new(seed);
434    let mut order: Vec<u32> = (0..nodes).collect();
435    for i in (1..order.len()).rev() {
436        let j = (rng.next_u64() % (i as u64 + 1)) as usize;
437        order.swap(i, j);
438    }
439    let mut to = vec![0u32; nodes as usize];
440    for (new, old) in order.iter().enumerate() {
441        to[*old as usize] = new as u32;
442    }
443    to
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449    use crate::Csr;
450    use crate::csr;
451
452    /// R-MAT with the Graph500 probabilities, the same generator `csr.rs` uses,
453    /// at a size a debug build can chew through.
454    fn rmat(scale: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
455        let nodes = 1u32 << scale;
456        let mut rng = Rng::new(seed);
457        let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
458        for _ in 0..(nodes as u64) * u64::from(degree) {
459            let (mut r, mut c) = (0u32, 0u32);
460            for level in 0..scale {
461                let bit = 1u32 << (scale - 1 - level);
462                let p = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
463                if p < 0.57 {
464                } else if p < 0.76 {
465                    c |= bit;
466                } else if p < 0.95 {
467                    r |= bit;
468                } else {
469                    r |= bit;
470                    c |= bit;
471                }
472            }
473            edges.push((r, c));
474        }
475        edges
476    }
477
478    fn uniform(nodes: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
479        let mut rng = Rng::new(seed);
480        let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
481        for src in 0..nodes {
482            for _ in 0..degree {
483                edges.push((src, (rng.next_u64() % u64::from(nodes)) as u32));
484            }
485        }
486        edges
487    }
488
489    fn bits(nodes: u32, edges: &[(u32, u32)], to: &[u32]) -> f64 {
490        let mut copy = edges.to_vec();
491        csr::renumber(&mut copy, to);
492        Csr::build(nodes, &mut copy).bits_per_edge()
493    }
494
495    /// A graph made of communities, with the ids shuffled so that nothing but
496    /// the edges says where they are. Every node has the same degree, so degree
497    /// ordering has nothing to sort by and this is a clean read on whether the
498    /// pass finds the structure.
499    fn communities(groups: u32, size: u32, inside: u32, across: u32, seed: u64) -> Vec<(u32, u32)> {
500        let nodes = groups * size;
501        let mut rng = Rng::new(seed);
502        let mut edges = Vec::new();
503        let names = shuffled(nodes, seed ^ 0x5eed);
504        for src in 0..nodes {
505            let home = (src / size) * size;
506            for _ in 0..inside {
507                let d = home + (rng.next_u64() % u64::from(size)) as u32;
508                edges.push((names[src as usize], names[d as usize]));
509            }
510            for _ in 0..across {
511                let d = (rng.next_u64() % u64::from(nodes)) as u32;
512                edges.push((names[src as usize], names[d as usize]));
513            }
514        }
515        edges
516    }
517
518    /// The whole point. On a graph whose communities are real, the numbering has
519    /// to find them, and finding them has to be worth bits.
520    ///
521    /// Not shrunk. Two bits an edge is the claim and it does not survive a cut:
522    /// on sixteen groups of sixteen the pass still finds the communities and
523    /// still wins, but by well under two bits, because most of what it wins is
524    /// the splits it makes after the first few and a small graph runs out of
525    /// them. A version with a smaller margin would pass without saying the
526    /// thing this test is here to say.
527    #[cfg_attr(miri, ignore = "two bits an edge is the claim and it needs the graph")]
528    #[test]
529    fn bisection_beats_degree_ordering_on_a_graph_with_communities() {
530        let (groups, size) = (64u32, 64u32);
531        let nodes = groups * size;
532        let edges = communities(groups, size, 12, 2, 7);
533        let plain = bits(nodes, &edges, &identity(nodes));
534        let degree = bits(nodes, &edges, &csr::order_by_degree(nodes, &edges));
535        let bisected = bits(nodes, &edges, &order(nodes, &edges));
536        assert!(
537            bisected < degree - 2.0,
538            "bisection {bisected:.2}, degree {degree:.2}, as they came {plain:.2}"
539        );
540    }
541
542    /// R-MAT is the synthetic graph the rest of this crate measures on, and it
543    /// is not a community graph: its hubs are shared by everything, so degree
544    /// ordering is close to the best numbering there is for it and bisection
545    /// does not beat it. Recording that here rather than leaving it to be
546    /// rediscovered, because it is the reason the real graphs are the ones the
547    /// module documentation quotes.
548    ///
549    /// Not shrunk, and this one is interesting: at scale nine bisection does
550    /// beat degree ordering, by a hundredth of a bit. R-MAT only stops having
551    /// community structure once it is big enough for the hubs to be shared by
552    /// everything, so the scale is the claim here rather than a way of reaching
553    /// it.
554    #[cfg_attr(
555        miri,
556        ignore = "the scale is the claim, R-MAT only stops looking like communities at size"
557    )]
558    #[test]
559    fn r_mat_is_not_a_community_graph_and_degree_ordering_is_enough_for_it() {
560        let scale = 12;
561        let nodes = 1u32 << scale;
562        let edges = rmat(scale, 8, 7);
563        let plain = bits(nodes, &edges, &identity(nodes));
564        let degree = bits(nodes, &edges, &csr::order_by_degree(nodes, &edges));
565        let bisected = bits(nodes, &edges, &order(nodes, &edges));
566        assert!(
567            bisected < plain,
568            "bisection {bisected:.2} did not even beat the ids as they came, {plain:.2}"
569        );
570        assert!(
571            bisected > degree,
572            "bisection {bisected:.2} now beats degree ordering {degree:.2} on R-MAT, which is a better result than this test was written for"
573        );
574    }
575
576    /// The control. A graph with no structure has nothing for an ordering to
577    /// find, and a pass that claimed a win here would be finding an artefact of
578    /// the encoder rather than a property of the graph.
579    #[test]
580    fn there_is_nothing_to_win_on_a_graph_with_no_structure() {
581        // A uniform graph has no structure at any size, and the assert is that
582        // nothing moves, so a smaller one asks the same question.
583        let nodes = 1u32 << if cfg!(miri) { 6 } else { 11 };
584        let edges = uniform(nodes, 8, 11);
585        let plain = bits(nodes, &edges, &identity(nodes));
586        let bisected = bits(nodes, &edges, &order(nodes, &edges));
587        assert!(
588            (bisected - plain).abs() < 0.5,
589            "uniform moved from {plain:.2} to {bisected:.2}"
590        );
591    }
592
593    /// A numbering has to be a numbering. Two nodes given the same id would
594    /// silently drop edges at the renumber and the encoder would happily encode
595    /// what was left.
596    #[test]
597    fn the_answer_is_a_permutation() {
598        // A permutation is a permutation at any size, and a duplicate id comes
599        // out of the split rather than out of the number of splits.
600        let scale = if cfg!(miri) { 7 } else { 10 };
601        let nodes = 1u32 << scale;
602        let edges = rmat(scale, 8, 3);
603        let to = order(nodes, &edges);
604        let mut seen = vec![false; nodes as usize];
605        for new in &to {
606            assert!(!seen[*new as usize], "{new} twice");
607            seen[*new as usize] = true;
608        }
609        assert!(seen.iter().all(|s| *s));
610    }
611
612    /// The same graph numbers the same way twice, and the same way on any
613    /// number of threads. A numbering that moved with the core count would make
614    /// every published bits an edge number unreproducible.
615    #[test]
616    fn the_numbering_does_not_depend_on_the_machine() {
617        // Smaller under Miri. Note that no size a test can afford forks at all,
618        // because a partition under `SPLIT_OFF` is recursed into on the same
619        // thread whatever the budget says, so what this checks is that the
620        // thread count is carried through the recursion without changing the
621        // answer. That was already true of the larger size and the cut does not
622        // give anything up.
623        // Three orderings of the same graph, so this costs three times what a
624        // test that orders once costs, which is why the scale is lower here
625        // than anywhere else in the module.
626        let scale = if cfg!(miri) { 6 } else { 11 };
627        let nodes = 1u32 << scale;
628        let edges = rmat(scale, 8, 5);
629        let once = order(nodes, &edges);
630        assert_eq!(once, order(nodes, &edges));
631        let threaded = order_with(
632            nodes,
633            &edges,
634            &Tuning {
635                threads: 4,
636                ..Tuning::default()
637            },
638        );
639        assert_eq!(once, threaded);
640    }
641
642    /// The cost function has to say that a term entirely on one side is free,
643    /// because that is the whole force pulling communities together.
644    #[test]
645    fn a_term_that_stays_together_is_charged_nothing() {
646        let log: Vec<f32> = (0..16).map(|k| (k as f32).max(1.0).log2()).collect();
647        // Four occurrences in a half of four is log2(4/5) a piece, which is
648        // negative, so a term with nowhere else to be is better than free.
649        assert!(charge(4, 2.0, &log) < 0.0);
650        // Split evenly across two halves of four it costs something.
651        assert!(charge(2, 2.0, &log) + charge(2, 2.0, &log) > charge(4, 2.0, &log));
652    }
653
654    /// Shuffling is the other control: it should make the encoder pay, and it
655    /// should be undone by the pass rather than fought by it.
656    #[test]
657    fn a_shuffle_costs_and_bisection_takes_most_of_it_back() {
658        // A shuffle costs whatever the graph is, and the bit the pass takes
659        // back is the structure R-MAT has at every scale. Nine is as low as
660        // this one goes and the two asserts say why: at eight the pass takes
661        // back 0.94 bits where the assert wants one, and at seven the shuffle
662        // stops costing anything at all, because a graph that small is already
663        // as good as random. It is the slowest test in the crate under Miri at
664        // just under two minutes, three times the next one, and that is the
665        // price of the only check that the numbering undoes a shuffle rather
666        // than fights it.
667        let scale = if cfg!(miri) { 9 } else { 12 };
668        let nodes = 1u32 << scale;
669        let mut edges = rmat(scale, 8, 13);
670        let plain = bits(nodes, &edges, &identity(nodes));
671        csr::renumber(&mut edges, &shuffled(nodes, 99));
672        let shuffled_bits = bits(nodes, &edges, &identity(nodes));
673        let bisected = bits(nodes, &edges, &order(nodes, &edges));
674        assert!(shuffled_bits > plain, "{shuffled_bits:.2} vs {plain:.2}");
675        assert!(
676            bisected < shuffled_bits - 1.0,
677            "shuffled {shuffled_bits:.2}, bisected {bisected:.2}"
678        );
679    }
680
681    fn identity(nodes: u32) -> Vec<u32> {
682        (0..nodes).collect()
683    }
684}