use cpu_time::ProcessTime;
use std::time::SystemTime;
use anyhow::*;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::fs::OpenOptions;
use std::io::{BufReader, BufWriter};
use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json::to_writer;
use petgraph::Undirected;
use petgraph::graph::Graph;
use hdrhistogram::Histogram;
use hnsw_rs::flatten::FlatNeighborhood;
use hnsw_rs::prelude::*;
use crate::embedding::*;
use crate::structure::density::stable::*;
use crate::structure::density::*;
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct BlockStat {
blocnum: usize,
block_degree: u32,
mean_dist: f64,
mean_dist_in: f32,
mean_dist_out: f32,
min_dist_in_block: f64,
transition_proba: Vec<f32>,
kl_divergence: f32,
}
impl BlockStat {
pub fn new(
blocnum: usize,
block_degree: u32,
mean_dist: f64,
mean_dist_in: f32,
mean_dist_out: f32,
min_dist_in_block: f64,
transition_proba: Vec<f32>,
kl_divergence: f32,
) -> Self {
BlockStat {
blocnum,
block_degree,
mean_dist,
mean_dist_in,
mean_dist_out,
min_dist_in_block,
transition_proba,
kl_divergence,
}
}
pub fn get_blocnum(&self) -> usize {
self.blocnum
}
pub fn get_block_degree(&self) -> u32 {
self.block_degree
}
pub fn get_mean(&self) -> f64 {
self.mean_dist
}
pub fn get_mean_dist_in(&self) -> f32 {
self.mean_dist_in
}
pub fn get_mean_dist_out(&self) -> f32 {
self.mean_dist_out
}
pub fn get_min_dist_in_block(&self) -> f64 {
self.min_dist_in_block
}
pub fn get_fraction_out(&self) -> f32 {
self.transition_proba[1 + self.blocnum..]
.iter()
.sum::<f32>()
}
pub fn get_kl_divergence(&self) -> f32 {
self.kl_divergence
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockCheck {
blocks: Vec<BlockStat>,
}
impl BlockCheck {
pub fn get_blockstat(&self) -> &Vec<BlockStat> {
&self.blocks
}
pub fn get_block(&self, numblock: usize) -> Result<&BlockStat> {
if numblock >= self.blocks.len() {
Err(anyhow!("BlockCheck get_block bzd block num arg"))
} else {
Ok(&self.blocks[numblock])
}
}
pub fn dump_json(&self, filepath: &Path) -> Result<()> {
log::info!("dumping BlockCheck in json file : {:?}", filepath);
let fileres = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(filepath);
if fileres.is_err() {
log::error!(
"BlockCheck dump : dump could not open file {:?}",
filepath.as_os_str()
);
println!(
"BlockCheck dump: could not open file {:?}",
filepath.as_os_str()
);
return Err(anyhow!("BlockCheck dump failed".to_string()));
}
let mut writer = BufWriter::new(fileres.unwrap());
to_writer(&mut writer, &self).unwrap();
Ok(())
}
pub fn reload_json(filepath: &Path) -> Result<Self> {
log::info!("in BlockCheck::reload_json");
let fileres = OpenOptions::new().read(true).open(filepath);
if fileres.is_err() {
log::error!(
"BlockCheck::reload_json : reload could not open file {:?}",
filepath.as_os_str()
);
println!(
"BlockCheck::reload_json: could not open file {:?}",
filepath.as_os_str()
);
return Err(anyhow!(
"BlockCheck::reload_json: could not open file".to_string()
));
}
let loadfile = fileres.unwrap();
let reader = BufReader::new(loadfile);
let blockcheck: Self = serde_json::from_reader(reader).unwrap();
log::info!("end of BlockCheck reload ");
Ok(blockcheck)
}
pub fn get_in_out_distance_ratio(&self) -> Vec<(f64, f64)> {
let mut histo = Histogram::<u64>::new(2).unwrap();
let scale: f32 = 500.;
log::info!("analyzing edge in/ edge out in blocks");
log::debug!("scaling ratios at {scale}");
self.blocks
.iter()
.for_each(|b| histo += (scale * b.get_mean_dist_in() / b.get_mean_dist_out()) as u64);
let quantiles = vec![0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95];
log::info!("quantiles used : {:?}", &quantiles);
for q in &quantiles {
log::info!(
"value at q : {:.3e} = {:.3e}",
q,
histo.value_at_quantile(*q) as f64 / scale as f64
);
}
let mut nb_edges: usize = 0;
let mut weighted_mean_ratio: f32 = 0.;
for i in 0..self.blocks.len() - 1 {
let b = &self.blocks[i];
nb_edges += b.get_block_degree() as usize;
weighted_mean_ratio +=
b.get_block_degree() as f32 * b.get_mean_dist_in() / b.get_mean_dist_out();
}
weighted_mean_ratio /= nb_edges as f32;
log::info!(
" blocks mean ratio edge in / edge out weighted by block degree : {:.3e}",
weighted_mean_ratio
);
let quant_res = quantiles
.into_iter()
.map(|q| (q, histo.value_at_quantile(q) as f64 / scale as f64))
.collect::<Vec<(f64, f64)>>();
for q in &quant_res {
log::info!(
" blocks quantiles ratio (edge in/ edge out) at proba {:.3e} = {:.3e}",
q.0,
q.1
);
}
quant_res
}
pub fn get_divergence_histogram(&self) -> Vec<(f64, f64)> {
log::info!("analyzing edge block transitions");
let mut histo = Histogram::<u64>::new(2).unwrap();
let scale: f32 = 500.;
self.blocks
.iter()
.for_each(|b| histo += (scale * b.get_kl_divergence()) as u64);
let quantiles = vec![0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95];
log::info!("quantiles used : {:?}", &quantiles);
for q in &quantiles {
log::info!(
"value at q : {:.3e} = {:.3e}",
q,
histo.value_at_quantile(*q) as f64 / scale as f64
);
}
let quant_res = quantiles
.into_iter()
.map(|q| (q, histo.value_at_quantile(q) as f64 / scale as f64))
.collect::<Vec<(f64, f64)>>();
for q in &quant_res {
log::info!(
" kl_divergence quantiles ratio between block transition law at proba {:.3e} = {:.3e}",
q.0,
q.1
);
}
quant_res
} }
pub fn embeddedtohnsw<'a, 'b, F, D>(
embedded: &'a dyn EmbeddedT<F>,
max_nb_connection: usize,
ef_c: usize,
) -> Result<Hnsw<'b, F, DistPtr<F, f64>>, anyhow::Error>
where
F: Copy + Clone + Send + Sync,
D: Distance<F>,
'b: 'a,
{
let distance_e = embedded.get_distance();
let distance = DistPtr::<F, f64>::new(distance_e);
let nbdata = embedded.get_nb_nodes();
let nb_layer = 16;
let cpu_start = ProcessTime::now();
let sys_start = SystemTime::now();
let hnsw = Hnsw::<F, DistPtr<F, f64>>::new(max_nb_connection, nbdata, nb_layer, ef_c, distance);
let block_size = 10000;
let nb_blocks_min = nbdata / block_size;
let nb_blocks = if nbdata > nb_blocks_min * block_size {
nb_blocks_min + 1
} else {
nb_blocks_min
};
let mut nb_sent = 0;
let mut embeded_v = Vec::<(Vec<F>, usize)>::with_capacity(block_size);
for block in 0..nb_blocks {
embeded_v.clear();
let first = block * block_size;
let last = (first + block_size).min(nbdata);
for rank in first..last {
let v = embedded.get_embedded_node(rank, TAG_IN_OUT).to_vec();
embeded_v.push((v, rank));
}
let data_with_id: Vec<(&[F], usize)> = embeded_v
.iter()
.map(|data| (data.0.as_slice(), data.1))
.collect();
hnsw.parallel_insert_slice(&data_with_id);
nb_sent += data_with_id.len();
}
let sys_t: f64 = sys_start.elapsed().unwrap().as_millis() as f64 / 1000.;
println!(
" embedding sys time(s) {:.2e} cpu time(s) {:.2e}",
sys_t,
cpu_start.elapsed().as_secs()
);
log::debug!("embedtohnsw , sent {nb_sent} to hnsw");
Ok(hnsw)
}
fn compare_block_density(
flathnsw: &FlatNeighborhood,
stable: &StableDecomposition,
blocnum: usize,
) -> BlockStat {
log::info!("compare_block_density for block : {blocnum}");
let highest = stable.get_block_points(blocnum).unwrap();
let block_size_out = highest.len();
let nb_blocks = stable.get_nb_blocks();
let mut block_counts = (0..nb_blocks).map(|_| 0.).collect::<Vec<f32>>();
let mut min_dist_in_block = f64::INFINITY;
let mut density = 0.;
let mut _nb_non_zero_dist: usize = 0;
let mut nb_dist = 0;
let mut nb_dist_in = 0;
let mut nb_dist_out = 0;
let mut mean_dist_in = 0.;
let mut mean_dist_out = 0.;
for node in &highest {
let neighbours = flathnsw.get_neighbours(*node).unwrap();
let max_nb_nbg = stable.get_node_degree(*node);
assert!(max_nb_nbg > 0);
for neighbour in &neighbours[0..neighbours.len().min(max_nb_nbg)] {
let dist = neighbour.get_distance();
let neighbour_blocknum = stable.get_densest_block(neighbour.get_origin_id()).unwrap();
if dist > 0. {
_nb_non_zero_dist += 1;
if neighbour_blocknum == blocnum {
min_dist_in_block = min_dist_in_block.min(dist as f64);
}
}
density += dist;
nb_dist += 1;
if neighbour_blocknum <= blocnum {
nb_dist_in += 1;
mean_dist_in += dist;
} else {
nb_dist_out += 1;
mean_dist_out += dist;
}
block_counts[neighbour_blocknum] += 1.0 / (block_size_out as f32);
}
} if nb_dist_in > 0 {
mean_dist_in /= nb_dist_in as f32;
}
if nb_dist_out > 0 {
mean_dist_out /= nb_dist_out as f32;
}
let sum = block_counts.iter().sum::<f32>();
block_counts.iter_mut().for_each(|v| *v /= sum);
let global_density = density as f64 / (nb_dist as f64);
let divergence = kl_divergence(
&block_counts,
stable
.get_block_transition()
.row(blocnum)
.as_slice()
.unwrap(),
);
log::info!(
"mean distance of neigbours in bloc {blocnum}: {:.3e}, block divergence : {:.3e}",
global_density,
divergence
);
BlockStat::new(
blocnum,
nb_dist_in + nb_dist_out,
global_density,
mean_dist_in,
mean_dist_out,
min_dist_in_block,
block_counts,
divergence,
)
}
fn kl_divergence(p1: &[f32], p2: &[f32]) -> f32 {
let div = p1.iter().zip(p2.iter()).fold(0., |acc, v| {
if v.0 > &0. {
acc + v.0 * (v.1 / v.0).ln()
} else {
acc
}
});
-div
}
#[allow(unused)]
fn get_block_stats(blocnum: usize, blockout: &[f32]) -> f64 {
let mut out = 0.;
let mut nb_edges = 0.;
for j in 0..blockout.len() {
nb_edges += blockout[j];
if j > blocnum {
out += blockout[j];
}
}
let frac_out: f64 = out as f64 / nb_edges as f64;
frac_out
}
pub fn density_analysis<F, D, N>(
graph: &Graph<N, f64, Undirected>,
embedded: &Embedded<F>,
hnsw_opt: Option<Hnsw<F, DistPtr<F, f64>>>,
decomposition_opt: Option<StableDecomposition>,
) -> Result<BlockCheck, anyhow::Error>
where
F: Copy + Clone + Send + Sync,
D: Distance<F>,
N: std::marker::Copy,
{
let decomposition = match decomposition_opt {
Some(decomposition) => decomposition,
None => {
let nb_iter = 500;
log::info!("doing approximate_decomposition");
approximate_decomposition(graph, nb_iter)
}
};
let hnsw = match hnsw_opt {
Some(hnsw) => hnsw,
None => {
let max_nb_connection: usize = decomposition.get_mean_block_size().min(64);
log::info!(
"density_analysis : construction hnsw using max_nb_onnection : {max_nb_connection}"
);
let ef_construction: usize = 48;
let hnsw_res = embeddedtohnsw::<F, D>(embedded, max_nb_connection, ef_construction);
if hnsw_res.is_err() {
return Err(anyhow!("density_analysis cannot do the hnsw construction"));
}
hnsw_res.unwrap()
}
};
let flathnsw = FlatNeighborhood::from(&hnsw);
let nb_blocks = decomposition.get_nb_blocks();
let nb_max_blocks = 500;
log::info!("keeping max {nb_max_blocks}");
let nb_dense_blocks = nb_blocks.min(nb_max_blocks);
let res_analysis: Vec<BlockStat> = (0..nb_dense_blocks)
.into_par_iter()
.map(|i| compare_block_density(&flathnsw, &decomposition, i))
.collect();
log::info!("\n\n nb neighbours by blocks");
for d in &res_analysis {
log::info!(
"\n\n density_analysis for densest block : {:?}, block size : {:#?}",
d,
decomposition
.get_nbpoints_in_block(d.get_blocnum())
.unwrap()
);
let frac_out = d.get_fraction_out();
log::info!(
" block : {:?}, fraction out : {:.2e}",
d.get_blocnum(),
frac_out
);
}
let blockcheck = BlockCheck {
blocks: res_analysis,
};
log::info!("\n\n computing in out ratio for blocks");
blockcheck.get_in_out_distance_ratio();
blockcheck.get_divergence_histogram();
Ok(blockcheck)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
fn log_init_test() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn ann_check_density_miserables() {
log_init_test();
log::debug!("in anndensity ann_check_density_miserables");
let path = std::path::Path::new(crate::DATADIR)
.join("moreno_lesmis")
.join("out.moreno_lesmis_lesmis");
log::info!("\n\n algodens::density_miserables, loading file {:?}", path);
let res = csv_to_trimat::<f64>(&path, false, b' ');
if res.is_err() {
log::error!("algodens::density_miserables failed in csv_to_trimat");
assert_eq!(1, 0);
}
let (trimat, node_index) = res.unwrap();
let sketch_size = 20;
let decay = 0.15;
let nb_iter = 3;
let parallel = false;
let symetric = true;
let params = NodeSketchParams {
sketch_size,
decay,
nb_iter,
symetric,
parallel,
};
let mut nodesketch = NodeSketch::new(params, trimat);
let embedding_res = Embedding::new(node_index, &mut nodesketch);
if embedding_res.is_err() {
log::error!("test_nodesketch_lesmiserables failed in compute_Embedded");
assert_eq!(1, 0);
}
let embedding = embedding_res.unwrap();
let embedded = embedding.get_embedded_data();
let res = weighted_csv_to_graphmap::<u32, f64, Undirected>(&path, b' ');
if res.is_err() {
log::error!("algodens::density_miserables failed in csv_to_trimat");
assert_eq!(1, 0);
}
let graph = res.unwrap().into_graph::<u32>();
let block_analysis =
density_analysis::<usize, DistPtr<usize, f64>, u32>(&graph, &embedded, None, None);
if block_analysis.is_err() {
log::error!("block_analysis failed");
}
let block_check = block_analysis.unwrap();
let _ratio_quants = block_check.get_in_out_distance_ratio();
} }