use crate::query::{answer_query, AtomicScorer, Query, QueryConfig};
use crate::truth::SelectiveOr;
#[derive(Debug, Clone, PartialEq)]
pub enum Witness {
Fact {
anchor: usize,
relation: usize,
entity: usize,
degree: f32,
},
Given {
entity: usize,
degree: f32,
},
All {
branches: Vec<Witness>,
degree: f32,
},
Any {
branch: usize,
inner: Box<Witness>,
degree: f32,
},
Refutation {
entity: usize,
inner_degree: f32,
degree: f32,
},
Implied {
premise_degree: f32,
conclusion: Option<Box<Witness>>,
degree: f32,
},
Via {
intermediate: usize,
inner: Box<Witness>,
hop: Box<Witness>,
degree: f32,
},
}
impl Witness {
pub fn degree(&self) -> f32 {
match self {
Witness::Fact { degree, .. }
| Witness::Given { degree, .. }
| Witness::All { degree, .. }
| Witness::Any { degree, .. }
| Witness::Refutation { degree, .. }
| Witness::Implied { degree, .. }
| Witness::Via { degree, .. } => *degree,
}
}
pub fn render(&self) -> String {
let mut out = String::new();
self.render_into(&mut out, 0);
out
}
fn render_into(&self, out: &mut String, depth: usize) {
use std::fmt::Write;
let pad = " ".repeat(depth);
match self {
Witness::Fact {
anchor,
relation,
entity,
degree,
} => {
let _ = writeln!(
out,
"{pad}fact ({anchor}, r{relation}, {entity}) [{degree:.3}]"
);
}
Witness::Given { entity, degree } => {
let _ = writeln!(out, "{pad}given({entity}) [{degree:.3}]");
}
Witness::All { branches, degree } => {
let _ = writeln!(out, "{pad}all [{degree:.3}]");
for b in branches {
b.render_into(out, depth + 1);
}
}
Witness::Any {
branch,
inner,
degree,
} => {
let _ = writeln!(out, "{pad}any: branch {branch} [{degree:.3}]");
inner.render_into(out, depth + 1);
}
Witness::Refutation {
entity,
inner_degree,
degree,
} => {
let _ = writeln!(
out,
"{pad}refuted({entity}): inner degree {inner_degree:.3} [{degree:.3}]"
);
}
Witness::Implied {
premise_degree,
conclusion,
degree,
} => {
let _ = writeln!(
out,
"{pad}implied: premise {premise_degree:.3} [{degree:.3}]"
);
if let Some(c) = conclusion {
c.render_into(out, depth + 1);
}
}
Witness::Via {
intermediate,
inner,
hop,
degree,
} => {
let _ = writeln!(out, "{pad}via {intermediate} [{degree:.3}]");
inner.render_into(out, depth + 1);
hop.render_into(out, depth + 1);
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WitnessError {
ZeroDegree,
}
impl std::fmt::Display for WitnessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroDegree => write!(f, "degree is zero: no derivation supports this entity"),
}
}
}
impl std::error::Error for WitnessError {}
pub fn explain_answer<T: SelectiveOr>(
scorer: &dyn AtomicScorer,
query: &Query,
config: &QueryConfig,
entity: usize,
) -> Result<Witness, WitnessError> {
let w = witness::<T>(scorer, query, config, entity)?;
if w.degree() <= 0.0 {
return Err(WitnessError::ZeroDegree);
}
Ok(w)
}
fn witness<T: SelectiveOr>(
scorer: &dyn AtomicScorer,
query: &Query,
config: &QueryConfig,
entity: usize,
) -> Result<Witness, WitnessError> {
match query {
Query::Anchor {
entity: anchor,
relation,
} => {
let degree = scorer
.project_subset(*anchor, *relation, &[entity])
.first()
.copied()
.unwrap_or(0.0);
Ok(Witness::Fact {
anchor: *anchor,
relation: *relation,
entity,
degree,
})
}
Query::Given { degrees } => Ok(Witness::Given {
entity,
degree: degrees.get(entity).copied().unwrap_or(0.0),
}),
Query::Intersection { branches } => {
let ws: Vec<Witness> = branches
.iter()
.map(|b| witness::<T>(scorer, b, config, entity))
.collect::<Result<_, _>>()?;
let degree = ws.iter().map(Witness::degree).fold(T::top(), T::and);
Ok(Witness::All {
branches: ws,
degree,
})
}
Query::Union { branches } => {
let ws: Vec<Witness> = branches
.iter()
.map(|b| witness::<T>(scorer, b, config, entity))
.collect::<Result<_, _>>()?;
let (branch, best) = ws
.into_iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.degree()
.partial_cmp(&b.degree())
.unwrap_or(std::cmp::Ordering::Equal)
})
.ok_or(WitnessError::ZeroDegree)?;
let degree = best.degree();
Ok(Witness::Any {
branch,
inner: Box::new(best),
degree,
})
}
Query::Project { inner, relation } => {
let inner_scores = answer_query::<T>(scorer, inner, config);
let mut order: Vec<usize> = (0..inner_scores.len()).collect();
order.sort_unstable_by(|&a, &b| {
inner_scores[b]
.partial_cmp(&inner_scores[a])
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(&b))
});
let mut best: Option<(usize, f32, f32)> = None; for &v in order.iter().take(config.beam_k) {
let iv = inner_scores[v];
if iv <= 0.0 {
break;
}
let hop = scorer
.project_subset(v, *relation, &[entity])
.first()
.copied()
.unwrap_or(0.0);
let path = T::and(iv, hop);
if best.is_none_or(|(_, bp, _)| path > bp) {
best = Some((v, path, hop));
}
}
let (v, path, hop_degree) = best.ok_or(WitnessError::ZeroDegree)?;
let inner_witness = witness::<T>(scorer, inner, config, v)?;
Ok(Witness::Via {
intermediate: v,
inner: Box::new(inner_witness),
hop: Box::new(Witness::Fact {
anchor: v,
relation: *relation,
entity,
degree: hop_degree,
}),
degree: path,
})
}
Query::Negation { inner } => {
let inner_degree = answer_query::<T>(scorer, inner, config)
.get(entity)
.copied()
.unwrap_or(0.0);
Ok(Witness::Refutation {
entity,
inner_degree,
degree: T::neg(inner_degree),
})
}
Query::Implication {
premise,
conclusion,
} => {
let p = answer_query::<T>(scorer, premise, config)
.get(entity)
.copied()
.unwrap_or(0.0);
let conclusion_witness = witness::<T>(scorer, conclusion, config, entity)
.ok()
.filter(|w| w.degree() > 0.0);
let c = conclusion_witness.as_ref().map_or(0.0, Witness::degree);
Ok(Witness::Implied {
premise_degree: p,
conclusion: conclusion_witness.map(Box::new),
degree: T::residuum(p, c),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kg::FuzzyKg;
use crate::truth::{Godel, Viterbi};
fn kg() -> FuzzyKg {
let mut kg = FuzzyKg::new(5);
kg.add_edge(2, 0, 1, 0.9); kg.add_edge(3, 0, 1, 0.8); kg.add_edge(1, 0, 0, 0.7); kg.add_edge(2, 1, 4, 0.4); kg
}
#[test]
fn witness_degree_matches_engine_degree() {
fn check<T: crate::truth::SelectiveOr>(kg: &FuzzyKg, cfg: &QueryConfig, label: &str) {
let queries = [
Query::anchor(2, 0),
Query::anchor(2, 0).then(0),
Query::intersection(vec![Query::anchor(2, 0), Query::anchor(3, 0)]),
Query::union(vec![Query::anchor(2, 0), Query::anchor(2, 1)]),
Query::intersection(vec![
Query::anchor(2, 0).then(0),
Query::given(vec![0.6; 5]),
]),
Query::anchor(2, 0).negate(),
Query::anchor(2, 0).implies(Query::anchor(3, 0)),
];
for q in &queries {
let dense = answer_query::<T>(kg, q, cfg);
for (e, &d) in dense.iter().enumerate() {
if d > 0.0 {
let w =
explain_answer::<T>(kg, q, cfg, e).expect("witness for nonzero degree");
assert!(
(w.degree() - d).abs() < 1e-6,
"{label} entity {e}: witness {} vs engine {d}\n{}",
w.degree(),
w.render()
);
}
}
}
}
let kg = kg();
let cfg = QueryConfig::default();
check::<Godel>(&kg, &cfg, "godel");
check::<Viterbi>(&kg, &cfg, "viterbi");
}
#[test]
fn chain_witness_names_the_intermediate() {
let kg = kg();
let cfg = QueryConfig::default();
let q = Query::anchor(2, 0).then(0);
let w = explain_answer::<Godel>(&kg, &q, &cfg, 0).unwrap();
match &w {
Witness::Via {
intermediate,
degree,
..
} => {
assert_eq!(*intermediate, 1, "must route via mammal");
assert!((degree - 0.7).abs() < 1e-6);
}
other => panic!("expected Via, got {other:?}"),
}
assert!(w.render().contains("via 1"), "{}", w.render());
}
#[test]
fn union_witness_names_the_winning_branch() {
let kg = kg();
let cfg = QueryConfig::default();
let q = Query::union(vec![Query::anchor(2, 1), Query::anchor(2, 0)]);
let w = explain_answer::<Godel>(&kg, &q, &cfg, 1).unwrap();
match w {
Witness::Any { branch, degree, .. } => {
assert_eq!(branch, 1);
assert!((degree - 0.9).abs() < 1e-6);
}
other => panic!("expected Any, got {other:?}"),
}
}
#[test]
fn zero_degree_refuses_and_negation_witnesses_are_refutations() {
let kg = kg();
let cfg = QueryConfig::default();
assert_eq!(
explain_answer::<Godel>(&kg, &Query::anchor(2, 0), &cfg, 4).unwrap_err(),
WitnessError::ZeroDegree
);
let w = explain_answer::<Godel>(&kg, &Query::anchor(2, 0).negate(), &cfg, 4).unwrap();
match w {
Witness::Refutation {
inner_degree,
degree,
..
} => {
assert_eq!(inner_degree, 0.0);
assert_eq!(degree, 1.0);
}
other => panic!("expected Refutation, got {other:?}"),
}
assert_eq!(
explain_answer::<Godel>(&kg, &Query::anchor(2, 0).negate(), &cfg, 1).unwrap_err(),
WitnessError::ZeroDegree
);
}
}