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,2704 @@
|
|
|
1
|
+
"""The **standard translation** of ALC into first-order logic.
|
|
2
|
+
|
|
3
|
+
ALC concepts denote unary relations over a domain, and roles denote binary
|
|
4
|
+
relations; the standard translation makes this concrete by rendering a
|
|
5
|
+
concept ``C`` as a first-order formula ``π(C, x)`` with one free variable
|
|
6
|
+
``x`` — "the individual currently under discussion" — such that ``d`` is in
|
|
7
|
+
the extension of ``C`` iff ``π(C, x)`` holds under ``x ↦ d``. Concretely:
|
|
8
|
+
|
|
9
|
+
- ``Atomic("C")`` ↦ ``C(x)`` (the concept becomes a unary predicate)
|
|
10
|
+
- ``⊤`` / ``⊥`` ↦ ``x = x`` / ``x ≠ x`` (see "Top and Bottom" below)
|
|
11
|
+
- ``¬C`` ↦ ``¬π(C, x)``
|
|
12
|
+
- ``C ⊓ D`` ↦ ``π(C, x) ∧ π(D, x)``
|
|
13
|
+
- ``C ⊔ D`` ↦ ``π(C, x) ∨ π(D, x)``
|
|
14
|
+
- ``∃r.C`` ↦ ``∃y (r(x, y) ∧ π(C, y))``
|
|
15
|
+
- ``∀r.C`` ↦ ``∀y (r(x, y) → π(C, y))``
|
|
16
|
+
- ``≥n r.C`` ↦ ``∃≥n y (r(x, y) ∧ π(C, y))`` (see "Qualified number restrictions")
|
|
17
|
+
- ``≤n r.C`` ↦ ``∃≤n y (r(x, y) ∧ π(C, y))``
|
|
18
|
+
|
|
19
|
+
with ``y`` a FRESH variable at every restriction, so nested restrictions
|
|
20
|
+
(``∃r.(∀s.C)``) never capture an outer variable (see "Variable freshness").
|
|
21
|
+
A role ``r`` becomes its own binary predicate ``r(·, ·)`` with no axioms
|
|
22
|
+
forced on it, which is exactly why the reduction is faithful: ALC roles are
|
|
23
|
+
arbitrary Kripke relations, and an uninterpreted FOL predicate is exactly
|
|
24
|
+
that (see also :func:`concept_to_modal` for the single-role reading through
|
|
25
|
+
propositional modal K instead of FOL).
|
|
26
|
+
|
|
27
|
+
Top and Bottom
|
|
28
|
+
--------------
|
|
29
|
+
The FOL node set has no first-class truth-constant (no ``⊤``/``⊥`` term
|
|
30
|
+
that ``Node.to_z3`` renders as Z3's literal ``True``/``False``); the closest
|
|
31
|
+
thing is the reserved falsum atom idiom used by :mod:`unicode_logic_kit.atp.fitch`
|
|
32
|
+
(``Atom("⊥", ())`` there, but that idiom is *propositional* and only means
|
|
33
|
+
something because the Fitch checker special-cases it — it does not carry to
|
|
34
|
+
an arbitrary Z3/Prover9/TPTP export, where a bare atom is just another
|
|
35
|
+
uninterpreted symbol that could be assigned False in some model). Rendering
|
|
36
|
+
``⊤`` as an uninterpreted atom (``P(x) ∨ ¬P(x)`` for some fresh ``P``) is
|
|
37
|
+
truth-functionally a tautology but is an ugly, semantically unmotivated
|
|
38
|
+
detour through a predicate the concept never mentions.
|
|
39
|
+
|
|
40
|
+
Instead this module renders ``⊤``/``⊥`` through **equality**, which the FOL
|
|
41
|
+
Atom node already treats natively (``Atom("=", …)`` / ``Atom("≠", …)`` map to
|
|
42
|
+
Z3's built-in ``==``/``!=``, Prover9's ``=``, TPTP's ``=``/``!=`` — see
|
|
43
|
+
``Atom.to_z3``/``to_prover9``/``to_tptp`` in ``fol/_fol_nodes.py``):
|
|
44
|
+
|
|
45
|
+
- ``⊤`` ↦ ``x = x`` — valid in every model (reflexivity), matching how the
|
|
46
|
+
tableau treats ``⊤``: it never causes a clash and never needs expansion,
|
|
47
|
+
i.e. it constrains nothing and holds of every individual.
|
|
48
|
+
- ``⊥`` ↦ ``x ≠ x`` — unsatisfiable in every model, matching how the
|
|
49
|
+
tableau treats ``⊥``: ``x : ⊥`` is *itself* a clash condition (see
|
|
50
|
+
``_clash`` in :mod:`unicode_logic_kit.dl.tableau`), i.e. no individual can
|
|
51
|
+
satisfy it.
|
|
52
|
+
|
|
53
|
+
This is the standard textbook rendering, adds no extra symbols to the
|
|
54
|
+
signature, and is a genuine validity/unsatisfiability rather than a
|
|
55
|
+
model-dependent coincidence.
|
|
56
|
+
|
|
57
|
+
Variable freshness
|
|
58
|
+
-------------------
|
|
59
|
+
Each ``∃r.C`` / ``∀r.C`` introduces a bound variable minted by
|
|
60
|
+
:func:`~unicode_logic_kit.fol._identifiers.fresh_variables`: one letter plus
|
|
61
|
+
digits (``x0``, ``x1``, …), which is the shape the kit's VARIABLE terminal
|
|
62
|
+
actually accepts. Until 0.30.0 the name was ``f"{var}_{n}"``, and
|
|
63
|
+
``∃x_1 (r(x, x_1) ∧ …)`` is text this kit printed and its own
|
|
64
|
+
:func:`unicode_logic_kit.api.parse_any` rejects — the underscore is not part of
|
|
65
|
+
the terminal. The letter is the seed name's first character when that is a
|
|
66
|
+
legal variable letter on its own and ``x`` otherwise, so an ABox individual
|
|
67
|
+
named ``alice`` yields ``x0`` rather than ``alice_1``; the seed name itself is
|
|
68
|
+
excluded, so the free variable can never be captured.
|
|
69
|
+
|
|
70
|
+
A single top-level call (one call to :func:`concept_to_fol`, or one of the two
|
|
71
|
+
independent halves of :func:`subsumption_to_fol`) shares ONE minter across its
|
|
72
|
+
entire recursive walk, so EVERY restriction anywhere in that call's concept
|
|
73
|
+
tree — nested *or* sibling — gets a pairwise-distinct name; in particular no
|
|
74
|
+
two restrictions nested along the same path can ever reuse a name, so
|
|
75
|
+
``∃r.(∀s.C)`` translates to ``∃x0 (r(x, x0) ∧ ∀x1 (s(x0, x1) → π(C, x1)))``
|
|
76
|
+
with no risk of the inner ``∀`` accidentally binding the outer ``∃``'s
|
|
77
|
+
variable. Two SEPARATE top-level calls each start their own minter back at
|
|
78
|
+
``x0`` and so may independently mint the same name (e.g.
|
|
79
|
+
``subsumption_to_fol``'s antecedent and consequent, translated by two
|
|
80
|
+
independent calls to the internal translator, may both use ``x0``) — this is
|
|
81
|
+
not a capture risk because the two calls' quantifier scopes are siblings under
|
|
82
|
+
``→``, not nested inside one another.
|
|
83
|
+
|
|
84
|
+
Bound variables never share a name with an individual
|
|
85
|
+
------------------------------------------------------
|
|
86
|
+
``Variable("x")`` and ``Constant("x")`` are two symbols to Z3 (a variable is the
|
|
87
|
+
symbol ``x!v`` and a constant is named as it is, see ``Z3Env``), and the unicode
|
|
88
|
+
text tells them apart (``x`` and ``'x'``). A target that gives the two one
|
|
89
|
+
namespace would not: it would write ``∀x P(x, 'x')`` with one symbol for both,
|
|
90
|
+
so a quantifier binding ``x`` would capture an individual called ``x`` there.
|
|
91
|
+
It captured it on Z3 as well while Z3 took the pair for one symbol: the image of
|
|
92
|
+
``∃r.{x} ⊑ A`` printed ``∀x (r(x, x) → A(x))``, and ``api.prove`` over it called
|
|
93
|
+
a consistent knowledge base inconsistent, while the tableau (which has no
|
|
94
|
+
variables to capture) said consistent. The same held for every FIXED prefix
|
|
95
|
+
variable — the GCI's ``x``, the role axioms' ``x``/``y``/``z``, the data axioms'
|
|
96
|
+
``x``/``v``/``w`` — not only the minted ones. So EVERY bound
|
|
97
|
+
variable of an image avoids EVERY individual of the knowledge base, whichever
|
|
98
|
+
target the image is written for: a prefix
|
|
99
|
+
variable that clashes is renamed (:func:`_binder_names`, to the first free
|
|
100
|
+
``letter + digits`` — exact, being alpha-equivalence), consistently across all
|
|
101
|
+
the axioms of one :func:`kb_to_fol` call, and the minted ones avoid the renamed
|
|
102
|
+
names too. The one variable that is FREE, :func:`concept_to_fol`'s ``var``, is
|
|
103
|
+
the caller's own and so is refused on a clash instead of renamed.
|
|
104
|
+
|
|
105
|
+
Qualified number restrictions
|
|
106
|
+
------------------------------
|
|
107
|
+
``AtLeast``/``AtMost`` (≥n r.C / ≤n r.C — see :mod:`unicode_logic_kit.dl.tableau`'s
|
|
108
|
+
"Qualified number restrictions" section for the tableau side of ALCQ) route through
|
|
109
|
+
the EXISTING :class:`~unicode_logic_kit.fol.nodes.Count` node rather than a new
|
|
110
|
+
counting encoding written here: ``≥n r.C`` becomes ``Count("ge", Number(n), y,
|
|
111
|
+
r(x, y) ∧ π(C, y))`` and ``≤n r.C`` becomes the same with ``"le"``. ``Count`` already
|
|
112
|
+
carries a tested standard "distinct witnesses" first-order expansion
|
|
113
|
+
(:meth:`~unicode_logic_kit.fol.nodes.Count._expand`, bounded at ``n ≤ 500`` — see that
|
|
114
|
+
class's docstring) that :meth:`Count.to_z3` lowers through automatically, so this is
|
|
115
|
+
exactly the independent (Z3-backed) differential oracle
|
|
116
|
+
``tests/test_dl_alcq.py`` cross-checks the tableau against, the same role
|
|
117
|
+
:func:`rbox_to_fol` plays for the RBox extension above. The counting variable ``y``
|
|
118
|
+
is minted by the SAME ``fresh()`` counter as every other restriction (see "Variable
|
|
119
|
+
freshness"), so a number restriction nested inside — or sibling to — an ordinary
|
|
120
|
+
∃/∀ still gets a pairwise-distinct name throughout one top-level call.
|
|
121
|
+
|
|
122
|
+
Multi-role concepts and modal K
|
|
123
|
+
--------------------------------
|
|
124
|
+
ALC is exactly multi-modal K: an ``∃r.C``/``∀r.C`` pair over a *single* role
|
|
125
|
+
``r`` is precisely ``◇C``/``□C`` in ordinary (single-relation) modal K. This
|
|
126
|
+
is what :func:`concept_to_modal` implements, generalising the single-role
|
|
127
|
+
``_to_modal`` test helper in ``tests/test_dl_alc.py`` from a hardcoded role to
|
|
128
|
+
any single role name. It genuinely cannot go further: propositional modal K
|
|
129
|
+
has exactly ONE accessibility relation, so a concept using two or more
|
|
130
|
+
*distinct* role names has no faithful Box/Diamond rendering — encoding role
|
|
131
|
+
identity into, say, an indexed modality (``Knows(Constant(role), …)``) would
|
|
132
|
+
silently reinterpret the DL role as an epistemic agent, which is a different
|
|
133
|
+
logic with different validities (KT45 vs K). :func:`concept_to_modal`
|
|
134
|
+
therefore raises :class:`NotImplementedError` for multi-role concepts and
|
|
135
|
+
points callers at :func:`concept_to_fol`, which has no such limitation — a
|
|
136
|
+
role is just another predicate name, so FOL scales to any number of roles
|
|
137
|
+
for free.
|
|
138
|
+
|
|
139
|
+
GCIs, TBoxes, ABoxes
|
|
140
|
+
---------------------
|
|
141
|
+
:func:`subsumption_to_fol` renders a general concept inclusion ``sub ⊑ sup``
|
|
142
|
+
as its universal closure ``∀x (π(sub, x) → π(sup, x))`` — the standard
|
|
143
|
+
reduction, matching :func:`unicode_logic_kit.dl.tableau.subsumes`'s own
|
|
144
|
+
``sub ⊓ ¬sup`` unsatisfiability reduction (they are interderivable: ``sub ⊑
|
|
145
|
+
sup`` iff ``∀x (π(sub,x) → π(sup,x))`` is valid iff ``π(sub,x) ∧ ¬π(sup,x)``
|
|
146
|
+
is unsatisfiable). :func:`tbox_to_fol` conjoins one such closure per GCI in a
|
|
147
|
+
:class:`unicode_logic_kit.dl.tableau.TBox`; :func:`abox_to_fol` renders a
|
|
148
|
+
:class:`unicode_logic_kit.dl.tableau.ABox`'s concept assertions (``a : C`` ↦
|
|
149
|
+
``π(C, a)`` with the individual ``a`` as a FOL *constant*, not a variable —
|
|
150
|
+
this matters for the ASCII-only Prover9/TPTP exporters, which uppercase
|
|
151
|
+
``Variable`` names into their variable syntax but leave ``Constant`` names
|
|
152
|
+
alone) and role assertions (``(a, b) : r`` ↦ ``r(a, b)``), conjoined
|
|
153
|
+
together. An empty TBox/ABox translates to the same equality tautology used
|
|
154
|
+
for ``⊤`` (over a nullary placeholder constant, since there is no ``x`` in
|
|
155
|
+
scope at that point) — vacuously true, matching "no axioms" / "no
|
|
156
|
+
assertions" imposing no constraint.
|
|
157
|
+
|
|
158
|
+
An individual of any name has a printed text
|
|
159
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
160
|
+
The kit decides predicate-versus-term by the first character's ``isupper()``
|
|
161
|
+
(see ``fol/_identifiers.py``'s "WHY THE FIRST CHARACTER DECIDES PREDICATE VS.
|
|
162
|
+
TERM"), so in the first-order grammar no BARE word is a
|
|
163
|
+
:class:`~unicode_logic_kit.fol.nodes.Constant` whose name starts upper-case, and
|
|
164
|
+
none is one whose name is a variable token (``x``, ``x0``). An OWL individual,
|
|
165
|
+
however, is an IRI or a label, where capitals are the norm — in the OEO
|
|
166
|
+
ontology every one of the 98 ``ObjectHasValue`` fillers and all 8
|
|
167
|
+
identity-axiom individuals start with one. The printer therefore writes such a
|
|
168
|
+
constant in single quotes (:func:`~unicode_logic_kit.fol.constant_text`),
|
|
169
|
+
and the quoted name is the constant of exactly that name: ``Person('Alice')``,
|
|
170
|
+
``'Alice' = 'Bob'`` and ``HasStateOfMatter(x, 'Liquid')`` read back as the
|
|
171
|
+
formulas this module built, an individual named ``x0`` prints ``'x0'`` and is no
|
|
172
|
+
free variable, and an individual whose name has a bare spelling stays bare
|
|
173
|
+
(``Person(alice)``). Until 0.30.0 the bare word was all there was, and the text of
|
|
174
|
+
an upper-case individual did not read back as the same formula, in two different
|
|
175
|
+
ways: in EQUALITY position (``abox_to_fol`` of an ``assert_same``/
|
|
176
|
+
``assert_distinct``, or a ``Nominal``) it did not parse at all, and in ARGUMENT
|
|
177
|
+
position (a role assertion, or a :class:`~unicode_logic_kit.dl.concepts.HasValue`
|
|
178
|
+
image) it parsed in the third-order dialect, as a different formula, with the
|
|
179
|
+
individual read as a ``PredicateTerm``. A role and a class are PREDICATES and
|
|
180
|
+
have no quoted form, so a role spelled in lower case (``hasChild``) is still a
|
|
181
|
+
name that does not read back, and so is the name of a built-in datatype
|
|
182
|
+
(``xsd:integer``); the route that never goes through text — ``dl.abox_consistent``/
|
|
183
|
+
``dl.instance_check`` over the tableau, and ``api.prove`` over the ``kb_to_fol``
|
|
184
|
+
NODES rather than their printed form — is unaffected by either. The cases are
|
|
185
|
+
asserted in ``tests/test_printed_text_reads_back.py``.
|
|
186
|
+
|
|
187
|
+
Inverse roles and nominals (I, O)
|
|
188
|
+
------------------------------------
|
|
189
|
+
Unlike :mod:`unicode_logic_kit.dl.tableau` (which refuses both by name — see
|
|
190
|
+
that module's "Inverse roles and nominals (I, O)" section), the standard
|
|
191
|
+
translation to FOL has no trouble with either, so this module TRANSLATES
|
|
192
|
+
them rather than refusing them:
|
|
193
|
+
|
|
194
|
+
- :class:`~unicode_logic_kit.dl.concepts.InverseRole` (``r⁻``), used as a
|
|
195
|
+
restriction's ``role`` field, swaps the translated role atom's argument
|
|
196
|
+
order: ``∃r⁻.C`` ↦ ``∃y (r(y, x) ∧ π(C, y))`` and ``∀r⁻.C`` ↦
|
|
197
|
+
``∀y (r(y, x) → π(C, y))`` (and likewise for ``AtLeast``/``AtMost``'s
|
|
198
|
+
``Count`` matrix) — exactly the FOL reading of "an r-predecessor", the
|
|
199
|
+
standard translation of an inverse role (Baader et al., *DL Handbook*,
|
|
200
|
+
Ch. 2). This is a one-line change confined to the atom's argument order;
|
|
201
|
+
everything else about the restriction (the quantifier, the ``Count``
|
|
202
|
+
encoding for ``AtLeast``/``AtMost``) is untouched.
|
|
203
|
+
- :class:`~unicode_logic_kit.dl.concepts.Nominal` (``{a}``) ↦ ``x = a``, with
|
|
204
|
+
``a`` rendered as the same kind of term ``concept_to_fol``'s own ``term``
|
|
205
|
+
argument already is (a :class:`~unicode_logic_kit.fol.nodes.Variable` when
|
|
206
|
+
translating a bare concept, a :class:`~unicode_logic_kit.fol.nodes.Constant`
|
|
207
|
+
when translating an ABox assertion via :func:`abox_to_fol` — see "GCIs,
|
|
208
|
+
TBoxes, ABoxes" above for why that distinction matters) — the textbook
|
|
209
|
+
reading of a nominal as "the singleton set named by the individual ``a``"
|
|
210
|
+
(Baader et al., *DL Handbook*, Ch. 2's "individual-generated" theories).
|
|
211
|
+
This is genuinely a NEW predicate-free case, not a reuse of an existing
|
|
212
|
+
branch (unlike ⊤/⊥, which reuse equality — see "Top and Bottom" above —
|
|
213
|
+
a nominal names a SPECIFIC individual, not a tautology/contradiction).
|
|
214
|
+
- :class:`~unicode_logic_kit.dl.concepts.HasValue` (``∃r.{a}``) ↦ the ground
|
|
215
|
+
atom ``r(x, a)``, NOT ``∃y (r(x, y) ∧ y = a)``. The two are the same
|
|
216
|
+
formula: ``y`` occurs only inside that conjunction, so one-point elimination
|
|
217
|
+
of the equality-bounded existential is a FOL validity. The reduced form is
|
|
218
|
+
the one rendered because it mints no variable and puts the individual in the
|
|
219
|
+
argument position OWL's own reading puts it in. The in-house tableau does NOT
|
|
220
|
+
decide a value restriction (it refuses it by name; see "Value restrictions
|
|
221
|
+
(ObjectHasValue)" in :mod:`unicode_logic_kit.dl.tableau`): this image with
|
|
222
|
+
``api.prove``, and ``dl.external_*``, are what decide it.
|
|
223
|
+
|
|
224
|
+
Both directions of this translation are differentially tested (see
|
|
225
|
+
``tests/test_dl_alc.py``): concept satisfiability/entailment against the
|
|
226
|
+
tableau where a genuinely independent second route exists (none does here —
|
|
227
|
+
these constructs are precisely what the tableau refuses — so the check runs
|
|
228
|
+
the OTHER direction instead, against a bounded FOL model finder/solver on the
|
|
229
|
+
emitted formula itself, per this kit's translation-faithfulness convention).
|
|
230
|
+
|
|
231
|
+
:func:`concept_to_modal` (single-role propositional K) is a DIFFERENT story:
|
|
232
|
+
propositional K has no converse modality and no nominal/naming construct at
|
|
233
|
+
all (that is HYBRID logic, a strictly different formalism this kit does not
|
|
234
|
+
implement), so it explicitly REFUSES both, by name, rather than attempt an
|
|
235
|
+
unfaithful encoding — see its own docstring.
|
|
236
|
+
|
|
237
|
+
RBoxes
|
|
238
|
+
------
|
|
239
|
+
:func:`rbox_to_fol` renders a :class:`~unicode_logic_kit.dl.tableau.TBox`'s RBox
|
|
240
|
+
(role inclusions and transitivity declarations — see "Role hierarchies and
|
|
241
|
+
transitive roles (RBox)" in :mod:`unicode_logic_kit.dl.tableau`'s module
|
|
242
|
+
docstring) the same way :func:`tbox_to_fol` renders its concept-level GCIs: a
|
|
243
|
+
role inclusion ``r ⊑ s`` becomes its universal closure ``∀x,y (r(x,y) →
|
|
244
|
+
s(x,y))`` and a transitivity declaration ``Trans(r)`` becomes ``∀x,y,z
|
|
245
|
+
(r(x,y) ∧ r(y,z) → r(x,z))``, all conjoined together. This is the reduction
|
|
246
|
+
the tableau's ∀-rule generalisation (RBox rule H) and ∀+-rule (RBox rule S)
|
|
247
|
+
implement as completion-graph propagation, so ``subsumption_to_fol(C, D)`` being
|
|
248
|
+
entailed by the premises ``tbox_to_fol(tbox, concept_inclusions_only=True)`` and
|
|
249
|
+
``rbox_to_fol(tbox)`` is the independent, structurally unrelated cross-check this
|
|
250
|
+
module's differential test battery runs through the kit's backends against
|
|
251
|
+
``tableau.subsumes(C, D, tbox)``.
|
|
252
|
+
|
|
253
|
+
The data layer: a guarded ONE-sorted image
|
|
254
|
+
--------------------------------------------
|
|
255
|
+
OWL 2 has two disjoint domains, the object domain ``Δ_I`` and the data domain
|
|
256
|
+
``Δ_D`` (see :mod:`unicode_logic_kit.dl.datatypes`). The image of the data layer
|
|
257
|
+
is plain first-order logic with two reserved unary predicates, ``OwlThing`` and
|
|
258
|
+
``OwlData``, NOT the kit's many-sorted logic: a sorted constant must be
|
|
259
|
+
lower-case and every OEO individual and literal is CamelCase, and many-sorted
|
|
260
|
+
logic here is only guards under another syntax (its ``SortedQuantifier`` is
|
|
261
|
+
relativised away on the way to plain FOL) that supplies non-emptiness and
|
|
262
|
+
sub-sorts but not the one thing two-sortedness needs, disjointness.
|
|
263
|
+
|
|
264
|
+
* ``DataExists``/``DataForAll``/``DataAtLeast``/``DataAtMost`` mint a bound
|
|
265
|
+
variable from the SAME minter as every object restriction and guard it with
|
|
266
|
+
the data range's image ``δ(DR, w)``; ``DataHasValue`` is the ground atom
|
|
267
|
+
``d(x, t)`` for the literal's term ``t`` and mints nothing.
|
|
268
|
+
* the data axioms are SIDE axioms, like the role box: ``SubDataPropertyOf``
|
|
269
|
+
``∀x ∀v (d(x, v) → e(x, v))``, ``DataPropertyDomain`` ``∀x ∀v (d(x, v) →
|
|
270
|
+
π(C, x))``, ``DataPropertyRange`` ``∀x ∀v (d(x, v) → δ(DR, v))`` (the
|
|
271
|
+
datatype read as a unary predicate over the one domain — NOT the GCI
|
|
272
|
+
``⊤ ⊑ ∀d.DR``, whose image would carry an ``x = x`` filler),
|
|
273
|
+
``DatatypeDefinition`` ``∀v (D(v) ↔ δ(DR, v))``, and so on.
|
|
274
|
+
* what makes the image TWO-sorted is not in any one axiom and is not part of
|
|
275
|
+
the formula: :func:`data_sort_axioms` derives, from the vocabulary the
|
|
276
|
+
knowledge base actually uses, the separation ``∀t ¬(OwlThing(t) ∧
|
|
277
|
+
OwlData(t))``, the typing of every property and individual, the datatype
|
|
278
|
+
lattice and its disjointness, and the distinctness of distinct literals.
|
|
279
|
+
:func:`kb_to_fol` puts them in ``side_axioms``.
|
|
280
|
+
* ONE consequence is easy to miss, and is why the image of a GCI changes: with
|
|
281
|
+
a non-empty data domain in the theory, ``∀x (π(sub, x) → π(sup, x))`` also
|
|
282
|
+
ranges over data values, and a data value satisfies every antecedent that
|
|
283
|
+
says nothing about it (``⊤``, ``¬A``, ``∀r.C``). ``⊤ ⊑ {a}`` would then
|
|
284
|
+
identify a data value with the individual ``a`` — a contradiction with the
|
|
285
|
+
separation, for a knowledge base OWL 2 finds consistent. So in the two-sorted
|
|
286
|
+
image every GCI is relativised to the object domain, ``∀x (OwlThing(x) ∧
|
|
287
|
+
π(sub, x) → π(sup, x))``, and so is ``Reflexive(r)``. :func:`kb_to_fol` does
|
|
288
|
+
it, and builds the GOAL the same way: :meth:`KnowledgeBaseFOL.subsumption_goal`,
|
|
289
|
+
:meth:`~KnowledgeBaseFOL.unsatisfiability_goal` and
|
|
290
|
+
:meth:`~KnowledgeBaseFOL.instance_goal` relativise to whatever
|
|
291
|
+
:attr:`KnowledgeBaseFOL.separation` says, so a caller never has to know. (For
|
|
292
|
+
a hand-built query ``object_sort=True`` on :func:`subsumption_to_fol` is the
|
|
293
|
+
same relativisation.)
|
|
294
|
+
* the side axioms are derived from the vocabulary the knowledge base uses, so a
|
|
295
|
+
QUESTION that names a datatype, a data property, a literal or an object
|
|
296
|
+
property the knowledge base does not is asked of a weaker theory: the datatype
|
|
297
|
+
lattice, the typing and the literal facts for that name are simply not there.
|
|
298
|
+
``kb_to_fol(tbox, abox, query=[concept, …])`` adds the vocabulary of the
|
|
299
|
+
concepts you are going to ask about to everything derived from the
|
|
300
|
+
vocabulary, and the three goal methods REFUSE, by name, a concept the bundle
|
|
301
|
+
does not cover — the wrong spelling cannot be followed silently.
|
|
302
|
+
* ONE name is ONE predicate, so a name used as both an object property and a
|
|
303
|
+
data property, or as both a class and a datatype, would be conflated into a
|
|
304
|
+
single predicate and change what follows (a functional ``P`` with an object
|
|
305
|
+
successor and a data value came out INCONSISTENT). OWL 2 DL forbids both
|
|
306
|
+
pairs; the image REFUSES them by name (``UnsupportedDatatypeError``, from
|
|
307
|
+
:func:`kb_to_fol`, :func:`data_sort_axioms`, :func:`databox_to_fol` and
|
|
308
|
+
:func:`abox_to_fol`) and asks for one of the two to be renamed. A class, an
|
|
309
|
+
object property and an individual sharing a name (OWL 2 DL punning that IS
|
|
310
|
+
allowed) are accepted, and are THREE symbols in the image: the unary
|
|
311
|
+
predicate ``A(x)`` of the class, the binary predicate ``A(x, y)`` of the
|
|
312
|
+
property and the constant ``A`` of the individual. A route that keys a
|
|
313
|
+
symbol on its kind and arity reads them as three — the TPTP writers do, so
|
|
314
|
+
``api.prove`` with Vampire or E answers over a punned knowledge base.
|
|
315
|
+
* that refusal is per call, over what the call is given, so boxes rendered one
|
|
316
|
+
at a time and conjoined by hand are not seen together by any of them.
|
|
317
|
+
:func:`check_kb_names` runs the same check over several pieces at once — the
|
|
318
|
+
``TBox`` and ``ABox`` the images were built from.
|
|
319
|
+
|
|
320
|
+
The two-sorted image is SOUND — every OWL 2 interpretation expands to a model of
|
|
321
|
+
it, so it entails nothing the knowledge base does not — and deliberately not
|
|
322
|
+
COMPLETE:
|
|
323
|
+
the lattice states the OWL 2 datatype map's subtype and disjointness facts,
|
|
324
|
+
literals are distinct within a family whose lexical-to-value map is injective,
|
|
325
|
+
and an ordering facet is an UNINTERPRETED comparison atom for ``api.prove`` (use
|
|
326
|
+
:mod:`unicode_logic_kit.atp.z3_arith` for arithmetic over the data ranges alone).
|
|
327
|
+
So ``proved`` transfers to OWL 2 and ``refuted`` does NOT, for a knowledge base
|
|
328
|
+
with a data layer: :attr:`KnowledgeBaseFOL.refutation_is_decisive` is ``False``
|
|
329
|
+
exactly then, and its docstring says what each prover status lets a caller
|
|
330
|
+
conclude. The other two separations are NOT sound for a data layer: without the
|
|
331
|
+
restriction of every inclusion to the object domain, ``⊤ ⊑ {a}`` also ranges
|
|
332
|
+
over data values, the premises are stronger than OWL 2's, and an OWL-consistent
|
|
333
|
+
knowledge base can have an inconsistent image. The goal methods of the bundle
|
|
334
|
+
refuse them for that reason.
|
|
335
|
+
|
|
336
|
+
Knowledge bases: the TBox/RBox split is a trap
|
|
337
|
+
-----------------------------------------------
|
|
338
|
+
:class:`~unicode_logic_kit.dl.tableau.TBox` holds BOTH the concept inclusions and the
|
|
339
|
+
RBox, but the FOL image of each lives in its own function, and
|
|
340
|
+
:func:`tbox_to_fol` renders ONLY the concept inclusions. A question proved against its
|
|
341
|
+
output alone is asked of a WEAKER theory — the GCIs hold, but the roles are unrelated
|
|
342
|
+
and none is transitive — so it can come back REFUTED for a subsumption the tableau
|
|
343
|
+
accepts (a false counterexample). The spelling that used to run, with the instance
|
|
344
|
+
that was recorded as a regression test::
|
|
345
|
+
|
|
346
|
+
t = TBox().add_role_inclusion("hasChild", "hasDescendant")
|
|
347
|
+
dl.subsumes(∃hasChild.⊤, ∃hasDescendant.⊤, t) # True
|
|
348
|
+
api.prove(subsumption_to_fol(∃hasChild.⊤, ∃hasDescendant.⊤),
|
|
349
|
+
[tbox_to_fol(t)]) # REFUTED: the role
|
|
350
|
+
# box was dropped
|
|
351
|
+
|
|
352
|
+
so :func:`tbox_to_fol` now RAISES :class:`RoleBoxOmittedError` for a TBox carrying a
|
|
353
|
+
role box, unless the caller writes ``concept_inclusions_only=True`` — the only way
|
|
354
|
+
to say "yes, I know the role box is not in this formula". The supported entry point
|
|
355
|
+
for a whole knowledge base is :func:`kb_to_fol`, which returns the knowledge base
|
|
356
|
+
and the role-box image SEPARATELY (a :class:`KnowledgeBaseFOL`), following the kit's
|
|
357
|
+
side-axiom convention (:class:`~unicode_logic_kit.comorphism.TranslationResult`): the
|
|
358
|
+
side axioms are never conjoined into the main formula, the caller passes them as
|
|
359
|
+
``premises`` (see ``Comorphism.side_axioms`` for why folding them in is wrong).
|
|
360
|
+
|
|
361
|
+
The other renderers have no analogous silent drop, because none of them is handed a
|
|
362
|
+
TBox: :func:`concept_to_fol`, :func:`subsumption_to_fol` and :func:`abox_to_fol`
|
|
363
|
+
render exactly what they are given, which makes each of them an answer about the
|
|
364
|
+
EMPTY knowledge base (no GCIs, no role box). That is the right reading for a bare
|
|
365
|
+
concept or an ABox-only question, and the wrong one for any question *relative to*
|
|
366
|
+
a knowledge base — there the TBox image and the role-box image must be premises
|
|
367
|
+
too, which is what :class:`KnowledgeBaseFOL` bundles.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
import re
|
|
371
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
372
|
+
from typing import FrozenSet, Iterable, List, Optional, Sequence, Set, Tuple
|
|
373
|
+
|
|
374
|
+
from ..fol.nodes import (
|
|
375
|
+
Node, Variable, Constant, Number, Atom,
|
|
376
|
+
Not as _FNot, And as _FAnd, Or as _FOr, Implies, Iff, Quantifier,
|
|
377
|
+
Box, Diamond, Count,
|
|
378
|
+
)
|
|
379
|
+
from ..fol._identifiers import fresh_variables, fresh_variable_like, variable_pattern
|
|
380
|
+
from ..fol._msfl_nodes import key_text
|
|
381
|
+
from .concepts import (
|
|
382
|
+
Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
|
|
383
|
+
InverseRole, Nominal, HasValue, DataExists, DataForAll, DataHasValue,
|
|
384
|
+
DataAtLeast, DataAtMost, DATA_CONCEPTS,
|
|
385
|
+
)
|
|
386
|
+
from .datatypes import (
|
|
387
|
+
OWL_DATA, OWL_THING, DataRange, Literal, UnsupportedDatatypeError,
|
|
388
|
+
canonical_datatype_name, datarange_literals, datarange_datatypes,
|
|
389
|
+
datarange_to_fol, datatype_ancestors, datatype_family,
|
|
390
|
+
EXACT_NUMBER_DATATYPES,
|
|
391
|
+
)
|
|
392
|
+
from .tableau import (
|
|
393
|
+
TBox, ABox, _AXIOM_KINDS, _abox_individual_names, _reject_abox_roles,
|
|
394
|
+
_reject_concept_role, _validate_data_box, _validate_role_box,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
__all__ = [
|
|
398
|
+
"concept_to_fol", "subsumption_to_fol", "tbox_to_fol", "abox_to_fol",
|
|
399
|
+
"rbox_to_fol", "databox_to_fol", "data_sort_axioms", "check_kb_names",
|
|
400
|
+
"kb_to_fol", "KnowledgeBaseFOL", "SideAxiom", "RoleBoxOmittedError",
|
|
401
|
+
"concept_to_modal",
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# --------------------------------------------------------------------------- #
|
|
406
|
+
# Standard translation to FOL.
|
|
407
|
+
# --------------------------------------------------------------------------- #
|
|
408
|
+
|
|
409
|
+
def _individual_names(concept) -> set:
|
|
410
|
+
"""Every individual name ``_translate`` will render as a ``Constant``.
|
|
411
|
+
|
|
412
|
+
Two concept kinds carry one: a :class:`~unicode_logic_kit.dl.concepts.Nominal`
|
|
413
|
+
(``{a}`` becomes ``term = a``) and a
|
|
414
|
+
:class:`~unicode_logic_kit.dl.concepts.HasValue` (``∃r.{a}`` becomes
|
|
415
|
+
``r(term, a)``). Roles and atomic concepts become predicates, which live in
|
|
416
|
+
their own namespace. The RECURSION is generic over the dataclass fields so
|
|
417
|
+
a new concept type with a nested concept is reached without being named
|
|
418
|
+
here — but the ``individual`` FIELD is not, and a kind missed here fails
|
|
419
|
+
SILENTLY: the avoid set handed to :func:`_fresh_var_factory` would be
|
|
420
|
+
incomplete, so a minted bound variable could share a name with the
|
|
421
|
+
individual and CAPTURE it in a target that writes a variable and a constant
|
|
422
|
+
of one name as one symbol (``Variable("x0")`` is the unicode text ``x0`` and
|
|
423
|
+
``Constant("x0")`` is ``'x0'``, and they are two symbols to Z3, but a target
|
|
424
|
+
with one namespace for both would read them as one: see "Bound variables
|
|
425
|
+
never share a name with an individual").
|
|
426
|
+
``tests/test_dl_has_value.py`` has the regression for exactly that, with a
|
|
427
|
+
HasValue individual named ``"x0"``.
|
|
428
|
+
"""
|
|
429
|
+
names = set()
|
|
430
|
+
stack = [concept]
|
|
431
|
+
while stack:
|
|
432
|
+
node = stack.pop()
|
|
433
|
+
if isinstance(node, (Nominal, HasValue)):
|
|
434
|
+
names.add(node.individual)
|
|
435
|
+
if is_dataclass(node):
|
|
436
|
+
for field in fields(node):
|
|
437
|
+
value = getattr(node, field.name)
|
|
438
|
+
if isinstance(value, Concept):
|
|
439
|
+
stack.append(value)
|
|
440
|
+
return names
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _fresh_var_factory(base: str, avoid=()):
|
|
444
|
+
"""Return a callable producing variable names distinct from ``base``, from
|
|
445
|
+
``avoid``, and from every name it has already produced (see "Variable
|
|
446
|
+
freshness" above).
|
|
447
|
+
|
|
448
|
+
The names come from :func:`~unicode_logic_kit.fol._identifiers.fresh_variables`
|
|
449
|
+
so that they are names the kit's own parser reads back; ``base`` only picks
|
|
450
|
+
the letter (and is itself excluded), because it may be an ABox individual's
|
|
451
|
+
name and so no legal variable at all. ``avoid`` is the individual names the
|
|
452
|
+
translation will render as constants (:func:`_individual_names`): a bound
|
|
453
|
+
variable that happened to share a name with one of them would CAPTURE it in
|
|
454
|
+
a target that writes ``Variable("a0")`` and ``Constant("a0")`` as one symbol
|
|
455
|
+
(the unicode text and Z3 keep the two apart, a target with one namespace for
|
|
456
|
+
both would not), so the nominal would stop denoting its own individual.
|
|
457
|
+
"""
|
|
458
|
+
letter = base[:1] if re.fullmatch(variable_pattern(), base[:1] or " ") else "x"
|
|
459
|
+
used = {base} | set(avoid)
|
|
460
|
+
|
|
461
|
+
def fresh() -> str:
|
|
462
|
+
name = fresh_variables(1, letter=letter, avoid=used)[0]
|
|
463
|
+
used.add(name)
|
|
464
|
+
return name
|
|
465
|
+
|
|
466
|
+
return fresh
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _kb_individual_names(tbox: Optional[TBox], abox: Optional[ABox]) -> Set[str]:
|
|
470
|
+
"""Every individual name of the whole knowledge base ``(tbox, abox)`` — the
|
|
471
|
+
set no BOUND variable of its image may share a name with.
|
|
472
|
+
|
|
473
|
+
It is :attr:`_Vocabulary.individuals`, the one walk that already reaches
|
|
474
|
+
every place a name can occur: the assertion positions of the ABox AND the
|
|
475
|
+
fillers of every nominal and value restriction in a concept inclusion, a
|
|
476
|
+
domain or range axiom, a data property domain or an ABox class assertion.
|
|
477
|
+
(:attr:`KnowledgeBaseFOL.individuals` deliberately lists only the ABox's
|
|
478
|
+
own, but a filler is a ``Constant`` of the image all the same.) Reusing it
|
|
479
|
+
keeps ONE definition of "an individual of this knowledge base" instead of a
|
|
480
|
+
second hand-written walk that could drift from it.
|
|
481
|
+
"""
|
|
482
|
+
return set(_collect_vocabulary(tbox, abox).individuals)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _binder_names(preferred: Sequence[str], avoid: Iterable[str]) -> Tuple[str, ...]:
|
|
486
|
+
"""The bound-variable names to use for the prefix variables ``preferred``,
|
|
487
|
+
given the individual names ``avoid`` the image renders as ``Constant``\\ s.
|
|
488
|
+
|
|
489
|
+
A name that clashes with an individual is RENAMED, never refused: a bound
|
|
490
|
+
variable called ``x`` and a constant called ``x`` are two symbols to Z3 and
|
|
491
|
+
two texts in the unicode syntax (``x`` and ``'x'``), but one symbol in a
|
|
492
|
+
target with a single namespace for both, and when Z3 took them for one,
|
|
493
|
+
``∃r.{x} ⊑ A`` printed ``∀x (r(x, x) → A(x))`` —
|
|
494
|
+
the quantifier captured the individual — and the knowledge base
|
|
495
|
+
``∃r.{x} ⊑ ⊥`` with ``r(a, a)``, ``a ≠ x`` (which only forbids an r-edge
|
|
496
|
+
INTO the individual ``x``) was reported inconsistent. Renaming a bound
|
|
497
|
+
variable is exact (alpha-equivalence), so a name that does not clash is
|
|
498
|
+
returned untouched and every image that never named an individual
|
|
499
|
+
``x``/``y``/``z`` is byte-for-byte what it was.
|
|
500
|
+
|
|
501
|
+
The replacement is :func:`~unicode_logic_kit.fol._identifiers.fresh_variable_like`
|
|
502
|
+
— the first free ``letter + digits`` — and avoids every individual, every
|
|
503
|
+
OTHER preferred name and every replacement already made, so two prefix
|
|
504
|
+
variables never collapse onto one. A caller that goes on to mint further
|
|
505
|
+
variables must hand THESE names to :func:`_fresh_var_factory`, not the
|
|
506
|
+
preferred ones: ``x`` renamed to ``x1`` leaves ``x`` itself free for the
|
|
507
|
+
minter, which would otherwise be allowed to reuse it.
|
|
508
|
+
"""
|
|
509
|
+
avoid = set(avoid)
|
|
510
|
+
taken = set(preferred)
|
|
511
|
+
chosen: List[str] = []
|
|
512
|
+
for name in preferred:
|
|
513
|
+
if name in avoid:
|
|
514
|
+
name = fresh_variable_like(name, avoid | taken)
|
|
515
|
+
taken.add(name)
|
|
516
|
+
chosen.append(name)
|
|
517
|
+
return tuple(chosen)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _role_atom(role, x: Node, y: Node) -> Node:
|
|
521
|
+
"""Render the role atom for an ``x —role→ y`` edge: ``role(x, y)``, or — when
|
|
522
|
+
``role`` is an :class:`~unicode_logic_kit.dl.concepts.InverseRole` — ``role(y,
|
|
523
|
+
x)`` (see the module docstring's "Inverse roles and nominals (I, O)"
|
|
524
|
+
section: the standard translation of ``r⁻`` swaps the atom's argument order,
|
|
525
|
+
nothing else).
|
|
526
|
+
"""
|
|
527
|
+
if isinstance(role, InverseRole):
|
|
528
|
+
return Atom(role.role, (y, x))
|
|
529
|
+
return Atom(role, (x, y))
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _t_top(concept, term, fresh) -> Node:
|
|
533
|
+
return Atom("=", (term, term))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _t_bottom(concept, term, fresh) -> Node:
|
|
537
|
+
return Atom("≠", (term, term))
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _t_atomic(concept, term, fresh) -> Node:
|
|
541
|
+
return Atom(concept.name, (term,))
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _t_nominal(concept, term, fresh) -> Node:
|
|
545
|
+
return Atom("=", (term, Constant(concept.individual)))
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _t_has_value(concept, term, fresh) -> Node:
|
|
549
|
+
"""``π(∃r.{a}, t)`` as the ground atom ``r(t, a)`` — see
|
|
550
|
+
:class:`~unicode_logic_kit.dl.concepts.HasValue` for the one-point
|
|
551
|
+
elimination that makes this the same formula as ``∃y (r(t, y) ∧ y = a)``.
|
|
552
|
+
|
|
553
|
+
``_role_atom``, not ``Atom`` by hand, so an
|
|
554
|
+
:class:`~unicode_logic_kit.dl.concepts.InverseRole`-valued role gets the
|
|
555
|
+
swapped argument order ``r(a, t)`` for free; ``Constant``, not
|
|
556
|
+
``Variable``, exactly as :func:`abox_to_fol` renders an ABox individual,
|
|
557
|
+
which is what the ASCII Prover9/TPTP exporters need to tell a name from a
|
|
558
|
+
bound variable. No variable is minted, so ``fresh`` is unused.
|
|
559
|
+
"""
|
|
560
|
+
return _role_atom(concept.role, term, Constant(concept.individual))
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _t_not(concept, term, fresh) -> Node:
|
|
564
|
+
return _FNot(_translate(concept.concept, term, fresh))
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _t_and(concept, term, fresh) -> Node:
|
|
568
|
+
return _FAnd(_translate(concept.left, term, fresh),
|
|
569
|
+
_translate(concept.right, term, fresh))
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _t_or(concept, term, fresh) -> Node:
|
|
573
|
+
return _FOr(_translate(concept.left, term, fresh),
|
|
574
|
+
_translate(concept.right, term, fresh))
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _t_exists(concept, term, fresh) -> Node:
|
|
578
|
+
w = Variable(fresh())
|
|
579
|
+
return Quantifier("∃", w, _FAnd(
|
|
580
|
+
_role_atom(concept.role, term, w), _translate(concept.concept, w, fresh)))
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _t_forall(concept, term, fresh) -> Node:
|
|
584
|
+
w = Variable(fresh())
|
|
585
|
+
return Quantifier("∀", w, Implies(
|
|
586
|
+
_role_atom(concept.role, term, w), _translate(concept.concept, w, fresh)))
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _t_count(concept, term, fresh) -> Node:
|
|
590
|
+
w = Variable(fresh())
|
|
591
|
+
matrix = _FAnd(_role_atom(concept.role, term, w),
|
|
592
|
+
_translate(concept.concept, w, fresh))
|
|
593
|
+
op = "ge" if isinstance(concept, AtLeast) else "le"
|
|
594
|
+
return Count(op, Number(concept.n), w, matrix)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _t_data_exists(concept, term, fresh) -> Node:
|
|
598
|
+
"""``π(∃d.DR, t)`` = ``∃w (d(t, w) ∧ δ(DR, w))``: the exact mirror of
|
|
599
|
+
:func:`_t_exists` with a data atom, the bound variable minted from the SAME
|
|
600
|
+
factory so it is pairwise distinct from every object restriction's."""
|
|
601
|
+
w = Variable(fresh())
|
|
602
|
+
return Quantifier("∃", w, _FAnd(
|
|
603
|
+
Atom(concept.prop, (term, w)), datarange_to_fol(concept.datarange, w)))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _t_data_forall(concept, term, fresh) -> Node:
|
|
607
|
+
"""``π(∀d.DR, t)`` = ``∀w (d(t, w) → δ(DR, w))``."""
|
|
608
|
+
w = Variable(fresh())
|
|
609
|
+
return Quantifier("∀", w, Implies(
|
|
610
|
+
Atom(concept.prop, (term, w)), datarange_to_fol(concept.datarange, w)))
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _t_data_has_value(concept, term, fresh) -> Node:
|
|
614
|
+
"""``π(DataHasValue(d lt), t)`` = the ground atom ``d(t, t_lt)``. A literal
|
|
615
|
+
denotes ONE fixed data value (OWL 2 §2.3.4: ``{ x | (x, lt^LT) ∈ d^DP }``),
|
|
616
|
+
so there is no quantifier, no bound variable and no datatype guard — the
|
|
617
|
+
literal already pins its datatype. ``fresh`` is unused."""
|
|
618
|
+
return Atom(concept.prop, (term, concept.value.to_term()))
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _t_data_count(concept, term, fresh) -> Node:
|
|
622
|
+
"""``≥n d.DR`` / ``≤n d.DR`` through the SAME ``Count`` node the object
|
|
623
|
+
cardinalities use, so ``Count._expand`` and ``Count.to_z3`` carry it."""
|
|
624
|
+
w = Variable(fresh())
|
|
625
|
+
matrix = _FAnd(Atom(concept.prop, (term, w)), datarange_to_fol(concept.datarange, w))
|
|
626
|
+
op = "ge" if isinstance(concept, DataAtLeast) else "le"
|
|
627
|
+
return Count(op, Number(concept.n), w, matrix)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# One entry per concrete concept class. A chain of `isinstance` branches was the
|
|
631
|
+
# single worst merge point in this file -- every new class expression kind added
|
|
632
|
+
# one branch in the middle of one function -- so the dispatch is a table a new
|
|
633
|
+
# kind appends ONE line to, keyed by exact type (dl.concepts' concept classes
|
|
634
|
+
# are all concrete frozen dataclasses with no subclassing, so exact-type lookup
|
|
635
|
+
# is total: an unlisted type is a genuine programming error, reported as such).
|
|
636
|
+
_TRANSLATORS = {
|
|
637
|
+
Top: _t_top,
|
|
638
|
+
Bottom: _t_bottom,
|
|
639
|
+
Atomic: _t_atomic,
|
|
640
|
+
Nominal: _t_nominal,
|
|
641
|
+
HasValue: _t_has_value,
|
|
642
|
+
Not: _t_not,
|
|
643
|
+
And: _t_and,
|
|
644
|
+
Or: _t_or,
|
|
645
|
+
Exists: _t_exists,
|
|
646
|
+
ForAll: _t_forall,
|
|
647
|
+
AtLeast: _t_count,
|
|
648
|
+
AtMost: _t_count,
|
|
649
|
+
DataExists: _t_data_exists,
|
|
650
|
+
DataForAll: _t_data_forall,
|
|
651
|
+
DataHasValue: _t_data_has_value,
|
|
652
|
+
DataAtLeast: _t_data_count,
|
|
653
|
+
DataAtMost: _t_data_count,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _translate(concept: Concept, term: Node, fresh) -> Node:
|
|
658
|
+
"""Render ``concept`` as a FOL formula with ``term`` (a Variable or
|
|
659
|
+
Constant) standing for the individual under discussion; ``fresh`` mints
|
|
660
|
+
the bound variable for each nested restriction (see :func:`_fresh_var_factory`).
|
|
661
|
+
"""
|
|
662
|
+
handler = _TRANSLATORS.get(type(concept))
|
|
663
|
+
if handler is None:
|
|
664
|
+
raise TypeError(f"concept_to_fol: unsupported concept {type(concept).__name__}")
|
|
665
|
+
# An OWL 2 built-in property name (or ``=``/``≠``) as THIS node's role is
|
|
666
|
+
# refused by name, not rendered as the ordinary predicate of that name: the
|
|
667
|
+
# universal/empty property is not one, and the image of ``∃owl:bottom…C``
|
|
668
|
+
# as an uninterpreted atom is satisfiable where the restriction is not.
|
|
669
|
+
# Here, in the one dispatch every concept passes through, so no handler has
|
|
670
|
+
# to remember it.
|
|
671
|
+
_reject_concept_role(concept, where="dl.translate")
|
|
672
|
+
return handler(concept, term, fresh)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def concept_to_fol(concept: Concept, var: str = "x") -> Node:
|
|
676
|
+
"""Render ``concept`` as a FOL formula ``π(concept, var)`` with one free
|
|
677
|
+
variable ``var`` (see the module docstring for the translation rules).
|
|
678
|
+
|
|
679
|
+
``concept`` is satisfiable (:func:`unicode_logic_kit.dl.tableau.concept_satisfiable`)
|
|
680
|
+
iff ``∃var π(concept, var)`` is FOL-satisfiable — wrap the result in a
|
|
681
|
+
``Quantifier("∃", Variable(var), …)`` to test that directly (e.g. via
|
|
682
|
+
:func:`unicode_logic_kit.is_satisfiable`).
|
|
683
|
+
|
|
684
|
+
This is satisfiability w.r.t. the EMPTY knowledge base: nothing about any
|
|
685
|
+
TBox or RBox is in the formula. For ``concept_satisfiable(concept, tbox)``
|
|
686
|
+
pass ``kb_to_fol(tbox).tbox_premises`` as the premises of the query (see
|
|
687
|
+
:func:`kb_to_fol`); the role box in particular is NOT implied by anything
|
|
688
|
+
here.
|
|
689
|
+
|
|
690
|
+
A concept with a DATA restriction (``DataExists`` and its four siblings) is
|
|
691
|
+
NOT answered by this formula closed under ``∃``: the formula says nothing
|
|
692
|
+
about the data domain, so a data value satisfies the existential and
|
|
693
|
+
``∃HasV.xsd:integer ⊓ ∀HasV.xsd:string`` would come out satisfiable, where
|
|
694
|
+
OWL 2 makes it unsatisfiable (the two datatypes are disjoint). Build the
|
|
695
|
+
knowledge base with ``kb_to_fol(tbox, abox, query=[concept])`` and ask
|
|
696
|
+
``kb.unsatisfiability_goal(concept)``, which carries the sort and datatype
|
|
697
|
+
axioms the question needs. The registry edge ``alc → fol`` refuses such a
|
|
698
|
+
concept for exactly that reason; this function renders it, because the
|
|
699
|
+
knowledge-base images are built from it.
|
|
700
|
+
|
|
701
|
+
Raises:
|
|
702
|
+
ValueError: ``var`` is the name of an individual the concept itself
|
|
703
|
+
names (a nominal or value restriction). ``var`` is the one FREE
|
|
704
|
+
variable of the result — the caller quantifies over it — so it
|
|
705
|
+
cannot be renamed behind the caller's back (every other bound
|
|
706
|
+
variable of an image is renamed to avoid a clash, see
|
|
707
|
+
:func:`_binder_names`); and a target that writes ``Variable("x")``
|
|
708
|
+
and ``Constant("x")`` as one symbol (Z3 and the unicode text do
|
|
709
|
+
not) would let ``∃x`` over ``π(∃r.{x}, x)`` bind
|
|
710
|
+
the individual. Pass another ``var``.
|
|
711
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
712
|
+
a restriction's role is an OWL 2 built-in property
|
|
713
|
+
name or ``=``/``≠`` (see :func:`~unicode_logic_kit.dl.tableau.reserved_role`):
|
|
714
|
+
an uninterpreted predicate of that name would be a different
|
|
715
|
+
restriction.
|
|
716
|
+
"""
|
|
717
|
+
_check_concepts_punning((concept,), "dl.concept_to_fol")
|
|
718
|
+
names = _individual_names(concept)
|
|
719
|
+
if var in names:
|
|
720
|
+
raise ValueError(
|
|
721
|
+
f"dl.concept_to_fol: the free variable {var!r} has the same name as "
|
|
722
|
+
f"an individual this concept names (a nominal or a value "
|
|
723
|
+
f"restriction). The unicode text ({var} against {var!r}) and Z3 "
|
|
724
|
+
f"keep a variable and a constant of one name apart, but a target "
|
|
725
|
+
f"that writes both as one symbol would let whatever quantifies "
|
|
726
|
+
f"{var!r} bind the individual, and the image is kept free of that "
|
|
727
|
+
f"clash for every target. Pass var= another name, "
|
|
728
|
+
f"e.g. var={fresh_variable_like(var, names | {var})!r}.")
|
|
729
|
+
return _translate(concept, Variable(var), _fresh_var_factory(var, names))
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def subsumption_to_fol(sub: Concept, sup: Concept, var: str = "x", *,
|
|
733
|
+
object_sort: bool = False) -> Node:
|
|
734
|
+
"""Render the GCI ``sub ⊑ sup`` as its universal closure
|
|
735
|
+
``∀var (π(sub, var) → π(sup, var))``.
|
|
736
|
+
|
|
737
|
+
``object_sort=True`` relativises the closure to the OBJECT domain,
|
|
738
|
+
``∀var (OwlThing(var) ∧ π(sub, var) → π(sup, var))`` — the form a GCI takes
|
|
739
|
+
in the two-sorted image of a knowledge base that has a data layer (see
|
|
740
|
+
"The data layer" in the module docstring: unrelativised, the quantifier
|
|
741
|
+
also ranges over data values, which satisfy every antecedent that says
|
|
742
|
+
nothing about them). It is what a GOAL must look like to be the question
|
|
743
|
+
:func:`kb_to_fol` ``(…, separation="two-sorted")`` poses. The default is
|
|
744
|
+
the plain closure, byte-identical to every release before the data layer.
|
|
745
|
+
|
|
746
|
+
A goal asked of a knowledge base is better built by the knowledge base:
|
|
747
|
+
:meth:`KnowledgeBaseFOL.subsumption_goal` picks the relativisation that
|
|
748
|
+
fits ``kb.separation`` and refuses a concept whose vocabulary the bundle's
|
|
749
|
+
side axioms do not cover. On a TWO-sorted bundle the plain default spelling
|
|
750
|
+
answers ``refuted`` for a pure object-level subsumption that holds
|
|
751
|
+
(``A ⊑ B``, ``B ⊑ C`` entail ``A ⊑ C``, and the unrelativised goal asks
|
|
752
|
+
about data values too) — silently, which is why the methods exist.
|
|
753
|
+
|
|
754
|
+
``sub ⊑ sup`` holds (:func:`unicode_logic_kit.dl.tableau.subsumes`) iff this
|
|
755
|
+
formula is FOL-valid — check via :func:`unicode_logic_kit.is_valid`.
|
|
756
|
+
|
|
757
|
+
"Valid" is subsumption w.r.t. the EMPTY knowledge base. Subsumption relative
|
|
758
|
+
to a TBox (``subsumes(sub, sup, tbox)``) is NOT ``is_valid`` of this formula
|
|
759
|
+
and NOT ``Implies(tbox_to_fol(tbox), …)`` either — the latter drops the role
|
|
760
|
+
box. Use ``api.prove(subsumption_to_fol(sub, sup), kb_to_fol(tbox).tbox_premises)``
|
|
761
|
+
(see :func:`kb_to_fol`).
|
|
762
|
+
|
|
763
|
+
``var`` names the variable the closure binds, and it is a PREFERENCE: when
|
|
764
|
+
``sub`` or ``sup`` names an individual (a nominal, a value restriction) of
|
|
765
|
+
that very name, the bound variable is renamed to the first free ``letter +
|
|
766
|
+
digits`` (``∃r.{x} ⊑ A`` prints ``∀x0 (r(x0, 'x') → A(x0))``, not the
|
|
767
|
+
``∀x (r(x, 'x') → A(x))`` that a target with one namespace for variables
|
|
768
|
+
and constants would read as a capture). The quantifier binds the variable, so
|
|
769
|
+
the rename is exact — the contrast with :func:`concept_to_fol`, whose
|
|
770
|
+
``var`` is FREE and is refused instead.
|
|
771
|
+
|
|
772
|
+
Raises:
|
|
773
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
774
|
+
a restriction's role is an OWL 2 built-in property
|
|
775
|
+
name or ``=``/``≠``.
|
|
776
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
777
|
+
``sub`` and ``sup`` together use one name as
|
|
778
|
+
two kinds OWL 2 DL keeps apart — an object property and a data
|
|
779
|
+
property, or a class and a datatype (``∃P.B ⊓ ∃P.xsd:integer``): the
|
|
780
|
+
image has one predicate per name. The refusal is per call, over
|
|
781
|
+
what the call is given; whether a name clashes with the knowledge
|
|
782
|
+
base the goal is asked of is checked by the bundle's own goal
|
|
783
|
+
methods (:meth:`KnowledgeBaseFOL.subsumption_goal`).
|
|
784
|
+
"""
|
|
785
|
+
_check_concepts_punning((sub, sup), "dl.subsumption_to_fol")
|
|
786
|
+
return _subsumption_image(sub, sup, var, object_sort, ())
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _subsumption_image(sub: Concept, sup: Concept, var: str, object_sort: bool,
|
|
790
|
+
avoid: Iterable[str]) -> Node:
|
|
791
|
+
""":func:`subsumption_to_fol`, with ``avoid`` — the individual names of the
|
|
792
|
+
WHOLE knowledge base the GCI is part of — added to the two concepts' own.
|
|
793
|
+
|
|
794
|
+
The prefix variable is bound by the closure, so a ``var`` that clashes with
|
|
795
|
+
an individual is renamed (:func:`_binder_names`) rather than left to capture
|
|
796
|
+
it, and the variables the two translations mint avoid the RENAMED name."""
|
|
797
|
+
avoid = set(avoid) | _individual_names(sub) | _individual_names(sup)
|
|
798
|
+
(var,) = _binder_names((var,), avoid)
|
|
799
|
+
v = Variable(var)
|
|
800
|
+
antecedent = _translate(sub, v, _fresh_var_factory(var, avoid))
|
|
801
|
+
consequent = _translate(sup, v, _fresh_var_factory(var, avoid))
|
|
802
|
+
if object_sort:
|
|
803
|
+
antecedent = _FAnd(Atom(OWL_THING, (v,)), antecedent)
|
|
804
|
+
return Quantifier("∀", v, Implies(antecedent, consequent))
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
# The constant the empty-knowledge-base tautology is built from. ``c_`` plus a
|
|
808
|
+
# word is the kit's CONSTANT terminal (fol._identifiers.constant_pattern), so
|
|
809
|
+
# the text this prints reads back through api.parse_any as the SAME formula.
|
|
810
|
+
# Until 0.30.0 this was Constant("_"), which printed `_ = _` — text the kit's
|
|
811
|
+
# own parser rejects ("Unexpected character '_'"), so an RBox-only or empty
|
|
812
|
+
# knowledge base violated the "everything printed reads back" property that
|
|
813
|
+
# tests/test_printed_text_reads_back.py exists to hold (measured on the OEO
|
|
814
|
+
# fragment: 150 of 3636 accepted axioms printed an unreadable image for exactly
|
|
815
|
+
# this reason, every one of them an RBox-only knowledge base whose `formula` is
|
|
816
|
+
# this tautology). `c = c` is valid whatever `c` denotes, so a knowledge base
|
|
817
|
+
# that happens to name an individual `c_tautology` is still rendered correctly
|
|
818
|
+
# — the same argument concept_to_modal's reserved `_MODAL_TRUE_ATOM` makes.
|
|
819
|
+
# (A constant of any other name has a text too, since the printer quotes what
|
|
820
|
+
# no bare word spells: `Constant("_")` would print `'_' = '_'`, which reads
|
|
821
|
+
# back. The bare `c_tautology` keeps the text of 0.30.0 for every image that
|
|
822
|
+
# holds the tautology.)
|
|
823
|
+
_TAUTOLOGY_CONSTANT = Constant("c_tautology")
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _tautology() -> Node:
|
|
827
|
+
"""A closed, genuinely valid formula — the FOL image of "no constraint"
|
|
828
|
+
(an empty TBox or ABox). See "GCIs, TBoxes, ABoxes" in the module docstring.
|
|
829
|
+
"""
|
|
830
|
+
return Atom("=", (_TAUTOLOGY_CONSTANT, _TAUTOLOGY_CONSTANT))
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _conjoin(parts: List[Node]) -> Node:
|
|
834
|
+
"""Fold a (possibly empty) list of formulas into a single conjunction,
|
|
835
|
+
BALANCED: depth ``ceil(log2 n)`` rather than ``n``.
|
|
836
|
+
|
|
837
|
+
A left fold — what this did until 0.30.0 — makes the conjunction's depth
|
|
838
|
+
equal the number of axioms, and the kit's node operations are recursive, so
|
|
839
|
+
a real ontology's image could be built but not used. Measured on this tree
|
|
840
|
+
with the default ``sys.getrecursionlimit() == 1000``: a TBox of 495 concept
|
|
841
|
+
inclusions printed, 496 raised ``RecursionError`` from the shared printer,
|
|
842
|
+
and at 1000 conjuncts EVERY operation failed — ``to_unicode_str``,
|
|
843
|
+
``to_dict``, ``to_tptp``, ``to_prover9``, ``to_z3``, and ``__eq__`` /
|
|
844
|
+
``__hash__``, the last two generated by ``@dataclass`` and so beyond any
|
|
845
|
+
printer fix. The OEO fragment (3091 concept inclusions) was already over
|
|
846
|
+
that line. Only reducing the TREE's depth fixes all seven at once; raising
|
|
847
|
+
the recursion limit would trade a clean exception for a segfault.
|
|
848
|
+
|
|
849
|
+
A conjunction is associative, so this is the SAME formula up to
|
|
850
|
+
associativity: identical models, and ``_formula_alpha_equal`` unaffected.
|
|
851
|
+
What DOES change is printed parenthesisation for three or more conjuncts
|
|
852
|
+
(``And(And(P, Q), R)`` prints ``P ∧ Q ∧ R``; the balanced
|
|
853
|
+
``And(And(P, Q), And(R, S))`` prints ``P ∧ Q ∧ (R ∧ S)``) — identical for
|
|
854
|
+
0, 1 and 2 conjuncts, which is every pinned string in the suite.
|
|
855
|
+
"""
|
|
856
|
+
if not parts:
|
|
857
|
+
return _tautology()
|
|
858
|
+
level = list(parts)
|
|
859
|
+
while len(level) > 1:
|
|
860
|
+
level = [_FAnd(level[i], level[i + 1]) if i + 1 < len(level) else level[i]
|
|
861
|
+
for i in range(0, len(level), 2)]
|
|
862
|
+
return level[0]
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _side_axiom_census(tbox: TBox) -> str:
|
|
866
|
+
"""``"2 SubObjectPropertyOf axiom(s), 1 TransitiveObjectProperty axiom(s)"``
|
|
867
|
+
— the side axioms ``tbox`` carries, named by their OWL keyword.
|
|
868
|
+
|
|
869
|
+
Derived from :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`' ``part ==
|
|
870
|
+
"side"`` rows, so a side-axiom kind added later names itself in
|
|
871
|
+
:class:`RoleBoxOmittedError`'s message without anyone editing the message.
|
|
872
|
+
"""
|
|
873
|
+
parts = []
|
|
874
|
+
for row in _AXIOM_KINDS:
|
|
875
|
+
if row.holder != "tbox" or row.part != "side":
|
|
876
|
+
continue
|
|
877
|
+
count = len(getattr(tbox, row.field))
|
|
878
|
+
if count:
|
|
879
|
+
parts.append(f"{count} {row.kind} axiom(s)")
|
|
880
|
+
return ", ".join(parts)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _carries_data_box(tbox: TBox) -> bool:
|
|
884
|
+
"""True iff ``tbox`` stores an axiom of a ``layer == "data"`` side kind (a
|
|
885
|
+
data-box axiom) -- the ones :func:`databox_to_fol`, not :func:`rbox_to_fol`,
|
|
886
|
+
renders. Derived from :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`, like
|
|
887
|
+
:func:`_side_axiom_census`, so a data-box kind added later is named too."""
|
|
888
|
+
return any(row.holder == "tbox" and row.part == "side" and row.layer == "data"
|
|
889
|
+
and getattr(tbox, row.field) for row in _AXIOM_KINDS)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
class RoleBoxOmittedError(ValueError):
|
|
893
|
+
"""Raised by :func:`tbox_to_fol` when the TBox carries a role box and the
|
|
894
|
+
caller did not say that rendering only the concept inclusions is intended.
|
|
895
|
+
|
|
896
|
+
See "Knowledge bases: the TBox/RBox split is a trap" in the module
|
|
897
|
+
docstring: the concept-inclusion image alone is not the TBox's meaning, and
|
|
898
|
+
using it as the knowledge base can produce false counterexamples.
|
|
899
|
+
"""
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def tbox_to_fol(tbox: TBox, var: str = "x", *, concept_inclusions_only: bool = False,
|
|
903
|
+
object_sort: bool = False) -> Node:
|
|
904
|
+
"""Render every GCI/equivalence in ``tbox`` as its universal closure
|
|
905
|
+
(:func:`subsumption_to_fol`) and conjoin them.
|
|
906
|
+
|
|
907
|
+
An empty TBox renders as a tautology (no axioms ⇒ no constraint). The
|
|
908
|
+
TBox is entailment-consistent with :func:`unicode_logic_kit.dl.tableau.TBox`:
|
|
909
|
+
equivalences are stored as the pair of inclusions they abbreviate
|
|
910
|
+
(``TBox.add_equivalence``), so they fall out of ``tbox.inclusions`` with
|
|
911
|
+
no special case needed here.
|
|
912
|
+
|
|
913
|
+
**Only the concept inclusions are rendered.** A :class:`TBox` also holds a
|
|
914
|
+
role box (role inclusions, transitivity and every other role-box axiom — the
|
|
915
|
+
``part == "side"`` rows of ``dl.tableau._AXIOM_KINDS``) and a data box, and
|
|
916
|
+
their image is NOT in the result; a formula built from this function alone
|
|
917
|
+
therefore describes a weaker theory than ``tbox`` and can be refuted where
|
|
918
|
+
the tableau's ``subsumes(…, tbox)`` succeeds (a false counterexample). To
|
|
919
|
+
make that impossible to do by accident, a TBox that carries any such side
|
|
920
|
+
axiom raises :class:`RoleBoxOmittedError` — use :func:`kb_to_fol`
|
|
921
|
+
(the knowledge base plus the role box as separate premises) or
|
|
922
|
+
:func:`rbox_to_fol` for the role-box image (:func:`databox_to_fol` for the
|
|
923
|
+
data-box image, which the message names when the TBox carries one). Pass
|
|
924
|
+
``concept_inclusions_only=True`` to say, at the call site, that the
|
|
925
|
+
concept-inclusion image alone is what you want (e.g. to render or inspect the
|
|
926
|
+
GCIs, or when the role box is supplied some other way).
|
|
927
|
+
|
|
928
|
+
The condition is DERIVED from :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`
|
|
929
|
+
via :meth:`~unicode_logic_kit.dl.tableau.TBox.has_side_axioms`, not written out
|
|
930
|
+
over the fields that happened to exist when it was added: a TBox field that
|
|
931
|
+
escaped a hand-written ``or`` here would render as a tautology and raise
|
|
932
|
+
nothing, which is a silent loss rather than an error.
|
|
933
|
+
|
|
934
|
+
``object_sort=True`` relativises every GCI to the object domain — see
|
|
935
|
+
:func:`subsumption_to_fol`.
|
|
936
|
+
|
|
937
|
+
A class and an object property of one name are two symbols in the result
|
|
938
|
+
(``A(x)`` and ``A(x, y)``), as in every image of this module — see
|
|
939
|
+
:func:`kb_to_fol`.
|
|
940
|
+
|
|
941
|
+
Raises:
|
|
942
|
+
RoleBoxOmittedError: ``tbox`` carries a side axiom — a role-box axiom or
|
|
943
|
+
a data-box axiom — and ``concept_inclusions_only`` is false.
|
|
944
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
945
|
+
the TBox uses one name as two kinds OWL 2 DL
|
|
946
|
+
keeps apart (an object property and a data property, a class and a
|
|
947
|
+
datatype) — see :func:`kb_to_fol`.
|
|
948
|
+
"""
|
|
949
|
+
if not concept_inclusions_only and tbox.has_side_axioms():
|
|
950
|
+
# rbox_to_fol renders the ROLE box only (a tautology for a TBox whose
|
|
951
|
+
# side axioms are all data-box ones); the data box has its own renderer,
|
|
952
|
+
# which the message names whenever the TBox carries a data-box axiom.
|
|
953
|
+
images = ("rbox_to_fol(tbox) for the role-box image"
|
|
954
|
+
+ (" and databox_to_fol(tbox) for the data-box image"
|
|
955
|
+
if _carries_data_box(tbox) else ""))
|
|
956
|
+
raise RoleBoxOmittedError(
|
|
957
|
+
f"tbox_to_fol: this TBox carries side axioms ({_side_axiom_census(tbox)}) "
|
|
958
|
+
"that tbox_to_fol does not render, so its output alone is a WEAKER theory "
|
|
959
|
+
"than the TBox and can be refuted where dl.subsumes(…, tbox) succeeds. Use "
|
|
960
|
+
"kb_to_fol(tbox, abox) (the knowledge base and the side axioms, to be "
|
|
961
|
+
f"passed as premises) or, for one box alone, {images}; "
|
|
962
|
+
"pass concept_inclusions_only=True if the concept-inclusion image alone "
|
|
963
|
+
"is really what you want.")
|
|
964
|
+
vocabulary = _collect_vocabulary(tbox, None)
|
|
965
|
+
_check_name_punning(vocabulary, "dl.tbox_to_fol")
|
|
966
|
+
return _tbox_image(tbox, var, object_sort, set(vocabulary.individuals))
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _tbox_image(tbox: TBox, var: str, object_sort: bool,
|
|
970
|
+
avoid: Iterable[str]) -> Node:
|
|
971
|
+
"""The conjunction of every GCI of ``tbox``, each bound variable avoiding
|
|
972
|
+
``avoid`` — the individual names of the whole knowledge base — so that one
|
|
973
|
+
:func:`kb_to_fol` call binds the same prefix variable in every conjunct."""
|
|
974
|
+
return _conjoin([_subsumption_image(sub, sup, var, object_sort, avoid)
|
|
975
|
+
for sub, sup in tbox.inclusions])
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
@dataclass(frozen=True)
|
|
979
|
+
class SideAxiom:
|
|
980
|
+
"""One side axiom of a knowledge base's FOL image: a premise that is NEVER
|
|
981
|
+
a conjunct of :attr:`KnowledgeBaseFOL.formula` (see :func:`kb_to_fol`).
|
|
982
|
+
|
|
983
|
+
Fields:
|
|
984
|
+
|
|
985
|
+
* ``kind`` — the OWL 2 keyword this axiom came from, e.g.
|
|
986
|
+
``"SubObjectPropertyOf"`` or ``"TransitiveObjectProperty"``. The OWL name
|
|
987
|
+
rather than an internal one, so a caller can census an ontology's image
|
|
988
|
+
per axiom kind (:meth:`KnowledgeBaseFOL.axioms_of_kind`) without a second
|
|
989
|
+
translation table. It is the ``kind`` column of
|
|
990
|
+
:data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`.
|
|
991
|
+
* ``group`` — which route owns the premise: ``"rbox"`` for a role-box
|
|
992
|
+
axiom, ``"data"`` for the image of a data-box axiom, ``"sort"`` for the
|
|
993
|
+
object/data separation and the typing it needs, ``"datatype"`` for the
|
|
994
|
+
datatype lattice and the literals. (:attr:`KnowledgeBaseFOL.rbox_axioms`
|
|
995
|
+
and :attr:`~KnowledgeBaseFOL.data_axioms` are the two cases the kit itself
|
|
996
|
+
asks about.) A ``group`` rather than a type hierarchy because the
|
|
997
|
+
caller's question is a fork, not a taxonomy. The ``kind`` of a ``"sort"``
|
|
998
|
+
or ``"datatype"`` axiom is a descriptive name, not an OWL keyword — there
|
|
999
|
+
is no OWL axiom it came from.
|
|
1000
|
+
* ``formula`` — the axiom itself.
|
|
1001
|
+
"""
|
|
1002
|
+
|
|
1003
|
+
kind: str
|
|
1004
|
+
group: str
|
|
1005
|
+
formula: Node
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _forall(variables: List[Variable], body: Node) -> Node:
|
|
1009
|
+
"""``∀v1 … ∀vn body`` — the universal closure, outermost variable first."""
|
|
1010
|
+
for variable in reversed(variables):
|
|
1011
|
+
body = Quantifier("∀", variable, body)
|
|
1012
|
+
return body
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _chain_variables(length: int, x: str, y: str, z: str,
|
|
1016
|
+
avoid: Iterable[str] = ()) -> List[Variable]:
|
|
1017
|
+
"""``length`` variable names for a property chain's quantifier prefix.
|
|
1018
|
+
|
|
1019
|
+
The first three are ``x``, ``y``, ``z`` — the same three
|
|
1020
|
+
:func:`_rbox_axioms` already uses for transitivity, which IS the chain
|
|
1021
|
+
``r ∘ r ⊑ r``, so the two images stay visually comparable. Beyond three the
|
|
1022
|
+
kit has no fourth canonical name, so the rest come from
|
|
1023
|
+
:func:`_fresh_var_factory` (``x0``, ``x1``, …), which yields the
|
|
1024
|
+
one-letter-plus-digits shape the kit's VARIABLE terminal accepts — never a
|
|
1025
|
+
hand-rolled ``f"{base}_{n}"``. ``avoid`` is the individual names of the
|
|
1026
|
+
knowledge base: a chain axiom names none, so it cannot capture one, but the
|
|
1027
|
+
invariant "no bound variable of an image shares a name with an individual"
|
|
1028
|
+
is kept without exception rather than only where it is needed.
|
|
1029
|
+
"""
|
|
1030
|
+
names = [x, y, z][:length]
|
|
1031
|
+
if length > 3:
|
|
1032
|
+
fresh = _fresh_var_factory(x, avoid={y, z} | set(avoid))
|
|
1033
|
+
while len(names) < length:
|
|
1034
|
+
names.append(fresh())
|
|
1035
|
+
return [Variable(name) for name in names]
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _chain_axiom(chain, super_role, x: str, y: str, z: str,
|
|
1039
|
+
avoid: Iterable[str] = ()) -> Node:
|
|
1040
|
+
"""The FOL image of ``P1 ∘ … ∘ Pn ⊑ Q``: for ``n = 2``,
|
|
1041
|
+
``∀x ∀y ∀z (P1(x, y) ∧ P2(y, z) → Q(x, z))``.
|
|
1042
|
+
|
|
1043
|
+
OWL 2 direct semantics: the axiom holds iff for every ``y0 … yn`` with
|
|
1044
|
+
``<y_{i-1}, y_i> ∈ Pi^I`` we have ``<y0, yn> ∈ Q^I`` — so the body is the
|
|
1045
|
+
conjunction of the ``n`` step atoms (folded by :func:`_conjoin`, like every
|
|
1046
|
+
other conjunction this module builds) and the head links the first variable
|
|
1047
|
+
to the last.
|
|
1048
|
+
"""
|
|
1049
|
+
variables = _chain_variables(len(chain) + 1, x, y, z, avoid)
|
|
1050
|
+
body = _conjoin([_role_atom(role, variables[i], variables[i + 1])
|
|
1051
|
+
for i, role in enumerate(chain)])
|
|
1052
|
+
return _forall(variables, Implies(body, _role_atom(super_role, variables[0],
|
|
1053
|
+
variables[-1])))
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _rbox_axioms(tbox: TBox, x: str = "x", y: str = "y",
|
|
1057
|
+
z: str = "z", *, object_sort: bool = False,
|
|
1058
|
+
avoid: Optional[Iterable[str]] = None) -> List[SideAxiom]:
|
|
1059
|
+
"""The role box of ``tbox`` as a list of :class:`SideAxiom` — one per role
|
|
1060
|
+
inclusion, then one per transitive role, then one per further role-box axiom
|
|
1061
|
+
in the order :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS` lists them.
|
|
1062
|
+
Every set-valued field is sorted, so the order is deterministic despite the
|
|
1063
|
+
fields being unordered sets.
|
|
1064
|
+
|
|
1065
|
+
The ONE place the role box becomes FOL: :func:`rbox_to_fol` conjoins what
|
|
1066
|
+
this returns and :func:`kb_to_fol` carries it as
|
|
1067
|
+
:attr:`KnowledgeBaseFOL.side_axioms`, so a new role-box axiom kind is
|
|
1068
|
+
rendered for both by appending one entry here (and, as every kind must,
|
|
1069
|
+
one row of :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`).
|
|
1070
|
+
|
|
1071
|
+
Every stored entry is re-validated first — the SECOND line of defence
|
|
1072
|
+
behind :meth:`~unicode_logic_kit.dl.tableau.TBox.add_role_inclusion` and
|
|
1073
|
+
friends, and the SAME validation
|
|
1074
|
+
(:func:`~unicode_logic_kit.dl.tableau._validate_role_box`) the tableau's
|
|
1075
|
+
shared guard runs, so the two routes cannot disagree about whether a ``TBox``
|
|
1076
|
+
assembled by hand, or mutated in place, is a question at all: it must fail
|
|
1077
|
+
LOUDLY rather than print an atom like ``('r', 's')(x, y)`` that looks like
|
|
1078
|
+
an axiom and is not.
|
|
1079
|
+
|
|
1080
|
+
The three prefix variables are the PREFERENCES ``x``, ``y``, ``z``: each one
|
|
1081
|
+
that is also the name of an individual of the knowledge base (``avoid`` —
|
|
1082
|
+
by default the individuals the TBox's own class expressions name;
|
|
1083
|
+
:func:`kb_to_fol` passes the whole knowledge base's) is renamed, so a domain
|
|
1084
|
+
axiom over ``∃r.{x}`` does not bind the individual ``x``
|
|
1085
|
+
(:func:`_binder_names`).
|
|
1086
|
+
"""
|
|
1087
|
+
where = "dl.rbox_to_fol"
|
|
1088
|
+
_validate_role_box(tbox, where=where)
|
|
1089
|
+
avoid = (_kb_individual_names(tbox, None) if avoid is None else set(avoid))
|
|
1090
|
+
x, y, z = _binder_names((x, y, z), avoid)
|
|
1091
|
+
parts: List[SideAxiom] = []
|
|
1092
|
+
for sub_role, super_role in tbox.role_inclusions:
|
|
1093
|
+
vx, vy = Variable(x), Variable(y)
|
|
1094
|
+
# _role_atom, not Atom(...) by hand: it is the function that already
|
|
1095
|
+
# knows an InverseRole swaps the atom's argument order, and is what
|
|
1096
|
+
# every concept restriction in this module uses.
|
|
1097
|
+
parts.append(SideAxiom("SubObjectPropertyOf", "rbox",
|
|
1098
|
+
Quantifier("∀", vx, Quantifier("∀", vy,
|
|
1099
|
+
Implies(_role_atom(sub_role, vx, vy),
|
|
1100
|
+
_role_atom(super_role, vx, vy))))))
|
|
1101
|
+
for role in sorted(tbox.transitive_roles):
|
|
1102
|
+
vx, vy, vz = Variable(x), Variable(y), Variable(z)
|
|
1103
|
+
parts.append(SideAxiom("TransitiveObjectProperty", "rbox",
|
|
1104
|
+
Quantifier("∀", vx, Quantifier("∀", vy, Quantifier("∀", vz,
|
|
1105
|
+
Implies(_FAnd(Atom(role, (vx, vy)), Atom(role, (vy, vz))),
|
|
1106
|
+
Atom(role, (vx, vz))))))))
|
|
1107
|
+
for left, right in tbox.disjoint_role_pairs:
|
|
1108
|
+
vx, vy = Variable(x), Variable(y)
|
|
1109
|
+
parts.append(SideAxiom("DisjointObjectProperties", "rbox",
|
|
1110
|
+
Quantifier("∀", vx, Quantifier("∀", vy,
|
|
1111
|
+
_FNot(_FAnd(Atom(left, (vx, vy)), Atom(right, (vx, vy))))))))
|
|
1112
|
+
for role in sorted(tbox.asymmetric_roles):
|
|
1113
|
+
vx, vy = Variable(x), Variable(y)
|
|
1114
|
+
parts.append(SideAxiom("AsymmetricObjectProperty", "rbox",
|
|
1115
|
+
Quantifier("∀", vx, Quantifier("∀", vy,
|
|
1116
|
+
Implies(Atom(role, (vx, vy)), _FNot(Atom(role, (vy, vx))))))))
|
|
1117
|
+
for role in sorted(tbox.irreflexive_roles):
|
|
1118
|
+
vx = Variable(x)
|
|
1119
|
+
parts.append(SideAxiom("IrreflexiveObjectProperty", "rbox",
|
|
1120
|
+
Quantifier("∀", vx, _FNot(Atom(role, (vx, vx))))))
|
|
1121
|
+
for role in sorted(tbox.functional_roles):
|
|
1122
|
+
vx, vy, vz = Variable(x), Variable(y), Variable(z)
|
|
1123
|
+
parts.append(SideAxiom("FunctionalObjectProperty", "rbox",
|
|
1124
|
+
Quantifier("∀", vx, Quantifier("∀", vy, Quantifier("∀", vz,
|
|
1125
|
+
Implies(_FAnd(Atom(role, (vx, vy)), Atom(role, (vx, vz))),
|
|
1126
|
+
Atom("=", (vy, vz))))))))
|
|
1127
|
+
for p, q in tbox.inverse_role_pairs:
|
|
1128
|
+
vx, vy = Variable(x), Variable(y)
|
|
1129
|
+
parts.append(SideAxiom("InverseObjectProperties", "rbox",
|
|
1130
|
+
Quantifier("∀", vx, Quantifier("∀", vy,
|
|
1131
|
+
Iff(Atom(p, (vx, vy)), Atom(q, (vy, vx)))))))
|
|
1132
|
+
for role in sorted(tbox.symmetric_roles):
|
|
1133
|
+
vx, vy = Variable(x), Variable(y)
|
|
1134
|
+
parts.append(SideAxiom("SymmetricObjectProperty", "rbox",
|
|
1135
|
+
Quantifier("∀", vx, Quantifier("∀", vy,
|
|
1136
|
+
Implies(Atom(role, (vx, vy)), Atom(role, (vy, vx)))))))
|
|
1137
|
+
for role in sorted(tbox.reflexive_roles):
|
|
1138
|
+
vx = Variable(x)
|
|
1139
|
+
# Reflexive(r) is "every INDIVIDUAL is r-related to itself". The one
|
|
1140
|
+
# role-box axiom with an unguarded universal over a single variable, so
|
|
1141
|
+
# in the two-sorted image it needs the object-domain guard (a data
|
|
1142
|
+
# value is not r-related to itself, and the typing of r forbids it).
|
|
1143
|
+
reflexive: Node = Atom(role, (vx, vx))
|
|
1144
|
+
if object_sort:
|
|
1145
|
+
reflexive = Implies(Atom(OWL_THING, (vx,)), reflexive)
|
|
1146
|
+
parts.append(SideAxiom("ReflexiveObjectProperty", "rbox",
|
|
1147
|
+
Quantifier("∀", vx, reflexive)))
|
|
1148
|
+
for role in sorted(tbox.inverse_functional_roles):
|
|
1149
|
+
vx, vy, vz = Variable(x), Variable(y), Variable(z)
|
|
1150
|
+
parts.append(SideAxiom("InverseFunctionalObjectProperty", "rbox",
|
|
1151
|
+
Quantifier("∀", vx, Quantifier("∀", vy, Quantifier("∀", vz,
|
|
1152
|
+
Implies(_FAnd(Atom(role, (vy, vx)), Atom(role, (vz, vx))),
|
|
1153
|
+
Atom("=", (vy, vz))))))))
|
|
1154
|
+
for chain, super_role in tbox.role_chains:
|
|
1155
|
+
parts.append(SideAxiom("ObjectPropertyChain", "rbox",
|
|
1156
|
+
_chain_axiom(chain, super_role, x, y, z, avoid)))
|
|
1157
|
+
# The two role-box kinds whose axiom carries a CLASS EXPRESSION, so the
|
|
1158
|
+
# filler goes through the full _translate (12 of the 110 OEO domain axioms
|
|
1159
|
+
# and 10 of the 108 range axioms have a non-atomic one) at the FIRST
|
|
1160
|
+
# variable for a domain axiom and the SECOND for a range axiom -- that
|
|
1161
|
+
# choice of variable is the whole difference between them.
|
|
1162
|
+
for kind, field_name, position in (("ObjectPropertyDomain", "role_domains", 0),
|
|
1163
|
+
("ObjectPropertyRange", "role_ranges", 1)):
|
|
1164
|
+
for role, concept in getattr(tbox, field_name):
|
|
1165
|
+
vx, vy = Variable(x), Variable(y)
|
|
1166
|
+
# The avoid set must carry the prefix variables AND every
|
|
1167
|
+
# individual of the knowledge base: a minted x0 that collided with
|
|
1168
|
+
# one of them would capture it (see _fresh_var_factory). The prefix
|
|
1169
|
+
# variables themselves were already renamed away from the
|
|
1170
|
+
# individuals above, which is what stops the FIXED letters x and y
|
|
1171
|
+
# -- not only the minted ones -- from binding a filler's individual.
|
|
1172
|
+
fresh = _fresh_var_factory(
|
|
1173
|
+
x, avoid={x, y, z} | avoid | _individual_names(concept))
|
|
1174
|
+
parts.append(SideAxiom(kind, "rbox",
|
|
1175
|
+
Quantifier("∀", vx, Quantifier("∀", vy,
|
|
1176
|
+
Implies(Atom(role, (vx, vy)),
|
|
1177
|
+
_translate(concept, (vx, vy)[position], fresh))))))
|
|
1178
|
+
return parts
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def rbox_to_fol(tbox: TBox, x: str = "x", y: str = "y", z: str = "z") -> Node:
|
|
1182
|
+
"""Render every role-box axiom in ``tbox`` as its FOL image (see "RBoxes" in
|
|
1183
|
+
the module docstring) and conjoin them. Each image is the OWL 2
|
|
1184
|
+
direct-semantics sentence for that axiom:
|
|
1185
|
+
|
|
1186
|
+
- ``r ⊑ s`` ↦ ``∀x ∀y (r(x, y) → s(x, y))``
|
|
1187
|
+
- ``r ⊑ s⁻`` ↦ ``∀x ∀y (r(x, y) → s(y, x))``
|
|
1188
|
+
- ``Trans(r)`` ↦ ``∀x ∀y ∀z (r(x, y) ∧ r(y, z) → r(x, z))``
|
|
1189
|
+
- ``Disj(p, q)`` ↦ ``∀x ∀y ¬(p(x, y) ∧ q(x, y))``
|
|
1190
|
+
- ``Asym(r)`` ↦ ``∀x ∀y (r(x, y) → ¬r(y, x))``
|
|
1191
|
+
- ``Irr(r)`` ↦ ``∀x ¬r(x, x)`` (ONE variable)
|
|
1192
|
+
- ``Func(r)`` ↦ ``∀x ∀y ∀z (r(x, y) ∧ r(x, z) → y = z)``
|
|
1193
|
+
- ``Inv(p, q)`` ↦ ``∀x ∀y (p(x, y) ↔ q(y, x))`` (ONE ↔: the
|
|
1194
|
+
axiom is an EQUALITY of relations, not two inclusions)
|
|
1195
|
+
- ``Sym(r)`` ↦ ``∀x ∀y (r(x, y) → r(y, x))``
|
|
1196
|
+
- ``Refl(r)`` ↦ ``∀x r(x, x)``
|
|
1197
|
+
- ``InvFunc(r)`` ↦ ``∀x ∀y ∀z (r(y, x) ∧ r(z, x) → y = z)``
|
|
1198
|
+
(the argument order is the whole content of the axiom)
|
|
1199
|
+
- ``p1 ∘ p2 ⊑ q`` ↦ ``∀x ∀y ∀z (p1(x, y) ∧ p2(y, z) → q(x, z))``
|
|
1200
|
+
- ``Dom(r, C)`` ↦ ``∀x ∀y (r(x, y) → π(C, x))``
|
|
1201
|
+
- ``Rng(r, C)`` ↦ ``∀x ∀y (r(x, y) → π(C, y))`` (the filler
|
|
1202
|
+
at the SECOND variable — the only difference from the domain axiom)
|
|
1203
|
+
|
|
1204
|
+
An ``EquivalentObjectProperties`` axiom has no entry of its own because
|
|
1205
|
+
:meth:`~unicode_logic_kit.dl.tableau.TBox.add_equivalent_roles` stores it as
|
|
1206
|
+
the role inclusions it abbreviates, so it renders as those.
|
|
1207
|
+
|
|
1208
|
+
This is the ROLE box only: the data box's images are
|
|
1209
|
+
:func:`databox_to_fol`'s, so a TBox carrying data axioms alone renders as
|
|
1210
|
+
the tautology here (the role box IS empty). :func:`kb_to_fol` renders both.
|
|
1211
|
+
|
|
1212
|
+
Set-valued fields are sorted before rendering so the output (and hence any
|
|
1213
|
+
string comparison of it) is deterministic despite them being unordered
|
|
1214
|
+
``set``s. A role box with no axioms at all — including every plain ALC
|
|
1215
|
+
``TBox`` predating this function — renders as a tautology, matching
|
|
1216
|
+
:func:`tbox_to_fol`'s own empty-TBox convention.
|
|
1217
|
+
|
|
1218
|
+
This is a SIDE axiom of every question asked relative to ``tbox``: pass it
|
|
1219
|
+
(or, better, the per-axiom tuple :attr:`KnowledgeBaseFOL.axioms`) as a
|
|
1220
|
+
premise of ``api.prove`` — :func:`kb_to_fol` does that bookkeeping.
|
|
1221
|
+
|
|
1222
|
+
``x``, ``y`` and ``z`` are the PREFERRED names of the bound variables: one
|
|
1223
|
+
that is also the name of an individual a domain or range filler names
|
|
1224
|
+
(``∃r.{x}``) is renamed to the first free ``letter + digits`` instead of
|
|
1225
|
+
binding that individual (see :func:`_binder_names`).
|
|
1226
|
+
|
|
1227
|
+
The images of the boxes are built one call at a time, so a name used as two
|
|
1228
|
+
kinds of thing ACROSS boxes (an object property here, a data property in the
|
|
1229
|
+
data box) cannot be seen by any one of them: the refusal of such a clash is
|
|
1230
|
+
per call, over what the call is given. :func:`kb_to_fol` is the call that
|
|
1231
|
+
sees everything, and refuses it; :func:`check_kb_names` runs the same check
|
|
1232
|
+
over the boxes of images rendered separately. A class, an object property and
|
|
1233
|
+
an individual of one name are three symbols here (see :func:`kb_to_fol`).
|
|
1234
|
+
|
|
1235
|
+
Raises:
|
|
1236
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
1237
|
+
a role-box entry does not hold a usable role —
|
|
1238
|
+
see :class:`~unicode_logic_kit.dl.tableau.RoleExpressionError`. The
|
|
1239
|
+
builders refuse the same values, so this fires only for a ``TBox``
|
|
1240
|
+
assembled or mutated by hand — and it is the SAME validation the
|
|
1241
|
+
tableau's shared guard runs, so both routes refuse it.
|
|
1242
|
+
"""
|
|
1243
|
+
return _conjoin([a.formula for a in _rbox_axioms(tbox, x, y, z)])
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _databox_axioms(tbox: TBox, x: str = "x", v: str = "v",
|
|
1247
|
+
w: str = "w", *,
|
|
1248
|
+
avoid: Optional[Iterable[str]] = None) -> List[SideAxiom]:
|
|
1249
|
+
"""The data box of ``tbox`` as :class:`SideAxiom` s of group ``"data"`` — in
|
|
1250
|
+
the order :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS` lists the kinds.
|
|
1251
|
+
Every OWL 2 direct-semantics sentence below is derived in
|
|
1252
|
+
:func:`databox_to_fol`'s docstring. Property names are re-validated here, the
|
|
1253
|
+
second line of defence behind the builders, by the SAME validation
|
|
1254
|
+
(:func:`~unicode_logic_kit.dl.tableau._validate_data_box`) the tableau's
|
|
1255
|
+
shared guard runs, and the prefix variables are renamed away from the
|
|
1256
|
+
knowledge base's individuals exactly as :func:`_rbox_axioms` does — a data
|
|
1257
|
+
property domain over ``∃r.{v}`` must not bind the individual ``v``.
|
|
1258
|
+
"""
|
|
1259
|
+
where = "dl.databox_to_fol"
|
|
1260
|
+
_validate_data_box(tbox, where=where)
|
|
1261
|
+
avoid = (_kb_individual_names(tbox, None) if avoid is None else set(avoid))
|
|
1262
|
+
x, v, w = _binder_names((x, v, w), avoid)
|
|
1263
|
+
parts: List[SideAxiom] = []
|
|
1264
|
+
|
|
1265
|
+
def closed(variables, body) -> Node:
|
|
1266
|
+
return _forall([Variable(name) for name in variables], body)
|
|
1267
|
+
|
|
1268
|
+
for sub_prop, super_prop in tbox.data_property_inclusions:
|
|
1269
|
+
vx, vv = Variable(x), Variable(v)
|
|
1270
|
+
parts.append(SideAxiom("SubDataPropertyOf", "data", closed((x, v), Implies(
|
|
1271
|
+
Atom(sub_prop, (vx, vv)), Atom(super_prop, (vx, vv))))))
|
|
1272
|
+
for left, right in tbox.disjoint_data_property_pairs:
|
|
1273
|
+
vx, vv = Variable(x), Variable(v)
|
|
1274
|
+
parts.append(SideAxiom("DisjointDataProperties", "data", closed((x, v), _FNot(
|
|
1275
|
+
_FAnd(Atom(left, (vx, vv)), Atom(right, (vx, vv)))))))
|
|
1276
|
+
for prop in sorted(tbox.functional_data_properties):
|
|
1277
|
+
vx, vv, vw = Variable(x), Variable(v), Variable(w)
|
|
1278
|
+
parts.append(SideAxiom("FunctionalDataProperty", "data", closed((x, v, w), Implies(
|
|
1279
|
+
_FAnd(Atom(prop, (vx, vv)), Atom(prop, (vx, vw))), Atom("=", (vv, vw))))))
|
|
1280
|
+
for prop, concept in tbox.data_property_domains:
|
|
1281
|
+
vx, vv = Variable(x), Variable(v)
|
|
1282
|
+
# The filler is a full class expression (a union in two OEO axioms), so
|
|
1283
|
+
# it goes through _translate at the FIRST variable; the avoid set
|
|
1284
|
+
# carries the prefix variables (already renamed away from every
|
|
1285
|
+
# individual) and every individual of the knowledge base.
|
|
1286
|
+
fresh = _fresh_var_factory(
|
|
1287
|
+
x, avoid={x, v, w} | avoid | _individual_names(concept))
|
|
1288
|
+
parts.append(SideAxiom("DataPropertyDomain", "data", closed((x, v), Implies(
|
|
1289
|
+
Atom(prop, (vx, vv)), _translate(concept, vx, fresh)))))
|
|
1290
|
+
for prop, datarange in tbox.data_property_ranges:
|
|
1291
|
+
vx, vv = Variable(x), Variable(v)
|
|
1292
|
+
parts.append(SideAxiom("DataPropertyRange", "data", closed((x, v), Implies(
|
|
1293
|
+
Atom(prop, (vx, vv)), datarange_to_fol(datarange, vv)))))
|
|
1294
|
+
for name, datarange in tbox.datatype_definitions:
|
|
1295
|
+
vv = Variable(v)
|
|
1296
|
+
parts.append(SideAxiom("DatatypeDefinition", "data", closed((v,), Iff(
|
|
1297
|
+
Atom(name, (vv,)), datarange_to_fol(datarange, vv)))))
|
|
1298
|
+
return parts
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def databox_to_fol(tbox: TBox, x: str = "x", v: str = "v", w: str = "w") -> Node:
|
|
1302
|
+
"""Render every data-box axiom in ``tbox`` as its FOL image and conjoin them
|
|
1303
|
+
(see "The data layer" in the module docstring). Each image is the OWL 2
|
|
1304
|
+
direct-semantics sentence for that axiom, the datatype read as a unary
|
|
1305
|
+
predicate over the one domain:
|
|
1306
|
+
|
|
1307
|
+
- ``SubDataPropertyOf(d e)`` ↦ ``∀x ∀v (d(x, v) → e(x, v))``
|
|
1308
|
+
- ``DisjointDataProperties(d e)`` ↦ ``∀x ∀v ¬(d(x, v) ∧ e(x, v))``
|
|
1309
|
+
- ``FunctionalDataProperty(d)`` ↦ ``∀x ∀v ∀w (d(x, v) ∧ d(x, w) → v = w)``
|
|
1310
|
+
- ``DataPropertyDomain(d C)`` ↦ ``∀x ∀v (d(x, v) → π(C, x))``
|
|
1311
|
+
- ``DataPropertyRange(d DR)`` ↦ ``∀x ∀v (d(x, v) → δ(DR, v))``
|
|
1312
|
+
- ``DatatypeDefinition(D DR)`` ↦ ``∀v (D(v) ↔ δ(DR, v))`` — a DEFINITION,
|
|
1313
|
+
so a biconditional and not a pair of inclusions read one way.
|
|
1314
|
+
|
|
1315
|
+
``EquivalentDataProperties`` has no entry of its own: it is stored as the
|
|
1316
|
+
inclusions it abbreviates. A data box with nothing in it is a tautology,
|
|
1317
|
+
like :func:`rbox_to_fol`'s empty role box. This is a SIDE axiom of every
|
|
1318
|
+
question asked relative to ``tbox`` — :func:`kb_to_fol` does the
|
|
1319
|
+
bookkeeping, and adds the two-sorted axioms of :func:`data_sort_axioms`.
|
|
1320
|
+
|
|
1321
|
+
The refusal of a name used as two kinds of thing is PER CALL: this function
|
|
1322
|
+
sees the vocabulary of ``tbox``, not of the ABox or of a role box rendered
|
|
1323
|
+
separately, so a clash across boxes built one call at a time is invisible to
|
|
1324
|
+
it. :func:`kb_to_fol` is the call that sees everything, and
|
|
1325
|
+
:func:`check_kb_names` runs the same check over boxes rendered separately.
|
|
1326
|
+
|
|
1327
|
+
Raises:
|
|
1328
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
1329
|
+
a data-box entry does not hold a usable property
|
|
1330
|
+
name (only reachable for a ``TBox`` assembled by hand).
|
|
1331
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
1332
|
+
a literal in a data range has no term, or a
|
|
1333
|
+
name of ``tbox`` is used both as an object property and a data
|
|
1334
|
+
property or both as a class and a datatype (see :func:`kb_to_fol`).
|
|
1335
|
+
"""
|
|
1336
|
+
where = "dl.databox_to_fol"
|
|
1337
|
+
_validate_data_box(tbox, where=where)
|
|
1338
|
+
# The whole TBox's vocabulary decides whether a predicate of this image is
|
|
1339
|
+
# also another kind's (``P`` an object role in a GCI, a data property here).
|
|
1340
|
+
vocabulary = _collect_vocabulary(tbox, None)
|
|
1341
|
+
_check_name_punning(vocabulary, where)
|
|
1342
|
+
return _conjoin([a.formula for a in _databox_axioms(
|
|
1343
|
+
tbox, x, v, w, avoid=vocabulary.individuals)])
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def abox_to_fol(abox: ABox) -> Node:
|
|
1347
|
+
"""Render every assertion in ``abox`` as a FOL formula and conjoin them.
|
|
1348
|
+
|
|
1349
|
+
A concept assertion ``a : C`` becomes ``π(C, a)`` with the individual
|
|
1350
|
+
``a`` translated as a :class:`~unicode_logic_kit.fol.nodes.Constant` (not a
|
|
1351
|
+
Variable — see "GCIs, TBoxes, ABoxes" above for why this matters). A role
|
|
1352
|
+
assertion ``(a, b) : r`` becomes ``r(a, b)``. A distinctness assertion
|
|
1353
|
+
``a ≠ b`` (see :meth:`~unicode_logic_kit.dl.tableau.ABox.assert_distinct` and
|
|
1354
|
+
"Qualified number restrictions" in :mod:`unicode_logic_kit.dl.tableau`'s module
|
|
1355
|
+
docstring — this reasoner has no unique name assumption, so this is the ONLY
|
|
1356
|
+
thing that ever forces two individuals apart) becomes the FOL atom ``a ≠ b``
|
|
1357
|
+
directly — no translation needed, ``Atom`` already renders it natively for
|
|
1358
|
+
every backend. A same-individual assertion ``a = b``
|
|
1359
|
+
(:meth:`~unicode_logic_kit.dl.tableau.ABox.assert_same`) is its exact mirror,
|
|
1360
|
+
and a negative role assertion
|
|
1361
|
+
(:meth:`~unicode_logic_kit.dl.tableau.ABox.assert_negative_role`) becomes the
|
|
1362
|
+
ground literal ``¬r(a, b)``. A data property assertion
|
|
1363
|
+
(:meth:`~unicode_logic_kit.dl.tableau.ABox.assert_data`) is the ground atom
|
|
1364
|
+
``d(a, t)`` for the literal's term ``t`` (``HasNumber(alice, 400)``), and a
|
|
1365
|
+
negative one its negation. An ABox with no assertions at all renders as a
|
|
1366
|
+
tautology.
|
|
1367
|
+
|
|
1368
|
+
The conjuncts come in assertion-kind order — concept assertions, role
|
|
1369
|
+
assertions, distinctness, sameness, negative role assertions, data
|
|
1370
|
+
assertions, negative data assertions — each kind in the order it was
|
|
1371
|
+
asserted, folded by :func:`_conjoin`.
|
|
1372
|
+
|
|
1373
|
+
The refusal of a name used as two kinds of thing is PER CALL, over the ABox
|
|
1374
|
+
alone: a name that is a role here and a data property in a TBox rendered
|
|
1375
|
+
separately is invisible to this function. :func:`kb_to_fol` is the call that
|
|
1376
|
+
sees everything, and :func:`check_kb_names` runs the same check over boxes
|
|
1377
|
+
rendered separately.
|
|
1378
|
+
|
|
1379
|
+
The result is the ABox ALONE. Whether the assertions are consistent, or
|
|
1380
|
+
entail ``a : C``, *relative to a TBox* (the usual question — GCIs apply to
|
|
1381
|
+
every named individual, and a role box changes which edges count) is not
|
|
1382
|
+
answered by this formula: pass ``kb_to_fol(tbox, abox).premises`` as the
|
|
1383
|
+
premises of the query instead (see :func:`kb_to_fol`). An ABox-only question
|
|
1384
|
+
— "do these bare assertions clash?" — is exactly what this renders.
|
|
1385
|
+
|
|
1386
|
+
``abox_to_fol(ABox().assert_concept(a, C))`` is also the way to spell the
|
|
1387
|
+
GOAL ``π(C, a)`` of an instance-checking query with the individual as a
|
|
1388
|
+
constant.
|
|
1389
|
+
|
|
1390
|
+
Raises:
|
|
1391
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
1392
|
+
an assertion's property is an OWL 2 built-in name
|
|
1393
|
+
(``owl:topObjectProperty`` …), ``=``/``≠``, an inverse role or no
|
|
1394
|
+
name at all — see
|
|
1395
|
+
:func:`~unicode_logic_kit.dl.tableau._reject_abox_roles`. Printed as
|
|
1396
|
+
the uninterpreted predicate of that name it would be a different
|
|
1397
|
+
assertion (the EMPTY property relates no pair, so ``(a, b) :
|
|
1398
|
+
owl:bottomObjectProperty`` is inconsistent, ``(a, b) : p`` is not).
|
|
1399
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
1400
|
+
one name is both the property of a role
|
|
1401
|
+
assertion and the property of a data assertion, or both a class
|
|
1402
|
+
of a concept assertion and a datatype of a literal (see
|
|
1403
|
+
:func:`kb_to_fol`).
|
|
1404
|
+
"""
|
|
1405
|
+
vocabulary = _collect_vocabulary(None, abox)
|
|
1406
|
+
_check_name_punning(vocabulary, "dl.abox_to_fol")
|
|
1407
|
+
return _abox_image(abox, vocabulary.individuals)
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def _abox_image(abox: ABox, avoid: Iterable[str]) -> Node:
|
|
1411
|
+
""":func:`abox_to_fol` with ``avoid`` — the individual names of the whole
|
|
1412
|
+
knowledge base — added to the names the minted variables must avoid."""
|
|
1413
|
+
_reject_abox_roles(abox, where="dl.abox_to_fol")
|
|
1414
|
+
avoid = set(avoid)
|
|
1415
|
+
parts: List[Node] = []
|
|
1416
|
+
for individual, concept in abox.concept_assertions:
|
|
1417
|
+
term = Constant(individual)
|
|
1418
|
+
parts.append(_translate(
|
|
1419
|
+
concept, term,
|
|
1420
|
+
_fresh_var_factory(individual, avoid | _individual_names(concept))))
|
|
1421
|
+
for a, b, role in abox.role_assertions:
|
|
1422
|
+
parts.append(Atom(role, (Constant(a), Constant(b))))
|
|
1423
|
+
for a, b in abox.distinct_assertions:
|
|
1424
|
+
parts.append(Atom("≠", (Constant(a), Constant(b))))
|
|
1425
|
+
# SameIndividual(a b) holds iff a^I = b^I: the exact mirror of the
|
|
1426
|
+
# distinctness atom above, and ``=`` is native in every backend (Z3 ==,
|
|
1427
|
+
# Prover9 =, TPTP =) exactly as ``≠`` is.
|
|
1428
|
+
for a, b in abox.same_assertions:
|
|
1429
|
+
parts.append(Atom("=", (Constant(a), Constant(b))))
|
|
1430
|
+
# NegativeObjectPropertyAssertion(r a b) holds iff <a^I, b^I> ∉ r^I: the
|
|
1431
|
+
# negated GROUND literal, not a concept assertion about a nominal.
|
|
1432
|
+
for a, b, role in abox.negative_role_assertions:
|
|
1433
|
+
parts.append(_FNot(Atom(role, (Constant(a), Constant(b)))))
|
|
1434
|
+
# DataPropertyAssertion(d a lt) holds iff <a^I, lt^LT> ∈ d^DP: the ground
|
|
1435
|
+
# atom, with the individual as a Constant (as for a role assertion) and the
|
|
1436
|
+
# literal as the TERM of its data value. Nothing is quantified, so no
|
|
1437
|
+
# variable is minted. The negative form is the negated ground literal.
|
|
1438
|
+
for individual, prop, value in abox.data_assertions:
|
|
1439
|
+
parts.append(Atom(prop, (Constant(individual), value.to_term())))
|
|
1440
|
+
for individual, prop, value in abox.negative_data_assertions:
|
|
1441
|
+
parts.append(_FNot(Atom(prop, (Constant(individual), value.to_term()))))
|
|
1442
|
+
return _conjoin(parts)
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
# --------------------------------------------------------------------------- #
|
|
1446
|
+
# A whole knowledge base: the TBox, the ABox and the role box, kept apart.
|
|
1447
|
+
# --------------------------------------------------------------------------- #
|
|
1448
|
+
|
|
1449
|
+
@dataclass(frozen=True)
|
|
1450
|
+
class KnowledgeBaseFOL:
|
|
1451
|
+
"""The FOL image of a knowledge base ``(tbox, abox)`` — see :func:`kb_to_fol`.
|
|
1452
|
+
|
|
1453
|
+
Fields:
|
|
1454
|
+
|
|
1455
|
+
* ``formula`` — the knowledge base itself: the conjunction of the concept-
|
|
1456
|
+
inclusion image and the ABox image (a half with nothing in it is left out,
|
|
1457
|
+
so a TBox-only knowledge base has ``formula == tbox``; with nothing at all
|
|
1458
|
+
it is the usual equality tautology).
|
|
1459
|
+
* ``side_axioms`` — the SIDE axioms as :class:`SideAxiom` values, each
|
|
1460
|
+
carrying the OWL ``kind`` it came from and its ``group``: the role-box
|
|
1461
|
+
image (one per role inclusion, then one per further role-box axiom in the
|
|
1462
|
+
order of ``dl.tableau._AXIOM_KINDS``, set-valued fields sorted), the
|
|
1463
|
+
data-box image and, for a knowledge base with a data layer, the sort
|
|
1464
|
+
axioms; empty when the TBox has none of them. They are premises,
|
|
1465
|
+
NEVER conjoined into ``formula``. ``axioms`` is the bare-formula view of
|
|
1466
|
+
this ONE field, not a second list that could drift out of step with it.
|
|
1467
|
+
* ``formulas`` — the per-axiom list behind ``formula``: the concept-
|
|
1468
|
+
inclusion image and the ABox image, each present only when it has
|
|
1469
|
+
content, so ``formula == _conjoin(formulas)``. A caller with an
|
|
1470
|
+
ontology-sized knowledge base can pass ``(*kb.formulas, *kb.axioms)``
|
|
1471
|
+
as premises instead of the single conjunction; the two are
|
|
1472
|
+
entailment-equivalent (a conjunction of premises and a list of premises
|
|
1473
|
+
have the same models).
|
|
1474
|
+
* ``tbox`` / ``abox`` — the two halves of ``formula`` on their own
|
|
1475
|
+
(:func:`tbox_to_fol` with ``concept_inclusions_only=True``, and
|
|
1476
|
+
:func:`abox_to_fol`), each a tautology when empty. A question that is about
|
|
1477
|
+
the terminology alone (subsumption, concept satisfiability) needs ``tbox``
|
|
1478
|
+
but not ``abox``.
|
|
1479
|
+
* ``individuals`` — the sorted names of the individuals the ABox mentions
|
|
1480
|
+
(concept assertions, role-assertion endpoints, identity, distinctness and
|
|
1481
|
+
negative role assertions — the ``individual_positions`` column of
|
|
1482
|
+
:data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`, so an assertion kind
|
|
1483
|
+
added later is counted without touching this scan); each is a FOL
|
|
1484
|
+
:class:`~unicode_logic_kit.fol.nodes.Constant` of that name in
|
|
1485
|
+
``abox``/``formula``, which is what a goal about an individual has to use.
|
|
1486
|
+
An individual named in an ABox assertion is an individual of the ABox
|
|
1487
|
+
wherever in the assertion it stands: ``a : ∃r.{b}`` (a value restriction
|
|
1488
|
+
or a nominal, at any depth of the asserted class expression) lists ``b``
|
|
1489
|
+
as well as ``a``. The in-house tableau refuses those concepts, so what
|
|
1490
|
+
this changes is this field and the sweeps of the external routes.
|
|
1491
|
+
|
|
1492
|
+
The ABox is the SCOPE, not merely where the scan happens to look: a
|
|
1493
|
+
caller's individual can also reach the TBox half, as the filler of a
|
|
1494
|
+
nominal (``C ⊑ {a}`` → ``∀x (C(x) → x = a)``) or of a value restriction
|
|
1495
|
+
(``C ⊑ ∃r.{a}`` → ``∀x (C(x) → r(x, a))``), and such a name is a
|
|
1496
|
+
``Constant`` in ``formula`` that this field does NOT report. That is
|
|
1497
|
+
deliberate: ``individuals`` is the list of things the knowledge base
|
|
1498
|
+
makes ASSERTIONS about, which is what ``instance_retrieval`` and
|
|
1499
|
+
``realize_all`` enumerate, and the two routes agree only because all
|
|
1500
|
+
three read the same scan. A caller that needs every name in term
|
|
1501
|
+
position should walk the image — ``node.walk()`` yields terms too.
|
|
1502
|
+
|
|
1503
|
+
* ``separation`` — which theory of the data layer ``side_axioms`` carries:
|
|
1504
|
+
``"two-sorted"`` (the object/data separation, the typing of every
|
|
1505
|
+
property and individual, the datatype lattice, literal distinctness — and
|
|
1506
|
+
every GCI of ``formula``/``tbox`` relativised to the object domain),
|
|
1507
|
+
``"data-lattice"`` (the datatype facts only, no object/data separation,
|
|
1508
|
+
GCIs as written) or ``"none"`` (the data-box images alone). ``"none"`` for
|
|
1509
|
+
every knowledge base without a data layer, whatever was asked for, since
|
|
1510
|
+
nothing is then added. A GOAL asked of a ``"two-sorted"`` knowledge base
|
|
1511
|
+
must be relativised too — :meth:`subsumption_goal`,
|
|
1512
|
+
:meth:`unsatisfiability_goal` and :meth:`instance_goal` build it so.
|
|
1513
|
+
* ``data_layer`` — whether the knowledge base, together with the ``query=``
|
|
1514
|
+
concepts it was built for, has a data layer at all: a data property, a
|
|
1515
|
+
datatype or a literal occurs in it. :attr:`refutation_is_decisive` is its
|
|
1516
|
+
negation.
|
|
1517
|
+
|
|
1518
|
+
``premises`` and ``tbox_premises`` are the two premise lists the common
|
|
1519
|
+
questions need, and the three ``*_goal`` methods build the matching goal.
|
|
1520
|
+
How to use them with :func:`unicode_logic_kit.api.prove` is in
|
|
1521
|
+
:func:`kb_to_fol`.
|
|
1522
|
+
"""
|
|
1523
|
+
|
|
1524
|
+
formula: Node
|
|
1525
|
+
side_axioms: Tuple[SideAxiom, ...]
|
|
1526
|
+
tbox: Node
|
|
1527
|
+
abox: Node
|
|
1528
|
+
individuals: Tuple[str, ...] = ()
|
|
1529
|
+
formulas: Tuple[Node, ...] = ()
|
|
1530
|
+
separation: str = "none"
|
|
1531
|
+
data_layer: bool = False
|
|
1532
|
+
# What the side axioms were derived from, read by the three goal methods to
|
|
1533
|
+
# tell a concept they cover from one they do not. Internal: not part of the
|
|
1534
|
+
# logical content, so it neither prints nor takes part in ==.
|
|
1535
|
+
_coverage: Optional["_Coverage"] = field(default=None, repr=False, compare=False)
|
|
1536
|
+
|
|
1537
|
+
@property
|
|
1538
|
+
def axioms(self) -> Tuple[Node, ...]:
|
|
1539
|
+
"""``side_axioms``' formulas alone, in the same order — the premise list
|
|
1540
|
+
to hand a prover.
|
|
1541
|
+
|
|
1542
|
+
DERIVED, not stored: one source of truth means no package can append to
|
|
1543
|
+
one list and forget the other.
|
|
1544
|
+
"""
|
|
1545
|
+
return tuple(a.formula for a in self.side_axioms)
|
|
1546
|
+
|
|
1547
|
+
@property
|
|
1548
|
+
def rbox_axioms(self) -> Tuple[Node, ...]:
|
|
1549
|
+
"""The ``group == "rbox"`` side axioms' formulas — the role-box image."""
|
|
1550
|
+
return tuple(a.formula for a in self.side_axioms if a.group == "rbox")
|
|
1551
|
+
|
|
1552
|
+
@property
|
|
1553
|
+
def data_axioms(self) -> Tuple[Node, ...]:
|
|
1554
|
+
"""The side axioms' formulas belonging to the data layer (``group`` in
|
|
1555
|
+
``"data"`` — the images of the data-box axioms —, ``"sort"`` — the
|
|
1556
|
+
object/data separation and the typing — and ``"datatype"`` — the
|
|
1557
|
+
datatype lattice, literal typing and literal distinctness).
|
|
1558
|
+
|
|
1559
|
+
Empty for a knowledge base without a data layer. A caller can ask "which
|
|
1560
|
+
of these premises belong to the data layer?" of any bundle, without
|
|
1561
|
+
knowing which entry point built it.
|
|
1562
|
+
"""
|
|
1563
|
+
return tuple(a.formula for a in self.side_axioms
|
|
1564
|
+
if a.group in ("data", "sort", "datatype"))
|
|
1565
|
+
|
|
1566
|
+
def axioms_of_kind(self, kind: str) -> Tuple[Node, ...]:
|
|
1567
|
+
"""The side axioms' formulas whose OWL ``kind`` is ``kind``, in order.
|
|
1568
|
+
|
|
1569
|
+
``kind`` is an OWL 2 keyword as :data:`SideAxiom.kind` spells it, e.g.
|
|
1570
|
+
``"TransitiveObjectProperty"``. An unknown kind gives ``()`` rather
|
|
1571
|
+
than raising: the question "did this knowledge base's image carry any
|
|
1572
|
+
axiom of kind K?" has a correct answer for every K, and "none" is it.
|
|
1573
|
+
"""
|
|
1574
|
+
return tuple(a.formula for a in self.side_axioms if a.kind == kind)
|
|
1575
|
+
|
|
1576
|
+
@property
|
|
1577
|
+
def premises(self) -> Tuple[Node, ...]:
|
|
1578
|
+
"""``(formula, *axioms)`` — "the whole knowledge base holds": the premises
|
|
1579
|
+
of a question relative to the TBox AND the ABox (instance checking).
|
|
1580
|
+
|
|
1581
|
+
For an ontology-sized knowledge base ``(*formulas, *axioms)`` says the
|
|
1582
|
+
same thing one axiom at a time; see ``formulas``."""
|
|
1583
|
+
return (self.formula, *self.axioms)
|
|
1584
|
+
|
|
1585
|
+
@property
|
|
1586
|
+
def tbox_premises(self) -> Tuple[Node, ...]:
|
|
1587
|
+
"""``(tbox, *axioms)`` — "the terminology holds", ABox left out: the
|
|
1588
|
+
premises of a question about concepts relative to the TBox
|
|
1589
|
+
(subsumption, concept satisfiability)."""
|
|
1590
|
+
return (self.tbox, *self.axioms)
|
|
1591
|
+
|
|
1592
|
+
@property
|
|
1593
|
+
def refutation_is_decisive(self) -> bool:
|
|
1594
|
+
"""Whether a ``"refuted"`` verdict of :func:`unicode_logic_kit.api.prove`
|
|
1595
|
+
over this bundle is an answer about OWL 2, and not only about the image:
|
|
1596
|
+
``True`` exactly when the knowledge base (with the ``query=`` concepts it
|
|
1597
|
+
was built for) has NO data layer.
|
|
1598
|
+
|
|
1599
|
+
What a caller may conclude from each status, for a goal built by
|
|
1600
|
+
:meth:`subsumption_goal`, :meth:`unsatisfiability_goal` or
|
|
1601
|
+
:meth:`instance_goal` and the premises the method's docstring names:
|
|
1602
|
+
|
|
1603
|
+
* ``"proved"`` — YES, for every goal these methods hand out. Under the
|
|
1604
|
+
two-sorted separation the image is SOUND for the data layer: every
|
|
1605
|
+
OWL 2 interpretation of the knowledge base expands to a model of the
|
|
1606
|
+
image, so what the image entails the knowledge base entails. That
|
|
1607
|
+
is NOT so for a data-layer bundle built with
|
|
1608
|
+
``separation="data-lattice"`` or ``"none"``: its inclusions also range
|
|
1609
|
+
over data values, which makes its premises stronger than OWL 2's, and
|
|
1610
|
+
the methods refuse such a bundle by name instead of building a goal.
|
|
1611
|
+
* ``"refuted"`` — NO when this property is ``True`` (the image of an
|
|
1612
|
+
object-only knowledge base is faithful: a countermodel of the image
|
|
1613
|
+
is one of the knowledge base). When it is ``False`` it means only
|
|
1614
|
+
that the IMAGE has a countermodel, and the image is deliberately not
|
|
1615
|
+
COMPLETE, so the countermodel may be one OWL 2 excludes. Three things
|
|
1616
|
+
it does not state: an ordering facet is an uninterpreted comparison
|
|
1617
|
+
(a range ``xsd:integer[>= 10]`` with ``HasN(a, 5)`` is OWL-inconsistent
|
|
1618
|
+
and ``refuted`` here; ``HasN(a, 15)`` entails ``a : ∃HasN.xsd:integer[>= 3]``
|
|
1619
|
+
in OWL and does not here); a literal is typed only by the datatype it
|
|
1620
|
+
was written with (``d(a, "1.0"^^xsd:decimal)`` entails
|
|
1621
|
+
``a : ∃d.xsd:integer`` in OWL and does not here, and
|
|
1622
|
+
``d(a, "5"^^xsd:int)`` entails ``a : ∃d.xsd:nonNegativeInteger``); and
|
|
1623
|
+
the size of a value space is not stated. Decide facet arithmetic over
|
|
1624
|
+
the data ranges alone with ``atp.z3_arith.is_valid_arith``.
|
|
1625
|
+
* ``"unknown"`` — no answer within the budget, in either direction.
|
|
1626
|
+
"""
|
|
1627
|
+
return not self.data_layer
|
|
1628
|
+
|
|
1629
|
+
def _covers(self, method: str, *concepts: Concept) -> "_Coverage":
|
|
1630
|
+
"""The coverage of this bundle, after checking that it covers
|
|
1631
|
+
``concepts``; raises by name otherwise (see :func:`_check_goal_concepts`)."""
|
|
1632
|
+
coverage = self._coverage
|
|
1633
|
+
if coverage is None:
|
|
1634
|
+
raise ValueError(
|
|
1635
|
+
f"KnowledgeBaseFOL.{method}: this bundle was not built by "
|
|
1636
|
+
f"dl.kb_to_fol, so it does not know which vocabulary its side "
|
|
1637
|
+
f"axioms cover and cannot say whether a goal is the question "
|
|
1638
|
+
f"it asks. Build it with dl.kb_to_fol(tbox, abox, query=[...]).")
|
|
1639
|
+
if self.data_layer and self.separation != "two-sorted":
|
|
1640
|
+
raise UnsupportedDatatypeError(
|
|
1641
|
+
f"KnowledgeBaseFOL.{method}: this bundle has a data layer and was "
|
|
1642
|
+
f"built with separation={self.separation!r}. Its inclusions are then "
|
|
1643
|
+
f"not restricted to the object domain, so its premises are STRONGER "
|
|
1644
|
+
f"than OWL 2's — '⊤ ⊑ {{a}}' also ranges over data values there — "
|
|
1645
|
+
f"and an OWL-consistent knowledge base can have an inconsistent "
|
|
1646
|
+
f"image, from which every goal is proved. A proof over this bundle "
|
|
1647
|
+
f"does not transfer to OWL 2. Build it with "
|
|
1648
|
+
f"separation='two-sorted' (the default).")
|
|
1649
|
+
_check_goal_concepts(coverage, self.separation, self.data_layer,
|
|
1650
|
+
concepts, where=f"KnowledgeBaseFOL.{method}")
|
|
1651
|
+
return coverage
|
|
1652
|
+
|
|
1653
|
+
def subsumption_goal(self, sub: Concept, sup: Concept) -> Node:
|
|
1654
|
+
"""The goal ``sub ⊑ sup``, relativised the way this bundle needs it
|
|
1655
|
+
(to the object domain when ``separation == "two-sorted"``, as written
|
|
1656
|
+
otherwise): ``api.prove(kb.subsumption_goal(sub, sup),
|
|
1657
|
+
kb.tbox_premises)`` is ``"proved"`` ⇒ ``sub ⊑ sup`` follows from the
|
|
1658
|
+
terminology (what ``dl.subsumes(sub, sup, tbox)`` asks, over the
|
|
1659
|
+
constructs the tableau does not refuse). With ``kb.premises`` instead
|
|
1660
|
+
the ABox is a premise as well. ``"refuted"`` is the answer NO only
|
|
1661
|
+
when :attr:`refutation_is_decisive`.
|
|
1662
|
+
|
|
1663
|
+
Raises:
|
|
1664
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
1665
|
+
a concept names a data property, a
|
|
1666
|
+
datatype, a literal (or, on a two-sorted bundle, an object
|
|
1667
|
+
property) the bundle's side axioms do not cover, so the
|
|
1668
|
+
question would be asked of a weaker theory than the one OWL 2
|
|
1669
|
+
describes — pass ``query=[sub, sup]`` to :func:`kb_to_fol`; or
|
|
1670
|
+
a name is used as two kinds of thing at once.
|
|
1671
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
1672
|
+
a restriction's role is an OWL 2 built-in
|
|
1673
|
+
property name.
|
|
1674
|
+
"""
|
|
1675
|
+
coverage = self._covers("subsumption_goal", sub, sup)
|
|
1676
|
+
return _subsumption_image(sub, sup, coverage.var,
|
|
1677
|
+
self.separation == "two-sorted", coverage.avoid)
|
|
1678
|
+
|
|
1679
|
+
def unsatisfiability_goal(self, concept: Concept) -> Node:
|
|
1680
|
+
"""The goal "no object is in ``concept``" — on a two-sorted bundle
|
|
1681
|
+
``¬∃x (OwlThing(x) ∧ π(concept, x))``, otherwise ``¬∃x π(concept, x)``:
|
|
1682
|
+
``api.prove(kb.unsatisfiability_goal(concept), kb.tbox_premises)`` is
|
|
1683
|
+
``"proved"`` ⇒ the concept is UNSATISFIABLE with respect to the
|
|
1684
|
+
terminology (``dl.concept_satisfiable(concept, tbox)`` is ``False``). A
|
|
1685
|
+
``"refuted"`` answers "satisfiable" only when
|
|
1686
|
+
:attr:`refutation_is_decisive`.
|
|
1687
|
+
|
|
1688
|
+
The ``OwlThing`` conjunct is what makes this the OWL 2 question: a data
|
|
1689
|
+
value satisfies every concept that says nothing about it, so without it
|
|
1690
|
+
``∃HasV.xsd:integer ⊓ ∀HasV.xsd:string`` would be "satisfiable" by a
|
|
1691
|
+
data value, where OWL 2 makes it unsatisfiable (the datatypes are
|
|
1692
|
+
disjoint).
|
|
1693
|
+
|
|
1694
|
+
Raises:
|
|
1695
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
1696
|
+
as :meth:`subsumption_goal`, with ``query=[concept]``.
|
|
1697
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
1698
|
+
as :meth:`subsumption_goal`, with ``query=[concept]``.
|
|
1699
|
+
"""
|
|
1700
|
+
coverage = self._covers("unsatisfiability_goal", concept)
|
|
1701
|
+
avoid = set(coverage.avoid) | _individual_names(concept)
|
|
1702
|
+
(var,) = _binder_names((coverage.var,), avoid)
|
|
1703
|
+
x = Variable(var)
|
|
1704
|
+
body = _translate(concept, x, _fresh_var_factory(var, avoid))
|
|
1705
|
+
if self.separation == "two-sorted":
|
|
1706
|
+
body = _FAnd(Atom(OWL_THING, (x,)), body)
|
|
1707
|
+
return _FNot(Quantifier("∃", x, body))
|
|
1708
|
+
|
|
1709
|
+
def instance_goal(self, individual: str, concept: Concept) -> Node:
|
|
1710
|
+
"""The goal ``individual : concept`` — ``π(concept, individual)`` with
|
|
1711
|
+
the individual as a constant: ``api.prove(kb.instance_goal(a, C),
|
|
1712
|
+
kb.premises)`` is ``"proved"`` ⇒ the knowledge base entails ``a : C``
|
|
1713
|
+
(``dl.instance_check(abox, a, C, tbox)``). An individual the bundle
|
|
1714
|
+
does not type (it occurs in no axiom) is asked about as an OBJECT on a
|
|
1715
|
+
two-sorted bundle, ``OwlThing(a) → π(C, a)``: every OWL 2 individual is
|
|
1716
|
+
in the object domain.
|
|
1717
|
+
|
|
1718
|
+
Raises:
|
|
1719
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
1720
|
+
as :meth:`subsumption_goal`, with ``query=[concept]``.
|
|
1721
|
+
~unicode_logic_kit.dl.tableau.RoleExpressionError:
|
|
1722
|
+
as :meth:`subsumption_goal`, with ``query=[concept]``.
|
|
1723
|
+
TypeError: ``individual`` is not a non-empty name.
|
|
1724
|
+
"""
|
|
1725
|
+
if not isinstance(individual, str) or not individual:
|
|
1726
|
+
raise TypeError(
|
|
1727
|
+
f"KnowledgeBaseFOL.instance_goal: the individual is a non-empty "
|
|
1728
|
+
f"name (str), got {type(individual).__name__} {individual!r}.")
|
|
1729
|
+
coverage = self._covers("instance_goal", concept)
|
|
1730
|
+
term = Constant(individual)
|
|
1731
|
+
avoid = set(coverage.avoid) | _individual_names(concept) | {individual}
|
|
1732
|
+
goal = _translate(concept, term, _fresh_var_factory(individual, avoid))
|
|
1733
|
+
if self.separation == "two-sorted" and individual not in coverage.individuals:
|
|
1734
|
+
return Implies(Atom(OWL_THING, (term,)), goal)
|
|
1735
|
+
return goal
|
|
1736
|
+
|
|
1737
|
+
def to_dict(self) -> dict:
|
|
1738
|
+
"""JSON-compatible form: every formula through its own ``to_dict``.
|
|
1739
|
+
|
|
1740
|
+
The five keys are the FORMULAS — ``side_axioms``' ``kind``/``group``
|
|
1741
|
+
labels are bookkeeping for the caller, not part of the logical content,
|
|
1742
|
+
and ``formulas`` is a regrouping of ``formula``; a reader of this dict
|
|
1743
|
+
loses nothing it could use to ask a prover a question.
|
|
1744
|
+
"""
|
|
1745
|
+
return {
|
|
1746
|
+
"formula": self.formula.to_dict(),
|
|
1747
|
+
"axioms": [a.to_dict() for a in self.axioms],
|
|
1748
|
+
"tbox": self.tbox.to_dict(),
|
|
1749
|
+
"abox": self.abox.to_dict(),
|
|
1750
|
+
"individuals": list(self.individuals),
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
def _abox_individuals(abox: ABox) -> Tuple[str, ...]:
|
|
1755
|
+
"""The individual names ``abox`` mentions, sorted — and NO anonymous
|
|
1756
|
+
fallback individual, unlike the tableau's own (private) ``_individuals``:
|
|
1757
|
+
there the placeholder exists so an empty ABox still has a node to run on,
|
|
1758
|
+
here it would be a constant no assertion ever named.
|
|
1759
|
+
|
|
1760
|
+
The scan itself is :func:`~unicode_logic_kit.dl.tableau._abox_individual_names`,
|
|
1761
|
+
driven by :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`'
|
|
1762
|
+
``individual_positions`` column: an assertion kind added later would
|
|
1763
|
+
otherwise have to be remembered in two independent field-by-field scans,
|
|
1764
|
+
and the one that forgot it would report ``individuals == ()`` for a
|
|
1765
|
+
knowledge base that names individuals.
|
|
1766
|
+
"""
|
|
1767
|
+
return tuple(sorted(_abox_individual_names(abox)))
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
# --------------------------------------------------------------------------- #
|
|
1771
|
+
# The two-sorted discipline: the vocabulary a knowledge base uses, and the
|
|
1772
|
+
# side axioms derived from it.
|
|
1773
|
+
# --------------------------------------------------------------------------- #
|
|
1774
|
+
|
|
1775
|
+
_SEPARATIONS = ("two-sorted", "data-lattice", "none")
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _check_separation(separation: str, where: str = "dl.kb_to_fol") -> None:
|
|
1779
|
+
if separation not in _SEPARATIONS:
|
|
1780
|
+
raise UnsupportedDatatypeError(
|
|
1781
|
+
f"{where}: separation={separation!r} is not one of "
|
|
1782
|
+
f"{', '.join(map(repr, _SEPARATIONS))}. 'two-sorted' is the full "
|
|
1783
|
+
f"discipline (the default), 'data-lattice' only the datatype facts, "
|
|
1784
|
+
f"'none' the data-box images alone.")
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
@dataclass
|
|
1788
|
+
class _Vocabulary:
|
|
1789
|
+
"""The names a knowledge base uses, by kind — what the sort axioms are
|
|
1790
|
+
derived FROM, so that they are scoped to the knowledge base's own vocabulary
|
|
1791
|
+
and not to every name in the OWL 2 datatype map."""
|
|
1792
|
+
|
|
1793
|
+
classes: Set[str] = field(default_factory=set)
|
|
1794
|
+
object_roles: Set[str] = field(default_factory=set)
|
|
1795
|
+
data_properties: Set[str] = field(default_factory=set)
|
|
1796
|
+
individuals: Set[str] = field(default_factory=set)
|
|
1797
|
+
datatypes: Set[str] = field(default_factory=set)
|
|
1798
|
+
literals: List[Literal] = field(default_factory=list)
|
|
1799
|
+
|
|
1800
|
+
def has_data(self) -> bool:
|
|
1801
|
+
"""True iff the knowledge base has a data layer at all: a data property,
|
|
1802
|
+
a datatype or a literal occurs in it. Every data axiom and every data
|
|
1803
|
+
restriction names at least a data property, so this is the whole test."""
|
|
1804
|
+
return bool(self.data_properties or self.datatypes or self.literals)
|
|
1805
|
+
|
|
1806
|
+
def add_role(self, role) -> None:
|
|
1807
|
+
self.object_roles.add(role.role if isinstance(role, InverseRole) else role)
|
|
1808
|
+
|
|
1809
|
+
def add_literal(self, literal: Literal) -> None:
|
|
1810
|
+
self.literals.append(literal)
|
|
1811
|
+
self.datatypes.add(literal.datatype)
|
|
1812
|
+
|
|
1813
|
+
def add_datarange(self, datarange: DataRange) -> None:
|
|
1814
|
+
self.datatypes.update(datarange_datatypes(datarange))
|
|
1815
|
+
for literal in datarange_literals(datarange):
|
|
1816
|
+
self.add_literal(literal)
|
|
1817
|
+
|
|
1818
|
+
def add_concept(self, concept: Concept) -> None:
|
|
1819
|
+
if isinstance(concept, Atomic):
|
|
1820
|
+
self.classes.add(concept.name)
|
|
1821
|
+
elif isinstance(concept, Nominal):
|
|
1822
|
+
self.individuals.add(concept.individual)
|
|
1823
|
+
elif isinstance(concept, HasValue):
|
|
1824
|
+
self.add_role(concept.role)
|
|
1825
|
+
self.individuals.add(concept.individual)
|
|
1826
|
+
elif isinstance(concept, Not):
|
|
1827
|
+
self.add_concept(concept.concept)
|
|
1828
|
+
elif isinstance(concept, (And, Or)):
|
|
1829
|
+
self.add_concept(concept.left)
|
|
1830
|
+
self.add_concept(concept.right)
|
|
1831
|
+
elif isinstance(concept, (Exists, ForAll, AtLeast, AtMost)):
|
|
1832
|
+
self.add_role(concept.role)
|
|
1833
|
+
self.add_concept(concept.concept)
|
|
1834
|
+
elif isinstance(concept, (DataExists, DataForAll, DataAtLeast, DataAtMost)):
|
|
1835
|
+
self.data_properties.add(concept.prop)
|
|
1836
|
+
self.add_datarange(concept.datarange)
|
|
1837
|
+
elif isinstance(concept, DataHasValue):
|
|
1838
|
+
self.data_properties.add(concept.prop)
|
|
1839
|
+
self.add_literal(concept.value)
|
|
1840
|
+
# Top / Bottom mention nothing.
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
def _collect_vocabulary(tbox: Optional[TBox], abox: Optional[ABox], *,
|
|
1844
|
+
where: str = "dl.translate",
|
|
1845
|
+
query: Iterable[Concept] = ()) -> _Vocabulary:
|
|
1846
|
+
"""Walk every axiom of ``(tbox, abox)`` once and collect its vocabulary,
|
|
1847
|
+
then the vocabulary of the ``query`` concepts: the concepts a caller is going
|
|
1848
|
+
to ASK about join the knowledge base's vocabulary, because everything derived
|
|
1849
|
+
from the vocabulary (the separation regime, the sort and datatype axioms, the
|
|
1850
|
+
punning check, the avoid set of the bound variables) must cover the question
|
|
1851
|
+
as well as the knowledge base. See :func:`kb_to_fol`.
|
|
1852
|
+
|
|
1853
|
+
Hand-listed over the fields, like every consumer of the TBox/ABox shape, and
|
|
1854
|
+
for that reason pinned by ``tests/test_dl_data.py``: a field the walk does
|
|
1855
|
+
not read would silently shrink the sort axioms.
|
|
1856
|
+
|
|
1857
|
+
Every stored value is VALIDATED first (``dl.tableau._validate_role_box`` and
|
|
1858
|
+
friends — the same checks the tableau's shared guard runs), because this walk
|
|
1859
|
+
is the first thing every image does with a ``TBox`` and a hand-built one with
|
|
1860
|
+
a malformed entry (a 3-tuple where a pair is stored) would otherwise die here,
|
|
1861
|
+
with the bare ``ValueError`` of an unpacking, before any second line of
|
|
1862
|
+
defence could name it.
|
|
1863
|
+
"""
|
|
1864
|
+
if tbox is not None:
|
|
1865
|
+
_validate_role_box(tbox, where=where)
|
|
1866
|
+
_validate_data_box(tbox, where=where)
|
|
1867
|
+
_reject_abox_roles(abox, where=where)
|
|
1868
|
+
v = _Vocabulary()
|
|
1869
|
+
if tbox is not None:
|
|
1870
|
+
for sub, sup in tbox.inclusions:
|
|
1871
|
+
v.add_concept(sub)
|
|
1872
|
+
v.add_concept(sup)
|
|
1873
|
+
for sub_role, super_role in tbox.role_inclusions:
|
|
1874
|
+
v.add_role(sub_role)
|
|
1875
|
+
v.add_role(super_role)
|
|
1876
|
+
for names in (tbox.transitive_roles, tbox.symmetric_roles,
|
|
1877
|
+
tbox.asymmetric_roles, tbox.reflexive_roles,
|
|
1878
|
+
tbox.irreflexive_roles, tbox.functional_roles,
|
|
1879
|
+
tbox.inverse_functional_roles):
|
|
1880
|
+
v.object_roles.update(names)
|
|
1881
|
+
for pair in tbox.inverse_role_pairs + tbox.disjoint_role_pairs:
|
|
1882
|
+
v.object_roles.update(pair)
|
|
1883
|
+
for chain, super_role in tbox.role_chains:
|
|
1884
|
+
v.object_roles.update(chain)
|
|
1885
|
+
v.object_roles.add(super_role)
|
|
1886
|
+
for role, concept in tbox.role_domains + tbox.role_ranges:
|
|
1887
|
+
v.object_roles.add(role)
|
|
1888
|
+
v.add_concept(concept)
|
|
1889
|
+
for pair in tbox.data_property_inclusions + tbox.disjoint_data_property_pairs:
|
|
1890
|
+
v.data_properties.update(pair)
|
|
1891
|
+
v.data_properties.update(tbox.functional_data_properties)
|
|
1892
|
+
for prop, concept in tbox.data_property_domains:
|
|
1893
|
+
v.data_properties.add(prop)
|
|
1894
|
+
v.add_concept(concept)
|
|
1895
|
+
for prop, datarange in tbox.data_property_ranges:
|
|
1896
|
+
v.data_properties.add(prop)
|
|
1897
|
+
v.add_datarange(datarange)
|
|
1898
|
+
for name, datarange in tbox.datatype_definitions:
|
|
1899
|
+
v.datatypes.add(canonical_datatype_name(name))
|
|
1900
|
+
v.add_datarange(datarange)
|
|
1901
|
+
if abox is not None:
|
|
1902
|
+
for individual, concept in abox.concept_assertions:
|
|
1903
|
+
v.individuals.add(individual)
|
|
1904
|
+
v.add_concept(concept)
|
|
1905
|
+
for a, b, role in abox.role_assertions + abox.negative_role_assertions:
|
|
1906
|
+
v.individuals.update((a, b))
|
|
1907
|
+
v.object_roles.add(role)
|
|
1908
|
+
for a, b in abox.distinct_assertions + abox.same_assertions:
|
|
1909
|
+
v.individuals.update((a, b))
|
|
1910
|
+
for individual, prop, literal in abox.data_assertions + abox.negative_data_assertions:
|
|
1911
|
+
v.individuals.add(individual)
|
|
1912
|
+
v.data_properties.add(prop)
|
|
1913
|
+
v.add_literal(literal)
|
|
1914
|
+
for concept in query:
|
|
1915
|
+
v.add_concept(concept)
|
|
1916
|
+
return v
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
def _check_reserved_names(vocabulary: _Vocabulary, separation: str,
|
|
1920
|
+
where: str = "dl.kb_to_fol") -> None:
|
|
1921
|
+
"""Refuse a knowledge base that uses a name the data image reserves.
|
|
1922
|
+
|
|
1923
|
+
``OwlData`` is reserved whenever there is a data layer (``rdfs:Literal`` and
|
|
1924
|
+
every complement are rendered with it); ``OwlThing`` only when the
|
|
1925
|
+
two-sorted axioms are asked for. A class that happened to be called
|
|
1926
|
+
``OwlData`` would otherwise be constrained by the sort axioms as if it were
|
|
1927
|
+
the data domain — a silent change to the ontology's own class.
|
|
1928
|
+
"""
|
|
1929
|
+
reserved = (OWL_DATA, OWL_THING) if separation == "two-sorted" else (OWL_DATA,)
|
|
1930
|
+
for name in reserved:
|
|
1931
|
+
for kind, names in (("class", vocabulary.classes),
|
|
1932
|
+
("object role", vocabulary.object_roles),
|
|
1933
|
+
("data property", vocabulary.data_properties),
|
|
1934
|
+
("individual", vocabulary.individuals),
|
|
1935
|
+
("datatype", vocabulary.datatypes)):
|
|
1936
|
+
if name in names:
|
|
1937
|
+
guard = ("the data domain" if name == OWL_DATA else "the object domain")
|
|
1938
|
+
raise UnsupportedDatatypeError(
|
|
1939
|
+
f"{where}: {name!r} is this module's reserved guard "
|
|
1940
|
+
f"predicate for {guard} of the OWL 2 two-sorted image (see "
|
|
1941
|
+
f"data_sort_axioms), but this knowledge base uses {name!r} as "
|
|
1942
|
+
f"a {kind} name, so the sort axioms would constrain the "
|
|
1943
|
+
f"ontology's own {kind}. Rename it"
|
|
1944
|
+
+ (", or pass separation='data-lattice' (which reserves only "
|
|
1945
|
+
"'OwlData') or 'none'." if name == OWL_THING else "."))
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
def _check_concepts_punning(concepts: Sequence[Concept], where: str) -> None:
|
|
1949
|
+
""":func:`_check_name_punning` over the vocabulary of ``concepts`` alone: the
|
|
1950
|
+
refusal of a pun INSIDE what a renderer is given (``∃P.B ⊓ ∃P.xsd:integer``),
|
|
1951
|
+
for the entry points that never see a knowledge base."""
|
|
1952
|
+
vocabulary = _Vocabulary()
|
|
1953
|
+
for concept in concepts:
|
|
1954
|
+
vocabulary.add_concept(concept)
|
|
1955
|
+
_check_name_punning(vocabulary, where, "the concepts it is given")
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
def _check_name_punning(vocabulary: _Vocabulary, where: str,
|
|
1959
|
+
scope: str = "this knowledge base") -> None:
|
|
1960
|
+
"""Refuse a knowledge base that uses ONE name for two kinds OWL 2 DL keeps
|
|
1961
|
+
apart: an object property and a data property, or a class and a datatype.
|
|
1962
|
+
|
|
1963
|
+
OWL 2 DL (Structural Specification, section 5.8.1) forbids both pairs. The
|
|
1964
|
+
stored axioms keep the kinds apart (the TBox has separate fields for roles
|
|
1965
|
+
and data properties), but the first-order image has ONE predicate per name:
|
|
1966
|
+
``P(x, y)`` of an object property and ``P(x, v)`` of a data property are the
|
|
1967
|
+
same predicate, so the conflation changes what follows from the knowledge
|
|
1968
|
+
base (a functional ``P`` with an object successor and a data value became
|
|
1969
|
+
INCONSISTENT, which the two-sorted reading says it is not). Printing it as
|
|
1970
|
+
one predicate would be an approximation; so it is refused, by name, before
|
|
1971
|
+
any image is built. A class, an object property and an individual that share
|
|
1972
|
+
a name (punning OWL 2 DL does allow) are not touched.
|
|
1973
|
+
|
|
1974
|
+
A class is compared with the datatypes by its CANONICAL name, so a class
|
|
1975
|
+
spelled with the full ``http://www.w3.org/2001/XMLSchema#integer`` IRI is
|
|
1976
|
+
the datatype ``xsd:integer`` too.
|
|
1977
|
+
"""
|
|
1978
|
+
pairs = (
|
|
1979
|
+
("an object property", vocabulary.object_roles,
|
|
1980
|
+
"a data property", vocabulary.data_properties,
|
|
1981
|
+
lambda name: name),
|
|
1982
|
+
("a class", vocabulary.classes,
|
|
1983
|
+
"a datatype", vocabulary.datatypes,
|
|
1984
|
+
canonical_datatype_name),
|
|
1985
|
+
)
|
|
1986
|
+
for first_kind, first, second_kind, second, canonical in pairs:
|
|
1987
|
+
if not first or not second:
|
|
1988
|
+
continue
|
|
1989
|
+
clash = sorted(name for name in first if canonical(name) in second)
|
|
1990
|
+
if clash:
|
|
1991
|
+
name = clash[0]
|
|
1992
|
+
raise UnsupportedDatatypeError(
|
|
1993
|
+
f"{where}: the name {name!r} is used as both {first_kind} and "
|
|
1994
|
+
f"{second_kind} in {scope}. OWL 2 DL forbids that "
|
|
1995
|
+
f"(the two are disjoint kinds), and the first-order image has "
|
|
1996
|
+
f"ONE predicate per name, so it would read {name!r} as both at "
|
|
1997
|
+
f"once and change what follows. Rename one of them"
|
|
1998
|
+
+ (f" (all clashing names: {', '.join(map(repr, clash))})."
|
|
1999
|
+
if len(clash) > 1 else "."))
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
def _literal_family(literal: Literal) -> Optional[str]:
|
|
2003
|
+
"""The group within which two distinct literal TERMS are known to be
|
|
2004
|
+
distinct VALUES, or ``None`` when this kit cannot say.
|
|
2005
|
+
|
|
2006
|
+
Exact numbers are compared by value (``Number``), the strings by their
|
|
2007
|
+
(whitespace-processed) text — ``xsd:string``, language-tagged strings, and
|
|
2008
|
+
``xsd:normalizedString``/``xsd:token``, whose term IS the ``xsd:string`` term
|
|
2009
|
+
of the processed text (:meth:`~unicode_logic_kit.dl.datatypes.Literal.to_term`)
|
|
2010
|
+
—, ``xsd:anyURI`` by its collapsed text, ``xsd:boolean`` by its canonical
|
|
2011
|
+
``true``/``false``. Other datatypes (``xsd:dateTime``, ``xsd:hexBinary``, …)
|
|
2012
|
+
have lexical forms that are NOT in one-to-one correspondence with values
|
|
2013
|
+
(``"0A"`` and ``"0a"`` are one hexBinary value), so claiming two different
|
|
2014
|
+
spellings denote different values would be unsound.
|
|
2015
|
+
"""
|
|
2016
|
+
if literal.datatype in EXACT_NUMBER_DATATYPES:
|
|
2017
|
+
return "number"
|
|
2018
|
+
if literal.datatype in ("xsd:string", "rdf:PlainLiteral", "xsd:normalizedString",
|
|
2019
|
+
"xsd:token"):
|
|
2020
|
+
return "string"
|
|
2021
|
+
if literal.datatype == "xsd:anyURI":
|
|
2022
|
+
return "anyURI"
|
|
2023
|
+
if literal.datatype == "xsd:boolean":
|
|
2024
|
+
return "boolean"
|
|
2025
|
+
return None
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
@dataclass(frozen=True)
|
|
2029
|
+
class _Coverage:
|
|
2030
|
+
"""What the side axioms of one :func:`kb_to_fol` bundle were derived from:
|
|
2031
|
+
the vocabulary of the knowledge base AND of the ``query=`` concepts, and the
|
|
2032
|
+
``var`` the bundle's GCIs bind. The three goal methods of
|
|
2033
|
+
:class:`KnowledgeBaseFOL` read it to tell a concept the bundle covers from one
|
|
2034
|
+
it does not. ``individuals`` is every individual the image types and so
|
|
2035
|
+
every one a bound variable must avoid.
|
|
2036
|
+
"""
|
|
2037
|
+
|
|
2038
|
+
var: str
|
|
2039
|
+
vocabulary: _Vocabulary
|
|
2040
|
+
individuals: FrozenSet[str]
|
|
2041
|
+
literals: FrozenSet[Tuple[Node, str]] # (term, written datatype) per literal
|
|
2042
|
+
|
|
2043
|
+
@property
|
|
2044
|
+
def avoid(self) -> FrozenSet[str]:
|
|
2045
|
+
return self.individuals
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def _reject_data_concept(concept: Concept, where: str) -> None:
|
|
2049
|
+
"""Refuse, by name, a concept with a data restriction for the registry edge
|
|
2050
|
+
``alc → fol``, which declares ``faithful`` and carries no side axioms.
|
|
2051
|
+
|
|
2052
|
+
That is true of a concept without a data layer. With one the image is a
|
|
2053
|
+
guarded ONE-sorted theory whose sorts, typing and datatype lattice are side
|
|
2054
|
+
axioms of a KNOWLEDGE BASE (:func:`data_sort_axioms`), so the bare image is a
|
|
2055
|
+
weaker theory: ``∃HasV.xsd:integer ⊓ ∀HasV.xsd:string`` closed existentially is
|
|
2056
|
+
satisfiable by a data value, where OWL 2 makes it unsatisfiable. A concept
|
|
2057
|
+
without one is untouched.
|
|
2058
|
+
"""
|
|
2059
|
+
vocabulary = _Vocabulary()
|
|
2060
|
+
vocabulary.add_concept(concept)
|
|
2061
|
+
if not vocabulary.has_data():
|
|
2062
|
+
return
|
|
2063
|
+
names = ([f"the data property {name!r}" for name in sorted(vocabulary.data_properties)]
|
|
2064
|
+
+ [f"the datatype {name!r}" for name in sorted(vocabulary.datatypes)])
|
|
2065
|
+
raise UnsupportedDatatypeError(
|
|
2066
|
+
f"{where}: the concept {concept.to_unicode()} has a data restriction "
|
|
2067
|
+
f"({', '.join(names)}). Its first-order image is ONE-sorted over "
|
|
2068
|
+
f"OwlThing and OwlData, and what makes it two-sorted — the separation "
|
|
2069
|
+
f"of the two domains, the typing of the property, the datatype lattice "
|
|
2070
|
+
f"and its disjointness, the literal facts — are side axioms of a "
|
|
2071
|
+
f"KNOWLEDGE BASE, not of one concept. This edge declares 'faithful' "
|
|
2072
|
+
f"and carries none, so it would answer for a weaker theory: "
|
|
2073
|
+
f"∃HasV.xsd:integer ⊓ ∀HasV.xsd:string would be satisfiable (a data "
|
|
2074
|
+
f"value satisfies it) where OWL 2 makes it unsatisfiable. Ask the "
|
|
2075
|
+
f"knowledge base instead: kb = dl.kb_to_fol(tbox, abox, "
|
|
2076
|
+
f"query=[concept]), then api.prove(kb.unsatisfiability_goal(concept), "
|
|
2077
|
+
f"kb.tbox_premises) (kb.subsumption_goal and kb.instance_goal are "
|
|
2078
|
+
f"the other two questions).")
|
|
2079
|
+
|
|
2080
|
+
|
|
2081
|
+
def _literal_key(literal: Literal) -> Tuple[Node, str]:
|
|
2082
|
+
"""``(term, datatype)`` of a literal — one ``LiteralTyping`` fact of the
|
|
2083
|
+
image. Raises :class:`UnsupportedDatatypeError` for a literal with no term."""
|
|
2084
|
+
return (literal.to_term(), literal.datatype)
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
def _query_concepts(query, where: str) -> Tuple[Concept, ...]:
|
|
2088
|
+
"""``query=`` as a tuple of concepts, or a :class:`TypeError` naming the
|
|
2089
|
+
mistake: a bare concept (``query=C``) or a string would otherwise iterate
|
|
2090
|
+
into nonsense, and a non-concept is no question."""
|
|
2091
|
+
if isinstance(query, (Concept, str)):
|
|
2092
|
+
raise TypeError(
|
|
2093
|
+
f"{where}: query= is an iterable of the concepts you are going to "
|
|
2094
|
+
f"ask about — write query=[concept], not query={query!r}.")
|
|
2095
|
+
concepts = () if query is None else tuple(query) # None: no query
|
|
2096
|
+
for concept in concepts:
|
|
2097
|
+
if not isinstance(concept, Concept):
|
|
2098
|
+
raise TypeError(
|
|
2099
|
+
f"{where}: query= takes concepts (dl.Atomic, dl.DataExists, …), "
|
|
2100
|
+
f"got {type(concept).__name__} {concept!r}.")
|
|
2101
|
+
# Rendered once, only to refuse by name what no goal could be built from:
|
|
2102
|
+
# an OWL 2 built-in property name as a role, a literal with no term. Better
|
|
2103
|
+
# here than as a typing axiom for a name that is no property at all.
|
|
2104
|
+
for concept in concepts:
|
|
2105
|
+
names = _individual_names(concept)
|
|
2106
|
+
_translate(concept, Variable("x"), _fresh_var_factory("x", names | {"x"}))
|
|
2107
|
+
return concepts
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
def _check_goal_concepts(coverage: _Coverage, separation: str, data_layer: bool,
|
|
2111
|
+
concepts: Sequence[Concept], where: str) -> None:
|
|
2112
|
+
"""Refuse, by name, a goal over ``concepts`` that the bundle behind
|
|
2113
|
+
``coverage`` is not the right theory for.
|
|
2114
|
+
|
|
2115
|
+
Two things make the answer wrong, and both are silent in the prover: a name
|
|
2116
|
+
used as two kinds of thing at once (the punning check, run over the bundle's
|
|
2117
|
+
vocabulary together with the goal's), and a name whose side axioms are not
|
|
2118
|
+
there. What a two-sorted bundle derives its axioms from is the object
|
|
2119
|
+
properties (typing), data properties (typing), datatypes (guard, lattice,
|
|
2120
|
+
disjointness) and literals (typing, distinctness); a data-lattice bundle
|
|
2121
|
+
only the last two; a bundle with no data layer none of them, so ANY data
|
|
2122
|
+
vocabulary in the goal is uncovered. ``separation="none"`` over a data layer
|
|
2123
|
+
says the caller supplies the discipline, so nothing is checked.
|
|
2124
|
+
"""
|
|
2125
|
+
goal = _Vocabulary()
|
|
2126
|
+
for concept in concepts:
|
|
2127
|
+
goal.add_concept(concept)
|
|
2128
|
+
have = coverage.vocabulary
|
|
2129
|
+
_check_name_punning(_Vocabulary(
|
|
2130
|
+
classes=have.classes | goal.classes,
|
|
2131
|
+
object_roles=have.object_roles | goal.object_roles,
|
|
2132
|
+
data_properties=have.data_properties | goal.data_properties,
|
|
2133
|
+
datatypes=have.datatypes | goal.datatypes), where,
|
|
2134
|
+
"this knowledge base together with the goal")
|
|
2135
|
+
if data_layer:
|
|
2136
|
+
_check_reserved_names(goal, separation, where)
|
|
2137
|
+
|
|
2138
|
+
roles = data_layer and separation == "two-sorted"
|
|
2139
|
+
properties = (not data_layer) or separation == "two-sorted"
|
|
2140
|
+
datatypes = (not data_layer) or separation in ("two-sorted", "data-lattice")
|
|
2141
|
+
missing: List[str] = []
|
|
2142
|
+
if roles:
|
|
2143
|
+
missing += [f"the object property {name!r}"
|
|
2144
|
+
for name in sorted(goal.object_roles - have.object_roles)]
|
|
2145
|
+
if properties:
|
|
2146
|
+
missing += [f"the data property {name!r}"
|
|
2147
|
+
for name in sorted(goal.data_properties - have.data_properties)]
|
|
2148
|
+
if datatypes:
|
|
2149
|
+
wanted = {canonical_datatype_name(name) for name in goal.datatypes}
|
|
2150
|
+
known = {canonical_datatype_name(name) for name in have.datatypes}
|
|
2151
|
+
missing += [f"the datatype {name!r}"
|
|
2152
|
+
for name in sorted(wanted - known - {"rdfs:Literal"})]
|
|
2153
|
+
seen = {}
|
|
2154
|
+
for literal in goal.literals:
|
|
2155
|
+
key = _literal_key(literal)
|
|
2156
|
+
if key not in coverage.literals:
|
|
2157
|
+
seen[literal.to_unicode()] = literal
|
|
2158
|
+
missing += [f"the literal {text}" for text in sorted(seen)]
|
|
2159
|
+
if not missing:
|
|
2160
|
+
return
|
|
2161
|
+
why = ("this knowledge base has no data layer, so its image carries no sort, "
|
|
2162
|
+
"typing or datatype axioms at all" if not data_layer else
|
|
2163
|
+
"the sort, typing, datatype-lattice and literal axioms of this "
|
|
2164
|
+
"knowledge base's image are derived from the names IT uses")
|
|
2165
|
+
raise UnsupportedDatatypeError(
|
|
2166
|
+
f"{where}: the goal names {', '.join(missing)}, which the knowledge "
|
|
2167
|
+
f"base this bundle was built from does not cover ({why}). A goal over "
|
|
2168
|
+
f"them is asked of a weaker theory than the one OWL 2 describes and "
|
|
2169
|
+
f"comes back 'refuted' for entailments OWL 2 makes — silently. Hand the "
|
|
2170
|
+
f"concepts you are going to ask about to the knowledge base when you "
|
|
2171
|
+
f"build it, dl.kb_to_fol(tbox, abox, query=[...]), and ask the new "
|
|
2172
|
+
f"bundle.")
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
def _sort_axioms(vocabulary: _Vocabulary, separation: str) -> List[SideAxiom]:
|
|
2176
|
+
"""The derived side axioms of a knowledge base that has a data layer. See
|
|
2177
|
+
:func:`data_sort_axioms` for what each one says and why it is there.
|
|
2178
|
+
|
|
2179
|
+
The four prefix variables avoid the knowledge base's individuals like every
|
|
2180
|
+
other binder of the image (:func:`_binder_names`). None of these axioms
|
|
2181
|
+
mentions an individual UNDER a quantifier, so none can capture one — it is
|
|
2182
|
+
kept as an invariant of the whole image, not a case analysis."""
|
|
2183
|
+
parts: List[SideAxiom] = []
|
|
2184
|
+
x, y, t, v = (Variable(name) for name in _binder_names(
|
|
2185
|
+
("x", "y", "t", "v"), vocabulary.individuals))
|
|
2186
|
+
thing, data = (lambda term: Atom(OWL_THING, (term,))), (lambda term: Atom(OWL_DATA, (term,)))
|
|
2187
|
+
|
|
2188
|
+
def forall(variables, body) -> Node:
|
|
2189
|
+
return _forall(list(variables), body)
|
|
2190
|
+
|
|
2191
|
+
if separation == "two-sorted":
|
|
2192
|
+
parts.append(SideAxiom("DomainSeparation", "sort",
|
|
2193
|
+
forall([t], _FNot(_FAnd(thing(t), data(t))))))
|
|
2194
|
+
parts.append(SideAxiom("DomainNonEmptiness", "sort",
|
|
2195
|
+
Quantifier("∃", t, thing(t))))
|
|
2196
|
+
parts.append(SideAxiom("DomainNonEmptiness", "sort",
|
|
2197
|
+
Quantifier("∃", t, data(t))))
|
|
2198
|
+
for role in sorted(vocabulary.object_roles):
|
|
2199
|
+
parts.append(SideAxiom("ObjectPropertyTyping", "sort", forall([x, y], Implies(
|
|
2200
|
+
Atom(role, (x, y)), _FAnd(thing(x), thing(y))))))
|
|
2201
|
+
for prop in sorted(vocabulary.data_properties):
|
|
2202
|
+
parts.append(SideAxiom("DataPropertyTyping", "sort", forall([x, v], Implies(
|
|
2203
|
+
Atom(prop, (x, v)), _FAnd(thing(x), data(v))))))
|
|
2204
|
+
for individual in sorted(vocabulary.individuals):
|
|
2205
|
+
parts.append(SideAxiom("IndividualTyping", "sort", thing(Constant(individual))))
|
|
2206
|
+
|
|
2207
|
+
datatypes = sorted(canonical_datatype_name(name) for name in vocabulary.datatypes
|
|
2208
|
+
if canonical_datatype_name(name) != "rdfs:Literal")
|
|
2209
|
+
used = set(datatypes)
|
|
2210
|
+
for name in datatypes:
|
|
2211
|
+
parts.append(SideAxiom("DatatypeGuard", "datatype", forall([v], Implies(
|
|
2212
|
+
Atom(name, (v,)), data(v)))))
|
|
2213
|
+
for name in datatypes:
|
|
2214
|
+
for ancestor in sorted(datatype_ancestors(name) & used):
|
|
2215
|
+
parts.append(SideAxiom("DatatypeSubsumption", "datatype", forall([v], Implies(
|
|
2216
|
+
Atom(name, (v,)), Atom(ancestor, (v,))))))
|
|
2217
|
+
for i, left in enumerate(datatypes):
|
|
2218
|
+
for right in datatypes[i + 1:]:
|
|
2219
|
+
family_left, family_right = datatype_family(left), datatype_family(right)
|
|
2220
|
+
if family_left is not None and family_right is not None \
|
|
2221
|
+
and family_left != family_right:
|
|
2222
|
+
parts.append(SideAxiom("DatatypeDisjointness", "datatype", forall([v], _FNot(
|
|
2223
|
+
_FAnd(Atom(left, (v,)), Atom(right, (v,)))))))
|
|
2224
|
+
|
|
2225
|
+
# Literals: one TERM per data value, however many spellings denote it.
|
|
2226
|
+
terms: dict = {}
|
|
2227
|
+
for literal in vocabulary.literals:
|
|
2228
|
+
term = literal.to_term()
|
|
2229
|
+
entry = terms.setdefault(term, {"datatypes": set(), "family": _literal_family(literal)})
|
|
2230
|
+
entry["datatypes"].add(literal.datatype)
|
|
2231
|
+
# An order, not a text a reader sees: the literals are sorted by the text of
|
|
2232
|
+
# 0.30.0 (every constant by its bare name), so the side axioms keep the order
|
|
2233
|
+
# they had whatever the names hold (an escape in a quoted name changes how
|
|
2234
|
+
# two names compare).
|
|
2235
|
+
ordered = sorted(terms, key=key_text)
|
|
2236
|
+
for term in ordered:
|
|
2237
|
+
parts.append(SideAxiom("LiteralTyping", "datatype", data(term)))
|
|
2238
|
+
for name in sorted(terms[term]["datatypes"]):
|
|
2239
|
+
if name != "rdfs:Literal":
|
|
2240
|
+
parts.append(SideAxiom("LiteralTyping", "datatype", Atom(name, (term,))))
|
|
2241
|
+
for family in ("number", "string", "anyURI", "boolean"):
|
|
2242
|
+
members = [term for term in ordered if terms[term]["family"] == family]
|
|
2243
|
+
for i, left in enumerate(members):
|
|
2244
|
+
for right in members[i + 1:]:
|
|
2245
|
+
parts.append(SideAxiom("LiteralDistinctness", "datatype",
|
|
2246
|
+
Atom("≠", (left, right))))
|
|
2247
|
+
return parts
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
def data_sort_axioms(tbox: Optional[TBox] = None, abox: Optional[ABox] = None, *,
|
|
2251
|
+
separation: str = "two-sorted",
|
|
2252
|
+
query: Iterable[Concept] = ()) -> Tuple[SideAxiom, ...]:
|
|
2253
|
+
"""The side axioms that turn the data layer's one-sorted image into the
|
|
2254
|
+
two-sorted theory OWL 2 describes, derived from the vocabulary ``(tbox,
|
|
2255
|
+
abox)`` actually uses — the same axioms :func:`kb_to_fol` puts in
|
|
2256
|
+
``side_axioms``, for a caller who builds a question by hand (a bare
|
|
2257
|
+
:func:`concept_to_fol` query, say) and needs them as premises.
|
|
2258
|
+
|
|
2259
|
+
Empty for a knowledge base without a data layer, and for
|
|
2260
|
+
``separation="none"``. ``query`` is :func:`kb_to_fol`'s: the concepts you are
|
|
2261
|
+
going to ask about, whose vocabulary joins the knowledge base's — a
|
|
2262
|
+
knowledge base without a data layer asked about a data restriction HAS one,
|
|
2263
|
+
for this purpose.
|
|
2264
|
+
|
|
2265
|
+
``separation="two-sorted"`` (``group "sort"``, then ``"datatype"``):
|
|
2266
|
+
|
|
2267
|
+
* ``DomainSeparation`` ``∀t ¬(OwlThing(t) ∧ OwlData(t))`` — OWL 2's object
|
|
2268
|
+
and data domains are disjoint;
|
|
2269
|
+
* ``DomainNonEmptiness`` ``∃t OwlThing(t)``, ``∃t OwlData(t)``;
|
|
2270
|
+
* ``ObjectPropertyTyping`` ``∀x ∀y (r(x, y) → OwlThing(x) ∧ OwlThing(y))``
|
|
2271
|
+
per object role, ``DataPropertyTyping`` ``∀x ∀v (d(x, v) → OwlThing(x) ∧
|
|
2272
|
+
OwlData(v))`` per data property, ``IndividualTyping`` ``OwlThing(a)`` per
|
|
2273
|
+
individual — one per NAME the knowledge base uses, not per class name:
|
|
2274
|
+
with every GCI relativised to the object domain a class atom needs no
|
|
2275
|
+
typing of its own.
|
|
2276
|
+
|
|
2277
|
+
Both ``"two-sorted"`` and ``"data-lattice"`` (``group "datatype"``), over the
|
|
2278
|
+
datatypes the knowledge base MENTIONS:
|
|
2279
|
+
|
|
2280
|
+
* ``DatatypeGuard`` ``∀v (D(v) → OwlData(v))`` — a datatype is a subset of
|
|
2281
|
+
the data domain (``rdfs:Literal`` IS the data domain and is rendered as
|
|
2282
|
+
``OwlData`` itself, so it has none);
|
|
2283
|
+
* ``DatatypeSubsumption`` ``∀v (D(v) → E(v))`` for ``D`` below ``E`` in the
|
|
2284
|
+
OWL 2 datatype map, both mentioned (``xsd:integer`` ⊑ ``xsd:decimal``);
|
|
2285
|
+
* ``DatatypeDisjointness`` ``∀v ¬(D(v) ∧ E(v))`` for two mentioned built-in
|
|
2286
|
+
datatypes of different families (``xsd:integer``/``xsd:string``): this is
|
|
2287
|
+
what makes ``DataPropertyRange(d xsd:integer)`` together with
|
|
2288
|
+
``DataPropertyRange(d xsd:string)`` force ``d`` empty;
|
|
2289
|
+
* ``LiteralTyping`` ``OwlData(t)`` and ``D(t)`` for each literal's term ``t``
|
|
2290
|
+
and datatype ``D``;
|
|
2291
|
+
* ``LiteralDistinctness`` ``t ≠ u`` for two distinct terms within the exact
|
|
2292
|
+
numbers, within the strings (``xsd:string``, language-tagged strings and
|
|
2293
|
+
the whitespace-processed ``xsd:normalizedString``/``xsd:token``), within
|
|
2294
|
+
the ``xsd:anyURI`` values, within the booleans — pairwise, so QUADRATIC in
|
|
2295
|
+
the number of distinct literals of one family (1000 distinct integers are
|
|
2296
|
+
499,500 axioms). Across families the typing and the disjointness already
|
|
2297
|
+
separate them; for the datatypes whose lexical forms are not in
|
|
2298
|
+
one-to-one correspondence with values (``xsd:dateTime``,
|
|
2299
|
+
``xsd:hexBinary``) nothing is claimed.
|
|
2300
|
+
|
|
2301
|
+
What it is not: complete. Facet comparisons are uninterpreted atoms for
|
|
2302
|
+
``api.prove`` (use :mod:`unicode_logic_kit.atp.z3_arith` for arithmetic over
|
|
2303
|
+
data ranges alone), and the cardinality of a value space (``xsd:boolean``
|
|
2304
|
+
has exactly two values) is not stated.
|
|
2305
|
+
|
|
2306
|
+
Raises:
|
|
2307
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
2308
|
+
``separation`` is not a known name, the
|
|
2309
|
+
knowledge base uses ``OwlThing``/``OwlData`` as its own name, a
|
|
2310
|
+
literal has no term, or one name is used both as an object property
|
|
2311
|
+
and a data property or both as a class and a datatype (OWL 2 DL
|
|
2312
|
+
forbids both; the image has one predicate per name — rename one).
|
|
2313
|
+
"""
|
|
2314
|
+
_check_separation(separation)
|
|
2315
|
+
queried = _query_concepts(query, "dl.data_sort_axioms")
|
|
2316
|
+
vocabulary = _collect_vocabulary(tbox, abox, query=queried)
|
|
2317
|
+
_check_name_punning(vocabulary, "dl.data_sort_axioms")
|
|
2318
|
+
if separation == "none" or not vocabulary.has_data():
|
|
2319
|
+
return ()
|
|
2320
|
+
_check_reserved_names(vocabulary, separation)
|
|
2321
|
+
return tuple(_sort_axioms(vocabulary, separation))
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
def _union_vocabularies(vocabularies: Iterable[_Vocabulary]) -> _Vocabulary:
|
|
2325
|
+
"""One :class:`_Vocabulary` holding every name of each of ``vocabularies``."""
|
|
2326
|
+
union = _Vocabulary()
|
|
2327
|
+
for vocabulary in vocabularies:
|
|
2328
|
+
union.classes |= vocabulary.classes
|
|
2329
|
+
union.object_roles |= vocabulary.object_roles
|
|
2330
|
+
union.data_properties |= vocabulary.data_properties
|
|
2331
|
+
union.individuals |= vocabulary.individuals
|
|
2332
|
+
union.datatypes |= vocabulary.datatypes
|
|
2333
|
+
union.literals.extend(vocabulary.literals)
|
|
2334
|
+
return union
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
def check_kb_names(*parts, separation: str = "two-sorted",
|
|
2338
|
+
query: Iterable[Concept] = ()) -> None:
|
|
2339
|
+
"""Run, over several pieces of one knowledge base at once, the check on names
|
|
2340
|
+
that :func:`kb_to_fol` runs over the whole of it.
|
|
2341
|
+
|
|
2342
|
+
Every function that renders one box of a knowledge base —
|
|
2343
|
+
:func:`tbox_to_fol`, :func:`rbox_to_fol`, :func:`databox_to_fol`,
|
|
2344
|
+
:func:`abox_to_fol` — refuses a name used as two kinds OWL 2 DL keeps apart
|
|
2345
|
+
(an object property and a data property, a class and a datatype, or a name
|
|
2346
|
+
the data image reserves) only over what it is given. A caller who renders the
|
|
2347
|
+
boxes one call at a time, from the same ontology or from several, and
|
|
2348
|
+
conjoins the images by hand, therefore has no call that sees the clash:
|
|
2349
|
+
``P`` as the object property of an ABox role assertion and as the data
|
|
2350
|
+
property of a TBox's ``DataPropertyRange`` is ONE predicate ``P`` in the
|
|
2351
|
+
combined image, and what follows from it changes. This function takes the
|
|
2352
|
+
pieces and answers what :func:`kb_to_fol` would answer about their union.
|
|
2353
|
+
|
|
2354
|
+
``parts`` are the :class:`~unicode_logic_kit.dl.tableau.TBox`,
|
|
2355
|
+
:class:`~unicode_logic_kit.dl.tableau.ABox` or
|
|
2356
|
+
:class:`KnowledgeBaseFOL` objects the images were built from, in any number
|
|
2357
|
+
and any mixture (a bundle contributes the vocabulary it was built over,
|
|
2358
|
+
``query=`` concepts included). A formula is not accepted: the image of a box
|
|
2359
|
+
is an ordinary first-order formula, in which ``P(x, y)`` of an object
|
|
2360
|
+
property and ``P(x, v)`` of a data property are the same atom, so which kind
|
|
2361
|
+
a predicate was is not recorded in it, and any guess from its shape would be
|
|
2362
|
+
an approximation. ``separation`` and ``query`` are :func:`kb_to_fol`'s.
|
|
2363
|
+
|
|
2364
|
+
A class, an object property and an individual that share a name (punning
|
|
2365
|
+
that OWL 2 DL does allow) are accepted: in the image they are three symbols,
|
|
2366
|
+
the unary predicate ``A(x)``, the binary predicate ``A(x, y)`` and the
|
|
2367
|
+
constant ``A``.
|
|
2368
|
+
|
|
2369
|
+
Returns ``None``; the function exists for what it refuses.
|
|
2370
|
+
|
|
2371
|
+
Raises:
|
|
2372
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
2373
|
+
``separation`` is not a known name, a name of the union is used as
|
|
2374
|
+
both an object property and a data property or as both a class and a
|
|
2375
|
+
datatype, or ``OwlThing``/``OwlData`` is used as one of its own
|
|
2376
|
+
names — the refusals :func:`kb_to_fol` makes, here over the union of
|
|
2377
|
+
the pieces.
|
|
2378
|
+
TypeError: a piece is not a ``TBox``, ``ABox`` or ``KnowledgeBaseFOL``
|
|
2379
|
+
(a formula is named in the message), or ``query`` is not an
|
|
2380
|
+
iterable of concepts.
|
|
2381
|
+
ValueError: a ``KnowledgeBaseFOL`` piece was not built by
|
|
2382
|
+
:func:`kb_to_fol`, so it does not record its vocabulary.
|
|
2383
|
+
"""
|
|
2384
|
+
where = "dl.check_kb_names"
|
|
2385
|
+
_check_separation(separation, where)
|
|
2386
|
+
queried = _query_concepts(query, where)
|
|
2387
|
+
vocabularies = []
|
|
2388
|
+
for part in parts:
|
|
2389
|
+
if isinstance(part, TBox):
|
|
2390
|
+
vocabularies.append(_collect_vocabulary(part, None, where=where))
|
|
2391
|
+
elif isinstance(part, ABox):
|
|
2392
|
+
vocabularies.append(_collect_vocabulary(None, part, where=where))
|
|
2393
|
+
elif isinstance(part, KnowledgeBaseFOL):
|
|
2394
|
+
if part._coverage is None:
|
|
2395
|
+
raise ValueError(
|
|
2396
|
+
f"{where}: this KnowledgeBaseFOL was not built by "
|
|
2397
|
+
f"dl.kb_to_fol, so it does not record the vocabulary it was "
|
|
2398
|
+
f"built over. Pass the TBox and ABox it was built from.")
|
|
2399
|
+
vocabularies.append(part._coverage.vocabulary)
|
|
2400
|
+
elif isinstance(part, Node):
|
|
2401
|
+
raise TypeError(
|
|
2402
|
+
f"{where}: got a formula ({type(part).__name__}), but the image "
|
|
2403
|
+
f"of a box does not record which kind each of its predicates is "
|
|
2404
|
+
f"(P(x, y) of an object property and P(x, v) of a data property "
|
|
2405
|
+
f"are the same atom), so the names cannot be checked from it. "
|
|
2406
|
+
f"Pass the TBox / ABox the image was built from.")
|
|
2407
|
+
else:
|
|
2408
|
+
raise TypeError(
|
|
2409
|
+
f"{where}: takes TBox, ABox or KnowledgeBaseFOL objects, got "
|
|
2410
|
+
f"{type(part).__name__} {part!r}.")
|
|
2411
|
+
vocabulary = _union_vocabularies(
|
|
2412
|
+
vocabularies + [_collect_vocabulary(None, None, where=where, query=queried)])
|
|
2413
|
+
_check_name_punning(vocabulary, where, "the pieces it is given, taken together")
|
|
2414
|
+
if vocabulary.has_data():
|
|
2415
|
+
_check_reserved_names(vocabulary, separation, where)
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
def kb_to_fol(tbox: Optional[TBox] = None, abox: Optional[ABox] = None,
|
|
2419
|
+
var: str = "x", *, separation: str = "two-sorted",
|
|
2420
|
+
query: Iterable[Concept] = ()) -> KnowledgeBaseFOL:
|
|
2421
|
+
"""Render the knowledge base ``(tbox, abox)`` as FOL — the supported way to ask
|
|
2422
|
+
a question *relative to* a TBox that has a role box.
|
|
2423
|
+
|
|
2424
|
+
The concept inclusions (:func:`tbox_to_fol`) and the ABox
|
|
2425
|
+
(:func:`abox_to_fol`) become :attr:`~KnowledgeBaseFOL.formula`; the role box
|
|
2426
|
+
(:func:`rbox_to_fol`'s content, one formula per axiom) becomes the SEPARATE
|
|
2427
|
+
:attr:`~KnowledgeBaseFOL.side_axioms`, each a :class:`SideAxiom` carrying
|
|
2428
|
+
the OWL keyword it came from, so a census of the image per axiom kind
|
|
2429
|
+
(:meth:`~KnowledgeBaseFOL.axioms_of_kind`) is a question with an answer;
|
|
2430
|
+
:attr:`~KnowledgeBaseFOL.axioms` is the bare-formula view of that one
|
|
2431
|
+
field. Following the kit's side-axiom convention
|
|
2432
|
+
(:class:`~unicode_logic_kit.comorphism.TranslationResult`), the axioms are never
|
|
2433
|
+
conjoined into the formula: the caller passes them as premises, which is what
|
|
2434
|
+
``premises`` / ``tbox_premises`` spell. ``tbox`` may be ``None`` (an ABox-only
|
|
2435
|
+
knowledge base) and ``abox`` may be ``None`` (a TBox-only one), the same
|
|
2436
|
+
defaults as the tableau API; ``var`` is :func:`tbox_to_fol`'s.
|
|
2437
|
+
|
|
2438
|
+
``query`` is an iterable of the concepts you are going to ASK about. Their
|
|
2439
|
+
vocabulary — classes, object properties, data properties, datatypes,
|
|
2440
|
+
literals, individuals — joins the knowledge base's for everything derived
|
|
2441
|
+
from the vocabulary: the separation regime (a data restriction in the query
|
|
2442
|
+
makes the image two-sorted even when the TBox and ABox have no data layer),
|
|
2443
|
+
the sort, typing, datatype-lattice, disjointness and literal axioms, the
|
|
2444
|
+
punning check and the variables a binder must avoid. Without ``query`` the
|
|
2445
|
+
bundle is what it always was. It matters because those axioms are scoped to
|
|
2446
|
+
the names the knowledge base mentions, so a goal naming ``xsd:decimal`` over
|
|
2447
|
+
a knowledge base that only mentions ``xsd:integer`` would be asked of a
|
|
2448
|
+
theory without ``integer ⊑ decimal`` — and a concept whose satisfiability
|
|
2449
|
+
turns on the datatype lattice would come out satisfiable. The goal methods
|
|
2450
|
+
below REFUSE a concept the bundle does not cover, by name, and say to pass
|
|
2451
|
+
``query=``.
|
|
2452
|
+
|
|
2453
|
+
Each of the standard questions, with the tableau function it must agree with
|
|
2454
|
+
(``kb = kb_to_fol(tbox, abox, query=[…the concepts below…])``; ``prove`` is
|
|
2455
|
+
:func:`unicode_logic_kit.api.prove`, whose verdict ``.status`` is ``"proved"``,
|
|
2456
|
+
``"refuted"`` or ``"unknown"``)::
|
|
2457
|
+
|
|
2458
|
+
# (a) subsumption relative to the TBox (the ABox plays no part)
|
|
2459
|
+
prove(kb.subsumption_goal(sub, sup), kb.tbox_premises)
|
|
2460
|
+
# proved <=> dl.subsumes(sub, sup, tbox) refuted <=> it does not
|
|
2461
|
+
|
|
2462
|
+
# (b) concept satisfiability relative to the TBox
|
|
2463
|
+
prove(kb.unsatisfiability_goal(C), kb.tbox_premises)
|
|
2464
|
+
# proved <=> dl.concept_satisfiable(C, tbox) is False
|
|
2465
|
+
|
|
2466
|
+
# (c) instance checking: does the knowledge base entail a : C ?
|
|
2467
|
+
prove(kb.instance_goal("a", C), kb.premises)
|
|
2468
|
+
# proved <=> dl.instance_check(abox, "a", C, tbox)
|
|
2469
|
+
|
|
2470
|
+
# (d) consistency of the knowledge base (Not = unicode_logic_kit.fol.nodes.Not)
|
|
2471
|
+
prove(Not(kb.formula), kb.axioms)
|
|
2472
|
+
# proved <=> INCONSISTENT (dl.abox_consistent(abox, tbox) is False)
|
|
2473
|
+
# refuted <=> consistent (same as is_satisfiable(kb.formula ∧ kb.axioms))
|
|
2474
|
+
|
|
2475
|
+
For a knowledge base without a data layer each goal method builds exactly the
|
|
2476
|
+
spelling the earlier releases documented (``subsumption_to_fol(sub, sup)``;
|
|
2477
|
+
``¬∃x π(C, x)``, the ``Quantifier("∃", Variable("x"), …)`` around the free
|
|
2478
|
+
``x``; ``abox_to_fol(ABox().assert_concept("a", C))``, ``π(C, a)`` with the
|
|
2479
|
+
individual as a constant), up to a renamed bound variable where an individual
|
|
2480
|
+
is called ``x``. The point of the methods is the knowledge base WITH a data
|
|
2481
|
+
layer, where the right goal depends on :attr:`KnowledgeBaseFOL.separation`
|
|
2482
|
+
and the wrong one answers silently wrong: they pick it, and
|
|
2483
|
+
:attr:`KnowledgeBaseFOL.refutation_is_decisive` says what a ``"refuted"`` is
|
|
2484
|
+
worth. An inconsistent knowledge base entails everything, in (c) as in the
|
|
2485
|
+
tableau; ``"unknown"`` means the backends gave no answer within budget
|
|
2486
|
+
(first-order entailment with transitivity is only semi-decidable), not that
|
|
2487
|
+
either answer holds.
|
|
2488
|
+
|
|
2489
|
+
Do NOT build these queries from the pieces by hand with
|
|
2490
|
+
``Implies(tbox_to_fol(tbox), …)``: that drops the role box and can report a
|
|
2491
|
+
false counterexample (see the module docstring); :func:`tbox_to_fol` refuses
|
|
2492
|
+
such a TBox unless told ``concept_inclusions_only=True``.
|
|
2493
|
+
|
|
2494
|
+
**A knowledge base with a data layer** (a data property, a datatype or a
|
|
2495
|
+
literal anywhere in it, the ``query`` concepts included) additionally gets
|
|
2496
|
+
the data box's images and — per ``separation`` — the axioms that make it a
|
|
2497
|
+
two-sorted theory, all in ``side_axioms`` (see "The data layer" in the module
|
|
2498
|
+
docstring and :func:`data_sort_axioms`). What makes the question the
|
|
2499
|
+
prover answers the TWO-SORTED one:
|
|
2500
|
+
|
|
2501
|
+
* the premises: ``kb.premises`` / ``kb.tbox_premises`` / ``kb.axioms`` as
|
|
2502
|
+
above — they already contain the separation, the typing, the lattice and
|
|
2503
|
+
the literal facts; ``kb.formula`` already has every GCI relativised to
|
|
2504
|
+
the object domain;
|
|
2505
|
+
* the goal from the three methods, which relativise it the same way when
|
|
2506
|
+
``kb.separation == "two-sorted"``; the consistency query
|
|
2507
|
+
``Not(kb.formula)`` needs nothing extra.
|
|
2508
|
+
|
|
2509
|
+
The two-sorted image of a data layer is SOUND and not complete (see
|
|
2510
|
+
:attr:`KnowledgeBaseFOL.refutation_is_decisive`): ``"proved"`` transfers to
|
|
2511
|
+
OWL 2, ``"refuted"`` does not.
|
|
2512
|
+
|
|
2513
|
+
``separation`` is ``"two-sorted"`` (the default: the full discipline),
|
|
2514
|
+
``"data-lattice"`` (only the datatype facts — faithful WITHIN the data
|
|
2515
|
+
domain, no object/data separation, GCIs as written, so the premise count is
|
|
2516
|
+
that of the knowledge base plus its datatypes and literals) or ``"none"``
|
|
2517
|
+
(the data-box images alone, and the caller supplies any sort discipline).
|
|
2518
|
+
Neither of those two is sound for a knowledge base with a data layer:
|
|
2519
|
+
their inclusions are not restricted to the object domain, so they also
|
|
2520
|
+
range over data values and the premises are STRONGER than OWL 2's — the
|
|
2521
|
+
three goal methods refuse such a bundle, and a proof obtained from its
|
|
2522
|
+
premises by hand does not transfer.
|
|
2523
|
+
The effective choice is recorded in :attr:`KnowledgeBaseFOL.separation`.
|
|
2524
|
+
A knowledge base WITHOUT a data layer gets none of this, whatever
|
|
2525
|
+
``separation`` says: its output is byte-identical to a release before the
|
|
2526
|
+
data layer existed.
|
|
2527
|
+
|
|
2528
|
+
Raises:
|
|
2529
|
+
~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
|
|
2530
|
+
``separation`` is not one of the three names;
|
|
2531
|
+
the knowledge base uses ``OwlThing``/``OwlData`` (the reserved
|
|
2532
|
+
guard predicates) as one of its own names; a literal has no term;
|
|
2533
|
+
or one name is used both as an object property and a data property
|
|
2534
|
+
or both as a class and a datatype — OWL 2 DL forbids both, and the
|
|
2535
|
+
image has ONE predicate per name, so it would conflate them and
|
|
2536
|
+
change what follows. Rename one of the two.
|
|
2537
|
+
TypeError: ``query`` is not an iterable of concepts (``query=C`` for one
|
|
2538
|
+
concept is a mistake: write ``query=[C]``).
|
|
2539
|
+
|
|
2540
|
+
A class, an object property and an individual that share a name (OWL 2 DL
|
|
2541
|
+
punning, which is allowed) are not refused and are not conflated: the image
|
|
2542
|
+
writes the class as the unary predicate ``A(x)``, the object property as the
|
|
2543
|
+
binary predicate ``A(x, y)`` and the individual as the constant ``A`` — three
|
|
2544
|
+
symbols. :func:`tbox_to_fol`, :func:`rbox_to_fol`, :func:`abox_to_fol` and
|
|
2545
|
+
:func:`databox_to_fol` write them the same way, so images of one ontology
|
|
2546
|
+
rendered box by box and conjoined are one theory. A route that keys a symbol
|
|
2547
|
+
on its kind and arity (the TPTP writers: Vampire, E) reads three symbols.
|
|
2548
|
+
Names that punning does NOT allow are refused — see :func:`check_kb_names`,
|
|
2549
|
+
which runs that check over boxes rendered separately.
|
|
2550
|
+
|
|
2551
|
+
The agreement with the tableau holds on the fragment it decides — ALCH with
|
|
2552
|
+
transitive roles, and qualified number restrictions on simple roles (the
|
|
2553
|
+
``Count`` image). The translation itself is wider (inverse roles, nominals)
|
|
2554
|
+
and is then decided by the FOL backends alone.
|
|
2555
|
+
"""
|
|
2556
|
+
_check_separation(separation)
|
|
2557
|
+
queried = _query_concepts(query, "dl.kb_to_fol")
|
|
2558
|
+
vocabulary = _collect_vocabulary(tbox, abox, where="dl.kb_to_fol", query=queried)
|
|
2559
|
+
_check_name_punning(vocabulary, "dl.kb_to_fol")
|
|
2560
|
+
effective = separation if vocabulary.has_data() else "none"
|
|
2561
|
+
if vocabulary.has_data():
|
|
2562
|
+
_check_reserved_names(vocabulary, separation)
|
|
2563
|
+
object_sort = effective == "two-sorted"
|
|
2564
|
+
# ONE avoid set for the whole call: every bound variable of every axiom
|
|
2565
|
+
# below — the GCI prefix variable, the role-box and data-box prefix
|
|
2566
|
+
# variables, the sort axioms', the minted ones — avoids every individual of
|
|
2567
|
+
# the knowledge base, so no binder anywhere in the image shares a name with
|
|
2568
|
+
# a constant and the image of one axiom never depends on which others
|
|
2569
|
+
# happen to be in the call (see _binder_names).
|
|
2570
|
+
avoid = vocabulary.individuals
|
|
2571
|
+
tbox_image = (_tbox_image(tbox, var, object_sort, avoid)
|
|
2572
|
+
if tbox is not None else _tautology())
|
|
2573
|
+
abox_image = _abox_image(abox, avoid) if abox is not None else _tautology()
|
|
2574
|
+
has_gcis = tbox is not None and bool(tbox.inclusions)
|
|
2575
|
+
# Derived from the axiom-kind table (ABox.is_empty), not from a hand-written
|
|
2576
|
+
# `or` over the assertion fields: an assertion kind this condition did not
|
|
2577
|
+
# know about would drop the ABox half of `formula` SILENTLY.
|
|
2578
|
+
has_assertions = abox is not None and not abox.is_empty()
|
|
2579
|
+
formulas = tuple(([tbox_image] if has_gcis else []) +
|
|
2580
|
+
([abox_image] if has_assertions else []))
|
|
2581
|
+
side_axioms: List[SideAxiom] = []
|
|
2582
|
+
if tbox is not None:
|
|
2583
|
+
side_axioms.extend(_rbox_axioms(tbox, object_sort=object_sort, avoid=avoid))
|
|
2584
|
+
side_axioms.extend(_databox_axioms(tbox, avoid=avoid))
|
|
2585
|
+
if effective != "none":
|
|
2586
|
+
side_axioms.extend(_sort_axioms(vocabulary, effective))
|
|
2587
|
+
return KnowledgeBaseFOL(
|
|
2588
|
+
formula=_conjoin(list(formulas)),
|
|
2589
|
+
side_axioms=tuple(side_axioms),
|
|
2590
|
+
tbox=tbox_image,
|
|
2591
|
+
abox=abox_image,
|
|
2592
|
+
individuals=_abox_individuals(abox) if abox is not None else (),
|
|
2593
|
+
formulas=formulas,
|
|
2594
|
+
separation=effective,
|
|
2595
|
+
data_layer=vocabulary.has_data(),
|
|
2596
|
+
_coverage=_Coverage(
|
|
2597
|
+
var=var, vocabulary=vocabulary,
|
|
2598
|
+
individuals=frozenset(vocabulary.individuals),
|
|
2599
|
+
literals=frozenset(_literal_key(literal)
|
|
2600
|
+
for literal in vocabulary.literals)),
|
|
2601
|
+
)
|
|
2602
|
+
|
|
2603
|
+
|
|
2604
|
+
# --------------------------------------------------------------------------- #
|
|
2605
|
+
# Single-role concepts as propositional modal K formulas.
|
|
2606
|
+
# --------------------------------------------------------------------------- #
|
|
2607
|
+
|
|
2608
|
+
_IO_MODAL_MSG = (
|
|
2609
|
+
"concept_to_modal: {what} has no faithful propositional-K rendering — "
|
|
2610
|
+
"plain modal K has no converse modality (inverse roles need TENSE/hybrid "
|
|
2611
|
+
"logic) and no naming/nominal construct at all (that is HYBRID logic, a "
|
|
2612
|
+
"strictly different formalism this kit does not implement); refusing "
|
|
2613
|
+
"rather than silently dropping or misencoding it. Use concept_to_fol "
|
|
2614
|
+
"instead: the standard translation to FOL handles both faithfully (see "
|
|
2615
|
+
"its module docstring's 'Inverse roles and nominals (I, O)' section)."
|
|
2616
|
+
)
|
|
2617
|
+
|
|
2618
|
+
|
|
2619
|
+
def _roles_used(concept: Concept) -> set:
|
|
2620
|
+
"""Return the set of distinct role names occurring anywhere in ``concept``."""
|
|
2621
|
+
if isinstance(concept, (Top, Bottom, Atomic)):
|
|
2622
|
+
return set()
|
|
2623
|
+
if isinstance(concept, Nominal):
|
|
2624
|
+
raise NotImplementedError(_IO_MODAL_MSG.format(what=f"the nominal {{{concept.individual}}}"))
|
|
2625
|
+
if isinstance(concept, DATA_CONCEPTS):
|
|
2626
|
+
raise NotImplementedError(
|
|
2627
|
+
f"concept_to_modal: the data restriction {type(concept).__name__} "
|
|
2628
|
+
f"({concept.to_unicode()}) has no propositional-K rendering — modal K "
|
|
2629
|
+
f"has no data domain, only possible worlds. Use concept_to_fol, whose "
|
|
2630
|
+
f"image handles it (see its 'The data layer' section).")
|
|
2631
|
+
if isinstance(concept, Not):
|
|
2632
|
+
return _roles_used(concept.concept)
|
|
2633
|
+
if isinstance(concept, (And, Or)):
|
|
2634
|
+
return _roles_used(concept.left) | _roles_used(concept.right)
|
|
2635
|
+
if isinstance(concept, (Exists, ForAll)):
|
|
2636
|
+
if isinstance(concept.role, InverseRole):
|
|
2637
|
+
raise NotImplementedError(
|
|
2638
|
+
_IO_MODAL_MSG.format(what=f"the inverse role {concept.role.role}⁻"))
|
|
2639
|
+
# A built-in property name is not "the one accessibility relation" of K
|
|
2640
|
+
# (the universal relation is a different modality, the empty one has no
|
|
2641
|
+
# worlds to move to): refused by name, as every other route does.
|
|
2642
|
+
_reject_concept_role(concept, where="dl.concept_to_modal")
|
|
2643
|
+
return {concept.role} | _roles_used(concept.concept)
|
|
2644
|
+
raise TypeError(f"concept_to_modal: unsupported concept {type(concept).__name__}")
|
|
2645
|
+
|
|
2646
|
+
|
|
2647
|
+
# A reserved nullary atom used only to spell out a structural tautology/
|
|
2648
|
+
# contradiction (mirrors the reserved-atom idiom in unicode_logic_kit.atp.fitch's
|
|
2649
|
+
# falsum handling). Since `p ∨ ¬p` is valid — and `p ∧ ¬p` unsatisfiable — for
|
|
2650
|
+
# ANY p, this is correct regardless of what this particular name denotes, even
|
|
2651
|
+
# in the (harmless) case that a concept happens to be literally named "⊤".
|
|
2652
|
+
_MODAL_TRUE_ATOM = Atom("⊤", ())
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
def _to_modal(concept: Concept) -> Node:
|
|
2656
|
+
"""Translate a single-role concept to a propositional modal-K formula."""
|
|
2657
|
+
if isinstance(concept, Atomic):
|
|
2658
|
+
return Atom(concept.name, ())
|
|
2659
|
+
if isinstance(concept, Top):
|
|
2660
|
+
return _FOr(_MODAL_TRUE_ATOM, _FNot(_MODAL_TRUE_ATOM))
|
|
2661
|
+
if isinstance(concept, Bottom):
|
|
2662
|
+
return _FAnd(_MODAL_TRUE_ATOM, _FNot(_MODAL_TRUE_ATOM))
|
|
2663
|
+
if isinstance(concept, Not):
|
|
2664
|
+
return _FNot(_to_modal(concept.concept))
|
|
2665
|
+
if isinstance(concept, And):
|
|
2666
|
+
return _FAnd(_to_modal(concept.left), _to_modal(concept.right))
|
|
2667
|
+
if isinstance(concept, Or):
|
|
2668
|
+
return _FOr(_to_modal(concept.left), _to_modal(concept.right))
|
|
2669
|
+
if isinstance(concept, Exists):
|
|
2670
|
+
return Diamond(_to_modal(concept.concept))
|
|
2671
|
+
if isinstance(concept, ForAll):
|
|
2672
|
+
return Box(_to_modal(concept.concept))
|
|
2673
|
+
raise TypeError(f"concept_to_modal: unsupported concept {type(concept).__name__}")
|
|
2674
|
+
|
|
2675
|
+
|
|
2676
|
+
_MULTI_ROLE_MSG = (
|
|
2677
|
+
"concept_to_modal: concept uses {n} distinct roles {roles}; propositional "
|
|
2678
|
+
"modal K has exactly ONE accessibility relation, so per-role restrictions "
|
|
2679
|
+
"(∃r.C / ∀r.C for different r) cannot be faithfully rendered as Box/Diamond "
|
|
2680
|
+
"— there is no honest way to recover which role a given □/◇ came from. Use "
|
|
2681
|
+
"concept_to_fol instead: it has no such limitation, since a role is just "
|
|
2682
|
+
"another binary FOL predicate r(x, y) and FOL scales to any number of them."
|
|
2683
|
+
)
|
|
2684
|
+
|
|
2685
|
+
|
|
2686
|
+
def concept_to_modal(concept: Concept) -> Node:
|
|
2687
|
+
"""Translate a SINGLE-role concept to a propositional modal-K formula.
|
|
2688
|
+
|
|
2689
|
+
``∃r.C`` ↦ ``◇C``, ``∀r.C`` ↦ ``□C`` (ALC restricted to one role is
|
|
2690
|
+
exactly modal K — see "Multi-role concepts and modal K" in the module
|
|
2691
|
+
docstring). ``concept`` is satisfiable (:func:`unicode_logic_kit.dl.tableau.concept_satisfiable`)
|
|
2692
|
+
iff its image here is satisfiable in K, decidable via
|
|
2693
|
+
:func:`unicode_logic_kit.atp.modal_tableau.is_modal_valid` on the negation
|
|
2694
|
+
(``not is_modal_valid(Not(concept_to_modal(concept)), frame="K")``).
|
|
2695
|
+
|
|
2696
|
+
Raises:
|
|
2697
|
+
NotImplementedError: ``concept`` mentions two or more distinct role
|
|
2698
|
+
names — see :func:`concept_to_fol` for the general (FOL) translation.
|
|
2699
|
+
"""
|
|
2700
|
+
roles = _roles_used(concept)
|
|
2701
|
+
if len(roles) > 1:
|
|
2702
|
+
raise NotImplementedError(
|
|
2703
|
+
_MULTI_ROLE_MSG.format(n=len(roles), roles=sorted(roles)))
|
|
2704
|
+
return _to_modal(concept)
|