use std::time::Instant;
use yo_graph::{Adjacency, Csr, bisect, csr};
fn main() {
let mut args = std::env::args().skip(1);
let Some(path) = args.next() else {
eprintln!("usage: compress <edge list> [--no-hot] [--codes] [--bisect] [--order <file>]");
std::process::exit(2);
};
let flags: Vec<String> = args.collect();
let hot_too = !flags.iter().any(|a| a == "--no-hot");
let codes = flags.iter().any(|a| a == "--codes");
let bisected = flags.iter().any(|a| a == "--bisect");
let keep = flags
.iter()
.position(|a| a == "--order")
.and_then(|i| flags.get(i + 1))
.cloned();
let t = Instant::now();
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| {
eprintln!("{path}: {e}");
std::process::exit(1);
});
let mut edges: Vec<(u32, u32)> = Vec::new();
let mut top = 0u32;
for line in text.lines() {
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
let mut f = line.split_whitespace();
let (Some(s), Some(d)) = (f.next(), f.next()) else {
continue;
};
let (s, d): (u32, u32) = (s.parse().unwrap(), d.parse().unwrap());
top = top.max(s).max(d);
edges.push((s, d));
}
drop(text);
let nodes = top + 1;
let m = edges.len() as f64;
println!(
"{path}: {nodes} nodes, {} edges, read in {:?}",
edges.len(),
t.elapsed()
);
if hot_too {
let t = Instant::now();
let mut hot = Adjacency::out_only();
for (s, d) in &edges {
hot.link(u64::from(*s), u64::from(*d), 1, 0);
}
let built = t.elapsed();
let raw = hot.bytes() as f64 / m;
hot.compact();
println!(
"hot {raw:6.2} bytes an edge, {:6.2} swept, built in {built:?}",
hot.bytes() as f64 / m
);
}
println!(
"{:19}{:>7}{:>9}{:>9}{:>9}{:>8}{:>8}{:>9}{:>8}{:>8}",
"", "total", "offsets", "degrees", "firsts", "widths", "gaps", "patches", "groups", "slack"
);
let show = |name: &str, e: &[(u32, u32)], took: Option<std::time::Duration>| {
let mut copy = e.to_vec();
let t = Instant::now();
let cold = Csr::build(nodes, &mut copy);
report(name, &cold, m, took.unwrap_or_else(|| t.elapsed()));
if codes {
gap_shapes(name, ©, m);
}
};
show("cold", &edges, None);
let t = Instant::now();
let to = csr::order_by_degree(nodes, &edges);
let ordered = t.elapsed();
let mut deg = edges.clone();
csr::renumber(&mut deg, &to);
show("cold, deg ordered", °, Some(ordered));
if bisected {
let t = Instant::now();
let to = match keep.as_deref().map(std::fs::read) {
Some(Ok(raw)) => raw
.as_chunks::<4>()
.0
.iter()
.copied()
.map(u32::from_le_bytes)
.collect(),
_ => {
let to = bisect::order(nodes, &edges);
if let Some(path) = keep.as_deref() {
let mut raw = Vec::with_capacity(to.len() * 4);
for v in &to {
raw.extend_from_slice(&v.to_le_bytes());
}
let _ = std::fs::write(path, raw);
}
to
}
};
let took = t.elapsed();
let mut b = edges.clone();
csr::renumber(&mut b, &to);
show("cold, bisected", &b, Some(took));
}
}
fn gap_shapes(name: &str, edges: &[(u32, u32)], m: f64) {
let mut lens = [0u64; 33];
let (mut ones, mut gaps) = (0u64, 0u64);
let (mut b32, mut b16, mut b8, mut pfor, mut delta) = (0u64, 0u64, 0u64, 0u64, 0u64);
let (mut i32s, mut i8s) = (0u64, 0u64);
let mut perrun = 0u64;
let (mut pf16, mut pf8, mut ipf) = (0u64, 0u64, 0u64);
let mut in_intervals = 0u64;
let mut run: Vec<u32> = Vec::new();
let mut i = 0usize;
while i < edges.len() {
let src = edges[i].0;
run.clear();
while i < edges.len() && edges[i].0 == src {
run.push(edges[i].1);
i += 1;
}
let mut streak = 1u64;
for j in 1..run.len() {
let g = run[j] - run[j - 1];
gaps += 1;
lens[bits(g) as usize] += 1;
if g == 1 {
ones += 1;
streak += 1;
} else {
if streak >= 4 {
in_intervals += streak;
}
streak = 1;
}
delta += u64::from(elias_delta(g));
}
if streak >= 4 {
in_intervals += streak;
}
let g: Vec<u32> = run.windows(2).map(|p| p[1] - p[0]).collect();
b32 += blocked(&g, 32);
b16 += blocked(&g, 16);
b8 += blocked(&g, 8);
pfor += patched(&g, 32);
pf16 += patched(&g, 16);
pf8 += patched(&g, 8);
ipf += intervals_patched(&run, 32, 4);
i32s += intervals(&run, 32, 4);
i8s += intervals(&run, 8, 4);
perrun += 2 + [8usize, 16, 32, 64]
.into_iter()
.map(|b| blocked(&g, b))
.min()
.unwrap_or(0);
}
let n = gaps as f64;
let mut h = 0.0f64;
for (l, c) in lens.iter().enumerate() {
if *c > 0 {
let p = *c as f64 / n;
h += p * (-p.log2() + (l.max(1) - 1) as f64);
}
}
println!();
println!("what the gaps look like, {name}");
println!(
"gaps {gaps}, {:.1}% of them 1",
ones as f64 * 100.0 / n
);
println!(
"in runs of 4 or more {:.1}% of all neighbours",
in_intervals as f64 * 100.0 / m
);
println!("by length floor {h:6.2} bits a gap");
println!(
"block of 32 {:6.2} bits a gap (this is the format)",
b32 as f64 / n
);
println!("block of 16 {:6.2}", b16 as f64 / n);
println!("block of 8 {:6.2}", b8 as f64 / n);
println!("block of 32, patched {:6.2}", pfor as f64 / n);
println!("elias delta {:6.2}", delta as f64 / n);
println!("block of 16, patched {:6.2}", pf16 as f64 / n);
println!("block of 8, patched {:6.2}", pf8 as f64 / n);
println!("block per run {:6.2}", perrun as f64 / n);
println!("intervals, patched {:6.2}", ipf as f64 / n);
println!("intervals, block 32 {:6.2}", i32s as f64 / n);
println!("intervals, block 8 {:6.2}", i8s as f64 / n);
}
fn intervals_patched(run: &[u32], block: usize, min: usize) -> u64 {
let (kept, count) = split_off_intervals(run, min);
let gaps: Vec<u32> = kept.windows(2).map(|p| p[1] - p[0]).collect();
6 + count * 8 + patched(&gaps, block)
}
fn split_off_intervals(run: &[u32], min: usize) -> (Vec<u32>, u64) {
let (mut kept, mut count) = (Vec::new(), 0u64);
let mut i = 0;
while i < run.len() {
let mut j = i + 1;
while j < run.len() && run[j] == run[j - 1] + 1 {
j += 1;
}
if j - i >= min {
count += 1;
kept.push(run[i]);
} else {
kept.extend_from_slice(&run[i..j]);
}
i = j;
}
(kept, count)
}
fn intervals(run: &[u32], block: usize, min: usize) -> u64 {
let (kept, count) = split_off_intervals(run, min);
let gaps: Vec<u32> = kept.windows(2).map(|p| p[1] - p[0]).collect();
6 + count * 8 + blocked(&gaps, block)
}
fn bits(v: u32) -> u32 {
32 - v.leading_zeros()
}
fn blocked(gaps: &[u32], block: usize) -> u64 {
gaps.chunks(block)
.map(|c| 6 + c.len() as u64 * u64::from(bits(c.iter().copied().max().unwrap_or(0))))
.sum()
}
fn patched(gaps: &[u32], block: usize) -> u64 {
let mut hist = [0u32; 33];
gaps.chunks(block)
.map(|c| {
hist.fill(0);
let mut top = 0u32;
for g in c {
let b = bits(*g);
hist[b as usize] += 1;
top = top.max(b);
}
let (mut over, mut wide) = (0u64, 0u32);
let mut best = u64::MAX;
for w in (0..=top).rev() {
let cost = 6
+ 5
+ if over == 0 { 0 } else { 6 }
+ c.len() as u64 * u64::from(w)
+ over * (5 + u64::from(wide));
best = best.min(cost);
if w > 0 {
over += u64::from(hist[w as usize]);
if hist[w as usize] > 0 {
wide = wide.max(w);
}
}
}
best
})
.sum()
}
fn elias_delta(g: u32) -> u32 {
let v = g + 1;
let n = bits(v) - 1;
let k = bits(n + 1) - 1;
2 * k + 1 + n
}
fn report(name: &str, cold: &Csr, m: f64, took: std::time::Duration) {
let c = cold.cost();
let per = |bits: u64| bits as f64 / m;
println!(
"{name:19}{:>7.2}{:>9.2}{:>9.2}{:>9.2}{:>8.2}{:>8.2}{:>9.2}{:>8.2}{:>8.2} {:>6.1} MB in {took:?}",
cold.bits_per_edge(),
per(c.offsets),
per(c.degrees),
per(c.firsts),
per(c.widths),
per(c.gaps),
per(c.patches),
per(c.groups),
per(c.slack),
cold.bytes() as f64 / (1 << 20) as f64,
);
}