use yo_common::Rng;
#[derive(Debug, Clone, Copy)]
pub struct Tuning {
pub iterations: u32,
pub leaf: u32,
pub threads: usize,
}
impl Default for Tuning {
fn default() -> Self {
Tuning {
iterations: 20,
leaf: 16,
threads: 1,
}
}
}
const SPLIT_OFF: usize = 1 << 16;
#[must_use]
pub fn order(nodes: u32, edges: &[(u32, u32)]) -> Vec<u32> {
order_with(nodes, edges, &Tuning::default())
}
#[must_use]
pub fn order_with(nodes: u32, edges: &[(u32, u32)], tuning: &Tuning) -> Vec<u32> {
let mut to = vec![0u32; nodes as usize];
if nodes == 0 {
return to;
}
let lists = Lists::build(nodes, edges);
let mut docs: Vec<u32> = (0..nodes).collect();
let mut scratch = Scratch::new(nodes, lists.widest());
descend(
&lists,
&mut docs,
tuning,
&mut scratch,
tuning.threads.max(1),
);
for (new, old) in docs.iter().enumerate() {
to[*old as usize] = new as u32;
}
to
}
struct Lists {
start: Vec<u64>,
items: Vec<u32>,
}
impl Lists {
fn build(nodes: u32, edges: &[(u32, u32)]) -> Lists {
let n = nodes as usize;
let mut start = vec![0u64; n + 1];
for (_, d) in edges {
start[*d as usize + 1] += 1;
}
for i in 0..n {
start[i + 1] += start[i];
}
let mut items = vec![0u32; edges.len()];
let mut at = start.clone();
for (s, d) in edges {
items[at[*d as usize] as usize] = *s;
at[*d as usize] += 1;
}
Lists { start, items }
}
#[inline]
fn of(&self, doc: u32) -> &[u32] {
let from = self.start[doc as usize] as usize;
let to = self.start[doc as usize + 1] as usize;
&self.items[from..to]
}
fn widest(&self) -> u32 {
let mut deg = vec![0u32; self.start.len() - 1];
for t in &self.items {
deg[*t as usize] += 1;
}
deg.into_iter().max().unwrap_or(0)
}
}
struct Scratch {
left_deg: Vec<u32>,
right_deg: Vec<u32>,
touched: Vec<u32>,
left: Vec<(f32, u32)>,
right: Vec<(f32, u32)>,
log: Vec<f32>,
}
impl Scratch {
fn new(nodes: u32, widest: u32) -> Scratch {
let mut log = vec![0.0f32; widest as usize + 3];
for (k, v) in log.iter_mut().enumerate() {
*v = (k as f32).max(1.0).log2();
}
Scratch {
left_deg: vec![0u32; nodes as usize],
right_deg: vec![0u32; nodes as usize],
touched: Vec::new(),
left: Vec::new(),
right: Vec::new(),
log,
}
}
}
fn descend(lists: &Lists, docs: &mut [u32], tuning: &Tuning, sc: &mut Scratch, budget: usize) {
if docs.len() <= tuning.leaf.max(2) as usize {
return;
}
let mid = docs.len() / 2;
split(lists, docs, tuning, sc, mid);
let (left, right) = docs.split_at_mut(mid);
if budget > 1 && right.len() >= SPLIT_OFF {
let half = budget / 2;
let widest = sc.log.len() as u32;
std::thread::scope(|s| {
let worker = s.spawn(|| {
let mut own = Scratch::new(lists.start.len() as u32 - 1, widest);
descend(lists, right, tuning, &mut own, budget - half);
});
descend(lists, left, tuning, sc, half);
worker.join().expect("the recursion does not panic");
});
} else {
descend(lists, left, tuning, sc, budget);
descend(lists, right, tuning, sc, budget);
}
}
fn split(lists: &Lists, docs: &mut [u32], tuning: &Tuning, sc: &mut Scratch, mid: usize) {
let Scratch {
left_deg,
right_deg,
touched,
left,
right,
log,
} = sc;
touched.clear();
for (i, doc) in docs.iter().enumerate() {
let left_side = i < mid;
for term in lists.of(*doc) {
let t = *term as usize;
if left_deg[t] == 0 && right_deg[t] == 0 {
touched.push(*term);
}
if left_side {
left_deg[t] += 1;
} else {
right_deg[t] += 1;
}
}
}
let logn1 = (mid as f32).log2();
let logn2 = ((docs.len() - mid) as f32).log2();
for _ in 0..tuning.iterations {
left.clear();
right.clear();
for doc in &docs[..mid] {
left.push((
gain(lists.of(*doc), left_deg, right_deg, logn1, logn2, log),
*doc,
));
}
for doc in &docs[mid..] {
right.push((
gain(lists.of(*doc), right_deg, left_deg, logn2, logn1, log),
*doc,
));
}
let by_gain = |a: &(f32, u32), b: &(f32, u32)| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1));
left.sort_unstable_by(by_gain);
right.sort_unstable_by(by_gain);
let mut swaps = 0usize;
for i in 0..mid.min(docs.len() - mid) {
if left[i].0 + right[i].0 <= 0.0 {
break;
}
let (a, b) = (left[i].1, right[i].1);
for term in lists.of(a) {
let t = *term as usize;
left_deg[t] -= 1;
right_deg[t] += 1;
}
for term in lists.of(b) {
let t = *term as usize;
right_deg[t] -= 1;
left_deg[t] += 1;
}
left[i].1 = b;
right[i].1 = a;
swaps += 1;
}
for (k, e) in left.iter().enumerate() {
docs[k] = e.1;
}
for (k, e) in right.iter().enumerate() {
docs[mid + k] = e.1;
}
if swaps == 0 {
break;
}
}
for term in touched.iter() {
left_deg[*term as usize] = 0;
right_deg[*term as usize] = 0;
}
}
#[inline]
fn gain(
terms: &[u32],
here: &[u32],
there: &[u32],
logn_here: f32,
logn_there: f32,
log: &[f32],
) -> f32 {
let mut total = 0.0f32;
for term in terms {
let t = *term as usize;
let (h, o) = (here[t], there[t]);
let before = charge(h, logn_here, log) + charge(o, logn_there, log);
let after = charge(h - 1, logn_here, log) + charge(o + 1, logn_there, log);
total += before - after;
}
total
}
#[inline]
fn charge(d: u32, logn: f32, log: &[f32]) -> f32 {
d as f32 * (logn - log[d as usize + 1])
}
#[must_use]
pub fn shuffled(nodes: u32, seed: u64) -> Vec<u32> {
let mut rng = Rng::new(seed);
let mut order: Vec<u32> = (0..nodes).collect();
for i in (1..order.len()).rev() {
let j = (rng.next_u64() % (i as u64 + 1)) as usize;
order.swap(i, j);
}
let mut to = vec![0u32; nodes as usize];
for (new, old) in order.iter().enumerate() {
to[*old as usize] = new as u32;
}
to
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Csr;
use crate::csr;
fn rmat(scale: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
let nodes = 1u32 << scale;
let mut rng = Rng::new(seed);
let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
for _ in 0..(nodes as u64) * u64::from(degree) {
let (mut r, mut c) = (0u32, 0u32);
for level in 0..scale {
let bit = 1u32 << (scale - 1 - level);
let p = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
if p < 0.57 {
} else if p < 0.76 {
c |= bit;
} else if p < 0.95 {
r |= bit;
} else {
r |= bit;
c |= bit;
}
}
edges.push((r, c));
}
edges
}
fn uniform(nodes: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
let mut rng = Rng::new(seed);
let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
for src in 0..nodes {
for _ in 0..degree {
edges.push((src, (rng.next_u64() % u64::from(nodes)) as u32));
}
}
edges
}
fn bits(nodes: u32, edges: &[(u32, u32)], to: &[u32]) -> f64 {
let mut copy = edges.to_vec();
csr::renumber(&mut copy, to);
Csr::build(nodes, &mut copy).bits_per_edge()
}
fn communities(groups: u32, size: u32, inside: u32, across: u32, seed: u64) -> Vec<(u32, u32)> {
let nodes = groups * size;
let mut rng = Rng::new(seed);
let mut edges = Vec::new();
let names = shuffled(nodes, seed ^ 0x5eed);
for src in 0..nodes {
let home = (src / size) * size;
for _ in 0..inside {
let d = home + (rng.next_u64() % u64::from(size)) as u32;
edges.push((names[src as usize], names[d as usize]));
}
for _ in 0..across {
let d = (rng.next_u64() % u64::from(nodes)) as u32;
edges.push((names[src as usize], names[d as usize]));
}
}
edges
}
#[test]
fn bisection_beats_degree_ordering_on_a_graph_with_communities() {
let (groups, size) = (64u32, 64u32);
let nodes = groups * size;
let edges = communities(groups, size, 12, 2, 7);
let plain = bits(nodes, &edges, &identity(nodes));
let degree = bits(nodes, &edges, &csr::order_by_degree(nodes, &edges));
let bisected = bits(nodes, &edges, &order(nodes, &edges));
assert!(
bisected < degree - 2.0,
"bisection {bisected:.2}, degree {degree:.2}, as they came {plain:.2}"
);
}
#[test]
fn r_mat_is_not_a_community_graph_and_degree_ordering_is_enough_for_it() {
let scale = 12;
let nodes = 1u32 << scale;
let edges = rmat(scale, 8, 7);
let plain = bits(nodes, &edges, &identity(nodes));
let degree = bits(nodes, &edges, &csr::order_by_degree(nodes, &edges));
let bisected = bits(nodes, &edges, &order(nodes, &edges));
assert!(
bisected < plain,
"bisection {bisected:.2} did not even beat the ids as they came, {plain:.2}"
);
assert!(
bisected > degree,
"bisection {bisected:.2} now beats degree ordering {degree:.2} on R-MAT, which is a better result than this test was written for"
);
}
#[test]
fn there_is_nothing_to_win_on_a_graph_with_no_structure() {
let nodes = 1u32 << 11;
let edges = uniform(nodes, 8, 11);
let plain = bits(nodes, &edges, &identity(nodes));
let bisected = bits(nodes, &edges, &order(nodes, &edges));
assert!(
(bisected - plain).abs() < 0.5,
"uniform moved from {plain:.2} to {bisected:.2}"
);
}
#[test]
fn the_answer_is_a_permutation() {
let nodes = 1u32 << 10;
let edges = rmat(10, 8, 3);
let to = order(nodes, &edges);
let mut seen = vec![false; nodes as usize];
for new in &to {
assert!(!seen[*new as usize], "{new} twice");
seen[*new as usize] = true;
}
assert!(seen.iter().all(|s| *s));
}
#[test]
fn the_numbering_does_not_depend_on_the_machine() {
let nodes = 1u32 << 11;
let edges = rmat(11, 8, 5);
let once = order(nodes, &edges);
assert_eq!(once, order(nodes, &edges));
let threaded = order_with(
nodes,
&edges,
&Tuning {
threads: 4,
..Tuning::default()
},
);
assert_eq!(once, threaded);
}
#[test]
fn a_term_that_stays_together_is_charged_nothing() {
let log: Vec<f32> = (0..16).map(|k| (k as f32).max(1.0).log2()).collect();
assert!(charge(4, 2.0, &log) < 0.0);
assert!(charge(2, 2.0, &log) + charge(2, 2.0, &log) > charge(4, 2.0, &log));
}
#[test]
fn a_shuffle_costs_and_bisection_takes_most_of_it_back() {
let scale = 12;
let nodes = 1u32 << scale;
let mut edges = rmat(scale, 8, 13);
let plain = bits(nodes, &edges, &identity(nodes));
csr::renumber(&mut edges, &shuffled(nodes, 99));
let shuffled_bits = bits(nodes, &edges, &identity(nodes));
let bisected = bits(nodes, &edges, &order(nodes, &edges));
assert!(shuffled_bits > plain, "{shuffled_bits:.2} vs {plain:.2}");
assert!(
bisected < shuffled_bits - 1.0,
"shuffled {shuffled_bits:.2}, bisected {bisected:.2}"
);
}
fn identity(nodes: u32) -> Vec<u32> {
(0..nodes).collect()
}
}