Files
obikmer/src/obicompactvec/examples/compare_sparse_dense_count.rs
T
Eric Coissac b7a8b5e6cf feat: add sparse compact integer matrix implementation and utilities
Introduces `PersistentSparseCompactIntMatrix`, a row-major sparse integer matrix combining a bitmatrix support layer with dual value streams for singleton and multi partitions. Adds builder and packing utilities to convert dense count matrices into the new on-disk format, featuring idempotency guards and crash-safe persistence workflows. Exposes core accessors, iterators, and column weight calculations while adjusting internal visibility for companion modules. Includes unit tests and diagnostic examples to validate equivalence with dense representations and measure compaction metrics.
2026-08-26 14:05:54 +02:00

213 lines
7.3 KiB
Rust

//! Diagnostic: build a `PersistentSparseCompactIntMatrix` from a real dense
//! `PersistentCompactIntMatrix` (batched `build_from_dense`), verify the
//! result is cell-for-cell identical on an actual count matrix, and report
//! the on-disk compaction ratio, value-distribution stats, and effective
//! bits/value.
//!
//! Usage: cargo run --release --example compare_sparse_dense_count -p obicompactvec -- <layer_dir>
//! (layer_dir is the directory containing `counts/matrix.pcmx` or
//! `counts/col_*.pciv`, e.g.
//! `benchmark/global_index_count/partitions/part_00003/index/layer_1`)
use std::error::Error;
use std::path::Path;
use std::time::Instant;
use obicompactvec::{PersistentCompactIntMatrix, PersistentSparseCompactIntMatrix, PersistentSparseCompactIntMatrixBuilder};
fn main() -> Result<(), Box<dyn Error>> {
let layer_dir = std::env::args().nth(1).expect("usage: compare_sparse_dense_count <layer_dir>");
let layer_dir = Path::new(&layer_dir);
let t0 = Instant::now();
let dense = PersistentCompactIntMatrix::open(layer_dir)?;
println!("dense ouverte en {:?} ({} lignes x {} colonnes)", t0.elapsed(), dense.n(), dense.n_cols());
let out_dir = tempfile::tempdir()?;
let t0 = Instant::now();
let sparse = PersistentSparseCompactIntMatrixBuilder::build_from_dense(&dense, out_dir.path())?.finish()?;
println!("build_from_dense: {:?}", t0.elapsed());
compare(&dense, &sparse)?;
content_stats(&dense);
compaction_stats(layer_dir, out_dir.path(), &dense, &sparse)?;
Ok(())
}
fn compare(dense: &PersistentCompactIntMatrix, sparse: &PersistentSparseCompactIntMatrix) -> Result<(), Box<dyn Error>> {
let n = dense.n();
let n_cols = dense.n_cols();
assert_eq!(n, sparse.n(), "n mismatch");
assert_eq!(n_cols, sparse.n_cols(), "n_cols mismatch");
let mut dense_row = vec![0u32; n_cols];
let mut sparse_row = vec![0u32; n_cols];
let mut total_cells = 0usize;
let mut mismatched_cells = 0usize;
let mut first_mismatch = None;
let t0 = Instant::now();
for slot in 0..n {
dense.fill_row(slot, &mut dense_row);
sparse.fill_row(slot, &mut sparse_row);
for c in 0..n_cols {
total_cells += 1;
if dense_row[c] != sparse_row[c] {
mismatched_cells += 1;
if first_mismatch.is_none() {
first_mismatch = Some((slot, c, dense_row[c], sparse_row[c]));
}
}
}
}
println!("comparaison case-a-case: {:?} ({total_cells} cellules)", t0.elapsed());
if let Some((slot, col, d, s)) = first_mismatch {
eprintln!("PREMIER MISMATCH: slot={slot} col={col} dense={d} sparse={s}");
println!("MISMATCHES: {mismatched_cells} cellules differentes sur {total_cells}");
return Err("dense et sparse divergent".into());
}
println!("OK: toutes les {total_cells} cellules sont identiques entre dense et sparse.");
Ok(())
}
/// Content stats computed straight from the dense matrix (source of truth
/// for what's actually stored) — sparsity, singleton-row fraction, and the
/// value-magnitude buckets this crate's overflow encoding is tuned around
/// (< 127, < 255, >= 255).
fn content_stats(dense: &PersistentCompactIntMatrix) {
let n = dense.n();
let n_cols = dense.n_cols();
let total_cells = n as u64 * n_cols as u64;
let mut nonzero_cells = 0u64;
let mut singleton_rows = 0u64;
let mut sum: u128 = 0;
let mut max_value = 0u32;
let mut under_127 = 0u64;
let mut under_255 = 0u64;
let mut overflow = 0u64;
let mut row = vec![0u32; n_cols];
for slot in 0..n {
dense.fill_row(slot, &mut row);
let mut row_nonzero = 0u32;
for &v in &row {
if v == 0 {
continue;
}
row_nonzero += 1;
nonzero_cells += 1;
sum += v as u128;
max_value = max_value.max(v);
if v < 127 {
under_127 += 1;
}
if v < 255 {
under_255 += 1;
} else {
overflow += 1;
}
}
if row_nonzero == 1 {
singleton_rows += 1;
}
}
println!("\n--- stats de contenu (source: dense) ---");
println!("cellules totales: {total_cells}");
println!(
"cellules non-nulles: {nonzero_cells} ({:.3}% du total)",
100.0 * nonzero_cells as f64 / total_cells as f64
);
println!(
"lignes singleton: {singleton_rows} / {n} ({:.3}%)",
100.0 * singleton_rows as f64 / n as f64
);
if nonzero_cells > 0 {
println!("valeur moyenne (non-nulles): {:.2}", sum as f64 / nonzero_cells as f64);
println!("valeur max: {max_value}");
println!(
"valeurs < 127: {under_127} ({:.3}% des non-nulles)",
100.0 * under_127 as f64 / nonzero_cells as f64
);
println!(
"valeurs < 255: {under_255} ({:.3}% des non-nulles)",
100.0 * under_255 as f64 / nonzero_cells as f64
);
println!(
"valeurs >= 255 (overflow): {overflow} ({:.3}% des non-nulles)",
100.0 * overflow as f64 / nonzero_cells as f64
);
}
}
/// Directory size in bytes — sums every regular file, one level deep
/// (matches both the dense `counts/` layout and the sparse builder's flat
/// output directory, neither of which nests further).
fn dir_size(dir: &Path) -> std::io::Result<u64> {
let mut total = 0u64;
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
total += entry.metadata()?.len();
}
}
Ok(total)
}
fn compaction_stats(
dense_layer_dir: &Path,
sparse_dir: &Path,
dense: &PersistentCompactIntMatrix,
_sparse: &PersistentSparseCompactIntMatrix,
) -> Result<(), Box<dyn Error>> {
let dense_size = dir_size(&dense_layer_dir.join("counts"))?;
let sparse_size = dir_size(sparse_dir)?;
let n = dense.n();
let n_cols = dense.n_cols();
let nonzero_cells: u64 = {
let mut row = vec![0u32; n_cols];
let mut count = 0u64;
for slot in 0..n {
dense.fill_row(slot, &mut row);
count += row.iter().filter(|&&v| v != 0).count() as u64;
}
count
};
println!("\n--- compaction ---");
println!("taille dense (counts/): {dense_size} octets");
println!("taille sparse: {sparse_size} octets");
println!(
"ratio sparse/dense: {:.4} ({:.2}% de la taille dense)",
sparse_size as f64 / dense_size as f64,
100.0 * sparse_size as f64 / dense_size as f64
);
if nonzero_cells > 0 {
println!(
"bits/valeur (dense, sur cellules non-nulles): {:.3}",
dense_size as f64 * 8.0 / nonzero_cells as f64
);
println!(
"bits/valeur (sparse, sur cellules non-nulles): {:.3}",
sparse_size as f64 * 8.0 / nonzero_cells as f64
);
}
let total_cells = n as u64 * n_cols as u64;
println!(
"bits/valeur (dense, sur toutes les cellules): {:.3}",
dense_size as f64 * 8.0 / total_cells as f64
);
println!(
"bits/valeur (sparse, sur toutes les cellules): {:.3}",
sparse_size as f64 * 8.0 / total_cells as f64
);
Ok(())
}