use std::collections::{HashMap, VecDeque};
use crate::census::{count, Census, Selector};
use crate::snapshot::{GraphAdapter, Snapshot};
fn inner_product<K: Eq + std::hash::Hash>(a: &HashMap<K, u64>, b: &HashMap<K, u64>) -> u64 {
a.iter()
.map(|(k, &v)| v * b.get(k).copied().unwrap_or(0))
.sum()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GraphletFeatures {
pub k: usize,
pub census: Census,
}
#[must_use]
pub fn graphlet_features<G: GraphAdapter>(g: G, k: usize) -> GraphletFeatures {
GraphletFeatures {
k,
census: count(g, &Selector::connected_k_subsets(k)),
}
}
#[must_use]
pub fn graphlet_kernel(a: &GraphletFeatures, b: &GraphletFeatures) -> u64 {
assert_eq!(
a.k, b.k,
"graphlet_kernel: mismatched orders (k={} vs k={}) — features must come from \
graphlet_features calls at the same k, since ClassId does not carry an order \
tag and a cross-k comparison would silently produce a bogus value",
a.k, b.k
);
inner_product(&a.census, &b.census)
}
#[must_use]
pub fn graphlet_kernel_cosine(a: &GraphletFeatures, b: &GraphletFeatures) -> f64 {
let kab = graphlet_kernel(a, b) as f64;
let kaa = graphlet_kernel(a, a) as f64;
let kbb = graphlet_kernel(b, b) as f64;
let denom = (kaa * kbb).sqrt();
if denom == 0.0 {
0.0
} else {
kab / denom
}
}
pub type Labeling = Vec<u64>;
#[must_use]
pub fn degree_labeling<G: GraphAdapter>(g: G) -> Labeling {
let snap = Snapshot::new(g);
(0..snap.len())
.map(|i| snap.neighbors(i).len() as u64)
.collect()
}
#[must_use]
pub fn wl_refine<G: GraphAdapter>(
graphs: &[G],
initial: &[Labeling],
iterations: usize,
) -> Vec<Vec<Labeling>> {
assert_eq!(
graphs.len(),
initial.len(),
"wl_refine: graphs/initial length mismatch"
);
let snaps: Vec<Snapshot<G::NodeId>> = graphs.iter().map(|&g| Snapshot::new(g)).collect();
for (i, snap) in snaps.iter().enumerate() {
assert_eq!(
snap.len(),
initial[i].len(),
"wl_refine: initial labeling {i} has {} entries, graph has {} nodes",
initial[i].len(),
snap.len()
);
}
let mut history: Vec<Vec<Labeling>> = initial.iter().map(|l| vec![l.clone()]).collect();
let mut current: Vec<Labeling> = initial.to_vec();
for _ in 0..iterations {
let mut sigs: HashMap<(u64, Vec<u64>), u64> = HashMap::new();
let mut next_id = 0u64;
let mut next: Vec<Labeling> = Vec::with_capacity(snaps.len());
for (gi, snap) in snaps.iter().enumerate() {
let mut labels = Vec::with_capacity(snap.len());
for v in 0..snap.len() {
let mut nbr_labels: Vec<u64> =
snap.neighbors(v).iter().map(|&u| current[gi][u]).collect();
nbr_labels.sort_unstable();
let sig = (current[gi][v], nbr_labels);
let id = *sigs.entry(sig).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
labels.push(id);
}
next.push(labels);
}
current = next;
for (gi, labels) in current.iter().enumerate() {
history[gi].push(labels.clone());
}
}
history
}
#[must_use]
pub fn label_histogram(labels: &Labeling) -> HashMap<u64, u64> {
let mut h = HashMap::new();
for &l in labels {
*h.entry(l).or_insert(0) += 1;
}
h
}
#[must_use]
pub fn wl_kernel(a: &[Labeling], b: &[Labeling]) -> u64 {
assert_eq!(
a.len(),
b.len(),
"wl_kernel: mismatched iteration counts ({} vs {})",
a.len(),
b.len()
);
a.iter()
.zip(b.iter())
.map(|(la, lb)| inner_product(&label_histogram(la), &label_histogram(lb)))
.sum()
}
#[must_use]
pub fn wl_kernel_pair<G: GraphAdapter>(g: G, other: G, iterations: usize) -> u64 {
let initial = vec![degree_labeling(g), degree_labeling(other)];
let history = wl_refine(&[g, other], &initial, iterations);
wl_kernel(&history[0], &history[1])
}
fn bfs_distances<N: Copy>(snap: &Snapshot<N>, s: usize) -> Vec<Option<usize>> {
let mut dist = vec![None; snap.len()];
dist[s] = Some(0);
let mut queue = VecDeque::new();
queue.push_back(s);
while let Some(u) = queue.pop_front() {
let du = dist[u].expect("queued node always has a distance");
for &v in snap.neighbors(u) {
if dist[v].is_none() {
dist[v] = Some(du + 1);
queue.push_back(v);
}
}
}
dist
}
#[must_use]
pub fn shortest_path_histogram<G: GraphAdapter>(g: G) -> HashMap<usize, u64> {
let snap = Snapshot::new(g);
let n = snap.len();
let mut hist = HashMap::new();
for s in 0..n {
let dist = bfs_distances(&snap, s);
for d in dist.iter().skip(s + 1).flatten() {
*hist.entry(*d).or_insert(0) += 1;
}
}
hist
}
#[must_use]
pub fn shortest_path_kernel(a: &HashMap<usize, u64>, b: &HashMap<usize, u64>) -> u64 {
inner_product(a, b)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GramNormalization {
Raw,
Cosine,
}
#[must_use]
pub fn gram_matrix<T>(
items: &[T],
kernel: impl Fn(&T, &T) -> f64,
normalization: GramNormalization,
) -> Vec<Vec<f64>> {
let n = items.len();
let mut raw = vec![vec![0.0; n]; n];
for i in 0..n {
for j in i..n {
let k = kernel(&items[i], &items[j]);
raw[i][j] = k;
raw[j][i] = k;
}
}
match normalization {
GramNormalization::Raw => raw,
GramNormalization::Cosine => {
let diag: Vec<f64> = (0..n).map(|i| raw[i][i]).collect();
let mut out = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
let denom = (diag[i] * diag[j]).sqrt();
out[i][j] = if denom == 0.0 { 0.0 } else { raw[i][j] / denom };
}
}
out
}
}
}
#[must_use]
pub fn graphlet_gram_matrix<G: GraphAdapter>(
graphs: &[G],
k: usize,
normalization: GramNormalization,
) -> Vec<Vec<f64>> {
let feats: Vec<GraphletFeatures> = graphs.iter().map(|&g| graphlet_features(g, k)).collect();
gram_matrix(&feats, |a, b| graphlet_kernel(a, b) as f64, normalization)
}
#[must_use]
pub fn wl_gram_matrix<G: GraphAdapter>(
graphs: &[G],
iterations: usize,
normalization: GramNormalization,
) -> Vec<Vec<f64>> {
let initial: Vec<Labeling> = graphs.iter().map(|&g| degree_labeling(g)).collect();
let history = wl_refine(graphs, &initial, iterations);
gram_matrix(&history, |a, b| wl_kernel(a, b) as f64, normalization)
}
#[must_use]
pub fn shortest_path_gram_matrix<G: GraphAdapter>(
graphs: &[G],
normalization: GramNormalization,
) -> Vec<Vec<f64>> {
let feats: Vec<HashMap<usize, u64>> =
graphs.iter().map(|&g| shortest_path_histogram(g)).collect();
gram_matrix(
&feats,
|a, b| shortest_path_kernel(a, b) as f64,
normalization,
)
}