use std::collections::HashSet;
use thiserror::Error;
use crate::owl_ql::{
ConjunctiveQuery, Owl2QLTBox, QlAxiom, QlConcept, QlError, QlRole, QueryAtom, QueryRewriter,
QueryTerm, RewrittenQuery,
};
use super::ontology::{Owl2Ontology, ProfileEntailment, ReasoningOutcome};
#[derive(Debug, Error)]
pub enum Ql2Error {
#[error("QL reasoning failed: {0}")]
Reasoning(#[from] QlError),
}
pub struct ProfileQlReasoner {
tbox: Owl2QLTBox,
classified: bool,
}
impl Default for ProfileQlReasoner {
fn default() -> Self {
Self::new()
}
}
impl ProfileQlReasoner {
pub fn new() -> Self {
Self {
tbox: Owl2QLTBox::new(),
classified: false,
}
}
pub fn add_axiom(&mut self, axiom: QlAxiom) {
self.classified = false;
self.tbox.add_axiom(axiom);
}
pub fn load(&mut self, ontology: &Owl2Ontology) {
for axiom in ontology.ql_axioms() {
self.add_axiom(axiom);
}
}
pub fn classify(&mut self) -> Result<(), Ql2Error> {
if !self.classified {
self.tbox.classify()?;
self.classified = true;
}
Ok(())
}
pub fn tbox(&self) -> &Owl2QLTBox {
&self.tbox
}
pub fn ucq_for_query(&mut self, query: &ConjunctiveQuery) -> Result<RewrittenQuery, Ql2Error> {
self.classify()?;
let rewriter = QueryRewriter::new(self.tbox.clone());
Ok(rewriter.rewrite_query(query)?)
}
pub fn entailments(
&mut self,
ontology: &Owl2Ontology,
) -> Result<Vec<ProfileEntailment>, Ql2Error> {
self.classify()?;
let rewriter = QueryRewriter::new(self.tbox.clone());
let mut out: HashSet<ProfileEntailment> = HashSet::new();
let individuals = ontology.individuals();
let classes = ontology.classes();
let properties = ontology.properties();
for individual in &individuals {
for class in &classes {
let q = ConjunctiveQuery::with_atoms(vec![QueryAtom::TypeAtom {
individual: QueryTerm::constant(individual),
class: class.clone(),
}]);
let rewritten = rewriter.rewrite_query(&q)?;
if rewritten_matches_data(&rewritten, ontology) {
out.insert(ProfileEntailment::Type {
individual: individual.clone(),
class: class.clone(),
});
}
}
}
for property in &properties {
for subject in &individuals {
for object in &individuals {
let q = ConjunctiveQuery::with_atoms(vec![QueryAtom::PropertyAtom {
subject: QueryTerm::constant(subject),
property: property.clone(),
object: QueryTerm::constant(object),
}]);
let rewritten = rewriter.rewrite_query(&q)?;
if rewritten_matches_data(&rewritten, ontology) {
out.insert(ProfileEntailment::Property {
subject: subject.clone(),
property: property.clone(),
object: object.clone(),
});
}
}
}
}
for sub in &classes {
let supers = self.tbox.superclasses(sub);
for sup in supers {
if sub != &sup {
out.insert(ProfileEntailment::SubClassOf {
sub: sub.clone(),
sup,
});
}
}
}
let mut sorted: Vec<ProfileEntailment> = out.into_iter().collect();
sorted.sort();
Ok(sorted)
}
pub fn reason(&mut self, ontology: &Owl2Ontology) -> Result<ReasoningOutcome, Ql2Error> {
let entailments = self.entailments(ontology)?;
Ok(ReasoningOutcome::new(entailments))
}
pub fn add_subclass_of(&mut self, sub: &str, sup: &str) {
self.add_axiom(QlAxiom::SubClassOf {
sub: QlConcept::Named(sub.to_string()),
sup: QlConcept::Named(sup.to_string()),
});
}
pub fn add_subproperty_of(&mut self, sub: &str, sup: &str) {
self.add_axiom(QlAxiom::SubObjectPropertyOf {
sub: QlRole::Named(sub.to_string()),
sup: QlRole::Named(sup.to_string()),
});
}
pub fn add_inverse(&mut self, p1: &str, p2: &str) {
self.add_axiom(QlAxiom::InverseObjectProperties(
p1.to_string(),
p2.to_string(),
));
}
}
fn rewritten_matches_data(rewritten: &RewrittenQuery, ontology: &Owl2Ontology) -> bool {
rewritten
.queries
.iter()
.any(|cq| ground_cq_holds(cq, ontology))
}
fn ground_cq_holds(cq: &ConjunctiveQuery, ontology: &Owl2Ontology) -> bool {
use std::collections::HashMap;
fn search(
atoms: &[QueryAtom],
bindings: &HashMap<String, String>,
ontology: &Owl2Ontology,
) -> bool {
let Some((first, rest)) = atoms.split_first() else {
return true;
};
let candidate_facts: Vec<(String, String, String)> = match first {
QueryAtom::TypeAtom { individual, class } => ontology
.data_triples
.iter()
.filter(|(_, p, o)| p == "rdf:type" && o == class)
.filter(|(s, _, _)| match individual {
QueryTerm::Variable(v) => match bindings.get(v) {
Some(bound) => bound == s,
None => true,
},
QueryTerm::Constant(c) => c == s,
})
.cloned()
.collect(),
QueryAtom::PropertyAtom {
subject,
property,
object,
} => ontology
.data_triples
.iter()
.filter(|(_, p, _)| p == property)
.filter(|(s, _, o)| {
let s_ok = match subject {
QueryTerm::Variable(v) => bindings.get(v).map_or(true, |b| b == s),
QueryTerm::Constant(c) => c == s,
};
let o_ok = match object {
QueryTerm::Variable(v) => bindings.get(v).map_or(true, |b| b == o),
QueryTerm::Constant(c) => c == o,
};
s_ok && o_ok
})
.cloned()
.collect(),
};
for fact in candidate_facts {
let mut new_bindings = bindings.clone();
let bind_ok = match first {
QueryAtom::TypeAtom { individual, .. } => match individual {
QueryTerm::Variable(v) => match new_bindings.get(v) {
Some(existing) => existing == &fact.0,
None => {
new_bindings.insert(v.clone(), fact.0.clone());
true
}
},
QueryTerm::Constant(c) => c == &fact.0,
},
QueryAtom::PropertyAtom {
subject, object, ..
} => {
let mut ok = true;
if let QueryTerm::Variable(v) = subject {
match new_bindings.get(v) {
Some(existing) if existing != &fact.0 => ok = false,
None => {
new_bindings.insert(v.clone(), fact.0.clone());
}
_ => {}
}
}
if ok {
if let QueryTerm::Variable(v) = object {
match new_bindings.get(v) {
Some(existing) if existing != &fact.2 => ok = false,
None => {
new_bindings.insert(v.clone(), fact.2.clone());
}
_ => {}
}
}
}
ok
}
};
if bind_ok && search(rest, &new_bindings, ontology) {
return true;
}
}
false
}
search(&cq.atoms, &HashMap::new(), ontology)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::owl2::ontology::Owl2OntologyBuilder;
#[test]
fn ql_subclass_entailment() {
let ontology = Owl2OntologyBuilder::new()
.sub_class_of("Dog", "Mammal")
.sub_class_of("Mammal", "Animal")
.type_of("rex", "Dog")
.build();
let mut r = ProfileQlReasoner::new();
r.load(&ontology);
let entailments = r.entailments(&ontology).expect("entailments");
let has_rex_animal = entailments.iter().any(|e| {
matches!(
e,
ProfileEntailment::Type { individual, class }
if individual == "rex" && class == "Animal"
)
});
assert!(
has_rex_animal,
"rex should be inferred as Animal via subclass; got: {entailments:?}"
);
}
#[test]
fn ql_inverse_property() {
let ontology = Owl2OntologyBuilder::new()
.inverse_of("hasParent", "hasChild")
.property("alice", "hasChild", "bob")
.build();
let mut r = ProfileQlReasoner::new();
r.load(&ontology);
r.classify().expect("classify ok");
let q = ConjunctiveQuery::with_atoms(vec![QueryAtom::PropertyAtom {
subject: QueryTerm::constant("bob"),
property: "hasParent".to_string(),
object: QueryTerm::constant("alice"),
}]);
let ucq = r.ucq_for_query(&q).expect("rewrite ok");
let contains_inverse_branch = ucq.queries.iter().any(|cq| {
cq.atoms.iter().any(|a| {
matches!(
a,
QueryAtom::PropertyAtom {
subject: QueryTerm::Constant(s),
property,
object: QueryTerm::Constant(o),
} if property == "hasChild" && s == "alice" && o == "bob"
)
})
});
assert!(
contains_inverse_branch,
"Expected UCQ branch using inverse property: {ucq:?}"
);
}
}