use std::collections::{HashMap, HashSet};
use panproto_gat::Name;
use panproto_schema::{Edge, Schema};
use super::evidence::Provenance;
use super::{
Anchor, StrategyTag, kinds_and_constraints_compatible, token_similarity::token_similarity,
};
#[must_use]
pub fn neighborhood_anchors(
src: &Schema,
tgt: &Schema,
seeds: &HashMap<Name, Name>,
threshold: f64,
) -> Vec<Anchor> {
let seeded_sources: HashSet<&Name> = seeds.keys().collect();
let seeded_targets: HashSet<&Name> = seeds.values().collect();
let mut seed_pairs: Vec<(&Name, &Name)> = seeds.iter().collect();
seed_pairs.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
let mut out = Vec::new();
let mut proposed_src: HashSet<Name> = HashSet::new();
for (s_parent, t_parent) in seed_pairs {
let src_children = child_edges(src, s_parent);
let tgt_children = child_edges(tgt, t_parent);
for src_edge in &src_children {
if seeded_sources.contains(&src_edge.tgt) || proposed_src.contains(&src_edge.tgt) {
continue;
}
let mut best: Option<(Name, f64)> = None;
for tgt_edge in &tgt_children {
if seeded_targets.contains(&tgt_edge.tgt) {
continue;
}
if src_edge.kind != tgt_edge.kind {
continue;
}
if !kinds_and_constraints_compatible(src, &src_edge.tgt, tgt, &tgt_edge.tgt) {
continue;
}
let label_sim = match (src_edge.name.as_deref(), tgt_edge.name.as_deref()) {
(Some(a), Some(b)) => token_similarity(a, b),
(None, None) => 0.5,
_ => 0.25,
};
let deg = degree_overlap(src, &src_edge.tgt, tgt, &tgt_edge.tgt);
let score = 0.5f64.mul_add(label_sim, 0.5 * deg);
if best.as_ref().is_none_or(|(_, bs)| score > *bs) {
best = Some((tgt_edge.tgt.clone(), score));
}
}
if let Some((t_child, score)) = best
&& score >= threshold
{
proposed_src.insert(src_edge.tgt.clone());
out.push(Anchor {
src: src_edge.tgt.clone(),
tgt: t_child,
confidence: score,
strategy: StrategyTag::Neighborhood,
provenance: Provenance::Inferred,
explanation: format!(
"neighborhood score {:.2}: {} child of seeded {} ↔ child of {}",
score,
src_edge.tgt.as_str(),
s_parent.as_str(),
t_parent.as_str(),
),
});
}
}
}
out
}
fn child_edges<'a>(schema: &'a Schema, parent: &Name) -> Vec<&'a Edge> {
let mut out: Vec<&Edge> = schema.outgoing_edges(parent).iter().collect();
out.sort();
out
}
fn degree_overlap(src: &Schema, s: &Name, tgt: &Schema, t: &Name) -> f64 {
let so = src.outgoing_edges(s).len();
let si = src.incoming_edges(s).len();
let to = tgt.outgoing_edges(t).len();
let ti = tgt.incoming_edges(t).len();
let axis = |a: usize, b: usize| -> f64 {
let hi = a.max(b);
if hi == 0 {
return 1.0;
}
let lo_f = f64::from(u32::try_from(a.min(b)).unwrap_or(u32::MAX));
let hi_f = f64::from(u32::try_from(hi).unwrap_or(u32::MAX));
lo_f / hi_f
};
0.5f64.mul_add(axis(so, to), 0.5 * axis(si, ti))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use panproto_schema::{EdgeRule, Protocol, SchemaBuilder};
fn proto() -> Protocol {
Protocol {
name: "t".into(),
schema_theory: "ThTest".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![EdgeRule {
edge_kind: "prop".into(),
src_kinds: vec!["object".into()],
tgt_kinds: vec!["object".into(), "string".into()],
}],
obj_kinds: vec!["object".into(), "string".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
#[test]
fn propagates_to_kind_compatible_child_pair() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("p", "object", None::<&str>)
.unwrap()
.vertex("c", "string", None::<&str>)
.unwrap()
.edge("p", "c", "prop", Some("id"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("q", "object", None::<&str>)
.unwrap()
.vertex("d", "string", None::<&str>)
.unwrap()
.vertex("e", "object", None::<&str>)
.unwrap()
.edge("q", "d", "prop", Some("id"))
.unwrap()
.edge("q", "e", "prop", Some("id2"))
.unwrap()
.build()
.unwrap();
let mut seeds = HashMap::new();
seeds.insert(Name::from("p"), Name::from("q"));
let anchors = neighborhood_anchors(&src, &tgt, &seeds, 0.4);
assert_eq!(anchors.len(), 1);
assert_eq!(anchors[0].src.as_str(), "c");
assert_eq!(anchors[0].tgt.as_str(), "d");
assert_eq!(anchors[0].strategy, StrategyTag::Neighborhood);
}
#[test]
fn skips_when_seed_parent_has_no_kind_compatible_child() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("p", "object", None::<&str>)
.unwrap()
.vertex("c", "string", None::<&str>)
.unwrap()
.edge("p", "c", "prop", Some("id"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("q", "object", None::<&str>)
.unwrap()
.vertex("d", "object", None::<&str>)
.unwrap()
.edge("q", "d", "prop", Some("id"))
.unwrap()
.build()
.unwrap();
let mut seeds = HashMap::new();
seeds.insert(Name::from("p"), Name::from("q"));
assert!(neighborhood_anchors(&src, &tgt, &seeds, 0.4).is_empty());
}
#[test]
fn does_not_repropose_seeded_vertices() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("p", "object", None::<&str>)
.unwrap()
.vertex("c", "string", None::<&str>)
.unwrap()
.edge("p", "c", "prop", Some("id"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("q", "object", None::<&str>)
.unwrap()
.vertex("d", "string", None::<&str>)
.unwrap()
.edge("q", "d", "prop", Some("id"))
.unwrap()
.build()
.unwrap();
let mut seeds = HashMap::new();
seeds.insert(Name::from("p"), Name::from("q"));
seeds.insert(Name::from("c"), Name::from("d"));
assert!(neighborhood_anchors(&src, &tgt, &seeds, 0.1).is_empty());
}
#[test]
fn no_seeds_yields_empty() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("p", "object", None::<&str>)
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("q", "object", None::<&str>)
.unwrap()
.build()
.unwrap();
let seeds = HashMap::new();
assert!(neighborhood_anchors(&src, &tgt, &seeds, 0.5).is_empty());
}
#[test]
fn prefers_higher_scoring_label_similarity() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("p", "object", None::<&str>)
.unwrap()
.vertex("c", "string", None::<&str>)
.unwrap()
.edge("p", "c", "prop", Some("userName"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("q", "object", None::<&str>)
.unwrap()
.vertex("d", "string", None::<&str>)
.unwrap()
.vertex("e", "string", None::<&str>)
.unwrap()
.edge("q", "d", "prop", Some("user_name"))
.unwrap()
.edge("q", "e", "prop", Some("zzz"))
.unwrap()
.build()
.unwrap();
let mut seeds = HashMap::new();
seeds.insert(Name::from("p"), Name::from("q"));
let anchors = neighborhood_anchors(&src, &tgt, &seeds, 0.4);
assert_eq!(anchors.len(), 1);
assert_eq!(anchors[0].tgt.as_str(), "d");
}
}