use crate::ir::{ClassId, ConceptExpr, ConceptId, ConceptPool, Role, RoleId};
use crate::normalize::nnf_axioms;
use crate::ontology::{Axiom, InternalOntology};
use std::collections::HashMap;
pub type Var = u32;
pub const X: Var = 0;
const ANTECEDENT_DNF_CAP: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Atom {
Class(ClassId, Var),
Role(Role, Var, Var),
Exists(Role, ClassId, Var),
AtMost(Role, Option<ClassId>, u32, Var),
AtLeast(Role, Option<ClassId>, u32, Var),
Equal(Var, Var),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DlClause {
pub body: Vec<Atom>,
pub head: Vec<Atom>,
}
impl DlClause {
#[must_use]
pub fn is_bottom_headed(&self) -> bool {
self.head.is_empty()
}
#[must_use]
pub fn is_horn(&self) -> bool {
self.head.len() <= 1
}
}
struct Clausifier {
pool: ConceptPool,
clauses: Vec<DlClause>,
next_fresh: u32,
nominal_base: u32,
next_var: Var,
deferred: usize,
deferred_kinds: std::collections::BTreeMap<&'static str, usize>,
inverse_canon: HashMap<RoleId, Role>,
}
impl Clausifier {
fn new(
pool: ConceptPool,
first_fresh: u32,
nominal_base: u32,
inverse_canon: HashMap<RoleId, Role>,
) -> Self {
Self {
pool,
clauses: Vec::new(),
next_fresh: first_fresh,
nominal_base,
next_var: X + 1,
deferred: 0,
deferred_kinds: std::collections::BTreeMap::new(),
inverse_canon,
}
}
fn canon_role(&self, r: Role) -> Role {
match self.inverse_canon.get(&r.role_id()) {
None => r,
Some(&canon) => {
if r.is_inverse() {
canon.flip()
} else {
canon
}
}
}
}
fn defer(&mut self, kind: &'static str) {
self.deferred += 1;
*self.deferred_kinds.entry(kind).or_insert(0) += 1;
}
fn fresh_class(&mut self) -> ClassId {
let id = ClassId::new(self.next_fresh);
self.next_fresh = self
.next_fresh
.checked_add(1)
.expect("fresh class id overflow");
id
}
fn class_id_of(&self, c: ConceptId) -> Option<ClassId> {
match self.pool.get(c) {
ConceptExpr::Atomic(a) => Some(*a),
ConceptExpr::Nominal(i) => Some(ClassId::new(self.nominal_base + i.index())),
_ => None,
}
}
fn fresh_var(&mut self) -> Var {
let v = self.next_var;
self.next_var = self.next_var.checked_add(1).expect("fresh var overflow");
v
}
fn clausify_axiom(&mut self, ax: &Axiom) {
match ax {
Axiom::SubClassOf { sub, sup } => self.clausify_gci(*sub, *sup),
Axiom::EquivalentClasses(ids) => {
for (i, &a) in ids.iter().enumerate() {
for (j, &b) in ids.iter().enumerate() {
if i != j {
self.clausify_gci(a, b);
}
}
}
}
Axiom::DisjointClasses(ids) => {
for i in 0..ids.len() {
for j in (i + 1)..ids.len() {
self.next_var = X + 1;
let (Some(alts_a), Some(alts_b)) = (
self.encode_antecedent(ids[i], X),
self.encode_antecedent(ids[j], X),
) else {
self.defer("disjoint-antecedent");
continue;
};
for a in &alts_a {
for b in &alts_b {
let mut body = a.clone();
body.extend_from_slice(b);
self.clauses.push(DlClause {
body,
head: Vec::new(),
});
}
}
}
}
}
Axiom::DisjointUnion { class, members } => {
let cls_concept_eq = members; let _ = cls_concept_eq;
for i in 0..members.len() {
for j in (i + 1)..members.len() {
self.next_var = X + 1;
if let (Some(alts_a), Some(alts_b)) = (
self.encode_antecedent(members[i], X),
self.encode_antecedent(members[j], X),
) {
for a in &alts_a {
for b in &alts_b {
let mut body = a.clone();
body.extend_from_slice(b);
self.clauses.push(DlClause {
body,
head: Vec::new(),
});
}
}
}
}
}
for &m in members {
self.next_var = X + 1;
if let Some(bodies) = self.encode_antecedent(m, X) {
for body in bodies {
self.clauses.push(DlClause {
body,
head: vec![Atom::Class(*class, X)],
});
}
} else {
self.defer("disjoint-union-member");
}
}
}
Axiom::ObjectPropertyDomain { role, domain } => {
self.next_var = X + 1;
let y = self.fresh_var();
let role = self.canon_role(*role);
self.clausify_consequent(vec![Atom::Role(role, X, y)], *domain, X);
}
Axiom::ObjectPropertyRange { role, range } => {
self.next_var = X + 1;
let y = self.fresh_var();
let role = self.canon_role(*role);
self.clausify_consequent(vec![Atom::Role(role, X, y)], *range, y);
}
_ => {}
}
}
fn clausify_gci(&mut self, sub: ConceptId, sup: ConceptId) {
self.next_var = X + 1;
match self.encode_antecedent(sub, X) {
Some(bodies) => {
for body in bodies {
self.clausify_consequent(body, sup, X);
}
}
None => self.absorb_hard_antecedent(sub, sup),
}
}
fn absorb_hard_antecedent(&mut self, sub: ConceptId, sup: ConceptId) {
let parts: Vec<ConceptId> = match self.pool.get(sub) {
ConceptExpr::And(ps) => ps.to_vec(),
_ => vec![sub],
};
let mut soft = Vec::new();
let mut hard = Vec::new();
for &p in &parts {
self.next_var = X + 1;
if self.encode_antecedent(p, X).is_some() {
soft.push(p);
} else {
hard.push(p);
}
}
if soft.is_empty() {
let neg_sub = crate::normalize::nnf_complement(sub, &mut self.pool);
let head = self.pool.or(vec![neg_sub, sup]);
self.next_var = X + 1;
self.emit_head(Vec::new(), head, X);
return;
}
let mut head_parts: Vec<ConceptId> = hard
.iter()
.map(|&h| crate::normalize::nnf_complement(h, &mut self.pool))
.collect();
head_parts.push(sup);
let head = self.pool.or(head_parts);
let soft_and = if soft.len() == 1 {
soft[0]
} else {
self.pool.and(soft)
};
self.next_var = X + 1;
match self.encode_antecedent(soft_and, X) {
Some(bodies) => {
for body in bodies {
self.emit_head(body, head, X);
}
}
None => self.defer("antecedent"),
}
}
fn encode_antecedent(&mut self, c: ConceptId, var: Var) -> Option<Vec<Vec<Atom>>> {
if let Some(cls) = self.class_id_of(c) {
return Some(vec![vec![Atom::Class(cls, var)]]);
}
match self.pool.get(c) {
ConceptExpr::Top => Some(vec![Vec::new()]),
ConceptExpr::And(parts) => {
let parts: Vec<ConceptId> = parts.to_vec();
let mut acc: Vec<Vec<Atom>> = vec![Vec::new()];
for p in parts {
let child = self.encode_antecedent(p, var)?;
let mut next = Vec::with_capacity(acc.len() * child.len());
for a in &acc {
for b in &child {
let mut combined = a.clone();
combined.extend_from_slice(b);
next.push(combined);
}
}
if next.len() > ANTECEDENT_DNF_CAP {
return None;
}
acc = next;
}
Some(acc)
}
ConceptExpr::Or(parts) => {
let parts: Vec<ConceptId> = parts.to_vec();
let mut out = Vec::new();
for p in parts {
out.extend(self.encode_antecedent(p, var)?);
if out.len() > ANTECEDENT_DNF_CAP {
return None;
}
}
Some(out)
}
ConceptExpr::Some(role, inner) => {
let (role, inner) = (self.canon_role(*role), *inner);
let y = self.fresh_var();
let inner_alts = self.encode_antecedent(inner, y)?;
let mut out = Vec::with_capacity(inner_alts.len());
for alt in inner_alts {
let mut body = vec![Atom::Role(role, var, y)];
body.extend(alt);
out.push(body);
}
Some(out)
}
ConceptExpr::All(_, _)
| ConceptExpr::Not(_)
| ConceptExpr::Bot
| ConceptExpr::Min(_, _, _)
| ConceptExpr::Max(_, _, _)
| ConceptExpr::Atomic(_)
| ConceptExpr::Nominal(_)
| ConceptExpr::SelfRestriction(_) => None,
}
}
fn clausify_consequent(&mut self, body: Vec<Atom>, sup: ConceptId, var: Var) {
self.emit_head(body, sup, var);
}
fn emit_head(&mut self, body: Vec<Atom>, head_concept: ConceptId, var: Var) {
match self.pool.get(head_concept) {
ConceptExpr::Top => { }
ConceptExpr::Bot => {
self.clauses.push(DlClause {
body,
head: Vec::new(),
});
}
ConceptExpr::Atomic(_) | ConceptExpr::Nominal(_) => {
let cls = self.class_id_of(head_concept).expect("atomic/nominal");
self.clauses.push(DlClause {
body,
head: vec![Atom::Class(cls, var)],
});
}
ConceptExpr::And(parts) => {
let parts: Vec<ConceptId> = parts.to_vec();
for p in parts {
self.emit_head(body.clone(), p, var);
}
}
ConceptExpr::Or(parts) => {
let parts: Vec<ConceptId> = parts.to_vec();
let mut head: Vec<Atom> = Vec::with_capacity(parts.len());
for p in parts {
if let Some(atom) = self.head_atom_for(p, var) {
head.push(atom);
} else {
self.defer("head-or-disjunct");
return;
}
}
self.clauses.push(DlClause { body, head });
}
ConceptExpr::Some(role, inner) => {
let (role, inner) = (self.canon_role(*role), *inner);
if let Some(cls) = self.atomic_name_of(inner) {
self.clauses.push(DlClause {
body,
head: vec![Atom::Exists(role, cls, var)],
});
} else {
self.defer("head-exists-inner");
}
}
ConceptExpr::All(role, inner) => {
let (role, inner) = (self.canon_role(*role), *inner);
let y = self.fresh_var();
let mut b = body;
b.push(Atom::Role(role, var, y));
self.emit_head(b, inner, y);
}
ConceptExpr::Not(inner) => {
if let Some(a) = self.class_id_of(*inner) {
let mut b = body;
b.push(Atom::Class(a, var));
self.clauses.push(DlClause {
body: b,
head: Vec::new(),
});
} else {
self.defer("head-not-nonatomic");
}
}
ConceptExpr::Min(n, role, inner) => {
let (n, role, inner) = (*n, self.canon_role(*role), *inner);
if n == 0 {
return; }
let qual = self.cardinality_qualifier(inner);
self.clauses.push(DlClause {
body,
head: vec![Atom::AtLeast(role, qual, n, var)],
});
}
ConceptExpr::Max(n, role, inner) => {
let (n, role, inner) = (*n, self.canon_role(*role), *inner);
let qual = self.cardinality_qualifier(inner);
self.clauses.push(DlClause {
body,
head: vec![Atom::AtMost(role, qual, n, var)],
});
}
ConceptExpr::SelfRestriction(role) => {
let role = self.canon_role(*role);
self.clauses.push(DlClause {
body,
head: vec![Atom::Role(role, var, var)],
});
}
}
}
fn cardinality_qualifier(&mut self, filler: ConceptId) -> Option<ClassId> {
match self.pool.get(filler) {
ConceptExpr::Top => None,
_ => self.atomic_name_of(filler),
}
}
fn head_atom_for(&mut self, c: ConceptId, var: Var) -> Option<Atom> {
if let Some(cls) = self.class_id_of(c) {
return Some(Atom::Class(cls, var));
}
let expr = self.pool.get(c).clone();
match expr {
ConceptExpr::Some(role, inner) => {
let cls = self.atomic_name_of(inner)?;
Some(Atom::Exists(self.canon_role(role), cls, var))
}
ConceptExpr::Not(inner) => {
if let Some(a) = self.class_id_of(inner) {
let q = self.fresh_class();
self.clauses.push(DlClause {
body: vec![Atom::Class(q, var), Atom::Class(a, var)],
head: Vec::new(),
});
Some(Atom::Class(q, var))
} else if let ConceptExpr::SelfRestriction(role) = *self.pool.get(inner) {
let role = self.canon_role(role);
let q = self.fresh_class();
self.clauses.push(DlClause {
body: vec![Atom::Class(q, var), Atom::Role(role, var, var)],
head: Vec::new(),
});
Some(Atom::Class(q, var))
} else {
None
}
}
_ => self.atomic_name_of(c).map(|q| Atom::Class(q, var)),
}
}
fn atomic_name_of(&mut self, c: ConceptId) -> Option<ClassId> {
if let Some(cls) = self.class_id_of(c) {
return Some(cls);
}
let q = self.fresh_class();
self.emit_head(vec![Atom::Class(q, X)], c, X);
Some(q)
}
}
#[must_use]
pub fn clausify(internal: &InternalOntology) -> Vec<DlClause> {
clausify_with_stats(internal).0
}
fn build_inverse_canon(axioms: &[Axiom]) -> HashMap<RoleId, Role> {
let mut m: HashMap<RoleId, Role> = HashMap::new();
for ax in axioms {
if let Axiom::InverseObjectProperties(a, b) = ax {
if a.is_inverse() || b.is_inverse() {
continue;
}
let (r, s) = (*a, *b);
if m.contains_key(&r.role_id()) || m.contains_key(&s.role_id()) {
continue;
}
m.insert(s.role_id(), r.flip());
}
}
m
}
#[must_use]
pub fn clausify_with_stats(internal: &InternalOntology) -> (Vec<DlClause>, ClauseStats) {
let mut internal = internal.clone();
let normalized = nnf_axioms(&mut internal);
let num_classes =
u32::try_from(internal.vocabulary.num_classes()).expect("class count fits in u32");
let num_individuals =
u32::try_from(internal.vocabulary.num_individuals()).expect("individual count fits in u32");
let nominal_base = num_classes;
let first_fresh = num_classes
.checked_add(num_individuals)
.expect("class+individual count fits in u32");
let inverse_canon = build_inverse_canon(&normalized);
let mut c = Clausifier::new(internal.concepts, first_fresh, nominal_base, inverse_canon);
for ax in &normalized {
c.clausify_axiom(ax);
}
let stats = ClauseStats::of(&c.clauses, c.deferred);
(c.clauses, stats)
}
#[must_use]
pub fn deferred_census(internal: &InternalOntology) -> Vec<(&'static str, usize)> {
let mut internal = internal.clone();
let normalized = nnf_axioms(&mut internal);
let num_classes =
u32::try_from(internal.vocabulary.num_classes()).expect("class count fits in u32");
let num_individuals =
u32::try_from(internal.vocabulary.num_individuals()).expect("individual count fits in u32");
let first_fresh = num_classes
.checked_add(num_individuals)
.expect("class+individual count fits in u32");
let inverse_canon = build_inverse_canon(&normalized);
let mut c = Clausifier::new(internal.concepts, first_fresh, num_classes, inverse_canon);
for ax in &normalized {
c.clausify_axiom(ax);
}
c.deferred_kinds.into_iter().collect()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ClauseStats {
pub total: usize,
pub horn: usize,
pub disjunctive: usize,
pub bottom_headed: usize,
pub with_exists_head: usize,
pub deferred: usize,
}
impl ClauseStats {
#[must_use]
fn of(clauses: &[DlClause], deferred: usize) -> Self {
let mut s = ClauseStats {
total: clauses.len(),
deferred,
..ClauseStats::default()
};
for cl in clauses {
if cl.is_bottom_headed() {
s.bottom_headed += 1;
}
if cl.is_horn() {
s.horn += 1;
} else {
s.disjunctive += 1;
}
if cl.head.iter().any(|a| matches!(a, Atom::Exists(..))) {
s.with_exists_head += 1;
}
}
s
}
}
#[cfg(test)]
#[allow(clippy::many_single_char_names)]
mod tests {
use super::*;
use crate::convert::convert_ontology;
use horned_owl::io::ParserConfiguration;
use horned_owl::io::ofn::reader::read;
use horned_owl::model::RcStr;
use horned_owl::ontology::set::SetOntology;
use std::io::Cursor;
fn clausify_ofn(src: &str) -> (Vec<DlClause>, ClauseStats) {
let mut reader = Cursor::new(src);
let (onto, _): (SetOntology<RcStr>, _) =
read(&mut reader, ParserConfiguration::default()).expect("parse");
let internal = convert_ontology(&onto).expect("convert");
clausify_with_stats(&internal)
}
const HEADER: &str = "Prefix(:=<http://x/>)\n\
Prefix(owl:=<http://www.w3.org/2002/07/owl#>)\n";
#[test]
fn told_subsumption_is_a_horn_clause() {
let (clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\n\
SubClassOf(:A :B)\n)\n"
));
assert!(stats.horn >= 1);
assert!(
clauses
.iter()
.any(|c| c.body.len() == 1 && c.head.len() == 1)
);
}
#[test]
fn covering_axiom_yields_a_disjunctive_clause() {
let (_clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\nDeclaration(Class(:C))\n\
SubClassOf(:A ObjectUnionOf(:B :C))\n)\n"
));
assert!(
stats.disjunctive >= 1,
"expected a disjunctive clause, stats={stats:?}"
);
}
#[test]
fn disjointness_yields_a_bottom_headed_clause() {
let (_clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\n\
DisjointClasses(:A :B)\n)\n"
));
assert!(
stats.bottom_headed >= 1,
"expected a ⊥-headed clause, stats={stats:?}"
);
}
#[test]
fn existential_yields_an_exists_head() {
let (_clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\nDeclaration(ObjectProperty(:r))\n\
SubClassOf(:A ObjectSomeValuesFrom(:r :B))\n)\n"
));
assert!(
stats.with_exists_head >= 1,
"expected an ∃-head clause, stats={stats:?}"
);
}
#[test]
fn universal_moves_role_into_body() {
let (clauses, _stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\nDeclaration(ObjectProperty(:r))\n\
SubClassOf(:A ObjectAllValuesFrom(:r :B))\n)\n"
));
assert!(clauses.iter().any(|c| {
c.body.iter().any(|a| matches!(a, Atom::Role(..)))
&& c.head
.iter()
.any(|a| matches!(a, Atom::Class(_, v) if *v != X))
}));
}
#[test]
fn antecedent_conjunction_with_or_distributes_to_horn() {
let (clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\n\
Declaration(Class(:C))\nDeclaration(Class(:D))\n\
SubClassOf(ObjectIntersectionOf(:A ObjectUnionOf(:B :C)) :D)\n)\n"
));
assert_eq!(stats.deferred, 0, "should no longer defer; stats={stats:?}");
let body_classes = |c: &DlClause| -> Vec<u32> {
let mut v: Vec<u32> = c
.body
.iter()
.filter_map(|a| match a {
Atom::Class(id, _) => Some(id.index()),
_ => None,
})
.collect();
v.sort_unstable();
v
};
let d_clauses: Vec<&DlClause> = clauses
.iter()
.filter(|c| c.head.len() == 1 && c.body.len() == 2)
.collect();
let bodies: Vec<Vec<u32>> = d_clauses.iter().map(|c| body_classes(c)).collect();
assert!(
bodies.contains(&vec![0, 1]),
"expected A⊓B body; got {bodies:?}"
);
assert!(
bodies.contains(&vec![0, 2]),
"expected A⊓C body; got {bodies:?}"
);
assert!(
clauses.iter().all(DlClause::is_horn),
"distributed clauses must be Horn"
);
}
#[test]
fn antecedent_cross_product_over_cap_defers() {
use std::fmt::Write;
let mut decls = String::new();
let mut ors = String::new();
for i in 0..7 {
let (l, r) = (format!("L{i}"), format!("R{i}"));
let _ = write!(
decls,
"Declaration(Class(:{l}))\nDeclaration(Class(:{r}))\n"
);
let _ = write!(ors, "ObjectUnionOf(:{l} :{r}) ");
}
let src = format!(
"{HEADER}Ontology(\nDeclaration(Class(:D))\n{decls}\
SubClassOf(ObjectIntersectionOf({ors}) :D)\n)\n"
);
let (_clauses, stats) = clausify_ofn(&src);
assert!(
stats.deferred >= 1,
"over-cap antecedent must defer; stats={stats:?}"
);
}
#[test]
fn head_cardinality_and_self_are_clausified() {
let (clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:B))\nDeclaration(Class(:C))\n\
Declaration(ObjectProperty(:r))\n\
SubClassOf(:A ObjectMinCardinality(2 :r :B))\n\
SubClassOf(:A ObjectMaxCardinality(1 :r :B))\n\
SubClassOf(:C ObjectHasSelf(:r))\n)\n"
));
assert_eq!(stats.deferred, 0, "no deferral expected; stats={stats:?}");
assert!(
clauses.iter().any(|c| c
.head
.iter()
.any(|a| matches!(a, Atom::AtLeast(_, Some(_), 2, _)))),
"expected an AtLeast(_,Some,2,_) head"
);
assert!(
clauses.iter().any(|c| c
.head
.iter()
.any(|a| matches!(a, Atom::AtMost(_, Some(_), 1, _)))),
"expected an AtMost(_,Some,1,_) head"
);
assert!(
clauses.iter().any(|c| c
.head
.iter()
.any(|a| matches!(a, Atom::Role(_, v, w) if v == w))),
"expected a self-loop Role(x,x) head"
);
}
#[test]
fn hard_antecedent_is_absorbed_with_trigger() {
let (clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(Class(:C))\nDeclaration(Class(:D))\n\
Declaration(ObjectProperty(:r))\n\
SubClassOf(ObjectIntersectionOf(:A ObjectAllValuesFrom(:r :C)) :D)\n)\n"
));
assert_eq!(stats.deferred, 0, "absorbed, not deferred; stats={stats:?}");
assert!(
clauses.iter().any(|c| {
!c.body.is_empty()
&& c.body
.iter()
.any(|a| matches!(a, Atom::Class(_, v) if *v == X))
&& c.head.len() >= 2
}),
"expected a triggered disjunctive clause; clauses={clauses:?}"
);
}
#[test]
fn min_zero_cardinality_is_trivial() {
let (clauses, stats) = clausify_ofn(&format!(
"{HEADER}Ontology(\n\
Declaration(Class(:A))\nDeclaration(ObjectProperty(:r))\n\
SubClassOf(:A ObjectMinCardinality(0 :r))\n)\n"
));
assert_eq!(stats.deferred, 0);
assert!(
!clauses
.iter()
.any(|c| c.head.iter().any(|a| matches!(a, Atom::AtLeast(..)))),
"≥0 must emit no AtLeast"
);
}
}