use std::collections::HashSet;
use std::time::Instant;
use thiserror::Error;
use crate::owl_el::{ElAxiom, ElConcept, Owl2ElReasoner};
use crate::owl_rl::{Owl2RlReasoner, RDFS_SUBCLASS_OF, RDF_TYPE};
use super::el_reasoner::El2Error;
use super::ontology::{Owl2Ontology, ProfileEntailment, ReasoningOutcome};
use super::rl_reasoner::Rl2Error;
#[derive(Debug, Error)]
pub enum RlElError {
#[error("RL pass failed: {0}")]
Rl(#[from] Rl2Error),
#[error("EL pass failed: {0}")]
El(#[from] El2Error),
#[error("RL+EL loop exceeded max alternations ({0})")]
MaxAlternationsExceeded(usize),
}
pub struct RlElReasoner {
rl: Owl2RlReasoner,
el: Owl2ElReasoner,
max_alternations: usize,
}
#[derive(Debug, Clone, Default)]
pub struct RlElReport {
pub alternations: usize,
pub rl_triples_added: usize,
pub el_axioms_added: usize,
pub duration: std::time::Duration,
}
impl Default for RlElReasoner {
fn default() -> Self {
Self::new()
}
}
impl RlElReasoner {
pub fn new() -> Self {
Self {
rl: Owl2RlReasoner::new(),
el: Owl2ElReasoner::new(),
max_alternations: 16,
}
}
pub fn with_max_alternations(mut self, n: usize) -> Self {
self.max_alternations = n;
self
}
pub fn load(&mut self, ontology: &Owl2Ontology) {
for (s, p, o) in ontology.rl_triples() {
self.rl.add_axiom(&s, &p, &o);
}
for axiom in ontology.el_axioms() {
self.el.add_axiom(axiom);
}
}
pub fn close(&mut self) -> Result<RlElReport, RlElError> {
let start = Instant::now();
let mut alternations = 0usize;
let mut rl_added_total = 0usize;
let mut el_added_total = 0usize;
let mut el_known_subclass: HashSet<(String, String)> = HashSet::new();
let mut el_known_type: HashSet<(String, String)> = HashSet::new();
let mut prev_rl_closure_size = 0usize;
loop {
if alternations >= self.max_alternations {
return Err(RlElError::MaxAlternationsExceeded(self.max_alternations));
}
alternations += 1;
let _rl_report = self
.rl
.materialize()
.map_err(|e| RlElError::Rl(Rl2Error::from(e)))?;
let new_rl_closure_size = self.rl.all_triples().len();
let rl_grew = new_rl_closure_size > prev_rl_closure_size;
prev_rl_closure_size = new_rl_closure_size;
let mut el_round_added = 0usize;
for (s, p, o) in self.rl.all_triples() {
if p == RDFS_SUBCLASS_OF
&& s != o
&& el_known_subclass.insert((s.clone(), o.clone()))
{
self.el.add_axiom(ElAxiom::SubConceptOf {
sub: ElConcept::named(s.clone()),
sup: ElConcept::named(o.clone()),
});
el_round_added += 1;
} else if p == RDF_TYPE
&& o != "http://www.w3.org/2002/07/owl#Class"
&& el_known_type.insert((s.clone(), o.clone()))
{
self.el.add_axiom(ElAxiom::ConceptAssertion {
individual: s.clone(),
concept: ElConcept::named(o.clone()),
});
el_round_added += 1;
}
}
el_added_total += el_round_added;
let cls = self
.el
.classify()
.map_err(|e| RlElError::El(El2Error::from(e)))?;
let mut rl_round_added = 0usize;
let rl_existing: HashSet<(String, String, String)> = self.rl.all_triples();
for (sub, supers) in &cls.subsumption_hierarchy {
for sup in supers {
if sub == sup {
continue;
}
let triple_key = (sub.clone(), RDFS_SUBCLASS_OF.to_string(), sup.clone());
if !rl_existing.contains(&triple_key) {
self.rl.add_axiom(sub, RDFS_SUBCLASS_OF, sup);
rl_round_added += 1;
}
}
}
for (individual, types) in &cls.individual_types {
if individual.starts_with("__wit_") {
continue;
}
for ty in types {
if ty.starts_with("__wit_") || ty == "owl:Thing" {
continue;
}
let triple_key = (individual.clone(), RDF_TYPE.to_string(), ty.clone());
if !rl_existing.contains(&triple_key) {
self.rl.add_axiom(individual, RDF_TYPE, ty);
rl_round_added += 1;
}
}
}
rl_added_total += rl_round_added;
if !rl_grew && el_round_added == 0 && rl_round_added == 0 {
break;
}
}
Ok(RlElReport {
alternations,
rl_triples_added: rl_added_total,
el_axioms_added: el_added_total,
duration: start.elapsed(),
})
}
pub fn rl(&self) -> &Owl2RlReasoner {
&self.rl
}
pub fn el(&self) -> &Owl2ElReasoner {
&self.el
}
pub fn entailments(&mut self) -> Result<Vec<ProfileEntailment>, RlElError> {
let _ = self.close()?;
let mut out: HashSet<ProfileEntailment> = HashSet::new();
for (s, p, o) in self.rl.all_triples() {
if s.starts_with("__wit_") || o.starts_with("__wit_") {
continue;
}
if p == RDF_TYPE {
out.insert(ProfileEntailment::Type {
individual: s.clone(),
class: o.clone(),
});
} else if p == RDFS_SUBCLASS_OF {
if s != o {
out.insert(ProfileEntailment::SubClassOf {
sub: s.clone(),
sup: o.clone(),
});
}
} else {
out.insert(ProfileEntailment::Property {
subject: s.clone(),
property: p.clone(),
object: o.clone(),
});
}
}
let mut sorted: Vec<ProfileEntailment> = out.into_iter().collect();
sorted.sort();
Ok(sorted)
}
pub fn reason(&mut self) -> Result<ReasoningOutcome, RlElError> {
let entailments = self.entailments()?;
Ok(ReasoningOutcome::new(entailments))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::owl2::ontology::Owl2OntologyBuilder;
#[test]
fn rlel_handles_pure_rl_subset() {
let ontology = Owl2OntologyBuilder::new()
.sub_class_of("Dog", "Mammal")
.sub_class_of("Mammal", "Animal")
.type_of("rex", "Dog")
.build();
let mut r = RlElReasoner::new();
r.load(&ontology);
let outcome = r.reason().expect("close ok");
let has_rex_animal = outcome.entailments.iter().any(|e| {
matches!(
e,
ProfileEntailment::Type { individual, class }
if individual == "rex" && class == "Animal"
)
});
assert!(
has_rex_animal,
"rex should be Animal: {:?}",
outcome.entailments
);
}
#[test]
fn rlel_handles_pure_el_subset() {
let ontology = Owl2OntologyBuilder::new()
.intersection_sub_class(vec!["Doctor".into(), "Surgeon".into()], "DoctorSurgeon")
.type_of("alice", "Doctor")
.type_of("alice", "Surgeon")
.build();
let mut r = RlElReasoner::new();
r.load(&ontology);
let outcome = r.reason().expect("close ok");
let alice_intersection = outcome.entailments.iter().any(|e| {
matches!(
e,
ProfileEntailment::Type { individual, class }
if individual == "alice" && class == "DoctorSurgeon"
)
});
assert!(
alice_intersection,
"alice should be DoctorSurgeon via combined intersection rule; got: {:?}",
outcome.entailments
);
}
#[test]
fn rlel_handles_hybrid_ontology() {
let ontology = Owl2OntologyBuilder::new()
.sub_class_of("Mammal", "Animal")
.intersection_sub_class(vec!["Doctor".into(), "ChiefSurgeon".into()], "ChiefDoctor")
.type_of("alice", "Mammal")
.type_of("alice", "Doctor")
.type_of("alice", "ChiefSurgeon")
.build();
let mut r = RlElReasoner::new();
r.load(&ontology);
let outcome = r.reason().expect("close ok");
let alice_animal = outcome.entailments.iter().any(|e| {
matches!(
e,
ProfileEntailment::Type { individual, class }
if individual == "alice" && class == "Animal"
)
});
let alice_chief_doctor = outcome.entailments.iter().any(|e| {
matches!(
e,
ProfileEntailment::Type { individual, class }
if individual == "alice" && class == "ChiefDoctor"
)
});
assert!(alice_animal, "alice should be Animal via RL");
assert!(
alice_chief_doctor,
"alice should be ChiefDoctor via EL intersection; got: {:?}",
outcome.entailments
);
}
}