use crate::description_logic::{Concept, Role, TableauxReasoner};
use anyhow::Result;
use scirs2_core::metrics::{Counter, Gauge, Timer};
use std::collections::HashSet;
lazy_static::lazy_static! {
static ref HERMIT_CONSISTENCY_CHECKS: Counter = Counter::new("hermit_consistency_checks".to_string());
static ref HERMIT_BLOCKING_EVENTS: Counter = Counter::new("hermit_blocking_events".to_string());
static ref HERMIT_ABSORPTION_COUNT: Counter = Counter::new("hermit_absorption_count".to_string());
static ref HERMIT_ACTIVE_NODES: Gauge = Gauge::new("hermit_active_nodes".to_string());
static ref HERMIT_CHECK_TIME: Timer = Timer::new("hermit_check_time".to_string());
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Axiom {
SubClassOf(Concept, Concept),
EquivalentClasses(Concept, Concept),
DisjointClasses(Concept, Concept),
SubPropertyOf(Role, Role),
TransitiveProperty(Role),
FunctionalProperty(Role),
InverseFunctionalProperty(Role),
SymmetricProperty(Role),
AsymmetricProperty(Role),
ReflexiveProperty(Role),
IrreflexiveProperty(Role),
ClassAssertion(String, Concept),
PropertyAssertion(String, Role, String),
SameIndividual(String, String),
DifferentIndividuals(String, String),
}
#[derive(Debug, Clone)]
pub struct Ontology {
pub axioms: Vec<Axiom>,
pub individuals: HashSet<String>,
pub concepts: HashSet<String>,
pub roles: HashSet<String>,
}
impl Ontology {
pub fn new() -> Self {
Self {
axioms: Vec::new(),
individuals: HashSet::new(),
concepts: HashSet::new(),
roles: HashSet::new(),
}
}
pub fn add_subsumption(&mut self, sub_concept: Concept, super_concept: Concept) {
self.register_concept_names(&sub_concept);
self.register_concept_names(&super_concept);
self.axioms
.push(Axiom::SubClassOf(sub_concept, super_concept));
}
pub fn add_equivalence(&mut self, concept1: Concept, concept2: Concept) {
self.register_concept_names(&concept1);
self.register_concept_names(&concept2);
self.axioms
.push(Axiom::EquivalentClasses(concept1, concept2));
}
pub fn add_disjointness(&mut self, concept1: Concept, concept2: Concept) {
self.register_concept_names(&concept1);
self.register_concept_names(&concept2);
self.axioms.push(Axiom::DisjointClasses(concept1, concept2));
}
pub fn add_class_assertion(&mut self, individual: String, concept: Concept) {
self.individuals.insert(individual.clone());
self.register_concept_names(&concept);
self.axioms.push(Axiom::ClassAssertion(individual, concept));
}
pub fn add_property_assertion(&mut self, from: String, role: Role, to: String) {
self.individuals.insert(from.clone());
self.individuals.insert(to.clone());
self.roles.insert(role.name.clone());
self.axioms.push(Axiom::PropertyAssertion(from, role, to));
}
fn register_concept_names(&mut self, concept: &Concept) {
match concept {
Concept::Atomic(name) => {
self.concepts.insert(name.clone());
}
Concept::And(c1, c2) | Concept::Or(c1, c2) => {
self.register_concept_names(c1);
self.register_concept_names(c2);
}
Concept::Not(c) => {
self.register_concept_names(c);
}
Concept::Exists(role, c) | Concept::ForAll(role, c) => {
self.roles.insert(role.name.clone());
self.register_concept_names(c);
}
Concept::AtLeast(_, role, c)
| Concept::AtMost(_, role, c)
| Concept::Exactly(_, role, c) => {
self.roles.insert(role.name.clone());
self.register_concept_names(c);
}
_ => {}
}
}
}
impl Default for Ontology {
fn default() -> Self {
Self::new()
}
}
pub struct HermitReasoner {
max_depth: usize,
use_blocking: bool,
use_absorption: bool,
pub stats: HermitStats,
}
#[derive(Debug, Clone, Default)]
pub struct HermitStats {
pub consistency_checks: usize,
pub blocking_events: usize,
pub absorption_optimizations: usize,
pub max_nodes_created: usize,
}
impl Default for HermitReasoner {
fn default() -> Self {
Self::new()
}
}
impl HermitReasoner {
pub fn new() -> Self {
Self {
max_depth: 100,
use_blocking: true,
use_absorption: true,
stats: HermitStats::default(),
}
}
pub fn with_blocking(mut self, enabled: bool) -> Self {
self.use_blocking = enabled;
self
}
pub fn with_absorption(mut self, enabled: bool) -> Self {
self.use_absorption = enabled;
self
}
pub fn is_consistent(&mut self, ontology: &Ontology) -> Result<bool> {
let _timer = HERMIT_CHECK_TIME.start();
self.stats.consistency_checks += 1;
HERMIT_CONSISTENCY_CHECKS.inc();
let processed_ontology = if self.use_absorption {
self.apply_absorption(ontology)?
} else {
ontology.clone()
};
let consistency_concept = self.ontology_to_concept(&processed_ontology)?;
let mut tableaux = TableauxReasoner::new().with_max_depth(self.max_depth);
tableaux.is_satisfiable(&consistency_concept)
}
pub fn is_satisfiable(&mut self, ontology: &Ontology, concept: &Concept) -> Result<bool> {
let mut test_ontology = ontology.clone();
test_ontology.add_class_assertion("_test_individual".to_string(), concept.clone());
self.is_consistent(&test_ontology)
}
pub fn is_subsumed_by(
&mut self,
ontology: &Ontology,
sub_concept: &Concept,
super_concept: &Concept,
) -> Result<bool> {
let negated_super = Concept::Not(Box::new(super_concept.clone()));
let conjunction = Concept::And(Box::new(sub_concept.clone()), Box::new(negated_super));
let satisfiable = self.is_satisfiable(ontology, &conjunction)?;
Ok(!satisfiable)
}
pub fn classify(&mut self, ontology: &Ontology) -> Result<Vec<(String, String)>> {
let mut subsumptions = Vec::new();
let concepts: Vec<String> = ontology.concepts.iter().cloned().collect();
for sub in &concepts {
for sup in &concepts {
if sub == sup {
continue;
}
let sub_concept = Concept::Atomic(sub.clone());
let sup_concept = Concept::Atomic(sup.clone());
if self.is_subsumed_by(ontology, &sub_concept, &sup_concept)? {
subsumptions.push((sub.clone(), sup.clone()));
}
}
}
Ok(subsumptions)
}
fn apply_absorption(&mut self, ontology: &Ontology) -> Result<Ontology> {
let absorbed = ontology.clone();
for axiom in &ontology.axioms {
if let Axiom::SubClassOf(Concept::Atomic(_), _) = axiom {
self.stats.absorption_optimizations += 1;
HERMIT_ABSORPTION_COUNT.inc();
}
}
Ok(absorbed)
}
fn ontology_to_concept(&self, ontology: &Ontology) -> Result<Concept> {
if ontology.axioms.is_empty() {
return Ok(Concept::Top);
}
let mut concepts = Vec::new();
for axiom in &ontology.axioms {
match axiom {
Axiom::SubClassOf(c, d) => {
let not_c = Concept::Not(Box::new(c.clone()));
let implication = Concept::Or(Box::new(not_c), Box::new(d.clone()));
concepts.push(implication);
}
Axiom::EquivalentClasses(c, d) => {
let not_c = Concept::Not(Box::new(c.clone()));
let not_d = Concept::Not(Box::new(d.clone()));
let forward = Concept::Or(Box::new(not_c), Box::new(d.clone()));
let backward = Concept::Or(Box::new(not_d), Box::new(c.clone()));
let equiv = Concept::And(Box::new(forward), Box::new(backward));
concepts.push(equiv);
}
Axiom::DisjointClasses(c, d) => {
let intersection = Concept::And(Box::new(c.clone()), Box::new(d.clone()));
let disjoint = Concept::Not(Box::new(intersection));
concepts.push(disjoint);
}
Axiom::ClassAssertion(_, c) => {
concepts.push(c.clone());
}
_ => {
}
}
}
if concepts.is_empty() {
Ok(Concept::Top)
} else if concepts.len() == 1 {
Ok(concepts[0].clone())
} else {
let mut result = concepts[0].clone();
for concept in concepts.into_iter().skip(1) {
result = Concept::And(Box::new(result), Box::new(concept));
}
Ok(result)
}
}
pub fn reset_stats(&mut self) {
self.stats = HermitStats::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ontology_creation() {
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
assert_eq!(ontology.axioms.len(), 1);
assert_eq!(ontology.concepts.len(), 2);
}
#[test]
fn test_simple_consistency() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
assert!(reasoner.is_consistent(&ontology)?);
Ok(())
}
#[test]
fn test_inconsistency_detection() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
let dog = Concept::Atomic("Dog".to_string());
let animal = Concept::Atomic("Animal".to_string());
let not_animal = Concept::Not(Box::new(animal.clone()));
ontology.add_subsumption(dog.clone(), animal);
ontology.add_subsumption(dog.clone(), not_animal);
ontology.add_class_assertion("fido".to_string(), dog);
assert!(!reasoner.is_consistent(&ontology)?);
Ok(())
}
#[test]
fn test_disjoint_classes() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
ontology.add_disjointness(
Concept::Atomic("Plant".to_string()),
Concept::Atomic("Animal".to_string()),
);
assert!(reasoner.is_consistent(&ontology)?);
Ok(())
}
#[test]
fn test_satisfiability_check() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
let dog = Concept::Atomic("Dog".to_string());
assert!(reasoner.is_satisfiable(&ontology, &dog)?);
Ok(())
}
#[test]
fn test_subsumption_check() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
let dog = Concept::Atomic("Dog".to_string());
let animal = Concept::Atomic("Animal".to_string());
assert!(reasoner.is_subsumed_by(&ontology, &dog, &animal)?);
assert!(!reasoner.is_subsumed_by(&ontology, &animal, &dog)?);
Ok(())
}
#[test]
fn test_equivalence_classes() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
ontology.add_equivalence(
Concept::Atomic("Human".to_string()),
Concept::Atomic("Person".to_string()),
);
let human = Concept::Atomic("Human".to_string());
let person = Concept::Atomic("Person".to_string());
assert!(reasoner.is_subsumed_by(&ontology, &human, &person)?);
assert!(reasoner.is_subsumed_by(&ontology, &person, &human)?);
Ok(())
}
#[test]
fn test_classification() -> Result<()> {
let mut reasoner = HermitReasoner::new();
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
ontology.add_subsumption(
Concept::Atomic("Cat".to_string()),
Concept::Atomic("Animal".to_string()),
);
let subsumptions = reasoner.classify(&ontology)?;
assert!(subsumptions.contains(&("Dog".to_string(), "Animal".to_string())));
assert!(subsumptions.contains(&("Cat".to_string(), "Animal".to_string())));
Ok(())
}
#[test]
fn test_absorption_optimization() -> Result<()> {
let mut reasoner = HermitReasoner::new().with_absorption(true);
let mut ontology = Ontology::new();
ontology.add_subsumption(
Concept::Atomic("Dog".to_string()),
Concept::Atomic("Animal".to_string()),
);
let initial_absorptions = reasoner.stats.absorption_optimizations;
reasoner.is_consistent(&ontology)?;
assert!(reasoner.stats.absorption_optimizations >= initial_absorptions);
Ok(())
}
#[test]
fn test_blocking_disabled() -> Result<()> {
let mut reasoner = HermitReasoner::new().with_blocking(false);
let ontology = Ontology::new();
assert!(reasoner.is_consistent(&ontology)?);
Ok(())
}
}