use crate::description_logic::{Concept, Role};
use crate::hermit_reasoner::{Axiom, Ontology};
use crate::{Rule, RuleAtom, Term};
use anyhow::Result;
use scirs2_core::metrics::{Counter, Timer};
use std::collections::{HashMap, HashSet};
lazy_static::lazy_static! {
static ref EL_CLASSIFICATIONS: Counter = Counter::new("el_classifications".to_string());
static ref EL_SUBSUMPTIONS: Counter = Counter::new("el_subsumptions".to_string());
static ref QL_QUERY_REWRITES: Counter = Counter::new("ql_query_rewrites".to_string());
static ref RL_RULE_APPLICATIONS: Counter = Counter::new("rl_rule_applications".to_string());
static ref PROFILE_REASONING_TIME: Timer = Timer::new("profile_reasoning_time".to_string());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OWLProfile {
EL,
QL,
RL,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Subsumption {
pub sub_class: String,
pub super_class: String,
}
#[derive(Debug, Clone)]
pub struct ClassHierarchy {
pub direct_subsumptions: HashSet<Subsumption>,
pub all_subsumptions: HashSet<Subsumption>,
pub equivalences: HashMap<String, HashSet<String>>,
}
impl ClassHierarchy {
pub fn new() -> Self {
Self {
direct_subsumptions: HashSet::new(),
all_subsumptions: HashSet::new(),
equivalences: HashMap::new(),
}
}
pub fn get_superclasses(&self, class: &str) -> HashSet<String> {
self.all_subsumptions
.iter()
.filter(|s| s.sub_class == class)
.map(|s| s.super_class.clone())
.collect()
}
pub fn get_subclasses(&self, class: &str) -> HashSet<String> {
self.all_subsumptions
.iter()
.filter(|s| s.super_class == class)
.map(|s| s.sub_class.clone())
.collect()
}
}
impl Default for ClassHierarchy {
fn default() -> Self {
Self::new()
}
}
pub struct ELReasoner {
pub stats: ELStats,
}
#[derive(Debug, Clone, Default)]
pub struct ELStats {
pub classifications: usize,
pub subsumption_tests: usize,
pub iterations: usize,
}
impl Default for ELReasoner {
fn default() -> Self {
Self::new()
}
}
impl ELReasoner {
pub fn new() -> Self {
Self {
stats: ELStats::default(),
}
}
pub fn classify(&mut self, ontology: &Ontology) -> Result<ClassHierarchy> {
let _timer = PROFILE_REASONING_TIME.start();
self.stats.classifications += 1;
EL_CLASSIFICATIONS.inc();
let mut hierarchy = ClassHierarchy::new();
let concepts: HashSet<String> = ontology.concepts.iter().cloned().collect();
let mut subsumptions: HashMap<String, HashSet<String>> = HashMap::new();
for axiom in &ontology.axioms {
match axiom {
Axiom::SubClassOf(Concept::Atomic(sub), Concept::Atomic(sup)) => {
subsumptions
.entry(sub.clone())
.or_default()
.insert(sup.clone());
}
Axiom::EquivalentClasses(Concept::Atomic(c1), Concept::Atomic(c2)) => {
subsumptions
.entry(c1.clone())
.or_default()
.insert(c2.clone());
subsumptions
.entry(c2.clone())
.or_default()
.insert(c1.clone());
hierarchy
.equivalences
.entry(c1.clone())
.or_default()
.insert(c2.clone());
hierarchy
.equivalences
.entry(c2.clone())
.or_default()
.insert(c1.clone());
}
_ => {}
}
}
let mut changed = true;
while changed {
changed = false;
self.stats.iterations += 1;
for concept in &concepts {
if let Some(supers) = subsumptions.get(concept).cloned() {
let mut new_supers = supers.clone();
for sup in &supers {
if let Some(sup_supers) = subsumptions.get(sup) {
for sup_sup in sup_supers {
if new_supers.insert(sup_sup.clone()) {
changed = true;
}
}
}
}
if new_supers.len() > supers.len() {
subsumptions.insert(concept.clone(), new_supers);
}
}
}
}
for (sub_class, super_classes) in &subsumptions {
for super_class in super_classes {
hierarchy.all_subsumptions.insert(Subsumption {
sub_class: sub_class.clone(),
super_class: super_class.clone(),
});
self.stats.subsumption_tests += 1;
EL_SUBSUMPTIONS.inc();
}
}
for (sub_class, super_classes) in &subsumptions {
let minimal_supers: HashSet<String> = super_classes
.iter()
.filter(|sup| {
!super_classes.iter().any(|other_sup| {
other_sup != *sup
&& subsumptions
.get(other_sup)
.map(|s| s.contains(*sup))
.unwrap_or(false)
})
})
.cloned()
.collect();
for super_class in minimal_supers {
hierarchy.direct_subsumptions.insert(Subsumption {
sub_class: sub_class.clone(),
super_class,
});
}
}
Ok(hierarchy)
}
pub fn is_subsumed_by(
&mut self,
ontology: &Ontology,
sub_concept: &str,
super_concept: &str,
) -> Result<bool> {
let hierarchy = self.classify(ontology)?;
Ok(hierarchy.all_subsumptions.contains(&Subsumption {
sub_class: sub_concept.to_string(),
super_class: super_concept.to_string(),
}))
}
}
pub struct QLReasoner {
pub stats: QLStats,
}
#[derive(Debug, Clone, Default)]
pub struct QLStats {
pub query_rewrites: usize,
pub rewritten_queries: usize,
}
impl Default for QLReasoner {
fn default() -> Self {
Self::new()
}
}
impl QLReasoner {
pub fn new() -> Self {
Self {
stats: QLStats::default(),
}
}
pub fn rewrite_query(
&mut self,
query: &RuleAtom,
ontology: &Ontology,
) -> Result<Vec<RuleAtom>> {
let _timer = PROFILE_REASONING_TIME.start();
self.stats.query_rewrites += 1;
QL_QUERY_REWRITES.inc();
let mut rewritten = vec![query.clone()];
if let RuleAtom::Triple {
subject,
predicate,
object,
} = query
{
if predicate == &Term::Constant("rdf:type".to_string()) {
if let Term::Constant(class) = object {
for axiom in &ontology.axioms {
if let Axiom::SubClassOf(Concept::Atomic(sub), Concept::Atomic(sup)) = axiom
{
if sub == class {
rewritten.push(RuleAtom::Triple {
subject: subject.clone(),
predicate: predicate.clone(),
object: Term::Constant(sup.clone()),
});
self.stats.rewritten_queries += 1;
}
}
}
}
}
for axiom in &ontology.axioms {
if let Axiom::SubPropertyOf(Role { name: sub_prop }, Role { name: super_prop }) =
axiom
{
if predicate == &Term::Constant(sub_prop.clone()) {
rewritten.push(RuleAtom::Triple {
subject: subject.clone(),
predicate: Term::Constant(super_prop.clone()),
object: object.clone(),
});
self.stats.rewritten_queries += 1;
}
}
}
}
Ok(rewritten)
}
pub fn answer_query(
&mut self,
query: &RuleAtom,
ontology: &Ontology,
facts: &HashSet<RuleAtom>,
) -> Result<bool> {
let rewritten_queries = self.rewrite_query(query, ontology)?;
for rewritten_query in rewritten_queries {
if facts.contains(&rewritten_query) {
return Ok(true);
}
}
Ok(false)
}
}
pub struct RLReasoner {
rules: Vec<Rule>,
pub stats: RLStats,
}
#[derive(Debug, Clone, Default)]
pub struct RLStats {
pub rule_applications: usize,
pub facts_derived: usize,
pub iterations: usize,
}
impl Default for RLReasoner {
fn default() -> Self {
Self::new()
}
}
impl RLReasoner {
pub fn new() -> Self {
let mut reasoner = Self {
rules: Vec::new(),
stats: RLStats::default(),
};
reasoner.initialize_rl_rules();
reasoner
}
fn initialize_rl_rules(&mut self) {
self.rules.push(Rule {
name: "scm-cls".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("C".to_string()),
predicate: Term::Constant("rdfs:subClassOf".to_string()),
object: Term::Variable("D".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("D".to_string()),
predicate: Term::Constant("rdfs:subClassOf".to_string()),
object: Term::Variable("E".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("C".to_string()),
predicate: Term::Constant("rdfs:subClassOf".to_string()),
object: Term::Variable("E".to_string()),
}],
});
self.rules.push(Rule {
name: "cls-hv1".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Variable("C".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("C".to_string()),
predicate: Term::Constant("rdfs:subClassOf".to_string()),
object: Term::Variable("D".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Variable("D".to_string()),
}],
});
self.rules.push(Rule {
name: "scm-spo".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("P".to_string()),
predicate: Term::Constant("rdfs:subPropertyOf".to_string()),
object: Term::Variable("Q".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("Q".to_string()),
predicate: Term::Constant("rdfs:subPropertyOf".to_string()),
object: Term::Variable("R".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("P".to_string()),
predicate: Term::Constant("rdfs:subPropertyOf".to_string()),
object: Term::Variable("R".to_string()),
}],
});
self.rules.push(Rule {
name: "prp-spo1".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("P".to_string()),
object: Term::Variable("y".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("P".to_string()),
predicate: Term::Constant("rdfs:subPropertyOf".to_string()),
object: Term::Variable("Q".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("Q".to_string()),
object: Term::Variable("y".to_string()),
}],
});
}
pub fn materialize(&mut self, initial_facts: &HashSet<RuleAtom>) -> Result<HashSet<RuleAtom>> {
let _timer = PROFILE_REASONING_TIME.start();
let mut facts = initial_facts.clone();
let mut changed = true;
while changed {
changed = false;
self.stats.iterations += 1;
for rule in &self.rules.clone() {
let new_facts = self.apply_rule(rule, &facts)?;
for fact in new_facts {
if facts.insert(fact) {
changed = true;
self.stats.facts_derived += 1;
}
}
self.stats.rule_applications += 1;
RL_RULE_APPLICATIONS.inc();
}
}
Ok(facts)
}
fn apply_rule(&self, rule: &Rule, facts: &HashSet<RuleAtom>) -> Result<Vec<RuleAtom>> {
let mut derived = Vec::new();
let body_satisfied = rule.body.iter().all(|atom| {
facts
.iter()
.any(|fact| self.atoms_structurally_match(atom, fact))
});
if body_satisfied {
for head_atom in &rule.head {
derived.push(head_atom.clone());
}
}
Ok(derived)
}
fn atoms_structurally_match(&self, pattern: &RuleAtom, fact: &RuleAtom) -> bool {
match (pattern, fact) {
(
RuleAtom::Triple {
subject: s1,
predicate: p1,
object: o1,
},
RuleAtom::Triple {
subject: s2,
predicate: p2,
object: o2,
},
) => self.terms_match(s1, s2) && self.terms_match(p1, p2) && self.terms_match(o1, o2),
_ => false,
}
}
fn terms_match(&self, t1: &Term, t2: &Term) -> bool {
match (t1, t2) {
(Term::Variable(_), _) | (_, Term::Variable(_)) => true,
(Term::Constant(c1), Term::Constant(c2)) => c1 == c2,
(Term::Literal(l1), Term::Literal(l2)) => l1 == l2,
_ => false,
}
}
pub fn get_rules(&self) -> &[Rule] {
&self.rules
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_el_classification() -> Result<()> {
let mut reasoner = ELReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Mammal".to_string()),
);
ontology.add_subsumption(
Concept::Atomic("Mammal".to_string()),
Concept::Atomic("Animal".to_string()),
);
let hierarchy = reasoner.classify(&ontology)?;
assert!(hierarchy.all_subsumptions.contains(&Subsumption {
sub_class: "Dog".to_string(),
super_class: "Animal".to_string(),
}));
Ok(())
}
#[test]
fn test_el_subsumption() -> Result<()> {
let mut reasoner = ELReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Cat".to_string()),
Concept::Atomic("Mammal".to_string()),
);
assert!(reasoner.is_subsumed_by(&ontology, "Cat", "Mammal")?);
assert!(!reasoner.is_subsumed_by(&ontology, "Mammal", "Cat")?);
Ok(())
}
#[test]
fn test_el_equivalence() -> Result<()> {
let mut reasoner = ELReasoner::new();
let mut ontology = Ontology::new();
ontology.add_equivalence(
Concept::Atomic("Human".to_string()),
Concept::Atomic("Person".to_string()),
);
let hierarchy = reasoner.classify(&ontology)?;
assert!(hierarchy.all_subsumptions.contains(&Subsumption {
sub_class: "Human".to_string(),
super_class: "Person".to_string(),
}));
assert!(hierarchy.all_subsumptions.contains(&Subsumption {
sub_class: "Person".to_string(),
super_class: "Human".to_string(),
}));
Ok(())
}
#[test]
fn test_ql_query_rewriting() -> Result<()> {
let mut reasoner = QLReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
let query = RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant("Dog".to_string()),
};
let rewritten = reasoner.rewrite_query(&query, &ontology)?;
assert!(rewritten.len() >= 2);
assert_eq!(reasoner.stats.rewritten_queries, 1);
Ok(())
}
#[test]
fn test_rl_rule_initialization() {
let reasoner = RLReasoner::new();
assert!(reasoner.get_rules().len() >= 4);
}
#[test]
fn test_rl_materialization() -> Result<()> {
let mut reasoner = RLReasoner::new();
let mut facts = HashSet::new();
facts.insert(RuleAtom::Triple {
subject: Term::Constant("fido".to_string()),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant("Dog".to_string()),
});
facts.insert(RuleAtom::Triple {
subject: Term::Constant("Dog".to_string()),
predicate: Term::Constant("rdfs:subClassOf".to_string()),
object: Term::Constant("Animal".to_string()),
});
let materialized = reasoner.materialize(&facts)?;
assert!(materialized.len() >= facts.len());
Ok(())
}
#[test]
fn test_class_hierarchy_queries() -> Result<()> {
let mut hierarchy = ClassHierarchy::new();
hierarchy.all_subsumptions.insert(Subsumption {
sub_class: "Dog".to_string(),
super_class: "Mammal".to_string(),
});
hierarchy.all_subsumptions.insert(Subsumption {
sub_class: "Dog".to_string(),
super_class: "Animal".to_string(),
});
let superclasses = hierarchy.get_superclasses("Dog");
assert_eq!(superclasses.len(), 2);
assert!(superclasses.contains("Mammal"));
assert!(superclasses.contains("Animal"));
Ok(())
}
#[test]
fn test_el_statistics() -> Result<()> {
let mut reasoner = ELReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("A".to_string()),
Concept::Atomic("B".to_string()),
);
reasoner.classify(&ontology)?;
assert!(reasoner.stats.classifications > 0);
assert!(reasoner.stats.iterations > 0);
Ok(())
}
#[test]
fn test_ql_answer_query() -> Result<()> {
let mut reasoner = QLReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
let mut facts = HashSet::new();
facts.insert(RuleAtom::Triple {
subject: Term::Constant("fido".to_string()),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant("Dog".to_string()),
});
let query = RuleAtom::Triple {
subject: Term::Constant("fido".to_string()),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant("Dog".to_string()),
};
assert!(reasoner.answer_query(&query, &ontology, &facts)?);
Ok(())
}
}