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}