unicode-logic-kit 0.31.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""The ``dl`` subpackage — the description logic **ALC**.
|
|
2
|
+
|
|
3
|
+
Concept constructors (``import unicode_logic_kit.dl as dl``)::
|
|
4
|
+
|
|
5
|
+
dl.Top(), dl.Bottom(), dl.Atomic("Person"), dl.Not(C),
|
|
6
|
+
dl.And(C, D), dl.Or(C, D), dl.Exists("hasChild", C), dl.ForAll("hasChild", C),
|
|
7
|
+
dl.AtLeast(2, "hasChild", C), dl.AtMost(1, "hasChild", C) # qualified number
|
|
8
|
+
# restrictions (ALCQ)
|
|
9
|
+
|
|
10
|
+
Reasoning over a (general) TBox / ABox::
|
|
11
|
+
|
|
12
|
+
dl.concept_satisfiable(C, tbox) # is C satisfiable w.r.t. the TBox?
|
|
13
|
+
dl.subsumes(C, D, tbox) # does the TBox entail C ⊑ D?
|
|
14
|
+
dl.equivalent(C, D, tbox) # C ≡ D?
|
|
15
|
+
dl.abox_consistent(abox, tbox) # is the knowledge base consistent?
|
|
16
|
+
|
|
17
|
+
Instance/realization queries over an ABox, all pure reductions to ``abox_consistent``
|
|
18
|
+
(see :mod:`unicode_logic_kit.dl.tableau`)::
|
|
19
|
+
|
|
20
|
+
dl.instance_check(abox, "alice", C, tbox) # does the KB entail alice : C?
|
|
21
|
+
dl.instance_retrieval(abox, C, tbox) # every individual entailed to be C
|
|
22
|
+
dl.realize(abox, "alice", vocabulary, tbox) # alice's most-specific concepts
|
|
23
|
+
dl.realize_all(abox, vocabulary, tbox) # realize() for every individual
|
|
24
|
+
|
|
25
|
+
TBox classification — the whole named-concept subsumption hierarchy as a pure
|
|
26
|
+
reduction to ``subsumes`` (see :mod:`unicode_logic_kit.dl.classification`)::
|
|
27
|
+
|
|
28
|
+
dl.classify(tbox) # -> Classification(equivalents, parents,
|
|
29
|
+
# children, ancestors)
|
|
30
|
+
|
|
31
|
+
ALC is exactly multi-modal K; the reasoner is a tableau with TBox internalisation
|
|
32
|
+
and subset blocking (see :mod:`unicode_logic_kit.dl.tableau`).
|
|
33
|
+
|
|
34
|
+
Parsing the glyph concept syntax back into a :class:`Concept`
|
|
35
|
+
(:mod:`unicode_logic_kit.dl.parser`)::
|
|
36
|
+
|
|
37
|
+
dl.parse_concept("∃hasChild.(Doctor ⊓ ¬Rich)") # -> Concept
|
|
38
|
+
dl.parse_gci("Doctor ⊑ ∃hasChild.⊤") # -> (Concept, Concept)
|
|
39
|
+
|
|
40
|
+
Parsing/rendering the W3C OWL 2 Manchester Syntax (ALC fragment only,
|
|
41
|
+
:mod:`unicode_logic_kit.dl.owl_manchester`)::
|
|
42
|
+
|
|
43
|
+
dl.parse_manchester("hasChild some (Doctor and not Rich)") # -> Concept
|
|
44
|
+
dl.to_manchester(C) # -> str
|
|
45
|
+
dl.parse_manchester_axiom("Doctor SubClassOf hasChild some owl:Thing")
|
|
46
|
+
# -> ("subclass", Concept, Concept)
|
|
47
|
+
dl.parse_manchester_role_axiom("hasChild SubPropertyOf hasDescendant")
|
|
48
|
+
# -> ("subproperty", "hasChild", "hasDescendant")
|
|
49
|
+
dl.parse_manchester_role_axiom("hasDescendant Characteristics: Transitive")
|
|
50
|
+
# -> ("transitive", "hasDescendant")
|
|
51
|
+
dl.parse_manchester_role_axiom("partOf InverseOf hasPart")
|
|
52
|
+
# -> ("inverse", "partOf", "hasPart")
|
|
53
|
+
dl.parse_manchester_role_axiom("hasSink DisjointWith hasSource")
|
|
54
|
+
# -> ("disjoint", "hasSink", "hasSource")
|
|
55
|
+
|
|
56
|
+
Parsing/rendering the W3C OWL 2 Functional-Style Syntax (ALCHQ fragment, a whole
|
|
57
|
+
ontology document rather than a single axiom, :mod:`unicode_logic_kit.dl.owl_functional`)::
|
|
58
|
+
|
|
59
|
+
dl.parse_owl_functional("Ontology(SubClassOf(Doctor Person))") # -> (TBox, ABox)
|
|
60
|
+
dl.to_owl_functional(tbox, abox) # -> str
|
|
61
|
+
dl.parse_owl_functional_class_expression("ObjectSomeValuesFrom(hasChild Doctor)")
|
|
62
|
+
# -> Exists("hasChild", Atomic("Doctor"))
|
|
63
|
+
dl.to_owl_functional_class_expression(C) # -> str
|
|
64
|
+
|
|
65
|
+
The standard translation to FOL, so ALC reuses the kit's FOL provers/exports
|
|
66
|
+
(:mod:`unicode_logic_kit.dl.translate`)::
|
|
67
|
+
|
|
68
|
+
dl.concept_to_fol(C) # -> FOL formula π(C, x), one free var x
|
|
69
|
+
dl.subsumption_to_fol(C, D) # -> ∀x (π(C, x) → π(D, x))
|
|
70
|
+
dl.tbox_to_fol(tbox) # -> conjunction of subsumption_to_fol per GCI
|
|
71
|
+
dl.rbox_to_fol(tbox) # -> conjunction of role-inclusion/transitivity closures
|
|
72
|
+
dl.abox_to_fol(abox) # -> conjunction of the ABox's assertions
|
|
73
|
+
dl.kb_to_fol(tbox, abox) # -> KnowledgeBaseFOL: the KB + the role box as SEPARATE axioms
|
|
74
|
+
dl.check_kb_names(tbox, abox) # -> None: kb_to_fol's check on names, over boxes rendered apart
|
|
75
|
+
dl.concept_to_modal(C) # -> propositional modal-K formula (single role only)
|
|
76
|
+
|
|
77
|
+
A :class:`TBox` holds the concept inclusions AND the role box, but ``tbox_to_fol``
|
|
78
|
+
renders the concept inclusions only — so it RAISES ``dl.RoleBoxOmittedError`` for a TBox
|
|
79
|
+
that has role inclusions or transitive roles (``concept_inclusions_only=True`` is the
|
|
80
|
+
explicit opt-out). Ask any question *relative to* a knowledge base through
|
|
81
|
+
``kb_to_fol``, passing the role box as premises, never folded into the formula::
|
|
82
|
+
|
|
83
|
+
kb = dl.kb_to_fol(tbox, abox)
|
|
84
|
+
api.prove(kb.subsumption_goal(C, D), kb.tbox_premises) # = dl.subsumes(C, D, tbox)
|
|
85
|
+
api.prove(kb.unsatisfiability_goal(C), kb.tbox_premises) # proved <=> C is unsatisfiable
|
|
86
|
+
api.prove(kb.instance_goal("a", C), kb.premises) # = dl.instance_check(...)
|
|
87
|
+
api.prove(fol.Not(kb.formula), kb.axioms) # proved <=> the knowledge base is inconsistent
|
|
88
|
+
|
|
89
|
+
Role hierarchies and transitive roles (RBox), on top of the concept-level TBox
|
|
90
|
+
(:mod:`unicode_logic_kit.dl.tableau`)::
|
|
91
|
+
|
|
92
|
+
t = dl.TBox().add_role_inclusion("hasChild", "hasDescendant").add_transitive_role("hasDescendant")
|
|
93
|
+
dl.subsumes(dl.Exists("hasChild", dl.Exists("hasChild", C)), dl.Exists("hasDescendant", C), t)
|
|
94
|
+
# -> True: ∃hasChild.∃hasChild.C ⊑ ∃hasDescendant.C, entailed by the RBox alone
|
|
95
|
+
|
|
96
|
+
Qualified number restrictions (ALCQ), on SIMPLE roles only (a role that is
|
|
97
|
+
transitive, or has a transitive sub-role, raises ``NonSimpleRoleError`` by name —
|
|
98
|
+
see :mod:`unicode_logic_kit.dl.tableau`'s "Qualified number restrictions" section)::
|
|
99
|
+
|
|
100
|
+
dl.concept_satisfiable(dl.And(dl.AtLeast(2, "r", C), dl.AtMost(1, "r", dl.Top())))
|
|
101
|
+
# -> False: at least 2 pairwise-distinct r-successors but at most 1 r-successor at all
|
|
102
|
+
|
|
103
|
+
``ABox.assert_distinct(a, b)`` records ``a ≠ b`` — this reasoner has no unique name
|
|
104
|
+
assumption (see the same tableau section), so two ABox individuals otherwise remain
|
|
105
|
+
mergeable, which matters as soon as a number restriction is in play.
|
|
106
|
+
|
|
107
|
+
``instance_check``/``classify``/the rest of the reasoning API above respect a TBox's
|
|
108
|
+
RBox automatically, since they all reduce to ``concept_satisfiable``/``abox_consistent``
|
|
109
|
+
— no separate RBox-aware entry point is needed anywhere else.
|
|
110
|
+
|
|
111
|
+
The rest of the OWL 2 object property box
|
|
112
|
+
------------------------------------------
|
|
113
|
+
A :class:`TBox` carries every OWL 2 object-property axiom kind, and
|
|
114
|
+
``dl.rbox_to_fol`` renders every one of them (so ``kb_to_fol`` + ``api.prove``
|
|
115
|
+
and ``dl.external_*`` answer them all). What the IN-HOUSE tableau does with
|
|
116
|
+
each is the second column of ``dl.tableau._AXIOM_KINDS``::
|
|
117
|
+
|
|
118
|
+
t = (dl.TBox()
|
|
119
|
+
.add_disjoint_roles("hasSink", "hasSource") # decided: a clash rule
|
|
120
|
+
.add_asymmetric_role("hasPhysicalInput") # decided: a clash rule
|
|
121
|
+
.add_irreflexive_role("hasPhysicalInput") # decided: a clash rule
|
|
122
|
+
.add_functional_role("hasState")) # decided: ⊤ ⊑ ≤1 P.⊤
|
|
123
|
+
dl.abox_consistent(dl.ABox().assert_role("a", "a", "hasPhysicalInput"), t)
|
|
124
|
+
# -> False: Irr(hasPhysicalInput) is ∀x ¬hasPhysicalInput(x, x)
|
|
125
|
+
|
|
126
|
+
dl.concept_satisfiable(dl.Top(), dl.TBox().add_symmetric_role("r"))
|
|
127
|
+
# -> raises dl.UnsupportedAxiomError, naming SymmetricObjectProperty and
|
|
128
|
+
# saying why (a symmetric role IS the inverse-role inclusion r ⊑ r⁻,
|
|
129
|
+
# and a back-edge breaks subset blocking) and what to use instead
|
|
130
|
+
|
|
131
|
+
Refused by name, each with its own remedy in the message:
|
|
132
|
+
``InverseObjectProperties`` (:meth:`~unicode_logic_kit.dl.TBox.add_inverse_roles`),
|
|
133
|
+
``SymmetricObjectProperty``, ``ReflexiveObjectProperty``,
|
|
134
|
+
``InverseFunctionalObjectProperty`` and a property chain
|
|
135
|
+
(:meth:`~unicode_logic_kit.dl.TBox.add_role_chain`) — plus an
|
|
136
|
+
:class:`~unicode_logic_kit.dl.concepts.InverseRole` on either side of a role
|
|
137
|
+
inclusion, which is accepted and rendered correctly but not decided in house.
|
|
138
|
+
``EquivalentObjectProperties``
|
|
139
|
+
(:meth:`~unicode_logic_kit.dl.TBox.add_equivalent_roles`) needs no rule at all:
|
|
140
|
+
it is stored as the role inclusions it abbreviates.
|
|
141
|
+
|
|
142
|
+
OWL 2 (Structural Specification §11) restricts asymmetry, irreflexivity, role
|
|
143
|
+
disjointness, functionality and inverse-functionality — and every qualified
|
|
144
|
+
number restriction — to SIMPLE roles, and the kit enforces exactly that
|
|
145
|
+
condition, by name (:class:`NonSimpleRoleError`), on the in-house AND the
|
|
146
|
+
external route. A role-box builder handed something that is not a usable role
|
|
147
|
+
in that position raises :class:`RoleExpressionError` on the call that is wrong::
|
|
148
|
+
|
|
149
|
+
dl.TBox().add_role_inclusion(("r", "s"), "t")
|
|
150
|
+
# -> raises dl.RoleExpressionError: a sequence of roles is a PROPERTY
|
|
151
|
+
# CHAIN -- use dl.TBox.add_role_chain(chain, super_role)
|
|
152
|
+
dl.TBox().add_role_inclusion("P", "owl:topObjectProperty")
|
|
153
|
+
# -> raises dl.RoleExpressionError: an OWL 2 BUILT-IN property name, not
|
|
154
|
+
# an ordinary role. ``dl.parse_owl_functional`` consumes the
|
|
155
|
+
# TAUTOLOGICAL inclusion into it as a documented no-op; every other use
|
|
156
|
+
# of a built-in name is refused by name there too.
|
|
157
|
+
|
|
158
|
+
Class expressions and the ABox: value restrictions, domain/range, identity
|
|
159
|
+
---------------------------------------------------------------------------
|
|
160
|
+
``dl.HasValue(role, individual)`` is OWL's ``ObjectHasValue(r a)``, written
|
|
161
|
+
``exists r.{a}``: a NOMINAL in disguise, so the in-house tableau REFUSES it, as
|
|
162
|
+
it refuses a bare ``Nominal`` (see :class:`~unicode_logic_kit.dl.concepts.HasValue`
|
|
163
|
+
for why it is a constructor of its own and not ``dl.Exists(role,
|
|
164
|
+
dl.Nominal(a))``, and "Value restrictions (ObjectHasValue)" in
|
|
165
|
+
:mod:`unicode_logic_kit.dl.tableau` for why no in-house rule for it is sound: a
|
|
166
|
+
value restriction gives a generated node an edge back to a named one, which the
|
|
167
|
+
subset blocking the tableau terminates by does not cover). Its FOL image is the
|
|
168
|
+
GROUND ATOM, the one-point reduction of ``exists y (r(x, y) and y = a)``, and
|
|
169
|
+
that image, with ``api.prove`` — or ``dl.external_*`` (HermiT) — is what decides
|
|
170
|
+
it::
|
|
171
|
+
|
|
172
|
+
dl.concept_to_fol(dl.HasValue("HasStateOfMatter", "Liquid"))
|
|
173
|
+
# -> HasStateOfMatter(x, Liquid)
|
|
174
|
+
dl.concept_satisfiable(dl.And(dl.HasValue("r", "a"), dl.ForAll("r", dl.Bottom())))
|
|
175
|
+
# -> raises dl.UnsupportedConceptError, naming the value restriction and
|
|
176
|
+
# the routes that do decide it. The FOL image proves the hand-derived
|
|
177
|
+
# verdict (unsatisfiable: a would have to be in bottom).
|
|
178
|
+
|
|
179
|
+
``TBox.add_role_domain`` / ``add_role_range`` store
|
|
180
|
+
``ObjectPropertyDomain``/``ObjectPropertyRange`` NATIVELY rather than as the
|
|
181
|
+
GCIs they are equivalent to, so the image is the direct two-variable sentence
|
|
182
|
+
and ``to_owl_functional`` round-trips the axiom to itself; the tableau decides
|
|
183
|
+
them by internalising those GCIs, with no new rule::
|
|
184
|
+
|
|
185
|
+
dl.rbox_to_fol(dl.TBox().add_role_domain("Covers", dl.Atomic("Study")))
|
|
186
|
+
# -> forall x forall y (Covers(x, y) -> Study(x))
|
|
187
|
+
|
|
188
|
+
``ABox.assert_same`` (``SameIndividual``) is decided by node MERGING, closed
|
|
189
|
+
under the equivalence the assertions generate, before any completion rule runs;
|
|
190
|
+
``ABox.assert_negative_role`` (``NegativeObjectPropertyAssertion``, arguments in
|
|
191
|
+
``assert_role``'s order) by a clash condition over FORBIDDEN edges. The second
|
|
192
|
+
refuses a NON-SIMPLE role by name, since seeing a forbidden edge is what its
|
|
193
|
+
verdict depends on and the tableau never materialises a transitive role's
|
|
194
|
+
derived edges::
|
|
195
|
+
|
|
196
|
+
dl.abox_consistent(dl.ABox().assert_same("a", "b").assert_distinct("a", "b"))
|
|
197
|
+
# -> False: a = b and a != b have no common model
|
|
198
|
+
|
|
199
|
+
``dl.parse_owl_functional_axioms(text)`` is the per-axiom reader for a whole
|
|
200
|
+
ontology: it returns an :class:`OwlFunctionalResult` with the TBox/ABox it could
|
|
201
|
+
build plus one :class:`RefusedAxiom` per axiom outside the fragment, each naming
|
|
202
|
+
the construct, its offset and its own source text. ``dl.parse_owl_functional``
|
|
203
|
+
stays strict, raising on the first such construct. The lenient reader recovers
|
|
204
|
+
from :class:`OwlFunctionalUnsupportedError` and nothing else: MALFORMED input
|
|
205
|
+
still raises::
|
|
206
|
+
|
|
207
|
+
doc = dl.parse_owl_functional_axioms(text)
|
|
208
|
+
doc.ok, doc.accepted, doc.refused_keywords
|
|
209
|
+
kb = doc.to_kb() # == dl.kb_to_fol(doc.tbox, doc.abox)
|
|
210
|
+
|
|
211
|
+
One limit to know before printing an image that mentions an individual: the kit
|
|
212
|
+
decides predicate-versus-term by the first character's case, so an UPPER-case
|
|
213
|
+
individual name -- the norm for an OWL IRI -- prints as itself and does not read
|
|
214
|
+
back as the same formula (``Alice = Bob`` does not parse;
|
|
215
|
+
``HasStateOfMatter(x, Liquid)`` parses only as third-order, with ``Liquid`` a
|
|
216
|
+
``PredicateTerm``). The AST is sound either way, and the routes that never go
|
|
217
|
+
through text are unaffected. See ``tests/test_printed_text_reads_back.py``.
|
|
218
|
+
|
|
219
|
+
Inverse roles and nominals (I, O) — outside ALCHQ, refused by the in-house tableau
|
|
220
|
+
(a value restriction, ``dl.HasValue``, is a nominal and is refused with them)::
|
|
221
|
+
|
|
222
|
+
dl.concept_satisfiable(dl.Exists(dl.InverseRole("hasChild"), dl.Top()))
|
|
223
|
+
# -> raises dl.UnsupportedConceptError: no in-house tableau rule decides I/O
|
|
224
|
+
|
|
225
|
+
``dl.translate`` translates both constructs faithfully to FOL regardless (a role
|
|
226
|
+
expression has a standard FOL image, argument order swapped; a nominal ``{a}``
|
|
227
|
+
becomes the equality ``x = a`` — see :mod:`unicode_logic_kit.dl.translate`'s module
|
|
228
|
+
docstring). To actually DECIDE a concept that needs either, use
|
|
229
|
+
:mod:`unicode_logic_kit.dl.owl_reasoner`'s external, HermiT-backed reasoner (an
|
|
230
|
+
optional dependency — install with ``pip install unicode-logic-kit[owl]``)::
|
|
231
|
+
|
|
232
|
+
dl.owl_reasoner_available() # owlready2 importable?
|
|
233
|
+
dl.external_concept_satisfiable(dl.And(dl.Nominal("a"), dl.Nominal("b")))
|
|
234
|
+
# -> True: {a} ⊓ {b} is satisfiable absent an explicit DifferentIndividuals
|
|
235
|
+
# assertion (OWL 2 has no unique name assumption)
|
|
236
|
+
dl.external_instance_check(
|
|
237
|
+
dl.ABox().assert_role("alice", "bob", "hasChild"), "bob",
|
|
238
|
+
dl.Exists(dl.InverseRole("hasChild"), dl.Top()))
|
|
239
|
+
# -> True: bob has an incoming hasChild edge, i.e. an hasChild-inverse successor
|
|
240
|
+
|
|
241
|
+
Which route decides what — the axiom-kind table
|
|
242
|
+
------------------------------------------------
|
|
243
|
+
Every axiom kind a :class:`TBox`/:class:`ABox` can hold has ONE row in
|
|
244
|
+
``dl.tableau._AXIOM_KINDS``, and the row says what the in-house tableau does
|
|
245
|
+
with it and what the FOL image does with it (see "The axiom-kind table" in
|
|
246
|
+
:mod:`unicode_logic_kit.dl.tableau`'s module docstring). Two rules follow, and
|
|
247
|
+
they are the kit's answer to "could these two routes disagree?":
|
|
248
|
+
|
|
249
|
+
* **Builders never refuse.** ``TBox.add_*``/``ABox.assert_*`` accept every
|
|
250
|
+
kind, because a TBox is what a parser fills from a file.
|
|
251
|
+
* **A kind the tableau has no rule for is refused BY NAME at query time**, by
|
|
252
|
+
:class:`UnsupportedAxiomError`, from ``concept_satisfiable``/
|
|
253
|
+
``abox_consistent`` — and so from everything that reduces to them. Never
|
|
254
|
+
approximated, never silently ignored. ``dl.kb_to_fol`` + ``api.prove``, or
|
|
255
|
+
``dl.external_*``, is where such a kind is answered instead.
|
|
256
|
+
|
|
257
|
+
The FOL image keeps the two apart as well: ``kb_to_fol(tbox, abox).formula``
|
|
258
|
+
is the knowledge base and ``.side_axioms`` the premises that are NOT part of
|
|
259
|
+
it (each a :class:`SideAxiom` carrying the OWL ``kind`` it came from, so
|
|
260
|
+
``kb.axioms_of_kind("TransitiveObjectProperty")`` is a question with an
|
|
261
|
+
answer), while ``tbox_to_fol`` refuses outright to hand back a concept-
|
|
262
|
+
inclusion image that silently drops them.
|
|
263
|
+
|
|
264
|
+
The data layer: two sorts
|
|
265
|
+
--------------------------
|
|
266
|
+
OWL 2's second sort -- data values, datatypes, data properties -- is STORED
|
|
267
|
+
(:class:`DataExists`, :class:`DataForAll`, :class:`DataHasValue`,
|
|
268
|
+
:class:`DataAtLeast`, :class:`DataAtMost`; ``TBox.add_data_property_*`` and
|
|
269
|
+
``add_datatype_definition``; ``ABox.assert_data``/``assert_negative_data``;
|
|
270
|
+
:class:`Literal` and the data ranges :class:`Datatype`,
|
|
271
|
+
:class:`DatatypeRestriction`, :class:`DataOneOf`, :class:`DataComplementOf`,
|
|
272
|
+
:class:`DataIntersectionOf`, :class:`DataUnionOf`), READ and WRITTEN by the
|
|
273
|
+
Manchester and Functional-Style syntaxes, and TRANSLATED by the FOL image --
|
|
274
|
+
and REFUSED by the in-house tableau, by name, kind by kind (it has no data
|
|
275
|
+
domain), and by ``dl.owl_reasoner``. What answers is ``dl.kb_to_fol`` +
|
|
276
|
+
``api.prove``; facets are decided by ``atp.z3_arith``.
|
|
277
|
+
|
|
278
|
+
The image is a guarded ONE-sorted theory over two reserved predicates,
|
|
279
|
+
:data:`OWL_THING` and :data:`OWL_DATA`. The separation of the two domains, the
|
|
280
|
+
typing of every data property and individual, the datatype lattice and literal
|
|
281
|
+
distinctness are SIDE axioms (``group`` ``"sort"``/``"datatype"``), so the
|
|
282
|
+
two-sorted question is the one the prover answers only if they are PREMISES:
|
|
283
|
+
pass ``kb.premises`` / ``kb.tbox_premises``, not ``kb.formula`` alone, and let the
|
|
284
|
+
bundle build the GOAL, ``kb.subsumption_goal(C, D)``, ``kb.unsatisfiability_goal(C)``
|
|
285
|
+
or ``kb.instance_goal(a, C)``, which relativise it the way ``kb.separation``
|
|
286
|
+
says (a hand-built goal needs ``subsumption_to_fol(..., object_sort=True)``).
|
|
287
|
+
Every GCI of such an image is restricted to ``OwlThing`` (an unrestricted
|
|
288
|
+
``⊤ ⊑ {a}`` would range over data values and make an OWL-consistent knowledge
|
|
289
|
+
base inconsistent). The sort, typing and datatype axioms are derived from the
|
|
290
|
+
vocabulary the knowledge base uses, so name the concepts you are going to ask
|
|
291
|
+
about when you build it, ``dl.kb_to_fol(tbox, abox, query=[C, D])`` -- a goal over
|
|
292
|
+
a name the bundle does not cover is refused by name, not answered wrongly. The
|
|
293
|
+
image is sound -- every OWL model expands to a model of it, so ``proved``
|
|
294
|
+
transfers to OWL 2 -- and deliberately not complete: facets are uninterpreted
|
|
295
|
+
for ``api.prove``, a literal is typed only by the datatype it was written with,
|
|
296
|
+
and the size of a value space is not stated, so ``refuted`` does not transfer
|
|
297
|
+
(``kb.refutation_is_decisive`` is ``False`` exactly when there is a data layer).
|
|
298
|
+
See "The data layer: two sorts" in the description-logic guide.
|
|
299
|
+
|
|
300
|
+
Every ``dl.external_*`` function mirrors its ``dl.<name>`` in-house-tableau
|
|
301
|
+
namesake's signature and reduction exactly (``external_subsumes`` reduces to
|
|
302
|
+
``external_concept_satisfiable`` the way ``subsumes`` reduces to
|
|
303
|
+
``concept_satisfiable``, and so on), just decided over the bigger ALCHQ + I + O
|
|
304
|
+
fragment instead — see :mod:`unicode_logic_kit.dl.owl_reasoner`'s module docstring.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
from .concepts import (
|
|
308
|
+
Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
|
|
309
|
+
InverseRole, Nominal, HasValue, DataExists, DataForAll, DataHasValue,
|
|
310
|
+
DataAtLeast, DataAtMost, nnf,
|
|
311
|
+
)
|
|
312
|
+
from .datatypes import (
|
|
313
|
+
Literal, DataRange, Datatype, DatatypeRestriction, DataOneOf,
|
|
314
|
+
DataComplementOf, DataIntersectionOf, DataUnionOf, UnsupportedDatatypeError,
|
|
315
|
+
OWL_THING, OWL_DATA, datarange_to_fol,
|
|
316
|
+
)
|
|
317
|
+
from .tableau import (
|
|
318
|
+
TBox, ABox,
|
|
319
|
+
concept_satisfiable, concept_unsatisfiable, subsumes, equivalent, abox_consistent,
|
|
320
|
+
instance_check, instance_retrieval, realize, realize_all,
|
|
321
|
+
NonSimpleRoleError, UnsupportedConceptError, UnsupportedAxiomError,
|
|
322
|
+
RoleExpressionError,
|
|
323
|
+
)
|
|
324
|
+
from .classification import classify, Classification
|
|
325
|
+
from .parser import parse_concept, parse_gci, ConceptSyntaxError
|
|
326
|
+
from .translate import (
|
|
327
|
+
concept_to_fol, subsumption_to_fol, tbox_to_fol, rbox_to_fol, abox_to_fol,
|
|
328
|
+
databox_to_fol, data_sort_axioms, check_kb_names,
|
|
329
|
+
kb_to_fol, KnowledgeBaseFOL, SideAxiom, RoleBoxOmittedError,
|
|
330
|
+
concept_to_modal,
|
|
331
|
+
)
|
|
332
|
+
from .owl_manchester import (
|
|
333
|
+
parse_manchester, to_manchester, parse_manchester_axiom,
|
|
334
|
+
parse_manchester_role_axiom, role_axiom_to_manchester, ManchesterSyntaxError,
|
|
335
|
+
parse_manchester_data_range, to_manchester_data_range, parse_manchester_literal,
|
|
336
|
+
)
|
|
337
|
+
from .owl_functional import (
|
|
338
|
+
parse_owl_functional, parse_owl_functional_axioms, to_owl_functional,
|
|
339
|
+
parse_owl_functional_class_expression, to_owl_functional_class_expression,
|
|
340
|
+
OwlFunctionalResult, RefusedAxiom, ConsumedAxiom,
|
|
341
|
+
OwlFunctionalSyntaxError, OwlFunctionalUnsupportedError,
|
|
342
|
+
)
|
|
343
|
+
from .owl_reasoner import (
|
|
344
|
+
available as owl_reasoner_available,
|
|
345
|
+
external_concept_satisfiable, external_concept_unsatisfiable,
|
|
346
|
+
external_subsumes, external_equivalent,
|
|
347
|
+
external_abox_consistent, external_instance_check, external_instance_retrieval,
|
|
348
|
+
external_realize, external_realize_all,
|
|
349
|
+
OwlReasonerError,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
__all__ = [
|
|
353
|
+
"Concept", "Top", "Bottom", "Atomic", "Not", "And", "Or", "Exists", "ForAll",
|
|
354
|
+
"AtLeast", "AtMost", "InverseRole", "Nominal", "HasValue", "nnf",
|
|
355
|
+
"DataExists", "DataForAll", "DataHasValue", "DataAtLeast", "DataAtMost",
|
|
356
|
+
"Literal", "DataRange", "Datatype", "DatatypeRestriction", "DataOneOf",
|
|
357
|
+
"DataComplementOf", "DataIntersectionOf", "DataUnionOf",
|
|
358
|
+
"UnsupportedDatatypeError", "OWL_THING", "OWL_DATA", "datarange_to_fol",
|
|
359
|
+
"TBox", "ABox",
|
|
360
|
+
"concept_satisfiable", "concept_unsatisfiable", "subsumes", "equivalent",
|
|
361
|
+
"abox_consistent",
|
|
362
|
+
"instance_check", "instance_retrieval", "realize", "realize_all",
|
|
363
|
+
"NonSimpleRoleError", "UnsupportedConceptError", "UnsupportedAxiomError",
|
|
364
|
+
"RoleExpressionError",
|
|
365
|
+
"classify", "Classification",
|
|
366
|
+
"parse_concept", "parse_gci", "ConceptSyntaxError",
|
|
367
|
+
"concept_to_fol", "subsumption_to_fol", "tbox_to_fol", "rbox_to_fol", "abox_to_fol",
|
|
368
|
+
"databox_to_fol", "data_sort_axioms", "check_kb_names",
|
|
369
|
+
"kb_to_fol", "KnowledgeBaseFOL", "SideAxiom", "RoleBoxOmittedError",
|
|
370
|
+
"concept_to_modal",
|
|
371
|
+
"parse_manchester", "to_manchester", "parse_manchester_axiom",
|
|
372
|
+
"parse_manchester_role_axiom", "role_axiom_to_manchester", "ManchesterSyntaxError",
|
|
373
|
+
"parse_manchester_data_range", "to_manchester_data_range", "parse_manchester_literal",
|
|
374
|
+
"parse_owl_functional", "parse_owl_functional_axioms", "to_owl_functional",
|
|
375
|
+
"parse_owl_functional_class_expression", "to_owl_functional_class_expression",
|
|
376
|
+
"OwlFunctionalResult", "RefusedAxiom", "ConsumedAxiom",
|
|
377
|
+
"OwlFunctionalSyntaxError", "OwlFunctionalUnsupportedError",
|
|
378
|
+
"owl_reasoner_available",
|
|
379
|
+
"external_concept_satisfiable", "external_concept_unsatisfiable",
|
|
380
|
+
"external_subsumes", "external_equivalent",
|
|
381
|
+
"external_abox_consistent", "external_instance_check", "external_instance_retrieval",
|
|
382
|
+
"external_realize", "external_realize_all",
|
|
383
|
+
"OwlReasonerError",
|
|
384
|
+
]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""TBox classification: the named-concept subsumption hierarchy, as a pure reduction
|
|
2
|
+
to :func:`unicode_logic_kit.dl.tableau.subsumes`.
|
|
3
|
+
|
|
4
|
+
Given a TBox, :func:`classify` collects every named (:class:`~unicode_logic_kit.dl.concepts.Atomic`)
|
|
5
|
+
concept mentioned in it, decides the full pairwise subsumption matrix over those names
|
|
6
|
+
with the already-tested ``subsumes``, and turns that matrix into the structure an OWL
|
|
7
|
+
reasoner's classifier reports: equivalence classes (for concepts a TBox happens to make
|
|
8
|
+
synonyms), a transitively-reduced Hasse diagram of direct parents/children, and the
|
|
9
|
+
free transitive closure (``ancestors``). No new tableau completion rules are added —
|
|
10
|
+
correctness rides entirely on ``subsumes``'s existing soundness and completeness for ALC.
|
|
11
|
+
|
|
12
|
+
Public API: :class:`Classification`, :func:`classify`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Dict, FrozenSet, Iterable, List, Optional, Set
|
|
17
|
+
|
|
18
|
+
from .concepts import (
|
|
19
|
+
Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
|
|
20
|
+
InverseRole, Nominal, HasValue, DATA_CONCEPTS,
|
|
21
|
+
)
|
|
22
|
+
from .tableau import TBox, subsumes, _reject_beyond_alc, _reject_inputs, _reject_role_box
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Classification:
|
|
27
|
+
"""The named-concept subsumption hierarchy of a TBox (see :func:`classify`).
|
|
28
|
+
|
|
29
|
+
Every key and every member of every value is a *canonical name*: the
|
|
30
|
+
lexicographically smallest name in its mutual-subsumption equivalence class, so
|
|
31
|
+
that a TBox with synonyms (``Bachelor ≡ Man ⊓ ¬Married``) still reports one node
|
|
32
|
+
per distinct concept rather than one per name.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
equivalents: Dict[str, FrozenSet[str]]
|
|
36
|
+
"""Canonical name -> its full synonym set (itself included)."""
|
|
37
|
+
|
|
38
|
+
parents: Dict[str, FrozenSet[str]]
|
|
39
|
+
"""Canonical name -> its *direct* super-concept names (the Hasse diagram)."""
|
|
40
|
+
|
|
41
|
+
children: Dict[str, FrozenSet[str]]
|
|
42
|
+
"""Canonical name -> its *direct* sub-concept names (the Hasse diagram)."""
|
|
43
|
+
|
|
44
|
+
ancestors: Dict[str, FrozenSet[str]]
|
|
45
|
+
"""Canonical name -> ALL (transitively closed) super-concept names."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _atomic_names(concept: Concept, names: Set[str]) -> None:
|
|
49
|
+
"""Recursively collect every :class:`Atomic` name occurring in ``concept`` into ``names``.
|
|
50
|
+
|
|
51
|
+
Refuses — through the tableau's own concept guard, with
|
|
52
|
+
:class:`~unicode_logic_kit.dl.tableau.UnsupportedConceptError`, the refusal every
|
|
53
|
+
other entry point gives — a :class:`~unicode_logic_kit.dl.concepts.Nominal`
|
|
54
|
+
(caught directly: it is not "atomic-shaped" enough to fall through the
|
|
55
|
+
isinstance chain below, so it hits the final ``else`` on its own) or an
|
|
56
|
+
:class:`~unicode_logic_kit.dl.concepts.InverseRole`-valued ``role`` field
|
|
57
|
+
(checked EXPLICITLY in the ``Exists``/``ForAll``/``AtLeast``/``AtMost``
|
|
58
|
+
branch below, unlike :func:`unicode_logic_kit.dl.concepts.nnf`'s matching
|
|
59
|
+
branches, which leave ``role`` untyped on purpose — see that function's
|
|
60
|
+
docstring): without this explicit check, an ``InverseRole``-only axiom
|
|
61
|
+
would silently contribute NO names to :func:`classify`'s vocabulary
|
|
62
|
+
(``Exists``/``ForAll`` only ever recurse into ``.concept``, never inspect
|
|
63
|
+
``.role``), so a TBox whose ONLY axiom uses one would classify as an empty,
|
|
64
|
+
vacuously "fine" hierarchy instead of raising — the one shape in this
|
|
65
|
+
module where :func:`unicode_logic_kit.dl.tableau.subsumes`'s own
|
|
66
|
+
``_reject_beyond_alc`` guard would never even get called to catch it (see
|
|
67
|
+
:func:`classify`'s docstring: it never calls ``subsumes`` at all when its
|
|
68
|
+
collected vocabulary is empty).
|
|
69
|
+
|
|
70
|
+
A :class:`~unicode_logic_kit.dl.concepts.HasValue` and a data restriction are
|
|
71
|
+
refused through the tableau's own concept guard, with
|
|
72
|
+
:class:`~unicode_logic_kit.dl.tableau.UnsupportedConceptError`.
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(concept, Atomic):
|
|
75
|
+
names.add(concept.name)
|
|
76
|
+
elif isinstance(concept, (Top, Bottom)):
|
|
77
|
+
pass
|
|
78
|
+
elif isinstance(concept, DATA_CONCEPTS):
|
|
79
|
+
# A data restriction names a DATA property and a data range, never a
|
|
80
|
+
# class, so it contributes no name -- but it is outside the fragment the
|
|
81
|
+
# reduction to ``subsumes`` decides, and with a one-name vocabulary
|
|
82
|
+
# ``subsumes`` is never called to say so. The tableau's own concept
|
|
83
|
+
# guard raises the SAME refusal every other entry point gives.
|
|
84
|
+
_reject_beyond_alc(concept)
|
|
85
|
+
elif isinstance(concept, HasValue):
|
|
86
|
+
# Contributes NO class name -- a value restriction names a ROLE and an
|
|
87
|
+
# INDIVIDUAL, neither of which belongs in a subsumption hierarchy over
|
|
88
|
+
# named CONCEPTS -- but it is a nominal in disguise, which the reduction
|
|
89
|
+
# to ``subsumes`` does not decide, and with a one-name vocabulary
|
|
90
|
+
# ``subsumes`` is never called to say so. The tableau's own concept
|
|
91
|
+
# guard raises the SAME refusal every other entry point gives.
|
|
92
|
+
_reject_beyond_alc(concept)
|
|
93
|
+
elif isinstance(concept, Not):
|
|
94
|
+
_atomic_names(concept.concept, names)
|
|
95
|
+
elif isinstance(concept, (And, Or)):
|
|
96
|
+
_atomic_names(concept.left, names)
|
|
97
|
+
_atomic_names(concept.right, names)
|
|
98
|
+
elif isinstance(concept, (Exists, ForAll, AtLeast, AtMost)):
|
|
99
|
+
if isinstance(concept.role, InverseRole):
|
|
100
|
+
# An inverse role contributes no class name either, and with a
|
|
101
|
+
# one-name vocabulary ``subsumes`` is never called to refuse it. The
|
|
102
|
+
# tableau's own concept guard raises the SAME refusal every other
|
|
103
|
+
# entry point gives (UnsupportedConceptError, which the MCP tools
|
|
104
|
+
# return as a structured error; a bare TypeError escaped them).
|
|
105
|
+
_reject_beyond_alc(concept)
|
|
106
|
+
_atomic_names(concept.concept, names)
|
|
107
|
+
else:
|
|
108
|
+
# A bare Nominal lands here: refused by the same guard, by name.
|
|
109
|
+
_reject_beyond_alc(concept)
|
|
110
|
+
raise TypeError(f"classify: unsupported concept {type(concept).__name__}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _equivalence_classes(names: List[str], holds) -> List[Set[str]]:
|
|
114
|
+
"""Connected components of the mutual-subsumption graph over ``names``."""
|
|
115
|
+
adjacency: Dict[str, Set[str]] = {n: set() for n in names}
|
|
116
|
+
for a in names:
|
|
117
|
+
for b in names:
|
|
118
|
+
if a != b and holds[(a, b)] and holds[(b, a)]:
|
|
119
|
+
adjacency[a].add(b)
|
|
120
|
+
adjacency[b].add(a)
|
|
121
|
+
seen: Set[str] = set()
|
|
122
|
+
classes: List[Set[str]] = []
|
|
123
|
+
for n in names:
|
|
124
|
+
if n in seen:
|
|
125
|
+
continue
|
|
126
|
+
stack, component = [n], set()
|
|
127
|
+
while stack:
|
|
128
|
+
x = stack.pop()
|
|
129
|
+
if x in component:
|
|
130
|
+
continue
|
|
131
|
+
component.add(x)
|
|
132
|
+
stack.extend(adjacency[x] - component)
|
|
133
|
+
seen |= component
|
|
134
|
+
classes.append(component)
|
|
135
|
+
return classes
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def classify(tbox: TBox, concepts: Optional[Iterable[Concept]] = None) -> Classification:
|
|
139
|
+
"""Classify every named concept of ``tbox`` into a subsumption hierarchy.
|
|
140
|
+
|
|
141
|
+
The vocabulary is every :class:`Atomic` name reachable from ``tbox.inclusions``
|
|
142
|
+
(walked through ``And``/``Or``/``Not``/``Exists``/``ForAll``), extended with the
|
|
143
|
+
names occurring in ``concepts`` when given — useful for a name of interest that
|
|
144
|
+
never appears in an axiom (and so would otherwise be invisible, an isolated node
|
|
145
|
+
with no parents/children/ancestors and a singleton equivalence class).
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
~unicode_logic_kit.dl.tableau.UnsupportedAxiomError:
|
|
149
|
+
``tbox`` carries an axiom KIND no in-house rule
|
|
150
|
+
decides, exactly as ``subsumes`` would raise -- including for a
|
|
151
|
+
TBox with ONE named concept, where the pair loop below never calls
|
|
152
|
+
``subsumes`` at all (its only ordered pair is ``(A, A)``, answered
|
|
153
|
+
``True`` without a tableau), so without the up-front guard that one
|
|
154
|
+
shape returned a hierarchy for a knowledge base every other entry
|
|
155
|
+
point refuses.
|
|
156
|
+
~unicode_logic_kit.dl.tableau.UnsupportedConceptError:
|
|
157
|
+
a stored class expression (or one of
|
|
158
|
+
``concepts``) contains a value restriction (``HasValue``, a
|
|
159
|
+
nominal in disguise) or a data restriction -- raised by the
|
|
160
|
+
tableau's concept guard whatever the vocabulary's size.
|
|
161
|
+
"""
|
|
162
|
+
# The axiom-level guard, once, before anything is decided -- the same call
|
|
163
|
+
# concept_satisfiable/abox_consistent open with. A TBox with zero or one
|
|
164
|
+
# named concept never reaches `subsumes`, so inheriting it was not enough.
|
|
165
|
+
_reject_role_box(tbox, None)
|
|
166
|
+
names: Set[str] = set()
|
|
167
|
+
for sub, sup in tbox.inclusions:
|
|
168
|
+
_atomic_names(sub, names)
|
|
169
|
+
_atomic_names(sup, names)
|
|
170
|
+
extra = list(concepts) if concepts is not None else []
|
|
171
|
+
for c in extra:
|
|
172
|
+
_atomic_names(c, names)
|
|
173
|
+
# The concept-level half of the same guard, over EVERY class expression the
|
|
174
|
+
# TBox stores (a domain or range filler included -- `_atomic_names` above
|
|
175
|
+
# reads inclusions only) and over the extra concepts. `subsumes` runs it per
|
|
176
|
+
# pair, but a vocabulary of fewer than two names makes no such call, and a
|
|
177
|
+
# knowledge base that every other entry point refuses (an OWL 2 built-in
|
|
178
|
+
# property name as a restriction's role, a nominal in a domain filler) got a
|
|
179
|
+
# hierarchy here.
|
|
180
|
+
_reject_inputs(tbox, None, extra)
|
|
181
|
+
ordered = sorted(names)
|
|
182
|
+
|
|
183
|
+
# The O(n^2) reduction: decide sub/sup for every ordered pair with `subsumes`.
|
|
184
|
+
holds = {}
|
|
185
|
+
for a in ordered:
|
|
186
|
+
for b in ordered:
|
|
187
|
+
holds[(a, b)] = True if a == b else subsumes(Atomic(a), Atomic(b), tbox)
|
|
188
|
+
|
|
189
|
+
classes = _equivalence_classes(ordered, holds)
|
|
190
|
+
canon_of: Dict[str, str] = {}
|
|
191
|
+
equivalents: Dict[str, FrozenSet[str]] = {}
|
|
192
|
+
for component in classes:
|
|
193
|
+
canon = min(component)
|
|
194
|
+
equivalents[canon] = frozenset(component)
|
|
195
|
+
for member in component:
|
|
196
|
+
canon_of[member] = canon
|
|
197
|
+
canon_names = sorted(equivalents)
|
|
198
|
+
|
|
199
|
+
# Free byproduct: the un-reduced transitive closure, at the canonical-name level
|
|
200
|
+
# (equivalence-class members all agree on what they subsume/are subsumed by, so one
|
|
201
|
+
# representative pairwise lookup per class is enough).
|
|
202
|
+
ancestors: Dict[str, Set[str]] = {c: set() for c in canon_names}
|
|
203
|
+
for a in canon_names:
|
|
204
|
+
for b in canon_names:
|
|
205
|
+
if a != b and holds[(a, b)]:
|
|
206
|
+
ancestors[a].add(b)
|
|
207
|
+
|
|
208
|
+
# Transitive reduction (Hasse diagram): drop a -> b when some c also ancestor of a
|
|
209
|
+
# already reaches b, i.e. a -> c -> b makes a -> b redundant.
|
|
210
|
+
parents: Dict[str, FrozenSet[str]] = {}
|
|
211
|
+
for a in canon_names:
|
|
212
|
+
direct = set(ancestors[a])
|
|
213
|
+
for c in ancestors[a]:
|
|
214
|
+
direct -= ancestors[c]
|
|
215
|
+
parents[a] = frozenset(direct)
|
|
216
|
+
|
|
217
|
+
children: Dict[str, Set[str]] = {c: set() for c in canon_names}
|
|
218
|
+
for a, ps in parents.items():
|
|
219
|
+
for p in ps:
|
|
220
|
+
children[p].add(a)
|
|
221
|
+
|
|
222
|
+
return Classification(
|
|
223
|
+
equivalents=equivalents,
|
|
224
|
+
parents=parents,
|
|
225
|
+
children={c: frozenset(s) for c, s in children.items()},
|
|
226
|
+
ancestors={c: frozenset(s) for c, s in ancestors.items()},
|
|
227
|
+
)
|