Description logic ALC¶
The unicode_fol_kit.dl subpackage (new in 0.9.0) implements ALC, the smallest propositionally closed description logic and the notation underlying OWL. It provides concept constructors, negation-normal-form rewriting, and a tableau reasoner that decides satisfiability, subsumption, equivalence, and ABox consistency over general TBoxes. Import it as import unicode_fol_kit.dl as dl.
Concept constructors¶
A concept describes a set of individuals; a role (a plain string) describes a binary relation between them. The constructors are dataclasses in the dl namespace — distinct from the FOL AST’s connectives.
Constructor |
Glyph |
Meaning |
|---|---|---|
|
⊤ |
every individual |
|
⊥ |
no individual |
|
|
a primitive concept name |
|
¬C |
complement |
|
C ⊓ D |
intersection |
|
C ⊔ D |
union |
|
∃r.C |
has an |
|
∀r.C |
all |
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
parent = dl.And(Person, dl.Exists("hasChild", Person))
print(parent.to_unicode()) # → Person ⊓ ∃hasChild.Person
Concepts are frozen dataclasses, so they nest freely and compose like any other
value. The two roles below (hasChild, hasPet) are just strings, and ⊤/⊥ act as
the trivial top and bottom concepts:
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
Dog = dl.Atomic("Dog")
# someone whose children are all people and who owns at least one dog
c = dl.And(dl.ForAll("hasChild", Person), dl.Exists("hasPet", Dog))
print(c.to_unicode()) # → ∀hasChild.Person ⊓ ∃hasPet.Dog
# "has a pet at all" — ∃hasPet.⊤ — versus "owns nothing" — ∀hasPet.⊥
print(dl.Exists("hasPet", dl.Top()).to_unicode()) # → ∃hasPet.⊤
print(dl.ForAll("hasPet", dl.Bottom()).to_unicode()) # → ∀hasPet.⊥
# being frozen dataclasses, structurally equal concepts compare equal
print(dl.Atomic("Person") == dl.Atomic("Person")) # → True
print(dl.Exists("r", dl.Top()) == dl.Exists("r", dl.Bottom())) # → False
More concept construction examples¶
Build richer descriptions by nesting constructors. Quantifiers can express cardinality constraints (at least one, all):
import unicode_fol_kit.dl as dl
A, B, C = dl.Atomic("A"), dl.Atomic("B"), dl.Atomic("C")
# a concept with three nested quantifiers: ∃r.∀s.∃t.A
deep = dl.Exists("r", dl.ForAll("s", dl.Exists("t", A)))
print(deep.to_unicode()) # → ∃r.∀s.∃t.A
# a three-way conjunction: A ⊓ B ⊓ C
triple_and = dl.And(dl.And(A, B), C)
print(triple_and.to_unicode()) # → A ⊓ B ⊓ C
# mixed negation: (A ⊓ ¬B) ⊔ C
mixed = dl.Or(dl.And(A, dl.Not(B)), C)
print(mixed.to_unicode()) # → A ⊓ ¬B ⊔ C
Multiple roles can be combined. An existential restriction ties requirements to exactly one role:
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
Happy = dl.Atomic("Happy")
# someone with at least one parent and at least one sibling
has_parents_and_siblings = dl.And(
dl.Exists("hasParent", Person),
dl.Exists("hasSibling", Person)
)
print(has_parents_and_siblings.to_unicode()) # → ∃hasParent.Person ⊓ ∃hasSibling.Person
# someone whose parents and siblings are all happy
all_family_happy = dl.And(
dl.ForAll("hasParent", Happy),
dl.ForAll("hasSibling", Happy)
)
print(all_family_happy.to_unicode()) # → ∀hasParent.Happy ⊓ ∀hasSibling.Happy
Rendering¶
Concept.to_unicode() renders with the standard DL glyphs and precedence-aware parenthesisation (binding order: atoms/⊤/⊥ tightest, then ¬ / ∃ / ∀, then ⊓, then ⊔). str(C) is an alias.
import unicode_fol_kit.dl as dl
c = dl.Or(dl.Atomic("A"), dl.And(dl.Atomic("B"), dl.Not(dl.Atomic("C"))))
print(c.to_unicode()) # → A ⊔ B ⊓ ¬C
print(dl.ForAll("r", dl.Or(dl.Atomic("A"), dl.Atomic("B"))).to_unicode()) # → ∀r.(A ⊔ B)
print(dl.Top().to_unicode(), dl.Bottom().to_unicode()) # → ⊤ ⊥
Rendering complex expressions¶
Precedence rules are consistent across deeply nested expressions. Understand the precedence order to predict parenthesisation:
import unicode_fol_kit.dl as dl
A, B, C, D = (dl.Atomic("A"), dl.Atomic("B"),
dl.Atomic("C"), dl.Atomic("D"))
# ⊔ has lowest precedence (loosest binding), so ⊔ inside ⊓ does not need parens
very_loose = dl.And(A, dl.Or(B, dl.And(C, D)))
print(very_loose.to_unicode()) # → A ⊓ B ⊔ C ⊓ D
# ⊓ is tighter than ⊔, so ⊓ inside ⊔ needs parens
tighter = dl.Or(dl.And(A, B), dl.And(C, D))
print(tighter.to_unicode()) # → A ⊓ B ⊔ C ⊓ D
# complex quantifier nesting with ⊔ and ⊓
complex_q = dl.ForAll("r", dl.Or(A, dl.Exists("s", B)))
print(complex_q.to_unicode()) # → ∀r.(A ⊔ ∃s.B)
# ¬ over quantifier needs parentheses
neg_quant = dl.Not(dl.Exists("r", A))
print(neg_quant.to_unicode()) # → ¬∃r.A
Parentheses appear only where precedence demands them. A ⊔ under a ⊓, a ⊓ under a ¬, or any binary concept under a quantifier or ¬ gets wrapped; tighter structure does not:
import unicode_fol_kit.dl as dl
A, B, C = dl.Atomic("A"), dl.Atomic("B"), dl.Atomic("C")
# ⊔ is loosest, so a ⊓ inside it needs no parens, but a ⊔ inside a ⊓ does
print(dl.And(dl.Or(A, B), C).to_unicode()) # → (A ⊔ B) ⊓ C
print(dl.Or(dl.And(A, B), C).to_unicode()) # → A ⊓ B ⊔ C
# ¬ binds as tightly as a quantifier, so it parenthesises any binary operand
print(dl.Not(dl.And(A, B)).to_unicode()) # → ¬(A ⊓ B)
print(dl.Not(dl.Or(A, B)).to_unicode()) # → ¬(A ⊔ B)
print(dl.Not(A).to_unicode()) # → ¬A
# nested quantifiers chain without parentheses (they bind equally tight)
print(dl.ForAll("r", dl.Exists("s", A)).to_unicode()) # → ∀r.∃s.A
print(dl.Exists("r", dl.Not(A)).to_unicode()) # → ∃r.¬A
# str(C) is exactly to_unicode()
print(str(dl.And(A, B))) # → A ⊓ B
Negation normal form¶
dl.nnf(C) pushes ¬ inward so that negation occurs only on atomic concepts, using the De Morgan and modal dualities (¬⊤=⊥, ¬¬C=C, ¬(C⊓D)=¬C⊔¬D, ¬∃r.C=∀r.¬C, ¬∀r.C=∃r.¬C). This is the shape the tableau consumes.
import unicode_fol_kit.dl as dl
neg = dl.Not(dl.Exists("r", dl.And(dl.Atomic("A"), dl.Atomic("B"))))
print(dl.nnf(neg).to_unicode()) # → ∀r.(¬A ⊔ ¬B)
print(dl.nnf(dl.Not(dl.ForAll("r", dl.Atomic("A")))).to_unicode()) # → ∃r.¬A
Each rewrite rule, in isolation:
import unicode_fol_kit.dl as dl
A, B = dl.Atomic("A"), dl.Atomic("B")
print(dl.nnf(dl.Not(dl.Top())).to_unicode()) # → ⊥ (¬⊤ = ⊥)
print(dl.nnf(dl.Not(dl.Bottom())).to_unicode()) # → ⊤ (¬⊥ = ⊤)
print(dl.nnf(dl.Not(dl.Not(A))).to_unicode()) # → A (¬¬C = C)
print(dl.nnf(dl.Not(dl.And(A, B))).to_unicode()) # → ¬A ⊔ ¬B
print(dl.nnf(dl.Not(dl.Or(A, B))).to_unicode()) # → ¬A ⊓ ¬B
print(dl.nnf(dl.Not(dl.Exists("r", A))).to_unicode()) # → ∀r.¬A
print(dl.nnf(dl.Not(dl.ForAll("r", A))).to_unicode()) # → ∃r.¬A
Negation is driven all the way down to the atoms in a single pass, even through deeply nested mixtures of quantifiers and connectives:
import unicode_fol_kit.dl as dl
A, B = dl.Atomic("A"), dl.Atomic("B")
deep = dl.Not(dl.And(dl.Exists("r", A), dl.ForAll("s", B)))
print(dl.nnf(deep).to_unicode()) # → ∀r.¬A ⊔ ∃s.¬B
# already-positive concepts are returned structurally unchanged
already = dl.Or(A, dl.Exists("r", dl.Not(B)))
print(dl.nnf(already) == already) # → True
Additional NNF examples: complex structures¶
Push negation through multiple layers of quantifiers and connectives:
import unicode_fol_kit.dl as dl
A, B, C = dl.Atomic("A"), dl.Atomic("B"), dl.Atomic("C")
# ¬(A ⊓ (¬B ⊔ C)) → ¬A ⊔ ¬(¬B ⊔ C) → ¬A ⊔ (¬¬B ⊓ ¬C) → ¬A ⊔ B ⊓ ¬C
complex1 = dl.Not(dl.And(A, dl.Or(dl.Not(B), C)))
print(dl.nnf(complex1).to_unicode()) # → ¬A ⊔ B ⊓ ¬C
# Negated universal over a disjunction
# ¬∀r.(A ⊔ B) → ∃r.¬(A ⊔ B) → ∃r.(¬A ⊓ ¬B)
nforall = dl.Not(dl.ForAll("r", dl.Or(A, B)))
print(dl.nnf(nforall).to_unicode()) # → ∃r.(¬A ⊓ ¬B)
# Multiple layers of quantifier negation
# ¬∃r.∀s.A → ∀r.¬∀s.A → ∀r.∃s.¬A
multi_q = dl.Not(dl.Exists("r", dl.ForAll("s", A)))
print(dl.nnf(multi_q).to_unicode()) # → ∀r.∃s.¬A
NNF is idempotent: applying it twice gives the same result as once:
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
complex_c = dl.Not(dl.And(dl.Exists("r", A), dl.ForAll("s", A)))
nnf1 = dl.nnf(complex_c)
nnf2 = dl.nnf(nnf1)
print(nnf1 == nnf2) # → True
print(nnf1.to_unicode()) # → ∀r.¬A ⊔ ∃s.¬A
Reasoning API¶
The reasoner is a tableau with TBox internalisation and subset blocking. Every reasoning function takes an optional tbox (default: the empty TBox).
dl.concept_satisfiable(C, tbox=None)— does some model place an individual inCwhile obeying every TBox axiom?dl.concept_unsatisfiable(C, tbox=None)— its negation.dl.subsumes(sub, sup, tbox=None)— does the TBox entailsub ⊑ sup? Decided by the standard reduction:sub ⊓ ¬supis unsatisfiable.dl.equivalent(C, D, tbox=None)— mutual subsumption,C ≡ D.dl.abox_consistent(abox, tbox=None)— does the knowledge base have a model?
ALC is exactly the multi-modal logic K — a role r is a modality, ∃r its ◇ and ∀r its □ — which is why these tasks are decidable.
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
print(dl.concept_satisfiable(dl.And(A, dl.Not(A)))) # → False
print(dl.concept_satisfiable(dl.Atomic("Person"))) # → True
# ⊓-elimination is a subsumption; the converse is not
print(dl.subsumes(dl.And(A, dl.Atomic("B")), A)) # → True
print(dl.subsumes(A, dl.And(A, dl.Atomic("B")))) # → False
# the modal duality ¬∃r.A ≡ ∀r.¬A
lhs = dl.Not(dl.Exists("r", A))
rhs = dl.ForAll("r", dl.Not(A))
print(dl.equivalent(lhs, rhs)) # → True
Satisfiability, including ⊤/⊥ and quantifier corner cases¶
concept_unsatisfiable is simply the negation of concept_satisfiable. ⊤ is always
satisfiable, ⊥ never; ∃r.⊥ cannot be witnessed (the successor would be in ⊥), while
∀r.⊥ is satisfiable by an individual with no r-successors:
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
print(dl.concept_satisfiable(dl.Top())) # → True
print(dl.concept_satisfiable(dl.Bottom())) # → False
print(dl.concept_unsatisfiable(dl.Bottom())) # → True
print(dl.concept_satisfiable(dl.Exists("r", dl.Bottom()))) # → False
print(dl.concept_satisfiable(dl.ForAll("r", dl.Bottom()))) # → True
# ∃r.A ⊓ ∀r.¬A forces a single r-successor to be both A and ¬A → clash
clash = dl.And(dl.Exists("r", A), dl.ForAll("r", dl.Not(A)))
print(dl.concept_satisfiable(clash)) # → False
Satisfiability with complex role patterns¶
Role interactions can indirectly force unsatisfiability. When existential and universal quantifiers conflict over a shared role:
import unicode_fol_kit.dl as dl
A, B = dl.Atomic("A"), dl.Atomic("B")
# ∃r.A ⊓ ∀r.B is satisfiable only if A and B can overlap
overlap_sat = dl.And(dl.Exists("r", A), dl.ForAll("r", B))
print(dl.concept_satisfiable(overlap_sat)) # → True (their r-successor can be in A ⊓ B)
# but forcing A and B to be disjoint makes it unsatisfiable
disjoint = dl.And(
dl.Exists("r", A),
dl.ForAll("r", dl.Not(B))
)
print(dl.concept_satisfiable(disjoint)) # → False (r-successor cannot be A and ¬B)
# multiple roles do not conflict: ∃r.A ⊓ ∀s.¬A is always satisfiable
no_conflict = dl.And(
dl.Exists("r", A),
dl.ForAll("s", dl.Not(A))
)
print(dl.concept_satisfiable(no_conflict)) # → True (different roles, different successors)
Subsumption with ⊤, ⊥, and under quantifiers¶
⊥ is below everything and ⊤ above everything; subsumption is also monotone inside ∃ and ∀ (replacing the filler by a superconcept preserves the inclusion):
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
print(dl.subsumes(dl.Bottom(), A)) # → True (⊥ ⊑ A)
print(dl.subsumes(A, dl.Top())) # → True (A ⊑ ⊤)
print(dl.subsumes(dl.Exists("r", A), dl.Exists("r", dl.Top()))) # → True
print(dl.subsumes(dl.ForAll("r", A), dl.ForAll("r", dl.Top()))) # → True
Subsumption with compound concepts¶
Subsumption behaves classically for conjunctions and complements:
import unicode_fol_kit.dl as dl
A, B, C = dl.Atomic("A"), dl.Atomic("B"), dl.Atomic("C")
# A ⊓ B ⊑ A (left elimination)
print(dl.subsumes(dl.And(A, B), A)) # → True
# A ⊑ A ⊔ B (weakening)
print(dl.subsumes(A, dl.Or(A, B))) # → True
# ¬A ⊑ ¬B iff B ⊑ A (contrapositive)
print(dl.subsumes(dl.Not(B), dl.Not(A))) # → True (since A ⊑ B is False)
print(dl.subsumes(B, A)) # → False
# transitivity: if A ⊑ B and B ⊑ C then A ⊑ C
print(dl.subsumes(A, B)) # → False (without TBox)
# but with a TBox (see below), transitivity holds
Equivalence: De Morgan, distributivity, and quantifier laws¶
equivalent(C, D) is mutual subsumption. It captures the propositional laws as well
as the ALC-specific facts that ∃ distributes over ⊔ and ∀ over ⊓:
import unicode_fol_kit.dl as dl
A, B, C = dl.Atomic("A"), dl.Atomic("B"), dl.Atomic("C")
# distributivity of ⊓ over ⊔
lhs = dl.And(A, dl.Or(B, C))
rhs = dl.Or(dl.And(A, B), dl.And(A, C))
print(dl.equivalent(lhs, rhs)) # → True
# ∃r.(A ⊔ B) ≡ ∃r.A ⊔ ∃r.B
print(dl.equivalent(dl.Exists("r", dl.Or(A, B)),
dl.Or(dl.Exists("r", A), dl.Exists("r", B)))) # → True
# ∀r.(A ⊓ B) ≡ ∀r.A ⊓ ∀r.B
print(dl.equivalent(dl.ForAll("r", dl.And(A, B)),
dl.And(dl.ForAll("r", A), dl.ForAll("r", B)))) # → True
Equivalence with negation and interaction¶
De Morgan’s laws and modal dualities hold:
import unicode_fol_kit.dl as dl
A, B = dl.Atomic("A"), dl.Atomic("B")
# De Morgan: ¬(A ⊓ B) ≡ ¬A ⊔ ¬B
print(dl.equivalent(
dl.Not(dl.And(A, B)),
dl.Or(dl.Not(A), dl.Not(B))
)) # → True
# Modal duality: ¬∀r.A ≡ ∃r.¬A
print(dl.equivalent(
dl.Not(dl.ForAll("r", A)),
dl.Exists("r", dl.Not(A))
)) # → True
# Double negation: ¬¬A ≡ A
print(dl.equivalent(dl.Not(dl.Not(A)), A)) # → True
# Duality chain: ¬∃r.∀s.A ≡ ∀r.∃s.¬A
print(dl.equivalent(
dl.Not(dl.Exists("r", dl.ForAll("s", A))),
dl.ForAll("r", dl.Exists("s", dl.Not(A)))
)) # → True
General TBoxes¶
dl.TBox() holds general concept inclusions (GCIs). add(sub, sup) adds sub ⊑ sup; add_equivalence(C, D) adds C ≡ D (the two inclusions C ⊑ D and D ⊑ C). Both return the TBox, so calls chain. Each GCI is internalised as the concept nnf(¬sub ⊔ sup), forced on every individual.
import unicode_fol_kit.dl as dl
t = dl.TBox()
t.add(dl.Atomic("Dog"), dl.Atomic("Mammal"))
t.add(dl.Atomic("Mammal"), dl.Atomic("Animal"))
print(dl.subsumes(dl.Atomic("Dog"), dl.Atomic("Animal"), t)) # → True (transitivity)
print(dl.subsumes(dl.Atomic("Animal"), dl.Atomic("Dog"), t)) # → False
Because both methods return the TBox, axioms can be added in a single chain. You can
inspect what each GCI becomes internally with internalized() — one nnf(¬sub ⊔ sup)
concept per inclusion, the form forced on every individual:
import unicode_fol_kit.dl as dl
t = (dl.TBox()
.add(dl.Atomic("Dog"), dl.Atomic("Mammal"))
.add(dl.Atomic("Mammal"), dl.Atomic("Animal")))
print(len(t.inclusions)) # → 2
print([c.to_unicode() for c in t.internalized()])
# → ['¬Dog ⊔ Mammal', '¬Mammal ⊔ Animal']
A GCI of the form C ⊑ ⊥ makes C itself unsatisfiable — the classic way to spot a
modelling error where a named concept can have no instances:
import unicode_fol_kit.dl as dl
t = dl.TBox().add(dl.Atomic("Squircle"), dl.Bottom())
print(dl.concept_satisfiable(dl.Atomic("Squircle"), t)) # → False
Domain/range-style axioms work too. Saying “anything with a hasChild edge is a
Parent” (∃hasChild.⊤ ⊑ Parent) makes any existential over hasChild subsumed by
Parent:
import unicode_fol_kit.dl as dl
t = dl.TBox().add(dl.Exists("hasChild", dl.Top()), dl.Atomic("Parent"))
print(dl.subsumes(dl.Exists("hasChild", dl.Atomic("Person")),
dl.Atomic("Parent"), t)) # → True
add_equivalence lets you give a concept a definition and then reason with it:
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
Parent = dl.Atomic("Parent")
t = dl.TBox()
t.add_equivalence(Parent, dl.And(Person, dl.Exists("hasChild", Person)))
print(dl.subsumes(Parent, Person, t)) # → True
definition = dl.And(Person, dl.Exists("hasChild", Person))
print(dl.equivalent(Parent, definition, t)) # → True
A disjointness axiom (Cat ⊑ ¬Dog) makes the conjunction unsatisfiable:
import unicode_fol_kit.dl as dl
t = dl.TBox()
t.add(dl.Atomic("Cat"), dl.Not(dl.Atomic("Dog")))
print(dl.concept_satisfiable(dl.And(dl.Atomic("Cat"), dl.Atomic("Dog")), t)) # → False
TBox examples: complex hierarchies¶
Build taxonomies with multiple levels and cross-cutting relationships:
import unicode_fol_kit.dl as dl
Animal = dl.Atomic("Animal")
Mammal = dl.Atomic("Mammal")
Bird = dl.Atomic("Bird")
Dog = dl.Atomic("Dog")
Cat = dl.Atomic("Cat")
Eagle = dl.Atomic("Eagle")
Penguin = dl.Atomic("Penguin")
t = (dl.TBox()
.add(Mammal, Animal)
.add(Bird, Animal)
.add(Dog, Mammal)
.add(Cat, Mammal)
.add(Eagle, Bird)
.add(Penguin, Bird))
# Classification: Dog ⊑ Mammal ⊑ Animal
print(dl.subsumes(Dog, Animal, t)) # → True
print(dl.subsumes(Dog, Mammal, t)) # → True
# Dog and Bird are incomparable (no subsumption in either direction)
print(dl.subsumes(Dog, Bird, t)) # → False
print(dl.subsumes(Bird, Dog, t)) # → False
# All birds and mammals are animals
print(dl.subsumes(Mammal, Animal, t)) # → True
print(dl.subsumes(Bird, Animal, t)) # → True
Add constraints to concepts: what must be true of all members of a concept:
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
Adult = dl.Atomic("Adult")
HasSSN = dl.Atomic("HasSSN")
Employed = dl.Atomic("Employed")
t = (dl.TBox()
.add(Adult, Person)
.add(Adult, dl.And(HasSSN, dl.Or(Employed, dl.Atomic("Retired")))))
# Being adult entails being a person
print(dl.subsumes(Adult, Person, t)) # → True
# Being adult entails having an SSN (via the conjunction)
print(dl.subsumes(Adult, HasSSN, t)) # → True
# But being a person does not entail being an adult
print(dl.subsumes(Person, Adult, t)) # → False
TBox examples: role constraints¶
Express constraints on roles — who can have what relationship:
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
Parent = dl.Atomic("Parent")
# "Anything with a child is a Parent" — domain/range axiom
t = dl.TBox().add(
dl.Exists("hasChild", dl.Top()), # "has at least one child"
Parent
)
# So anyone with any child (of any type) is classified as a parent
anyone_with_child = dl.Exists("hasChild", dl.Atomic("Being"))
print(dl.subsumes(anyone_with_child, Parent, t)) # → True
# Role ranges: "All children must be persons"
# ∀hasChild.Person means "all children are persons"
t2 = dl.TBox().add(dl.Top(), dl.ForAll("hasChild", Person))
# Under this axiom, something with a child that is not a person is unsatisfiable
non_person = dl.Atomic("NonPerson")
bad_concept = dl.And(
dl.Exists("hasChild", non_person),
dl.ForAll("hasChild", Person)
)
print(dl.concept_satisfiable(bad_concept, t2)) # → False
ABoxes¶
dl.ABox() collects assertions. assert_concept(individual, C) adds individual : C; assert_role(a, b, role) adds (a, b) : role. Both chain. dl.abox_consistent(abox, tbox) checks the whole knowledge base.
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
t = dl.TBox().add_equivalence(
dl.Atomic("Parent"), dl.And(Person, dl.Exists("hasChild", Person)))
abox = dl.ABox()
abox.assert_concept("alice", Person)
abox.assert_role("alice", "bob", "hasChild")
abox.assert_concept("bob", Person)
print(dl.abox_consistent(abox, t)) # → True
An empty ABox is trivially consistent, and a single individual asserted to be both
P and ¬P is the simplest inconsistency:
import unicode_fol_kit.dl as dl
print(dl.abox_consistent(dl.ABox())) # → True
clash = (dl.ABox()
.assert_concept("a", dl.Atomic("P"))
.assert_concept("a", dl.Not(dl.Atomic("P"))))
print(dl.abox_consistent(clash)) # → False
The TBox constrains every named individual too. With Cat ⊑ ¬Dog, asserting that
nemo is both a Cat and a Dog is inconsistent:
import unicode_fol_kit.dl as dl
t = dl.TBox().add(dl.Atomic("Cat"), dl.Not(dl.Atomic("Dog")))
abox = dl.ABox().assert_concept("nemo", dl.And(dl.Atomic("Cat"), dl.Atomic("Dog")))
print(dl.abox_consistent(abox, t)) # → False
Role assertions propagate value restrictions: alice has only happy children, but bob is asserted not happy, so the ∀-rule produces a clash.
import unicode_fol_kit.dl as dl
abox = dl.ABox()
abox.assert_concept("alice", dl.ForAll("hasChild", dl.Atomic("Happy")))
abox.assert_role("alice", "bob", "hasChild")
abox.assert_concept("bob", dl.Not(dl.Atomic("Happy")))
print(dl.abox_consistent(abox)) # → False
A ∀r and an ∃r on the same individual interact even without an explicit role edge:
asserting a : ∀r.A together with a : ∃r.¬A forces the generated witness to be both
A and ¬A:
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
abox = (dl.ABox()
.assert_concept("a", dl.ForAll("r", A))
.assert_concept("a", dl.Exists("r", dl.Not(A))))
print(dl.abox_consistent(abox)) # → False
Cyclic TBoxes terminate¶
A GCI such as A ⊑ ∃r.A would naively generate an infinite chain of r-successors. The tableau uses subset blocking: a generated individual whose label is contained in that of an earlier individual is not expanded (its successors are reused). This is sound and complete for ALC, so cyclic axioms terminate.
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
t = dl.TBox().add(A, dl.Exists("r", A)) # A ⊑ ∃r.A
print(dl.concept_satisfiable(A, t)) # → True (terminates via blocking)
Blocking only suppresses redundant expansion; a genuine contradiction along the
generated chain is still found. If the same r-successor required by the cycle is also
forced to be empty (∀r.⊥), the concept is unsatisfiable:
import unicode_fol_kit.dl as dl
A = dl.Atomic("A")
t = dl.TBox().add(A, dl.And(dl.Exists("r", A), dl.ForAll("r", dl.Bottom())))
print(dl.concept_satisfiable(A, t)) # → False (∃r.A and ∀r.⊥ clash)
And the cyclic concept can still impose constraints that interact with extra
assumptions. Here A ⊑ ∃r.A ⊓ ¬B, so nothing in A is ever B:
import unicode_fol_kit.dl as dl
A, B = dl.Atomic("A"), dl.Atomic("B")
t = dl.TBox().add(A, dl.And(dl.Exists("r", A), dl.Not(B)))
print(dl.concept_satisfiable(A, t)) # → True
print(dl.concept_satisfiable(dl.And(A, B), t)) # → False
Cyclic TBox examples: linked structures¶
Express recursive structures that reference themselves:
import unicode_fol_kit.dl as dl
Node = dl.Atomic("Node")
Terminal = dl.Atomic("Terminal")
t = dl.TBox().add(
Node,
dl.Or(Terminal, dl.Exists("successor", Node))
)
print(dl.concept_satisfiable(Node, t)) # → True
nonterminal = dl.And(Node, dl.Not(Terminal))
print(dl.concept_satisfiable(nonterminal, t)) # → True
acyclic_one_level = dl.And(
Node,
dl.Exists("successor", Terminal)
)
print(dl.concept_satisfiable(acyclic_one_level, t)) # → True
Combine cyclic axioms with role constraints:
import unicode_fol_kit.dl as dl
Tree = dl.Atomic("Tree")
Leaf = dl.Atomic("Leaf")
t = (dl.TBox()
.add(Tree, dl.Or(Leaf, dl.Exists("child", Tree)))
.add(Tree, dl.ForAll("child", Tree)))
print(dl.concept_satisfiable(Tree, t)) # → True
non_leaf = dl.And(Tree, dl.Not(Leaf))
print(dl.concept_satisfiable(non_leaf, t)) # → True
End-to-end: a small family ontology¶
Putting it together — define a vocabulary as a TBox, classify the concepts by subsumption, then check a concrete ABox of individuals against it.
import unicode_fol_kit.dl as dl
Person, Male, Female = dl.Atomic("Person"), dl.Atomic("Male"), dl.Atomic("Female")
Parent, Mother, Father = dl.Atomic("Parent"), dl.Atomic("Mother"), dl.Atomic("Father")
t = (dl.TBox()
.add_equivalence(Parent, dl.And(Person, dl.Exists("hasChild", Person)))
.add_equivalence(Mother, dl.And(Parent, Female))
.add_equivalence(Father, dl.And(Parent, Male))
.add(Male, dl.Not(Female))) # sexes are disjoint
# classification: Mother ⊑ Parent ⊑ Person, and Mother/Father are disjoint
print(dl.subsumes(Mother, Parent, t)) # → True
print(dl.subsumes(Mother, Person, t)) # → True
print(dl.concept_satisfiable(dl.And(Mother, Father), t)) # → False
print(dl.subsumes(Parent, Mother, t)) # → False (not every parent is a mother)
# a consistent knowledge base: Alice is a mother with a child Bob
kb = (dl.ABox()
.assert_concept("alice", Mother)
.assert_role("alice", "bob", "hasChild")
.assert_concept("bob", Person))
print(dl.abox_consistent(kb, t)) # → True
# add a contradiction: a Mother is Female, but Males are not Female
bad = (dl.ABox()
.assert_concept("alice", Mother)
.assert_concept("alice", Male))
print(dl.abox_consistent(bad, t)) # → False
End-to-end example: university domain¶
A realistic scenario with multiple concept levels, roles, and consistency checking:
import unicode_fol_kit.dl as dl
Person = dl.Atomic("Person")
Student = dl.Atomic("Student")
Faculty = dl.Atomic("Faculty")
GradStudent = dl.Atomic("GradStudent")
Professor = dl.Atomic("Professor")
Course = dl.Atomic("Course")
t = (dl.TBox()
.add(Student, Person)
.add(GradStudent, Student)
.add(Faculty, Person)
.add(Professor, Faculty)
.add(Professor, dl.Exists("advises", GradStudent))
.add(GradStudent, dl.Exists("hasAdvisor", Professor))
.add(dl.Exists("enrolledIn", Course), Student)
.add(Student, dl.Not(Faculty)))
kb = (dl.ABox()
.assert_concept("alice", Professor)
.assert_concept("bob", GradStudent)
.assert_role("bob", "alice", "hasAdvisor")
.assert_role("alice", "bob", "advises")
.assert_role("bob", "cs101", "enrolledIn")
.assert_concept("cs101", Course))
print(dl.abox_consistent(kb, t)) # → True
bad_kb = (dl.ABox()
.assert_concept("bob", GradStudent)
.assert_concept("bob", Faculty))
print(dl.abox_consistent(bad_kb, t)) # → False
End-to-end example: product classification¶
Organize products with constraints on features and relationships:
import unicode_fol_kit.dl as dl
Product = dl.Atomic("Product")
PhysicalProduct = dl.Atomic("PhysicalProduct")
DigitalProduct = dl.Atomic("DigitalProduct")
Book = dl.Atomic("Book")
EBook = dl.Atomic("EBook")
PrintedBook = dl.Atomic("PrintedBook")
Tangible = dl.Atomic("Tangible")
Downloadable = dl.Atomic("Downloadable")
t = (dl.TBox()
.add(PhysicalProduct, Product)
.add(DigitalProduct, Product)
.add(Book, Product)
.add(EBook, dl.And(Book, DigitalProduct))
.add(PrintedBook, dl.And(Book, PhysicalProduct))
.add(PhysicalProduct, Tangible)
.add(DigitalProduct, Downloadable)
.add(PhysicalProduct, dl.Not(DigitalProduct)))
print(dl.subsumes(EBook, Book, t)) # → True
print(dl.subsumes(EBook, DigitalProduct, t)) # → True
print(dl.subsumes(EBook, Downloadable, t)) # → True
print(dl.subsumes(PrintedBook, Tangible, t)) # → True
print(dl.concept_satisfiable(EBook, t)) # → True
print(dl.concept_satisfiable(PrintedBook, t)) # → True
bad = dl.And(EBook, Tangible)
print(dl.concept_satisfiable(bad, t)) # → False
inventory = (dl.ABox()
.assert_concept("item-1", PrintedBook)
.assert_concept("item-2", EBook))
print(dl.abox_consistent(inventory, t)) # → True