use heyting::{answer_query_topk, AtomicScorer, Godel, Lukasiewicz, Product, Query, QueryConfig};
#[derive(Clone)]
struct Concept {
name: &'static str,
center: Vec<f32>,
offset: Vec<f32>,
}
const SUB: usize = 0;
struct ElBoxScorer {
concepts: Vec<Concept>,
temperature: f32,
}
impl ElBoxScorer {
fn subsumption_degree(&self, a: &Concept, b: &Concept) -> f32 {
let mut acc = 0.0f32;
for d in 0..a.center.len() {
let v = ((a.center[d] - b.center[d]).abs() + a.offset[d] - b.offset[d]).max(0.0);
acc += v * v;
}
(-acc.sqrt() / self.temperature).exp()
}
}
impl AtomicScorer for ElBoxScorer {
fn num_entities(&self) -> usize {
self.concepts.len()
}
fn project(&self, anchor: usize, relation: usize) -> Vec<f32> {
let n = self.concepts.len();
if relation != SUB || anchor >= n {
return vec![0.0; n];
}
let a = &self.concepts[anchor];
self.concepts
.iter()
.map(|t| self.subsumption_degree(a, t))
.collect()
}
}
fn main() {
let concepts = vec![
Concept {
name: "Animal",
center: vec![0.0, 0.0],
offset: vec![10.0, 10.0],
},
Concept {
name: "Mammal",
center: vec![0.0, 0.0],
offset: vec![2.5, 2.5],
},
Concept {
name: "Dog",
center: vec![-2.0, 0.0],
offset: vec![1.0, 1.0],
},
Concept {
name: "Cat",
center: vec![2.0, 0.0],
offset: vec![1.0, 1.0],
},
Concept {
name: "Bird",
center: vec![0.0, 8.0],
offset: vec![1.0, 1.0],
},
];
let names: Vec<&str> = concepts.iter().map(|c| c.name).collect();
let id = |n: &str| names.iter().position(|&x| x == n).unwrap();
let (animal, mammal, dog, cat, bird) =
(id("Animal"), id("Mammal"), id("Dog"), id("Cat"), id("Bird"));
let scorer = ElBoxScorer {
concepts: concepts.clone(),
temperature: 1.0,
};
let cfg = QueryConfig::default();
let show = |label: &str, ranked: &[(usize, f32)]| {
let body: Vec<String> = ranked
.iter()
.map(|&(e, d)| format!("{}={:.3}", names[e], d))
.collect();
println!(" {label:<12} {}", body.join(" "));
};
println!("Query A: X such that Dog ⊑ X AND Cat ⊑ X (common superclasses)");
let q_a = Query::intersection(vec![Query::anchor(dog, SUB), Query::anchor(cat, SUB)]);
let godel = answer_query_topk::<Godel>(&scorer, &q_a, &cfg, 3);
let product = answer_query_topk::<Product>(&scorer, &q_a, &cfg, 3);
let luka = answer_query_topk::<Lukasiewicz>(&scorer, &q_a, &cfg, 3);
show("Godel:", &godel);
show("Product:", &product);
show("Lukasiewicz:", &luka);
for (algo, ranked) in [
("Godel", &godel),
("Product", &product),
("Lukasiewicz", &luka),
] {
let top2: Vec<usize> = ranked.iter().take(2).map(|&(e, _)| e).collect();
assert_eq!(
top2,
vec![animal, mammal],
"{algo}: top-2 must be Animal, Mammal"
);
}
assert!(
(godel[0].1 - 1.0).abs() < 1e-3,
"Animal must be a certain superclass"
);
let (g_mammal, p_mammal, l_mammal) = (godel[1].1, product[1].1, luka[1].1);
assert!(
g_mammal > p_mammal && p_mammal > l_mammal,
"t-norm ordering must hold: Godel {g_mammal:.3} > Product {p_mammal:.3} > Lukasiewicz {l_mammal:.3}"
);
println!(
" -> Mammal graded truth by logic: Godel {g_mammal:.3} > Product {p_mammal:.3} > Lukasiewicz {l_mammal:.3}\n"
);
println!("Query B: X such that Dog ⊑ X AND NOT (Bird ⊑ X) (Dog-only superclasses)");
let q_b = Query::intersection(vec![
Query::anchor(dog, SUB),
Query::anchor(bird, SUB).negate(),
]);
let ranked_b = answer_query_topk::<Lukasiewicz>(&scorer, &q_b, &cfg, 4);
show("Lukasiewicz:", &ranked_b);
let proper: Vec<(usize, f32)> = ranked_b
.iter()
.copied()
.filter(|&(e, _)| e != dog)
.collect();
assert_eq!(proper[0].0, mammal, "top proper superclass must be Mammal");
let animal_rank = proper.iter().position(|&(e, _)| e == animal);
let mammal_deg = proper[0].1;
let animal_deg = animal_rank.map(|r| proper[r].1).unwrap_or(0.0);
assert!(
mammal_deg > animal_deg,
"Mammal ({mammal_deg:.3}) must outrank Animal ({animal_deg:.3}) under negation"
);
println!(
" -> excluding reflexive self, Mammal ranks first ({:.3}); Animal is suppressed to {} because it also subsumes Bird\n",
mammal_deg,
animal_rank.map_or("outside top-3".into(), |r| format!("{:.3}", proper[r].1))
);
println!("All assertions passed: graded EL++ subsumption CLQA over region embeddings.");
let _ = bird;
}