use hippmem_core::model::links::{AssociationKeys, MatchDimension};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct CandidateResult {
pub matched_dimensions: Vec<MatchDimension>,
pub entity_jaccard: f32,
pub topic_jaccard: f32,
pub temporal_overlap: usize,
pub goal_jaccard: f32,
pub event_jaccard: f32,
pub causal_overlap: usize,
pub emotion_overlap: usize,
pub importance_value: f32,
pub co_context_score: f32,
pub lexical_similarity: f32,
pub semantic_binary_similarity: f32,
}
pub fn discover_candidates(a: &AssociationKeys, b: &AssociationKeys) -> CandidateResult {
let mut dims = Vec::new();
let set = |v: &[u64]| -> HashSet<u64> { v.iter().copied().collect() };
let a_ent = set(&a.entity_keys);
let b_ent = set(&b.entity_keys);
let ent_intersect = a_ent.intersection(&b_ent).count();
let ent_union = a_ent.len() + b_ent.len() - ent_intersect;
let entity_jaccard = if ent_union > 0 {
ent_intersect as f32 / ent_union as f32
} else {
0.0
};
if entity_jaccard > 0.0 {
dims.push(MatchDimension::Entity);
}
let a_top = set(&a.topic_keys);
let b_top = set(&b.topic_keys);
let top_intersect = a_top.intersection(&b_top).count();
let top_union = a_top.len() + b_top.len() - top_intersect;
let topic_jaccard = if top_union > 0 {
top_intersect as f32 / top_union as f32
} else {
0.0
};
if topic_jaccard > 0.0 {
dims.push(MatchDimension::Topic);
}
let a_tmp: HashSet<u32> = a.temporal_keys.iter().copied().collect();
let b_tmp: HashSet<u32> = b.temporal_keys.iter().copied().collect();
let temporal_overlap = a_tmp.intersection(&b_tmp).count();
if temporal_overlap > 0 {
dims.push(MatchDimension::Temporal);
}
let a_goal = set(&a.goal_keys);
let b_goal = set(&b.goal_keys);
let goal_intersect = a_goal.intersection(&b_goal).count();
let goal_union = a_goal.len() + b_goal.len() - goal_intersect;
let goal_jaccard = if goal_union > 0 {
goal_intersect as f32 / goal_union as f32
} else {
0.0
};
if goal_jaccard > 0.0 {
dims.push(MatchDimension::Goal);
}
let a_evt = set(&a.event_keys);
let b_evt = set(&b.event_keys);
let evt_intersect = a_evt.intersection(&b_evt).count();
let evt_union = a_evt.len() + b_evt.len() - evt_intersect;
let event_jaccard = if evt_union > 0 {
evt_intersect as f32 / evt_union as f32
} else {
0.0
};
if event_jaccard > 0.0 {
dims.push(MatchDimension::Event);
}
let a_cau = set(&a.causal_keys);
let b_cau = set(&b.causal_keys);
let causal_overlap = a_cau.intersection(&b_cau).count();
if causal_overlap > 0 {
dims.push(MatchDimension::Causal);
}
let a_emo: HashSet<u8> = a.emotion_keys.iter().copied().collect();
let b_emo: HashSet<u8> = b.emotion_keys.iter().copied().collect();
let emotion_overlap = a_emo.intersection(&b_emo).count();
if emotion_overlap > 0 {
dims.push(MatchDimension::Emotion);
}
let lexical_similarity =
simhash_similarity(&a.lexical_signature.simhash, &b.lexical_signature.simhash);
let a_has_signal = a.lexical_signature.simhash.iter().any(|&x| x != 0);
let b_has_signal = b.lexical_signature.simhash.iter().any(|&x| x != 0);
if a_has_signal && b_has_signal && lexical_similarity > 0.7 {
dims.push(MatchDimension::Semantic);
}
let binary_sim = binary_similarity(
&a.semantic_signature.binary_code,
&b.semantic_signature.binary_code,
);
CandidateResult {
matched_dimensions: dims,
entity_jaccard,
topic_jaccard,
temporal_overlap,
goal_jaccard,
event_jaccard,
causal_overlap,
emotion_overlap,
importance_value: 0.0,
co_context_score: 0.0,
lexical_similarity,
semantic_binary_similarity: binary_sim,
}
}
pub(crate) fn simhash_similarity(a: &[u64; 4], b: &[u64; 4]) -> f32 {
let same: u32 = a
.iter()
.zip(b.iter())
.map(|(x, y)| 64 - (x ^ y).count_ones())
.sum();
same as f32 / 256.0
}
fn binary_similarity(a: &[u64; 2], b: &[u64; 2]) -> f32 {
let same: u32 = a
.iter()
.zip(b.iter())
.map(|(x, y)| 64 - (x ^ y).count_ones())
.sum();
same as f32 / 128.0
}