use std::collections::BTreeMap;
use crate::dictionary::Dictionary;
use crate::pyramid::Dendrogram;
use crate::triples::TripleBlockBuilder;
fn subject_comm(dict: &Dictionary, dend: &Dendrogram, round: usize, sid: u32) -> usize {
if dend.rounds() == 0 {
0
} else {
dend.base_community(dict.subject_node(sid) as usize, round)
}
}
fn object_comm(dict: &Dictionary, dend: &Dendrogram, round: usize, oid: u32) -> usize {
if dend.rounds() == 0 {
0
} else {
dend.base_community(dict.object_node(oid) as usize, round)
}
}
#[derive(Debug, Clone)]
pub struct Tile {
pub community: usize,
pub triples: Vec<(u32, u32, u32)>,
pub encoded: Vec<u8>,
}
pub fn tile_by_community(
dict: &Dictionary,
triples: &[(u32, u32, u32)],
dend: &Dendrogram,
round: usize,
) -> Vec<Tile> {
let mut groups: BTreeMap<usize, Vec<(u32, u32, u32)>> = BTreeMap::new();
for &t in triples {
let c = subject_comm(dict, dend, round, t.0);
groups.entry(c).or_default().push(t);
}
groups
.into_iter()
.map(|(community, ts)| {
let mut b = TripleBlockBuilder::new();
for &t in &ts {
b.push(t);
}
Tile {
community,
encoded: b.build(),
triples: ts,
}
})
.collect()
}
pub fn choose_round_for_budget(
dict: &Dictionary,
triples: &[(u32, u32, u32)],
dend: &Dendrogram,
budget_bytes: usize,
) -> usize {
if dend.rounds() <= 1 {
return 0;
}
for round in (0..dend.rounds()).rev() {
let max_tile = tile_by_community(dict, triples, dend, round)
.iter()
.map(|t| t.encoded.len())
.max()
.unwrap_or(0);
if max_tile <= budget_bytes {
return round;
}
}
0
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuperEdge {
pub s_comm: usize,
pub predicate: u32,
pub o_comm: usize,
pub count: u32,
}
pub fn summarize(
dict: &Dictionary,
triples: &[(u32, u32, u32)],
dend: &Dendrogram,
round: usize,
) -> Vec<SuperEdge> {
let mut counts: BTreeMap<(usize, u32, usize), u32> = BTreeMap::new();
for &(s, p, o) in triples {
let sc = subject_comm(dict, dend, round, s);
let oc = object_comm(dict, dend, round, o);
*counts.entry((sc, p, oc)).or_default() += 1;
}
counts
.into_iter()
.map(|((s_comm, predicate, o_comm), count)| SuperEdge {
s_comm,
predicate,
o_comm,
count,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dictionary::DictionaryBuilder;
use crate::pyramid::{build_dendrogram, project_graph};
fn fixture() -> (Dictionary, Vec<(u32, u32, u32)>, Dendrogram) {
let edges = [
("A", "B"),
("B", "C"),
("A", "C"),
("C", "A"),
("D", "E"),
("E", "F"),
("D", "F"),
("F", "D"),
("C", "D"), ];
let mut db = DictionaryBuilder::new();
for (s, o) in edges {
db.observe(s, "knows", o);
}
let dict = db.build();
let triples: Vec<_> = edges
.iter()
.map(|(s, o)| dict.encode(s, "knows", o).unwrap())
.collect();
let g = project_graph(&dict, &triples);
let dend = build_dendrogram(&g);
(dict, triples, dend)
}
#[test]
fn tiles_reconstruct_all_triples_losslessly() {
let (dict, triples, dend) = fixture();
let round = dend.rounds().saturating_sub(1);
let tiles = tile_by_community(&dict, &triples, &dend, round);
let mut from_tiles: Vec<_> = tiles.iter().flat_map(|t| t.triples.clone()).collect();
from_tiles.sort_unstable();
let mut expected = triples.clone();
expected.sort_unstable();
assert_eq!(
from_tiles, expected,
"tiles must cover every triple exactly"
);
assert!(tiles.len() >= 2);
}
#[test]
fn summary_preserves_total_count() {
let (dict, triples, dend) = fixture();
let round = dend.rounds().saturating_sub(1);
let summary = summarize(&dict, &triples, &dend, round);
let total: u32 = summary.iter().map(|e| e.count).sum();
assert_eq!(total as usize, triples.len());
assert!(summary.iter().any(|e| e.s_comm != e.o_comm));
}
#[test]
fn budget_selects_finer_round_when_tight() {
let (dict, triples, dend) = fixture();
let coarse = choose_round_for_budget(&dict, &triples, &dend, 1_000_000);
let fine = choose_round_for_budget(&dict, &triples, &dend, 0);
assert!(coarse >= fine);
assert_eq!(fine, 0);
}
}