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,632 @@
|
|
|
1
|
+
"""The description logic **ALC** (extended to **ALCQ**): concept expressions, roles,
|
|
2
|
+
and negation normal form.
|
|
3
|
+
|
|
4
|
+
ALC (*Attributive Language with Complements*) is the smallest propositionally closed
|
|
5
|
+
description logic and the notation underlying OWL. Its *concepts* describe sets of
|
|
6
|
+
individuals and its *roles* binary relations between them:
|
|
7
|
+
|
|
8
|
+
- ``Top`` (⊤) — everything; ``Bottom`` (⊥) — nothing;
|
|
9
|
+
- ``Atomic("C")`` — a primitive concept name;
|
|
10
|
+
- ``Not(C)`` (¬C), ``And(C, D)`` (C ⊓ D), ``Or(C, D)`` (C ⊔ D);
|
|
11
|
+
- ``Exists("r", C)`` (∃r.C) — has an ``r``-successor in ``C``;
|
|
12
|
+
- ``ForAll("r", C)`` (∀r.C) — all ``r``-successors are in ``C``;
|
|
13
|
+
- ``AtLeast(n, "r", C)`` (≥n r.C) — at least ``n`` PAIRWISE-DISTINCT ``r``-successors
|
|
14
|
+
are in ``C``;
|
|
15
|
+
- ``AtMost(n, "r", C)`` (≤n r.C) — at most ``n`` pairwise-distinct ``r``-successors
|
|
16
|
+
are in ``C``;
|
|
17
|
+
- ``HasValue("r", "a")`` (∃r.{a}) — has the named individual ``a`` among its
|
|
18
|
+
``r``-successors (OWL's ``ObjectHasValue``). A nominal in disguise: the
|
|
19
|
+
in-house tableau refuses it by name, and the first-order image
|
|
20
|
+
(:func:`~unicode_logic_kit.dl.translate.kb_to_fol` with ``api.prove``) and the
|
|
21
|
+
``dl.external_*`` entry points decide it; see :class:`HasValue` for why it is
|
|
22
|
+
a constructor of its own rather than ``Exists("r", Nominal("a"))``.
|
|
23
|
+
|
|
24
|
+
ALC alone is exactly the multi-modal logic **K** (a role ``r`` is a modality, ``∃r``
|
|
25
|
+
its ◇ and ``∀r`` its □), which is why it is decidable; ``AtLeast``/``AtMost`` add
|
|
26
|
+
*qualified number restrictions* (the DL usually written **ALCQ**), decided by the
|
|
27
|
+
tableau's dedicated ≥/≤/choose completion rules — see the "Qualified number
|
|
28
|
+
restrictions" section of :mod:`unicode_logic_kit.dl.tableau`'s module docstring for the
|
|
29
|
+
algorithm, and note the "simple roles" restriction documented there (number
|
|
30
|
+
restrictions may not target a transitive role, or one with a transitive sub-role).
|
|
31
|
+
:func:`nnf` rewrites a concept to *negation normal form* (negation only on atomic
|
|
32
|
+
concepts), the shape the tableau in :mod:`unicode_logic_kit.dl.tableau` consumes.
|
|
33
|
+
|
|
34
|
+
Inverse roles and nominals (I, O) — represented, but not decided here
|
|
35
|
+
------------------------------------------------------------------------
|
|
36
|
+
Two more constructs are recognised by this AST but sit OUTSIDE ALCHQ and are
|
|
37
|
+
NEVER decided by :mod:`unicode_logic_kit.dl.tableau`'s in-house tableau, which
|
|
38
|
+
refuses both by name (see that module's ``_reject_beyond_alc``):
|
|
39
|
+
|
|
40
|
+
- :class:`InverseRole` (``r⁻``) — a *role expression*, not a concept: it is
|
|
41
|
+
used wherever a plain role NAME (a ``str``) is expected, i.e. as the
|
|
42
|
+
``role`` field of :class:`Exists`/:class:`ForAll`/:class:`AtLeast`/
|
|
43
|
+
:class:`AtMost` (``Exists(InverseRole("hasChild"), C)`` is ``∃hasChild⁻.C``).
|
|
44
|
+
This is deliberate: that field is never type-checked anywhere in this
|
|
45
|
+
module (see :func:`nnf`'s Exists/ForAll branches), so an
|
|
46
|
+
``InverseRole``-wrapped role sails through unrecognised rather than
|
|
47
|
+
crashing — which is exactly why :mod:`unicode_logic_kit.dl.tableau` needs its
|
|
48
|
+
own explicit guard rather than relying on this module to reject it "for
|
|
49
|
+
free" (see that module's docstring for the full soundness argument).
|
|
50
|
+
- :class:`Nominal` (``{a}``) — the singleton concept containing exactly the
|
|
51
|
+
named individual ``a`` (OWL's ``ObjectOneOf`` restricted to one
|
|
52
|
+
individual; a multi-individual nominal set ``{a, b}`` is expressed as
|
|
53
|
+
``Or(Nominal("a"), Nominal("b"))`` — semantically identical, since OWL 2
|
|
54
|
+
gives ``ObjectOneOf`` exactly that union reading, and it avoids a second
|
|
55
|
+
AST shape for "a set of names").
|
|
56
|
+
|
|
57
|
+
:class:`HasValue` (``∃r.{a}``) belongs on that list too. It names an
|
|
58
|
+
individual inside a concept, so it is a NOMINAL in disguise, and the in-house
|
|
59
|
+
tableau refuses it by name exactly as it refuses a bare :class:`Nominal`. An
|
|
60
|
+
in-house value rule was built and removed again, because it is unsound under
|
|
61
|
+
subset blocking; "Value restrictions (ObjectHasValue)" in
|
|
62
|
+
:mod:`unicode_logic_kit.dl.tableau`'s module docstring has the two-axiom
|
|
63
|
+
counterexample. What decides a value restriction is the first-order image
|
|
64
|
+
(:func:`~unicode_logic_kit.dl.translate.kb_to_fol`, then ``api.prove``) and the
|
|
65
|
+
external, HermiT-backed reasoner (the ``dl.external_*`` entry points).
|
|
66
|
+
:func:`nnf` passes it through (it refuses a ``Nominal``).
|
|
67
|
+
|
|
68
|
+
Only :mod:`unicode_logic_kit.dl.owl_reasoner`'s external, HermiT-backed reasoner
|
|
69
|
+
decides concepts built from an :class:`InverseRole` or a :class:`Nominal` — see
|
|
70
|
+
that module's docstring.
|
|
71
|
+
:func:`nnf` refuses EVERY concept containing a ``Nominal`` — bare
|
|
72
|
+
(``nnf(Nominal("a"))``) or nested (``nnf(Not(Nominal("a")))``,
|
|
73
|
+
``nnf(And(Nominal("a"), C))``, …) — with a :class:`TypeError`, by design (see
|
|
74
|
+
:func:`nnf`'s own docstring); this is deliberately NOT weakened anywhere to
|
|
75
|
+
give ``Nominal`` "free" passage the way ``Top``/``Bottom``/``Atomic`` get.
|
|
76
|
+
``to_unicode``/:func:`_render`, by contrast, DO handle ``Nominal`` (and an
|
|
77
|
+
``InverseRole``-valued role), since rendering has no soundness consequence —
|
|
78
|
+
a mixed ALCHQ+I/O concept can still be printed for diagnostics even though
|
|
79
|
+
the in-house tableau will never reason over it.
|
|
80
|
+
|
|
81
|
+
The classes live in the ``dl`` namespace (``import unicode_logic_kit.dl as dl``); the
|
|
82
|
+
names ``And`` / ``Or`` / ``Not`` are the concept constructors, distinct from the FOL
|
|
83
|
+
AST's connectives.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
from dataclasses import dataclass
|
|
87
|
+
from typing import Iterator
|
|
88
|
+
|
|
89
|
+
from .datatypes import DataRange, Literal, _paren_range
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Concept:
|
|
93
|
+
"""Base class for ALC concept expressions (see the module docstring)."""
|
|
94
|
+
|
|
95
|
+
def to_unicode(self) -> str:
|
|
96
|
+
"""Render the concept with the standard DL glyphs (⊤ ⊥ ¬ ⊓ ⊔ ∃ ∀).
|
|
97
|
+
|
|
98
|
+
The text reads back as the same concept with
|
|
99
|
+
:func:`~unicode_logic_kit.dl.parser.parse_concept` (a right operand of the
|
|
100
|
+
same connective is parenthesised: ``And(A, And(B, C))`` is
|
|
101
|
+
``A ⊓ (B ⊓ C)``), except for the constructors the glyph syntax cannot
|
|
102
|
+
read — see that module's docstring.
|
|
103
|
+
|
|
104
|
+
A NAME that holds whitespace or one of the glyphs the syntax reserves
|
|
105
|
+
(``⊓ ⊔ ¬ ⊤ ⊥ ∃ ∀ ≥ ≤ ( ) . ⊑ { }``) has no spelling in the glyph syntax
|
|
106
|
+
(there is no escape), and such a name is written as it is, for display: the
|
|
107
|
+
reader then refuses the text. A name for which it would instead read some
|
|
108
|
+
OTHER concept — ``Atomic("A ⊓ B")`` prints ``A ⊓ B``, which is the
|
|
109
|
+
intersection of two classes — is refused here.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: a class, role or individual name makes the text read back
|
|
113
|
+
as a different concept.
|
|
114
|
+
"""
|
|
115
|
+
text = _render(self)
|
|
116
|
+
_reject_misread_names(self, text)
|
|
117
|
+
return text
|
|
118
|
+
|
|
119
|
+
def __str__(self) -> str:
|
|
120
|
+
# Display only, so it never raises: a name the glyph syntax cannot spell
|
|
121
|
+
# is shown as it is (``to_unicode`` is the one that refuses it).
|
|
122
|
+
return _render(self)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class Top(Concept):
|
|
127
|
+
"""The universal concept ⊤ (every individual)."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class Bottom(Concept):
|
|
132
|
+
"""The empty concept ⊥ (no individual)."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class Atomic(Concept):
|
|
137
|
+
"""A primitive concept name, e.g. ``Atomic("Person")``."""
|
|
138
|
+
|
|
139
|
+
name: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class Not(Concept):
|
|
144
|
+
"""Concept complement ¬C."""
|
|
145
|
+
|
|
146
|
+
concept: Concept
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class And(Concept):
|
|
151
|
+
"""Concept intersection C ⊓ D."""
|
|
152
|
+
|
|
153
|
+
left: Concept
|
|
154
|
+
right: Concept
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class Or(Concept):
|
|
159
|
+
"""Concept union C ⊔ D."""
|
|
160
|
+
|
|
161
|
+
left: Concept
|
|
162
|
+
right: Concept
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class Exists(Concept):
|
|
167
|
+
"""Existential restriction ∃r.C — at least one ``role``-successor is in ``concept``."""
|
|
168
|
+
|
|
169
|
+
role: str
|
|
170
|
+
concept: Concept
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class ForAll(Concept):
|
|
175
|
+
"""Value restriction ∀r.C — every ``role``-successor is in ``concept``."""
|
|
176
|
+
|
|
177
|
+
role: str
|
|
178
|
+
concept: Concept
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class AtLeast(Concept):
|
|
183
|
+
"""Qualified at-least number restriction ≥n r.C — at least ``n`` PAIRWISE-DISTINCT
|
|
184
|
+
``role``-successors are in ``concept``.
|
|
185
|
+
|
|
186
|
+
No unique name assumption is in force anywhere in this kit's DL reasoner (see
|
|
187
|
+
:mod:`unicode_logic_kit.dl.tableau`), so "pairwise-distinct" is a genuine semantic
|
|
188
|
+
requirement, not just "n role-successors that happen to carry different names" —
|
|
189
|
+
two successors are only known distinct when something *forces* it (see the
|
|
190
|
+
tableau module docstring). ``n = 0`` is a tautology (every individual vacuously
|
|
191
|
+
has at least zero such successors, so it never constrains anything); ``n = 1``
|
|
192
|
+
is semantically ``Exists(role, concept)`` but kept as a separate constructor
|
|
193
|
+
(the tableau still gives it its own, simpler ∃-rule) rather than folded away.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
n: int
|
|
197
|
+
role: str
|
|
198
|
+
concept: Concept
|
|
199
|
+
|
|
200
|
+
def __post_init__(self):
|
|
201
|
+
"""Validate that n is a non-negative integer (mirrors Count's own check)."""
|
|
202
|
+
if not (isinstance(self.n, int) and self.n >= 0):
|
|
203
|
+
raise ValueError(f"AtLeast: n must be a non-negative integer, got {self.n!r}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True)
|
|
207
|
+
class AtMost(Concept):
|
|
208
|
+
"""Qualified at-most number restriction ≤n r.C — at most ``n`` pairwise-distinct
|
|
209
|
+
``role``-successors are in ``concept`` (see :class:`AtLeast` for the "no unique
|
|
210
|
+
name assumption" caveat, which applies here identically). ``n = 0`` forbids any
|
|
211
|
+
``role``-successor in ``concept`` at all.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
n: int
|
|
215
|
+
role: str
|
|
216
|
+
concept: Concept
|
|
217
|
+
|
|
218
|
+
def __post_init__(self):
|
|
219
|
+
"""Validate that n is a non-negative integer (mirrors Count's own check)."""
|
|
220
|
+
if not (isinstance(self.n, int) and self.n >= 0):
|
|
221
|
+
raise ValueError(f"AtMost: n must be a non-negative integer, got {self.n!r}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class InverseRole:
|
|
226
|
+
"""A role EXPRESSION ``r⁻`` (OWL's ``ObjectInverseOf``) — the inverse of the
|
|
227
|
+
named role ``role``: ``x —r⁻→ y`` holds exactly when ``y —r→ x`` does.
|
|
228
|
+
|
|
229
|
+
Deliberately NOT a :class:`Concept` subclass: it is used in place of a plain
|
|
230
|
+
role NAME (a ``str``), as the ``role`` field of :class:`Exists`/:class:`ForAll`/
|
|
231
|
+
:class:`AtLeast`/:class:`AtMost` (``Exists(InverseRole("hasChild"), C)`` is
|
|
232
|
+
``∃hasChild⁻.C``) — see the module docstring's "Inverse roles and nominals"
|
|
233
|
+
section for why that field being untyped is exactly what lets this work
|
|
234
|
+
without any change to :func:`nnf`'s Exists/ForAll/AtLeast/AtMost branches,
|
|
235
|
+
and why that same fact makes it the in-house tableau's job, not this
|
|
236
|
+
module's, to refuse it (:mod:`unicode_logic_kit.dl.tableau`'s
|
|
237
|
+
``_reject_beyond_alc``).
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
role: str
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class Nominal(Concept):
|
|
245
|
+
"""The nominal concept ``{a}`` (OWL's ``ObjectOneOf`` restricted to one
|
|
246
|
+
individual) — the singleton concept containing exactly the named individual
|
|
247
|
+
``individual``, and nothing else, in EVERY model (no unique name assumption
|
|
248
|
+
is needed for this: a nominal fixes membership by naming the element
|
|
249
|
+
directly, not by comparison with another name — see
|
|
250
|
+
:mod:`unicode_logic_kit.dl.tableau`'s "Qualified number restrictions" section
|
|
251
|
+
for how the reasoner treats individual identity elsewhere).
|
|
252
|
+
|
|
253
|
+
Outside ALCHQ — see the module docstring's "Inverse roles and nominals"
|
|
254
|
+
section: :func:`nnf` refuses any concept containing one, and only
|
|
255
|
+
:mod:`unicode_logic_kit.dl.owl_reasoner`'s external reasoner decides concepts
|
|
256
|
+
that use it. A multi-individual nominal set ``{a, b}`` is
|
|
257
|
+
``Or(Nominal("a"), Nominal("b"))`` — see that section for why this needs no
|
|
258
|
+
separate AST shape.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
individual: str
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass(frozen=True)
|
|
265
|
+
class HasValue(Concept):
|
|
266
|
+
"""The value restriction ``∃r.{a}`` (OWL's ``ObjectHasValue(r a)``) — the
|
|
267
|
+
concept of the individuals that have ``individual`` among their
|
|
268
|
+
``role``-successors.
|
|
269
|
+
|
|
270
|
+
OWL 2 direct semantics: ``(ObjectHasValue(P a))^C = { x | <x, a^I> ∈ P^OP }``.
|
|
271
|
+
The standard translation of the equivalent ``∃P.{a}`` is
|
|
272
|
+
``∃y (P(x, y) ∧ y = a)``, and one-point elimination of the equality-bounded
|
|
273
|
+
existential (``y`` occurs only in that conjunction, so the equivalence is a
|
|
274
|
+
FOL validity) reduces it to the atom ``P(x, a)`` — which is the image
|
|
275
|
+
:mod:`unicode_logic_kit.dl.translate` produces: same models, no minted
|
|
276
|
+
variable, and the individual in the argument position OWL's own reading
|
|
277
|
+
puts it in.
|
|
278
|
+
|
|
279
|
+
A node of its OWN rather than ``Exists(role, Nominal(individual))``, for
|
|
280
|
+
two reasons:
|
|
281
|
+
|
|
282
|
+
* **Round trip.** ``ObjectHasValue(r a)`` and the (still refused)
|
|
283
|
+
``ObjectSomeValuesFrom(r ObjectOneOf(a))`` are two distinct OWL 2
|
|
284
|
+
structural objects; folding both into one AST shape would make
|
|
285
|
+
``parse(render(c)) == c`` stop holding for one of them. Same on the
|
|
286
|
+
Manchester side: ``r value a`` versus the refused ``r some {a}``.
|
|
287
|
+
* **It is the textbook DL reading.** There is no separate glyph for a value
|
|
288
|
+
restriction, so this renders as ``∃r.{a}`` — identical to
|
|
289
|
+
``Exists(role, Nominal(a))``'s rendering, which is honest, because the
|
|
290
|
+
two say the same thing. The glyph syntax therefore has two AST shapes
|
|
291
|
+
with one rendering, and :mod:`unicode_logic_kit.dl.parser` refuses that
|
|
292
|
+
text by name rather than guessing which was meant.
|
|
293
|
+
|
|
294
|
+
Like :class:`Nominal`, this is OUTSIDE the fragment the in-house tableau
|
|
295
|
+
decides: a value restriction is a nominal in disguise, the tableau refuses
|
|
296
|
+
it by name, and the first-order image
|
|
297
|
+
(:func:`~unicode_logic_kit.dl.translate.kb_to_fol`, then ``api.prove``) and the
|
|
298
|
+
``dl.external_*`` entry points decide it instead — see "Value restrictions (ObjectHasValue)" in
|
|
299
|
+
:mod:`unicode_logic_kit.dl.tableau`'s module docstring for why a tableau rule
|
|
300
|
+
for it is unsound (a two-axiom counterexample). Unlike :class:`Nominal`,
|
|
301
|
+
:func:`nnf` passes it through, and ``Not(HasValue(...))`` is a LITERAL (like
|
|
302
|
+
``Not(Atomic(...))``) with no dual node — the dual of "has ``a`` as an
|
|
303
|
+
``r``-successor" is "does not", which is a condition on an EDGE, not a
|
|
304
|
+
concept :func:`nnf` could push inward.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
role: str
|
|
308
|
+
individual: str
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# --------------------------------------------------------------------------- #
|
|
312
|
+
# Data restrictions: the concept constructors that mention a DATA property.
|
|
313
|
+
# --------------------------------------------------------------------------- #
|
|
314
|
+
#
|
|
315
|
+
# Field named ``prop`` and not ``role`` on purpose: a data property is not an
|
|
316
|
+
# object role, and a generic walker that reads ``.role`` (the simple-role check,
|
|
317
|
+
# the modal translation, the OWL writers) must NOT take it for one.
|
|
318
|
+
|
|
319
|
+
@dataclass(frozen=True)
|
|
320
|
+
class DataExists(Concept):
|
|
321
|
+
"""The data restriction ``∃d.DR`` (OWL's ``DataSomeValuesFrom(d DR)``) — has
|
|
322
|
+
at least one ``prop``-value in the data range ``datarange``.
|
|
323
|
+
|
|
324
|
+
OWL 2 direct semantics: ``{ x | ∃v. (x, v) ∈ d^DP ∧ v ∈ DR^DT }``. The
|
|
325
|
+
first-order image is ``∃w (d(x, w) ∧ δ(DR, w))``.
|
|
326
|
+
|
|
327
|
+
Outside the fragment the in-house tableau decides: it has no data domain,
|
|
328
|
+
so it REFUSES a concept containing one by name. The first-order image is
|
|
329
|
+
the route that answers (see :func:`unicode_logic_kit.dl.translate.kb_to_fol`
|
|
330
|
+
and :func:`unicode_logic_kit.dl.translate.data_sort_axioms`).
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
prop: str
|
|
334
|
+
datarange: DataRange
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass(frozen=True)
|
|
338
|
+
class DataForAll(Concept):
|
|
339
|
+
"""The data restriction ``∀d.DR`` (OWL's ``DataAllValuesFrom(d DR)``) —
|
|
340
|
+
every ``prop``-value is in the data range ``datarange``. Image:
|
|
341
|
+
``∀w (d(x, w) → δ(DR, w))``."""
|
|
342
|
+
|
|
343
|
+
prop: str
|
|
344
|
+
datarange: DataRange
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass(frozen=True)
|
|
348
|
+
class DataHasValue(Concept):
|
|
349
|
+
"""The data value restriction (OWL's ``DataHasValue(d lt)``) — has the
|
|
350
|
+
literal ``value`` among its ``prop``-values.
|
|
351
|
+
|
|
352
|
+
OWL 2 direct semantics: ``{ x | (x, lt^LT) ∈ d^DP }``. There is no
|
|
353
|
+
quantifier — a literal denotes ONE fixed data value — so the image is the
|
|
354
|
+
ground atom ``d(x, t)`` for the literal's term ``t``: no bound variable and
|
|
355
|
+
no datatype guard (the literal already pins its datatype).
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
prop: str
|
|
359
|
+
value: Literal
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@dataclass(frozen=True)
|
|
363
|
+
class DataAtLeast(Concept):
|
|
364
|
+
"""The qualified data number restriction ``≥n d.DR`` (OWL's
|
|
365
|
+
``DataMinCardinality(n d DR)``) — at least ``n`` PAIRWISE-DISTINCT
|
|
366
|
+
``prop``-values are in ``datarange``. Image: the same ``Count`` node the
|
|
367
|
+
object ``AtLeast`` uses, over the data atom. ``DataExactCardinality`` is
|
|
368
|
+
not a node of its own: the parsers desugar it to ``DataAtLeast ⊓ DataAtMost``,
|
|
369
|
+
as ``ObjectExactCardinality`` is."""
|
|
370
|
+
|
|
371
|
+
n: int
|
|
372
|
+
prop: str
|
|
373
|
+
datarange: DataRange
|
|
374
|
+
|
|
375
|
+
def __post_init__(self):
|
|
376
|
+
"""Validate that n is a non-negative integer (mirrors AtLeast's check)."""
|
|
377
|
+
if not (isinstance(self.n, int) and self.n >= 0):
|
|
378
|
+
raise ValueError(f"DataAtLeast: n must be a non-negative integer, got {self.n!r}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclass(frozen=True)
|
|
382
|
+
class DataAtMost(Concept):
|
|
383
|
+
"""The qualified data number restriction ``≤n d.DR`` (OWL's
|
|
384
|
+
``DataMaxCardinality(n d DR)``) — at most ``n`` pairwise-distinct
|
|
385
|
+
``prop``-values are in ``datarange``."""
|
|
386
|
+
|
|
387
|
+
n: int
|
|
388
|
+
prop: str
|
|
389
|
+
datarange: DataRange
|
|
390
|
+
|
|
391
|
+
def __post_init__(self):
|
|
392
|
+
"""Validate that n is a non-negative integer (mirrors AtMost's check)."""
|
|
393
|
+
if not (isinstance(self.n, int) and self.n >= 0):
|
|
394
|
+
raise ValueError(f"DataAtMost: n must be a non-negative integer, got {self.n!r}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
#: Every data restriction class, for the walkers that must treat them alike.
|
|
398
|
+
DATA_CONCEPTS = (DataExists, DataForAll, DataHasValue, DataAtLeast, DataAtMost)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _refuse_data_concept(concept: Concept) -> None:
|
|
402
|
+
"""Raise the loud ``TypeError`` :func:`nnf` gives a data restriction."""
|
|
403
|
+
raise TypeError(
|
|
404
|
+
f"nnf: the data restriction {type(concept).__name__} ({_render(concept)}) "
|
|
405
|
+
f"has no negation normal form in this kit: the in-house tableau has no "
|
|
406
|
+
f"data domain, so its dual (a restriction over the COMPLEMENT of the "
|
|
407
|
+
f"data range, taken within the data domain) would be a rule nothing "
|
|
408
|
+
f"implements. The route that answers is the first-order image, which is "
|
|
409
|
+
f"sound for the data layer and not complete: kb = dl.kb_to_fol(tbox, abox, "
|
|
410
|
+
f"query=[concept]) and its goal methods (kb.unsatisfiability_goal, "
|
|
411
|
+
f"kb.subsumption_goal, kb.instance_goal) with api.prove.")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def nnf(concept: Concept) -> Concept:
|
|
415
|
+
"""Return ``concept`` in negation normal form (negation only on atomic concepts).
|
|
416
|
+
|
|
417
|
+
Pushes ¬ inward with the De Morgan / modal dualities:
|
|
418
|
+
``¬⊤ = ⊥``, ``¬⊥ = ⊤``, ``¬¬C = C``, ``¬(C⊓D) = ¬C⊔¬D``, ``¬(C⊔D) = ¬C⊓¬D``,
|
|
419
|
+
``¬∃r.C = ∀r.¬C``, ``¬∀r.C = ∃r.¬C``, and the number-restriction duals
|
|
420
|
+
``¬(≥n r.C) = ≤(n-1) r.C`` for ``n ≥ 1`` (``¬(≥0 r.C) = ⊥``, since ``≥0`` is a
|
|
421
|
+
tautology), ``¬(≤n r.C) = ≥(n+1) r.C``.
|
|
422
|
+
|
|
423
|
+
Raises ``TypeError`` for any concept outside ALCHQ, in particular a
|
|
424
|
+
:class:`Nominal` anywhere in ``concept`` — bare or nested under ``Not``/
|
|
425
|
+
``And``/``Or``/a restriction. This is INTENTIONAL and must stay this way:
|
|
426
|
+
``Nominal`` is deliberately left OUT of the top-level ``(Top, Bottom,
|
|
427
|
+
Atomic)`` pass-through tuple below, even though it is "atomic-shaped" and
|
|
428
|
+
could technically be passed through unchanged like they are. Doing so
|
|
429
|
+
would remove a free, structurally-guaranteed second line of defense that
|
|
430
|
+
:mod:`unicode_logic_kit.dl.tableau` relies on: every call this kit makes
|
|
431
|
+
into ``nnf`` from ``tableau.py`` is already guarded by that module's own
|
|
432
|
+
``_reject_beyond_alc`` walker, so this raise is never *supposed* to fire
|
|
433
|
+
in normal operation — but if that guard ever has a gap (a future call
|
|
434
|
+
site, a missed nested occurrence), a ``Nominal`` reaching here still fails
|
|
435
|
+
LOUDLY instead of silently being treated as an ordinary opaque
|
|
436
|
+
:class:`Atomic` concept and producing a too-permissive verdict. See
|
|
437
|
+
``tests/test_dl_alc.py``'s dedicated regression test (which calls ``nnf``
|
|
438
|
+
directly, bypassing the tableau's guard on purpose) and
|
|
439
|
+
:mod:`unicode_logic_kit.dl.tableau`'s module docstring for the full
|
|
440
|
+
soundness argument. An ``InverseRole``-valued ``role`` field needs no
|
|
441
|
+
parallel case here at all: ``Exists``/``ForAll``/``AtLeast``/``AtMost``
|
|
442
|
+
never inspect their ``role`` field's type, so it already sails through
|
|
443
|
+
every branch below untouched, for exactly the same reason it is NOT safe
|
|
444
|
+
on its own — see ``tableau.py`` for why that, too, is refused there.
|
|
445
|
+
"""
|
|
446
|
+
if isinstance(concept, (Top, Bottom, Atomic)):
|
|
447
|
+
return concept
|
|
448
|
+
# A LITERAL of its own, like Atomic: a value restriction has no dual
|
|
449
|
+
# concept to push a negation into (see HasValue's class docstring), so both
|
|
450
|
+
# polarities pass through unchanged -- the positive one here, the negative
|
|
451
|
+
# one in the Not(inner) dispatch below. A new literal-shaped concept kind
|
|
452
|
+
# belongs here and there, never in the (Top, Bottom, Atomic) tuple above,
|
|
453
|
+
# whose membership carries the Nominal argument documented in this
|
|
454
|
+
# function's own docstring.
|
|
455
|
+
if isinstance(concept, HasValue):
|
|
456
|
+
return concept
|
|
457
|
+
if isinstance(concept, DATA_CONCEPTS):
|
|
458
|
+
_refuse_data_concept(concept)
|
|
459
|
+
if isinstance(concept, And):
|
|
460
|
+
return And(nnf(concept.left), nnf(concept.right))
|
|
461
|
+
if isinstance(concept, Or):
|
|
462
|
+
return Or(nnf(concept.left), nnf(concept.right))
|
|
463
|
+
if isinstance(concept, Exists):
|
|
464
|
+
return Exists(concept.role, nnf(concept.concept))
|
|
465
|
+
if isinstance(concept, ForAll):
|
|
466
|
+
return ForAll(concept.role, nnf(concept.concept))
|
|
467
|
+
if isinstance(concept, AtLeast):
|
|
468
|
+
return AtLeast(concept.n, concept.role, nnf(concept.concept))
|
|
469
|
+
if isinstance(concept, AtMost):
|
|
470
|
+
return AtMost(concept.n, concept.role, nnf(concept.concept))
|
|
471
|
+
if isinstance(concept, Not):
|
|
472
|
+
inner = concept.concept
|
|
473
|
+
if isinstance(inner, Top):
|
|
474
|
+
return Bottom()
|
|
475
|
+
if isinstance(inner, Bottom):
|
|
476
|
+
return Top()
|
|
477
|
+
if isinstance(inner, Atomic):
|
|
478
|
+
return concept
|
|
479
|
+
if isinstance(inner, HasValue):
|
|
480
|
+
return concept # a literal; see the positive case above
|
|
481
|
+
if isinstance(inner, DATA_CONCEPTS):
|
|
482
|
+
_refuse_data_concept(inner)
|
|
483
|
+
if isinstance(inner, Not):
|
|
484
|
+
return nnf(inner.concept)
|
|
485
|
+
if isinstance(inner, And):
|
|
486
|
+
return Or(nnf(Not(inner.left)), nnf(Not(inner.right)))
|
|
487
|
+
if isinstance(inner, Or):
|
|
488
|
+
return And(nnf(Not(inner.left)), nnf(Not(inner.right)))
|
|
489
|
+
if isinstance(inner, Exists):
|
|
490
|
+
return ForAll(inner.role, nnf(Not(inner.concept)))
|
|
491
|
+
if isinstance(inner, ForAll):
|
|
492
|
+
return Exists(inner.role, nnf(Not(inner.concept)))
|
|
493
|
+
if isinstance(inner, AtLeast):
|
|
494
|
+
if inner.n == 0:
|
|
495
|
+
return Bottom() # ¬(≥0 r.C): ≥0 is a tautology
|
|
496
|
+
return AtMost(inner.n - 1, inner.role, nnf(inner.concept))
|
|
497
|
+
if isinstance(inner, AtMost):
|
|
498
|
+
return AtLeast(inner.n + 1, inner.role, nnf(inner.concept))
|
|
499
|
+
raise TypeError(f"nnf: unsupported concept {type(concept).__name__}")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
_PREC = {Or: 1, And: 2, Not: 3, Exists: 3, ForAll: 3, AtLeast: 3, AtMost: 3,
|
|
503
|
+
HasValue: 3, DataExists: 3, DataForAll: 3, DataHasValue: 3,
|
|
504
|
+
DataAtLeast: 3, DataAtMost: 3,
|
|
505
|
+
Atomic: 4, Top: 4, Bottom: 4, Nominal: 4}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _render_role(role) -> str:
|
|
509
|
+
"""Render a ``role`` field (a plain ``str``, or an :class:`InverseRole` —
|
|
510
|
+
see the module docstring's "Inverse roles and nominals" section): ``r⁻``
|
|
511
|
+
for the latter, matching :func:`_render`'s other glyph choices. This is
|
|
512
|
+
display-only, unlike :mod:`unicode_logic_kit.dl.tableau`'s refusal of
|
|
513
|
+
``InverseRole``: rendering carries no soundness consequence.
|
|
514
|
+
"""
|
|
515
|
+
if isinstance(role, InverseRole):
|
|
516
|
+
return f"{role.role}⁻"
|
|
517
|
+
return role
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _render(c: Concept) -> str:
|
|
521
|
+
"""Render a concept with precedence-aware parenthesisation."""
|
|
522
|
+
if isinstance(c, Top):
|
|
523
|
+
return "⊤"
|
|
524
|
+
if isinstance(c, Bottom):
|
|
525
|
+
return "⊥"
|
|
526
|
+
if isinstance(c, Atomic):
|
|
527
|
+
return c.name
|
|
528
|
+
if isinstance(c, Nominal):
|
|
529
|
+
return "{" + c.individual + "}"
|
|
530
|
+
if isinstance(c, HasValue):
|
|
531
|
+
# The textbook DL spelling of a value restriction -- there is no glyph
|
|
532
|
+
# of its own, so this is ∃r.{a}, the same text Exists(r, Nominal(a))
|
|
533
|
+
# renders to. Honest (the two concepts have the same models) and the
|
|
534
|
+
# reason dl.parser refuses to read it back: see HasValue's docstring.
|
|
535
|
+
return f"∃{_render_role(c.role)}.{{{c.individual}}}"
|
|
536
|
+
if isinstance(c, Not):
|
|
537
|
+
return "¬" + _paren(c.concept, 3)
|
|
538
|
+
# The reader folds a flat chain of one connective to the LEFT (``A ⊓ B ⊓ C``
|
|
539
|
+
# is ``(A ⊓ B) ⊓ C``), so only a LEFT operand of the same connective may go
|
|
540
|
+
# unparenthesised: the right operand is written one level tighter, and an
|
|
541
|
+
# ``⊓`` inside an ``⊓`` (an ``⊔`` inside an ``⊔``) on the right keeps its
|
|
542
|
+
# parentheses.
|
|
543
|
+
if isinstance(c, And):
|
|
544
|
+
return f"{_paren(c.left, 2)} ⊓ {_paren(c.right, 3)}"
|
|
545
|
+
if isinstance(c, Or):
|
|
546
|
+
return f"{_paren(c.left, 1)} ⊔ {_paren(c.right, 2)}"
|
|
547
|
+
if isinstance(c, Exists):
|
|
548
|
+
return f"∃{_render_role(c.role)}.{_paren(c.concept, 3)}"
|
|
549
|
+
if isinstance(c, ForAll):
|
|
550
|
+
return f"∀{_render_role(c.role)}.{_paren(c.concept, 3)}"
|
|
551
|
+
if isinstance(c, AtLeast):
|
|
552
|
+
return f"≥{c.n} {_render_role(c.role)}.{_paren(c.concept, 3)}"
|
|
553
|
+
if isinstance(c, AtMost):
|
|
554
|
+
return f"≤{c.n} {_render_role(c.role)}.{_paren(c.concept, 3)}"
|
|
555
|
+
# Display-only, like everything here: the glyph parser has no data syntax,
|
|
556
|
+
# so these are read with dl.parse_manchester or dl.parse_owl_functional. A
|
|
557
|
+
# COMPOUND data range is parenthesised, so `∃d.(xsd:integer ⊔ xsd:string)`
|
|
558
|
+
# is not mistaken for `(∃d.xsd:integer) ⊔ xsd:string`.
|
|
559
|
+
if isinstance(c, DataExists):
|
|
560
|
+
return f"∃{c.prop}.{_paren_range(c.datarange)}"
|
|
561
|
+
if isinstance(c, DataForAll):
|
|
562
|
+
return f"∀{c.prop}.{_paren_range(c.datarange)}"
|
|
563
|
+
if isinstance(c, DataHasValue):
|
|
564
|
+
return f"∃{c.prop}.{{{c.value.to_unicode()}}}"
|
|
565
|
+
if isinstance(c, DataAtLeast):
|
|
566
|
+
return f"≥{c.n} {c.prop}.{_paren_range(c.datarange)}"
|
|
567
|
+
if isinstance(c, DataAtMost):
|
|
568
|
+
return f"≤{c.n} {c.prop}.{_paren_range(c.datarange)}"
|
|
569
|
+
raise TypeError(f"render: unsupported concept {type(c).__name__}")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _paren(c: Concept, parent_prec: int) -> str:
|
|
573
|
+
"""Parenthesise ``c`` when its precedence is below the parent's."""
|
|
574
|
+
inner = _render(c)
|
|
575
|
+
return f"({inner})" if _PREC.get(type(c), 4) < parent_prec else inner
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _role_name(role) -> str:
|
|
579
|
+
"""The name inside a ``role`` field: the string, or an :class:`InverseRole`'s."""
|
|
580
|
+
return role.role if isinstance(role, InverseRole) else role
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _names_of(c: Concept) -> Iterator[str]:
|
|
584
|
+
"""Every class, role, individual and data-property name inside ``c``."""
|
|
585
|
+
if isinstance(c, Atomic):
|
|
586
|
+
yield c.name
|
|
587
|
+
elif isinstance(c, Nominal):
|
|
588
|
+
yield c.individual
|
|
589
|
+
elif isinstance(c, Not):
|
|
590
|
+
yield from _names_of(c.concept)
|
|
591
|
+
elif isinstance(c, (And, Or)):
|
|
592
|
+
yield from _names_of(c.left)
|
|
593
|
+
yield from _names_of(c.right)
|
|
594
|
+
elif isinstance(c, (Exists, ForAll, AtLeast, AtMost)):
|
|
595
|
+
yield _role_name(c.role)
|
|
596
|
+
yield from _names_of(c.concept)
|
|
597
|
+
elif isinstance(c, HasValue):
|
|
598
|
+
yield _role_name(c.role)
|
|
599
|
+
yield c.individual
|
|
600
|
+
elif isinstance(c, DATA_CONCEPTS):
|
|
601
|
+
yield c.prop
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _reject_misread_names(concept: Concept, text: str) -> None:
|
|
605
|
+
"""Raise :class:`ValueError` unless ``text``, the glyph rendering of ``concept``,
|
|
606
|
+
is either read back as ``concept`` or refused by the reader.
|
|
607
|
+
|
|
608
|
+
A name made of ordinary characters is one NAME token and is read back as
|
|
609
|
+
itself; the only way the text can read as ANOTHER concept is a name that holds
|
|
610
|
+
whitespace or one of the reader's reserved glyphs. So the reader is asked only
|
|
611
|
+
when such a name occurs (the glyph syntax has no escape, and an IRI with a dot
|
|
612
|
+
in it is a common name that the reader refuses, which is fine: that is
|
|
613
|
+
loud). Imported here because the parser module imports this one.
|
|
614
|
+
"""
|
|
615
|
+
from .parser import _GLYPH_TOKENS, parse_concept
|
|
616
|
+
|
|
617
|
+
odd = sorted({name for name in _names_of(concept)
|
|
618
|
+
if isinstance(name, str)
|
|
619
|
+
and (name == "" or any(ch.isspace() or ch in _GLYPH_TOKENS for ch in name))})
|
|
620
|
+
if not odd:
|
|
621
|
+
return
|
|
622
|
+
try:
|
|
623
|
+
back = parse_concept(text)
|
|
624
|
+
except (ValueError, RecursionError):
|
|
625
|
+
return # the reader refuses this text: nothing reads it as another concept
|
|
626
|
+
if back != concept:
|
|
627
|
+
raise ValueError(
|
|
628
|
+
f"to_unicode: {text!r} reads back as {back!r}, not as the concept it "
|
|
629
|
+
f"was written from: the name(s) {odd!r} hold whitespace or a glyph the glyph syntax "
|
|
630
|
+
f"reserves, and that syntax has no escape for them. Rename the class or role, or "
|
|
631
|
+
f"write the concept with dl.to_owl_functional_class_expression, whose <...> carries "
|
|
632
|
+
f"any name that has no '>' in it.")
|