use std::collections::BTreeMap;
use super::vector::{
MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED, MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED,
MethodFeatureVector, cosine_threshold_met, dot_product,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SimilarityEdge {
left: usize,
right: usize,
weight: u64,
}
impl SimilarityEdge {
pub(crate) fn new(left: usize, right: usize, weight: u64) -> Self {
Self {
left,
right,
weight,
}
}
#[cfg(test)]
pub(crate) fn left(&self) -> usize {
self.left
}
#[cfg(test)]
pub(crate) fn right(&self) -> usize {
self.right
}
#[cfg(test)]
pub(crate) fn weight(&self) -> u64 {
self.weight
}
}
pub(crate) fn build_similarity_edges(vectors: &[MethodFeatureVector]) -> Vec<SimilarityEdge> {
let mut edges = Vec::new();
for left in 0..vectors.len() {
for right in (left + 1)..vectors.len() {
if !cosine_threshold_met(
&vectors[left],
&vectors[right],
MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED,
MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED,
) {
continue;
}
let weight = dot_product(vectors[left].weights(), vectors[right].weights());
if weight == 0 {
continue;
}
edges.push(SimilarityEdge {
left,
right,
weight,
});
}
}
edges
}
pub(crate) fn detect_communities(vectors: &[MethodFeatureVector]) -> Vec<Vec<usize>> {
if vectors.is_empty() {
return Vec::new();
}
let edges = build_similarity_edges(vectors);
let adjacency = build_adjacency(vectors.len(), &edges);
let max_iterations = vectors.len().saturating_mul(2).max(1);
let report = propagate_labels_report(vectors, &adjacency, max_iterations);
if log::log_enabled!(log::Level::Debug) {
let converged = labels_are_stable(vectors, &adjacency, &report.labels);
log::debug!(
"label propagation complete: nodes={}, iterations={}, converged={}",
vectors.len(),
report.iteration_count,
converged,
);
}
let labels = report.labels;
let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for (node, label) in labels.into_iter().enumerate() {
groups.entry(label).or_default().push(node);
}
let mut communities: Vec<Vec<usize>> = groups.into_values().collect();
for community in &mut communities {
community.sort_by(|left, right| {
vectors[*left]
.method_name()
.cmp(vectors[*right].method_name())
});
}
communities.sort_by(|left, right| {
right.len().cmp(&left.len()).then_with(|| {
vectors[left[0]]
.method_name()
.cmp(vectors[right[0]].method_name())
})
});
communities
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct LabelPropagationReport {
pub(crate) labels: Vec<usize>,
pub(crate) iteration_count: usize,
}
pub(crate) fn build_adjacency(
node_count: usize,
edges: &[SimilarityEdge],
) -> Vec<Vec<(usize, u64)>> {
let mut adjacency = vec![Vec::new(); node_count];
for edge in edges {
adjacency[edge.left].push((edge.right, edge.weight));
adjacency[edge.right].push((edge.left, edge.weight));
}
for neighbours in &mut adjacency {
neighbours.sort_by_key(|left| left.0);
}
adjacency
}
pub(crate) fn propagate_labels_report(
vectors: &[MethodFeatureVector],
adjacency: &[Vec<(usize, u64)>],
max_iterations: usize,
) -> LabelPropagationReport {
assert_eq!(
adjacency.len(),
vectors.len(),
"propagate_labels_report requires adjacency rows to match vectors"
);
let mut labels: Vec<usize> = (0..vectors.len()).collect();
let active_nodes: Vec<_> = adjacency
.iter()
.enumerate()
.filter_map(|(node, neighbours)| (!neighbours.is_empty()).then_some(node))
.collect();
if active_nodes.is_empty() {
log::debug!(
"label propagation: no active nodes, skipping (total_nodes={})",
vectors.len(),
);
return LabelPropagationReport {
labels,
iteration_count: 0,
};
}
let mut iteration_count = 0;
for _ in 0..max_iterations {
iteration_count += 1;
let mut changed = false;
for &node in &active_nodes {
let Some(best_label) = best_neighbour_label(node, &labels, adjacency, vectors) else {
continue;
};
if best_label != labels[node] {
labels[node] = best_label;
changed = true;
}
}
if !changed {
log::debug!(
"label propagation converged: nodes={}, active_nodes={}, iterations={}",
vectors.len(),
active_nodes.len(),
iteration_count,
);
break;
}
}
if iteration_count == max_iterations {
log::debug!(
"label propagation reached iteration limit: nodes={}, active_nodes={}, max_iterations={}",
vectors.len(),
active_nodes.len(),
max_iterations,
);
}
LabelPropagationReport {
labels,
iteration_count,
}
}
fn best_neighbour_label(
node: usize,
labels: &[usize],
adjacency: &[Vec<(usize, u64)>],
vectors: &[MethodFeatureVector],
) -> Option<usize> {
let neighbours = &adjacency[node];
if neighbours.is_empty() {
return None;
}
let mut scores = BTreeMap::new();
let mut best: Option<(usize, u64)> = None;
for &(neighbour, weight) in neighbours {
let label = labels[neighbour];
let score = score_label(&mut scores, label, weight);
if should_replace_best(best, label, score, vectors) {
best = Some((label, score));
}
}
best.map(|(label, _)| label)
}
fn labels_are_stable(
vectors: &[MethodFeatureVector],
adjacency: &[Vec<(usize, u64)>],
labels: &[usize],
) -> bool {
adjacency.iter().enumerate().all(|(node, neighbours)| {
if neighbours.is_empty() {
return true;
}
match best_neighbour_label(node, labels, adjacency, vectors) {
Some(best_label) => labels[node] == best_label,
None => true,
}
})
}
fn score_label(scores: &mut BTreeMap<usize, u64>, label: usize, weight: u64) -> u64 {
let score = scores.entry(label).or_default();
*score += weight;
*score
}
fn should_replace_best(
current_best: Option<(usize, u64)>,
candidate_label: usize,
candidate_score: u64,
vectors: &[MethodFeatureVector],
) -> bool {
match current_best {
None => true,
Some((best_label, best_score)) => {
if candidate_score != best_score {
candidate_score > best_score
} else {
let candidate_name = vectors[candidate_label].method_name();
let best_name = vectors[best_label].method_name();
candidate_name < best_name
|| (candidate_name == best_name && candidate_label < best_label)
}
}
}
}
#[cfg(kani)]
#[path = "community_kani/mod.rs"]
mod verify;