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,790 @@
|
|
|
1
|
+
"""An external OWL 2 DL reasoner backend for ``dl/`` — ALCHQ **+ inverse roles
|
|
2
|
+
(I) + nominals (O)**, via `owlready2 <https://owlready2.readthedocs.io>`_'s
|
|
3
|
+
bundled HermiT.
|
|
4
|
+
|
|
5
|
+
:mod:`unicode_logic_kit.dl.tableau`'s in-house tableau decides ALCHQ (and
|
|
6
|
+
refuses :class:`~unicode_logic_kit.dl.concepts.InverseRole`/
|
|
7
|
+
:class:`~unicode_logic_kit.dl.concepts.Nominal` by name — see that module's
|
|
8
|
+
"Inverse roles and nominals (I, O)" section for why: inverse roles break its
|
|
9
|
+
subset-blocking termination argument, and nominals need an individual-merging
|
|
10
|
+
machinery it has none of). This module decides the FULL fragment — ALCHQ plus
|
|
11
|
+
both — by handing the same :class:`~unicode_logic_kit.dl.tableau.TBox`/
|
|
12
|
+
:class:`~unicode_logic_kit.dl.tableau.ABox`/:class:`~unicode_logic_kit.dl.concepts.Concept`
|
|
13
|
+
AST to `HermiT <http://www.hermit-reasoner.com/>`_, a mainstream,
|
|
14
|
+
OWL2-DL-complete, actively maintained tableau reasoner, via ``owlready2``'s
|
|
15
|
+
Python bindings and bundled ``HermiT.jar``. This is the intentionally
|
|
16
|
+
NON-in-house route C45 (an in-house SHIQ/SHOIQ tableau rewrite) was rejected
|
|
17
|
+
in favour of: the boundary this kit draws is "detect and refuse I/O by name",
|
|
18
|
+
never "attempt to decide them with a homegrown algorithm".
|
|
19
|
+
|
|
20
|
+
Optional dependency, availability check
|
|
21
|
+
------------------------------------------
|
|
22
|
+
``owlready2`` (`PyPI <https://pypi.org/project/owlready2/>`_) is an optional
|
|
23
|
+
extra — :func:`available` is PURE DISCOVERY (``importlib.util.find_spec``, no
|
|
24
|
+
import), mirroring :meth:`unicode_logic_kit.atp.cvc5_backend.Cvc5Backend.available`'s
|
|
25
|
+
exact convention, so probing it never pays the import cost. Every public
|
|
26
|
+
function in this module calls :func:`available` internally and raises
|
|
27
|
+
:class:`OwlReasonerError` (reusing
|
|
28
|
+
:class:`unicode_logic_kit.atp.protocol.BackendUnavailable` the way
|
|
29
|
+
:mod:`unicode_logic_kit.hets.docker` already imports across a subpackage
|
|
30
|
+
boundary — the same precedent this module follows for ``dl/``'s own first
|
|
31
|
+
``atp`` import) when it is missing, rather than silently skipping. A JVM is
|
|
32
|
+
also required (HermiT runs as a ``java`` subprocess, invoked BY owlready2,
|
|
33
|
+
not by this module): this kit assumes one is already on ``PATH`` (Java is
|
|
34
|
+
NOT probed by :func:`available`, since owlready2 only ever discovers it at
|
|
35
|
+
actual reasoner-invocation time — a JVM launch failure surfaces as
|
|
36
|
+
``OwlReadyJavaError``, re-raised here as :class:`OwlReasonerError` naming
|
|
37
|
+
what failed, never silently). On Windows, ``owlready2.JAVA_EXE`` (default
|
|
38
|
+
``"java"``) may need to be set explicitly if ``java`` is not literally on
|
|
39
|
+
``PATH`` under that name; this module never touches that setting itself.
|
|
40
|
+
|
|
41
|
+
Licensing
|
|
42
|
+
----------
|
|
43
|
+
``owlready2`` itself is **LGPL-3.0-or-later** (confirmed from the installed
|
|
44
|
+
package's own metadata: ``pip show owlready2`` prints
|
|
45
|
+
``License-Expression: LGPL-3.0-or-later``) — the first LGPL/copyleft optional
|
|
46
|
+
dependency this kit carries (see the ``owl`` extra note in ``pyproject.toml``,
|
|
47
|
+
added via this task's ``integration`` request). Its wheel ALSO bundles a
|
|
48
|
+
second reasoner, Pellet, which is dual-licensed **AGPL-3.0** (confirmed from
|
|
49
|
+
``owlready2/pellet/LICENSE.txt`` in the installed package) — this module
|
|
50
|
+
never calls ``owlready2.sync_reasoner_pellet``, only
|
|
51
|
+
``sync_reasoner_hermit`` (whose bundled ``HermiT.jar`` is LGPL, same as the
|
|
52
|
+
Python package), so no AGPL code path is ever exercised by this kit; the AGPL
|
|
53
|
+
jar merely ships, unused, inside the dependency's wheel.
|
|
54
|
+
|
|
55
|
+
Translation route — direct to owlready2's Python object model, not through
|
|
56
|
+
``owl_functional.py``'s text
|
|
57
|
+
------------------------------------------------------------------------------
|
|
58
|
+
:mod:`unicode_logic_kit.dl.owl_functional` is this kit's OWL 2 Functional-Style
|
|
59
|
+
Syntax document reader/writer, and stays the ALCHQ-only "document writer" for
|
|
60
|
+
that fragment (unchanged by this module — see its own docstring): it is NOT
|
|
61
|
+
extended to parse/render ``ObjectInverseOf``/``ObjectOneOf``, because
|
|
62
|
+
``owlready2`` has no Functional-Style Syntax reader at all (confirmed: it
|
|
63
|
+
reads only RDF/XML, OWL/XML and NTriples — see ``owlready2/namespace.py``'s
|
|
64
|
+
``Ontology.load``/``graph.parse``), so round-tripping through that text would
|
|
65
|
+
buy nothing here and risk a second, redundant, untested translation path.
|
|
66
|
+
Instead this module translates :class:`~unicode_logic_kit.dl.tableau.TBox`/
|
|
67
|
+
:class:`~unicode_logic_kit.dl.tableau.ABox`/:class:`~unicode_logic_kit.dl.concepts.Concept`
|
|
68
|
+
DIRECTLY into ``owlready2``'s own Python class-expression API
|
|
69
|
+
(``And``/``Or``/``Not``/``Inverse``/``OneOf``/``Restriction`` — see
|
|
70
|
+
``owlready2/class_construct.py``), the same object model ``owlready2``'s own
|
|
71
|
+
documentation builds ontologies with. Every entity (class/role/individual)
|
|
72
|
+
gets a FRESH, synthetic, opaque local name (``C1``, ``R1``, ``I1``, …), never
|
|
73
|
+
this kit's own name string verbatim: this kit's :class:`~unicode_logic_kit.dl.concepts.Atomic`/
|
|
74
|
+
role/individual names are arbitrary opaque strings that may contain
|
|
75
|
+
characters illegal in an OWL/RDF local name (whitespace, ``#``, non-ASCII
|
|
76
|
+
glyphs used elsewhere in this kit — see :mod:`unicode_logic_kit.dl.owl_functional`'s
|
|
77
|
+
own "IRIs and names" section for the identical concern there), and since
|
|
78
|
+
every query this module answers is a yes/no verdict (never "what is this
|
|
79
|
+
entity called back"), there is nothing to lose by never round-tripping the
|
|
80
|
+
original name through owlready2's IRI machinery at all.
|
|
81
|
+
|
|
82
|
+
Each of this module's ``external_*`` functions builds a FRESH ``owlready2.World()``
|
|
83
|
+
(never the shared global default world) and a fresh ``Ontology`` inside it, so
|
|
84
|
+
two calls never share reasoner state — confirmed empirically (see this
|
|
85
|
+
task's scratch notes): mutating one ``World``'s entities is invisible to
|
|
86
|
+
another. A **general concept inclusion** ``sub ⊑ sup`` (this kit's ``TBox``
|
|
87
|
+
is always a GENERAL TBox — see :mod:`unicode_logic_kit.dl.tableau`) is asserted
|
|
88
|
+
via ``owlready2.GeneralClassAxiom``, the native OWL 2 mechanism for a
|
|
89
|
+
``SubClassOf`` whose LEFT side is an arbitrary (possibly anonymous) class
|
|
90
|
+
expression — EXCEPT for the two antecedent shapes ``GeneralClassAxiom``
|
|
91
|
+
cannot take directly (confirmed empirically: its left side must be a genuine
|
|
92
|
+
``Construct``, e.g. ``And``/``Or``/``Not``/``Restriction``/``OneOf``, not a
|
|
93
|
+
plain named entity like ``owl:Thing`` — mutating ``owl:Thing`` itself would
|
|
94
|
+
also leak across every ``World`` at once, since it is a single shared Python
|
|
95
|
+
object, not a per-world one): ``sub = Bottom()`` is skipped outright (``∅ ⊑
|
|
96
|
+
sup`` is a tautology, exactly mirroring how the in-house tableau's own
|
|
97
|
+
``internalized()`` would trivially satisfy it too), and ``sub = Top()``
|
|
98
|
+
routes through a lazily-created per-ontology helper class equivalent to
|
|
99
|
+
``owl:Thing`` (``_Domain.equivalent_to = [Thing]``, then
|
|
100
|
+
``_Domain.is_a.append(translated sup)``) — which correctly forces every
|
|
101
|
+
individual into ``sup``, confirmed empirically, without ever touching the
|
|
102
|
+
shared ``Thing`` object itself. An ``Atomic`` antecedent uses the normal,
|
|
103
|
+
better-supported ``named_class.is_a.append(...)`` path instead of a
|
|
104
|
+
``GeneralClassAxiom`` at all (no anonymous left side needed when the
|
|
105
|
+
antecedent already names a class).
|
|
106
|
+
|
|
107
|
+
What each function decides
|
|
108
|
+
------------------------------
|
|
109
|
+
Every ``external_*`` function mirrors its :mod:`unicode_logic_kit.dl.tableau`
|
|
110
|
+
namesake's SIGNATURE and REDUCTION exactly (``external_subsumes`` reduces to
|
|
111
|
+
``external_concept_satisfiable`` exactly the way ``subsumes`` reduces to
|
|
112
|
+
``concept_satisfiable``, and so on) — this is deliberate: it is what makes
|
|
113
|
+
the differential test battery in ``tests/test_owl_reasoner.py`` meaningful
|
|
114
|
+
(every ALCHQ fixture in ``tests/test_dl_alc.py``/``test_dl_alcq.py``/
|
|
115
|
+
``test_dl_rbox.py``/``test_dl_classify.py`` decided through BOTH this
|
|
116
|
+
module's reduction chain and the tableau's own is comparing the SAME
|
|
117
|
+
question, not two differently-shaped ones), and it lets this module cover
|
|
118
|
+
the full I/O-extended fragment (concept/ABox consistency, subsumption,
|
|
119
|
+
equivalence, instance checking/retrieval, realization) with only ONE genuine
|
|
120
|
+
HermiT-invoking primitive underneath (:func:`_kb_consistent`), exactly the
|
|
121
|
+
way the in-house tableau has only ``_solve`` underneath ITS whole public API.
|
|
122
|
+
|
|
123
|
+
Each call to :func:`_kb_consistent` spawns a fresh ``java`` subprocess (via
|
|
124
|
+
``owlready2.sync_reasoner_hermit``), so this module is orders of magnitude
|
|
125
|
+
slower per call than the in-house tableau — expected and acceptable for an
|
|
126
|
+
occasional external cross-check, not a hot-path reasoner; the differential
|
|
127
|
+
test battery deliberately runs a curated, small fixture set rather than
|
|
128
|
+
every existing ALCHQ test file verbatim, to keep the live test run's JVM
|
|
129
|
+
subprocess count bounded.
|
|
130
|
+
|
|
131
|
+
Public API: :func:`available`, :func:`external_concept_satisfiable`,
|
|
132
|
+
:func:`external_concept_unsatisfiable`, :func:`external_subsumes`,
|
|
133
|
+
:func:`external_equivalent`, :func:`external_abox_consistent`,
|
|
134
|
+
:func:`external_instance_check`, :func:`external_instance_retrieval`,
|
|
135
|
+
:func:`external_realize`, :func:`external_realize_all`, :class:`OwlReasonerError`.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
import importlib.util
|
|
139
|
+
from typing import Dict, List, Optional, Set
|
|
140
|
+
|
|
141
|
+
from ..atp.protocol import BackendUnavailable
|
|
142
|
+
from .concepts import (
|
|
143
|
+
Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
|
|
144
|
+
InverseRole, Nominal, HasValue, DATA_CONCEPTS,
|
|
145
|
+
)
|
|
146
|
+
from .tableau import (
|
|
147
|
+
TBox, ABox, UnsupportedAxiomError, UnsupportedConceptError, _RBox,
|
|
148
|
+
_abox_individual_names, _check_simple_role_box, _concept_individual_names,
|
|
149
|
+
_data_layer_kinds, _reject_abox_roles, _reject_concept_role,
|
|
150
|
+
_reject_concept_roles_deep, _tbox_class_expressions, _validate_data_box,
|
|
151
|
+
_validate_role_box,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
__all__ = [
|
|
155
|
+
"available",
|
|
156
|
+
"external_concept_satisfiable", "external_concept_unsatisfiable",
|
|
157
|
+
"external_subsumes", "external_equivalent",
|
|
158
|
+
"external_abox_consistent", "external_instance_check", "external_instance_retrieval",
|
|
159
|
+
"external_realize", "external_realize_all",
|
|
160
|
+
"OwlReasonerError",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class OwlReasonerError(BackendUnavailable):
|
|
165
|
+
"""``owlready2`` is not installed, or HermiT (invoked through it) failed to
|
|
166
|
+
run — a missing/broken JVM, a HermiT crash, or any other reasoner-invocation
|
|
167
|
+
failure that is not itself an ``OwlReadyInconsistentOntologyError`` (which
|
|
168
|
+
this module treats as an ordinary, expected "inconsistent" verdict, not an
|
|
169
|
+
error — see :func:`_kb_consistent`). Reuses
|
|
170
|
+
:class:`unicode_logic_kit.atp.protocol.BackendUnavailable` (see the module
|
|
171
|
+
docstring), so a caller that already handles that exception for the kit's
|
|
172
|
+
other optional backends (cvc5, Hets/Docker, …) handles this one the same
|
|
173
|
+
way for free.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def available() -> bool:
|
|
178
|
+
"""True iff ``owlready2`` is importable — pure discovery, no import, no JVM
|
|
179
|
+
probe (see the module docstring's "Optional dependency" section).
|
|
180
|
+
"""
|
|
181
|
+
return importlib.util.find_spec("owlready2") is not None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _require_available():
|
|
185
|
+
"""Return the imported ``owlready2`` module, or raise :class:`OwlReasonerError`
|
|
186
|
+
naming the missing extra (see :func:`available`). The import itself stays
|
|
187
|
+
lazy — done here, not at this module's top level — so importing
|
|
188
|
+
``dl.owl_reasoner`` never requires ``owlready2`` to be installed; only
|
|
189
|
+
actually USING it does (see the module docstring's "Optional dependency"
|
|
190
|
+
section).
|
|
191
|
+
"""
|
|
192
|
+
if not available():
|
|
193
|
+
raise OwlReasonerError(
|
|
194
|
+
"dl.owl_reasoner: owlready2 is not installed — install the optional "
|
|
195
|
+
"'owl' extra (`pip install unicode-logic-kit[owl]`, i.e. "
|
|
196
|
+
"`pip install owlready2`) to use the external, HermiT-backed OWL 2 "
|
|
197
|
+
"DL reasoner for InverseRole/Nominal (I/O) beyond this kit's "
|
|
198
|
+
"in-house ALCHQ tableau (see dl.tableau's 'Inverse roles and "
|
|
199
|
+
"nominals (I, O)' section).")
|
|
200
|
+
import owlready2
|
|
201
|
+
return owlready2
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# --------------------------------------------------------------------------- #
|
|
205
|
+
# Translation: Concept/TBox/ABox -> a fresh owlready2 World/Ontology.
|
|
206
|
+
# --------------------------------------------------------------------------- #
|
|
207
|
+
|
|
208
|
+
class _Ctx:
|
|
209
|
+
"""Per-call translation state: the fresh ``owlready2``/``World``/``Ontology``
|
|
210
|
+
triple, and lazily-populated name -> owlready2-entity maps (see the module
|
|
211
|
+
docstring's "Translation route" section for why entities get fresh
|
|
212
|
+
synthetic names rather than this kit's own name strings verbatim).
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
__slots__ = ("ow", "world", "onto", "characteristics",
|
|
216
|
+
"_classes", "_roles", "_individuals", "_domain", "_counter", "_below")
|
|
217
|
+
|
|
218
|
+
def __init__(self, ow, world, onto, characteristics: Dict[str, Set[str]]):
|
|
219
|
+
self.ow = ow
|
|
220
|
+
self.world = world
|
|
221
|
+
self.onto = onto
|
|
222
|
+
# ``owlready2 base name -> the role names carrying that characteristic``
|
|
223
|
+
# (see :data:`_CHARACTERISTIC_BASES`). All SIX characteristics, not just
|
|
224
|
+
# transitivity: owlready2 needs every one of them among the property
|
|
225
|
+
# class's BASES at creation time, so they cannot be added later and a
|
|
226
|
+
# route that carried only one would silently drop the other five — and
|
|
227
|
+
# this route is where the in-house tableau's own refusals send the user.
|
|
228
|
+
self.characteristics = characteristics
|
|
229
|
+
self._classes: Dict[str, object] = {}
|
|
230
|
+
self._roles: Dict[str, object] = {}
|
|
231
|
+
self._individuals: Dict[str, object] = {}
|
|
232
|
+
self._domain = None
|
|
233
|
+
self._counter = 0
|
|
234
|
+
# ``("class" | "role") -> name -> the names directly above it`` for the inclusions
|
|
235
|
+
# between two NAMED entities that were stated as owlready2 sub-class / sub-property
|
|
236
|
+
# links (see :meth:`state_named_inclusion`).
|
|
237
|
+
self._below: Dict[str, Dict[str, Set[str]]] = {"class": {}, "role": {}}
|
|
238
|
+
|
|
239
|
+
def state_named_inclusion(self, kind: str, sub_name: str, sup_name: str,
|
|
240
|
+
sub_entity, sup_entity) -> None:
|
|
241
|
+
"""Assert ``sub ⊑ sup`` between two named classes (``kind="class"``) or two plain
|
|
242
|
+
properties (``kind="role"``).
|
|
243
|
+
|
|
244
|
+
owlready2 keeps a named class (property) under another as a Python base class, and
|
|
245
|
+
Python refuses a cycle of bases with a ``TypeError``. Two named entities below each
|
|
246
|
+
other, an entity below itself, a longer ring, ``EquivalentClasses`` and
|
|
247
|
+
``EquivalentObjectProperties`` all read as such a cycle. An inclusion that would close
|
|
248
|
+
one is therefore stated as the equivalence ``sub ≡ sup``, which is what the ring says:
|
|
249
|
+
``sup`` is already below ``sub`` through the links stated so far, so ``sub ⊑ sup``
|
|
250
|
+
makes every entity on the ring equivalent, and ``sub ≡ sup`` adds only the converse
|
|
251
|
+
that the ring already gives. An entity below itself says nothing and is not stated.
|
|
252
|
+
"""
|
|
253
|
+
if sub_name == sup_name:
|
|
254
|
+
return
|
|
255
|
+
links = self._below[kind]
|
|
256
|
+
reachable, pending = set(), [sup_name]
|
|
257
|
+
while pending:
|
|
258
|
+
name = pending.pop()
|
|
259
|
+
if name in reachable:
|
|
260
|
+
continue
|
|
261
|
+
reachable.add(name)
|
|
262
|
+
pending.extend(links.get(name, ()))
|
|
263
|
+
if sub_name in reachable:
|
|
264
|
+
sub_entity.equivalent_to.append(sup_entity)
|
|
265
|
+
else:
|
|
266
|
+
links.setdefault(sub_name, set()).add(sup_name)
|
|
267
|
+
sub_entity.is_a.append(sup_entity)
|
|
268
|
+
|
|
269
|
+
def _fresh_name(self, prefix: str) -> str:
|
|
270
|
+
self._counter += 1
|
|
271
|
+
return f"{prefix}{self._counter}"
|
|
272
|
+
|
|
273
|
+
def cls(self, name: str):
|
|
274
|
+
"""The owlready2 named class for this kit's ``Atomic`` name ``name``."""
|
|
275
|
+
if name not in self._classes:
|
|
276
|
+
self._classes[name] = self.ow.types.new_class(self._fresh_name("C"), (self.ow.Thing,))
|
|
277
|
+
return self._classes[name]
|
|
278
|
+
|
|
279
|
+
def _role_bases(self, name: str) -> tuple:
|
|
280
|
+
"""``(ObjectProperty, *every declared characteristic's owlready2 class)``
|
|
281
|
+
for the role named ``name`` — the bases tuple ``role_obj`` creates it with.
|
|
282
|
+
"""
|
|
283
|
+
bases = [self.ow.ObjectProperty]
|
|
284
|
+
for base_name, roles in self.characteristics.items():
|
|
285
|
+
if name in roles:
|
|
286
|
+
bases.append(getattr(self.ow, base_name))
|
|
287
|
+
return tuple(bases)
|
|
288
|
+
|
|
289
|
+
def role_obj(self, name: str):
|
|
290
|
+
"""The owlready2 object property for this kit's role name ``name``, with
|
|
291
|
+
every declared characteristic among its bases at CREATION time
|
|
292
|
+
(owlready2 needs them up front, not added after the fact).
|
|
293
|
+
"""
|
|
294
|
+
if name not in self._roles:
|
|
295
|
+
self._roles[name] = self.ow.types.new_class(
|
|
296
|
+
self._fresh_name("R"), self._role_bases(name))
|
|
297
|
+
return self._roles[name]
|
|
298
|
+
|
|
299
|
+
def role(self, role_field):
|
|
300
|
+
"""Resolve a ``role`` field (a plain ``str``, or an
|
|
301
|
+
:class:`~unicode_logic_kit.dl.concepts.InverseRole`) to the owlready2
|
|
302
|
+
property expression to use in a restriction: the property itself, or
|
|
303
|
+
``owlready2.Inverse(property)``.
|
|
304
|
+
"""
|
|
305
|
+
if isinstance(role_field, InverseRole):
|
|
306
|
+
return self.ow.Inverse(self.role_obj(role_field.role))
|
|
307
|
+
return self.role_obj(role_field)
|
|
308
|
+
|
|
309
|
+
def ind(self, name: str):
|
|
310
|
+
"""The owlready2 named individual for this kit's individual name ``name``."""
|
|
311
|
+
if name not in self._individuals:
|
|
312
|
+
self._individuals[name] = self.ow.Thing(self._fresh_name("I"))
|
|
313
|
+
return self._individuals[name]
|
|
314
|
+
|
|
315
|
+
def domain_class(self):
|
|
316
|
+
"""A per-ontology class equivalent to ``owl:Thing`` — see the module
|
|
317
|
+
docstring's "Translation route" section for why a ``Top()`` GCI
|
|
318
|
+
antecedent routes through this rather than ``owlready2.Thing`` itself.
|
|
319
|
+
"""
|
|
320
|
+
if self._domain is None:
|
|
321
|
+
self._domain = self.ow.types.new_class(self._fresh_name("D"), (self.ow.Thing,))
|
|
322
|
+
self._domain.equivalent_to = [self.ow.Thing]
|
|
323
|
+
return self._domain
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _translate_concept(concept: Concept, ctx: _Ctx):
|
|
327
|
+
"""Translate ``concept`` into an owlready2 class expression (see the module
|
|
328
|
+
docstring's "Translation route" section). Covers the FULL ALCHQ + I + O
|
|
329
|
+
fragment this module decides — every :class:`Concept` subtype, including
|
|
330
|
+
:class:`InverseRole`-valued roles and :class:`Nominal`.
|
|
331
|
+
|
|
332
|
+
An OWL 2 built-in property name as a restriction's role is refused by name
|
|
333
|
+
(:func:`~unicode_logic_kit.dl.tableau._reject_concept_role`), exactly as the
|
|
334
|
+
in-house tableau and the FOL image refuse it: owlready2 would create an
|
|
335
|
+
ORDINARY property of that name, and an oracle that answers about a
|
|
336
|
+
different restriction agrees with nothing for the right reason.
|
|
337
|
+
"""
|
|
338
|
+
_reject_concept_role(concept, where="dl.owl_reasoner")
|
|
339
|
+
if isinstance(concept, Top):
|
|
340
|
+
return ctx.ow.Thing
|
|
341
|
+
if isinstance(concept, Bottom):
|
|
342
|
+
return ctx.ow.Nothing
|
|
343
|
+
if isinstance(concept, Atomic):
|
|
344
|
+
return ctx.cls(concept.name)
|
|
345
|
+
if isinstance(concept, Nominal):
|
|
346
|
+
return ctx.ow.OneOf([ctx.ind(concept.individual)])
|
|
347
|
+
if isinstance(concept, HasValue):
|
|
348
|
+
# owlready2 spells ObjectHasValue(P a) as `prop.value(individual)`.
|
|
349
|
+
return ctx.role(concept.role).value(ctx.ind(concept.individual))
|
|
350
|
+
if isinstance(concept, Not):
|
|
351
|
+
return ctx.ow.Not(_translate_concept(concept.concept, ctx))
|
|
352
|
+
if isinstance(concept, And):
|
|
353
|
+
return ctx.ow.And([_translate_concept(concept.left, ctx),
|
|
354
|
+
_translate_concept(concept.right, ctx)])
|
|
355
|
+
if isinstance(concept, Or):
|
|
356
|
+
return ctx.ow.Or([_translate_concept(concept.left, ctx),
|
|
357
|
+
_translate_concept(concept.right, ctx)])
|
|
358
|
+
if isinstance(concept, Exists):
|
|
359
|
+
return ctx.role(concept.role).some(_translate_concept(concept.concept, ctx))
|
|
360
|
+
if isinstance(concept, ForAll):
|
|
361
|
+
return ctx.role(concept.role).only(_translate_concept(concept.concept, ctx))
|
|
362
|
+
if isinstance(concept, AtLeast):
|
|
363
|
+
return ctx.role(concept.role).min(concept.n, _translate_concept(concept.concept, ctx))
|
|
364
|
+
if isinstance(concept, AtMost):
|
|
365
|
+
return ctx.role(concept.role).max(concept.n, _translate_concept(concept.concept, ctx))
|
|
366
|
+
if isinstance(concept, DATA_CONCEPTS):
|
|
367
|
+
raise UnsupportedConceptError(
|
|
368
|
+
f"dl.owl_reasoner: the data restriction {type(concept).__name__} "
|
|
369
|
+
f"({concept.to_unicode()}) is not wired to this external route: "
|
|
370
|
+
f"owlready2 could express it, but this module does not translate the "
|
|
371
|
+
f"data layer, and an oracle that quietly dropped it would agree with "
|
|
372
|
+
f"everything. Ask the FOL image: kb = dl.kb_to_fol(tbox, abox, "
|
|
373
|
+
f"query=[concept]), then api.prove(kb.unsatisfiability_goal(concept), "
|
|
374
|
+
f"kb.tbox_premises) (kb.subsumption_goal and kb.instance_goal are the "
|
|
375
|
+
f"other two questions; 'proved' transfers to OWL 2, 'refuted' does not: "
|
|
376
|
+
f"see kb.refutation_is_decisive) — or decide facet arithmetic over "
|
|
377
|
+
f"the data ranges alone with atp.z3_arith.is_valid_arith.")
|
|
378
|
+
raise TypeError(f"dl.owl_reasoner: unsupported concept {type(concept).__name__}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _add_gci(sub: Concept, sup: Concept, ctx: _Ctx) -> None:
|
|
382
|
+
"""Assert the general concept inclusion ``sub ⊑ sup`` into ``ctx``'s
|
|
383
|
+
ontology, choosing the cheapest correct owlready2 encoding for the shape
|
|
384
|
+
of ``sub`` (see the module docstring's "Translation route" section for
|
|
385
|
+
why ``Top``/``Bottom``/``Atomic`` antecedents each need their own case).
|
|
386
|
+
"""
|
|
387
|
+
if isinstance(sub, Bottom):
|
|
388
|
+
return # ∅ ⊑ sup is a tautology; nothing to assert
|
|
389
|
+
if isinstance(sub, Top):
|
|
390
|
+
ctx.domain_class().is_a.append(_translate_concept(sup, ctx))
|
|
391
|
+
return
|
|
392
|
+
if isinstance(sub, Atomic):
|
|
393
|
+
if isinstance(sup, Atomic):
|
|
394
|
+
ctx.state_named_inclusion("class", sub.name, sup.name, ctx.cls(sub.name), ctx.cls(sup.name))
|
|
395
|
+
else:
|
|
396
|
+
ctx.cls(sub.name).is_a.append(_translate_concept(sup, ctx))
|
|
397
|
+
return
|
|
398
|
+
gca = ctx.ow.GeneralClassAxiom(_translate_concept(sub, ctx))
|
|
399
|
+
gca.is_a.append(_translate_concept(sup, ctx))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
#: ``TBox field -> the owlready2 property class that expresses it as a BASE``.
|
|
403
|
+
#: owlready2 requires a characteristic among the property class's bases at
|
|
404
|
+
#: creation time, so ``_Ctx`` has to know all six BEFORE any role is built —
|
|
405
|
+
#: hence a table read by ``_characteristics`` rather than six inline tests.
|
|
406
|
+
_CHARACTERISTIC_BASES = {
|
|
407
|
+
"transitive_roles": "TransitiveProperty",
|
|
408
|
+
"symmetric_roles": "SymmetricProperty",
|
|
409
|
+
"asymmetric_roles": "AsymmetricProperty",
|
|
410
|
+
"reflexive_roles": "ReflexiveProperty",
|
|
411
|
+
"irreflexive_roles": "IrreflexiveProperty",
|
|
412
|
+
"functional_roles": "FunctionalProperty",
|
|
413
|
+
"inverse_functional_roles": "InverseFunctionalProperty",
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _characteristics(tbox: TBox) -> Dict[str, Set[str]]:
|
|
418
|
+
"""``owlready2 base name -> the role names carrying it`` for ``tbox``."""
|
|
419
|
+
return {base: set(getattr(tbox, field))
|
|
420
|
+
for field, base in _CHARACTERISTIC_BASES.items()}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _role_expr(role_field, ctx: _Ctx):
|
|
424
|
+
"""A role-box entry (a plain name, or an
|
|
425
|
+
:class:`~unicode_logic_kit.dl.concepts.InverseRole`) as the owlready2
|
|
426
|
+
property expression to use on either side of a sub-property axiom.
|
|
427
|
+
|
|
428
|
+
``ctx.role`` already does exactly this for a restriction's role; calling it
|
|
429
|
+
here is what stops an ``InverseRole`` being used as a DICT KEY and coming
|
|
430
|
+
back out as a brand-new atomic property — which is what ``_build_kb`` did
|
|
431
|
+
until 0.30.0, making ``external_subsumes(…, TBox().add_role_inclusion('r',
|
|
432
|
+
InverseRole('s')))`` answer a question about an ontology that never had the
|
|
433
|
+
axiom.
|
|
434
|
+
"""
|
|
435
|
+
return ctx.role(role_field)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _plain_sub_role(sub_role, super_role):
|
|
439
|
+
"""``(sub_role, super_role)`` with a PLAIN role on the left — owlready2
|
|
440
|
+
attaches a sub-property axiom to the property CLASS on the left
|
|
441
|
+
(``is_a.append``), which an ``Inverse(...)`` expression is not (it crashed
|
|
442
|
+
with an ``AttributeError`` for ``s⁻ ⊑ r``, while ``r ⊑ s⁻`` built).
|
|
443
|
+
|
|
444
|
+
The rewrite is an equivalence, not an approximation: ``r⁻ ⊑ s`` says every
|
|
445
|
+
``r(y, x)`` is an ``s(x, y)``, i.e. every ``r(x, y)`` is an ``s(y, x)``,
|
|
446
|
+
which is ``r ⊑ s⁻``; and ``r⁻ ⊑ s⁻`` is ``r ⊑ s``, the inverses cancelling.
|
|
447
|
+
"""
|
|
448
|
+
if isinstance(sub_role, InverseRole):
|
|
449
|
+
inner = (super_role.role if isinstance(super_role, InverseRole)
|
|
450
|
+
else InverseRole(super_role))
|
|
451
|
+
return sub_role.role, inner
|
|
452
|
+
return sub_role, super_role
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _add_inverse_pair(ctx: _Ctx, partner: Dict[str, str], p: str, q: str) -> None:
|
|
456
|
+
"""Assert ``InverseObjectProperties(p q)`` (``p ≡ q⁻``) into ``ctx``.
|
|
457
|
+
|
|
458
|
+
owlready2's ``inverse_property`` holds ONE property, and assigning it a
|
|
459
|
+
second time REPLACES the first — so ``Inv(p, q)`` together with ``Inv(p, r)``
|
|
460
|
+
kept only one of them and the oracle reasoned over a weaker knowledge base.
|
|
461
|
+
The second pair is not lost by being kept as an EQUIVALENCE instead: if ``p``
|
|
462
|
+
already has the inverse ``q0`` then ``q0 ≡ q`` (both are ``p⁻``), and the
|
|
463
|
+
symmetric case likewise. ``partner`` records the one inverse each property
|
|
464
|
+
was given.
|
|
465
|
+
"""
|
|
466
|
+
if p in partner:
|
|
467
|
+
if partner[p] != q:
|
|
468
|
+
ctx.role_obj(partner[p]).equivalent_to.append(ctx.role_obj(q))
|
|
469
|
+
elif q in partner:
|
|
470
|
+
if partner[q] != p:
|
|
471
|
+
ctx.role_obj(partner[q]).equivalent_to.append(ctx.role_obj(p))
|
|
472
|
+
else:
|
|
473
|
+
ctx.role_obj(p).inverse_property = ctx.role_obj(q)
|
|
474
|
+
partner[p], partner[q] = q, p
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _build_kb(ctx: _Ctx, tbox: TBox, abox: ABox) -> None:
|
|
478
|
+
"""Populate ``ctx``'s ontology with ``tbox``'s role box + GCIs and ``abox``'s
|
|
479
|
+
assertions. Must run inside a ``with ctx.onto:`` block (every
|
|
480
|
+
``ctx.ow.types.new_class``/``GeneralClassAxiom`` call needs the current
|
|
481
|
+
namespace set — see ``owlready2``'s own convention).
|
|
482
|
+
|
|
483
|
+
EVERY role-box field is rendered. This module is one of the kit's two
|
|
484
|
+
INDEPENDENT oracles, and it is the one the in-house tableau's own refusal
|
|
485
|
+
messages send the user to — an oracle that silently dropped the axiom under
|
|
486
|
+
test would agree with everything, and would make those messages dishonest.
|
|
487
|
+
``tests/test_dl_route_agreement.py`` checks a verdict per field that FLIPS
|
|
488
|
+
when the field is dropped, which is what makes that claim checkable.
|
|
489
|
+
"""
|
|
490
|
+
for sub_role, super_role in tbox.role_inclusions:
|
|
491
|
+
sub_role, super_role = _plain_sub_role(sub_role, super_role)
|
|
492
|
+
if isinstance(super_role, str):
|
|
493
|
+
ctx.state_named_inclusion("role", sub_role, super_role,
|
|
494
|
+
_role_expr(sub_role, ctx), _role_expr(super_role, ctx))
|
|
495
|
+
else:
|
|
496
|
+
_role_expr(sub_role, ctx).is_a.append(_role_expr(super_role, ctx))
|
|
497
|
+
# APPEND, never assign: owlready2 stores a property's property chains,
|
|
498
|
+
# domains and ranges as lists, and `role.domain = [...]` REPLACES what an
|
|
499
|
+
# earlier axiom put there. Two ObjectPropertyDomain axioms on one role are
|
|
500
|
+
# a CONJUNCTION (an element with an r-successor is in both classes), so
|
|
501
|
+
# assigning kept only the last and the oracle reasoned over a weaker
|
|
502
|
+
# knowledge base than the tableau and the FOL image -- silently, which is
|
|
503
|
+
# the one thing an oracle must not do. The same for ranges and for two
|
|
504
|
+
# chains with one super-property.
|
|
505
|
+
for chain, super_role in tbox.role_chains:
|
|
506
|
+
ctx.role_obj(super_role).property_chain.append(
|
|
507
|
+
ctx.ow.PropertyChain([ctx.role_obj(role) for role in chain]))
|
|
508
|
+
partner: Dict[str, str] = {}
|
|
509
|
+
for p, q in tbox.inverse_role_pairs:
|
|
510
|
+
_add_inverse_pair(ctx, partner, p, q)
|
|
511
|
+
for left, right in tbox.disjoint_role_pairs:
|
|
512
|
+
ctx.ow.AllDisjoint([ctx.role_obj(left), ctx.role_obj(right)])
|
|
513
|
+
# The six characteristics need no statement here: they are already among
|
|
514
|
+
# each property class's bases (see _Ctx.role_obj). A role that occurs ONLY
|
|
515
|
+
# in a characteristic declaration still has to be created, though, or the
|
|
516
|
+
# declaration would never reach the ontology at all.
|
|
517
|
+
for roles in ctx.characteristics.values():
|
|
518
|
+
for role in sorted(roles):
|
|
519
|
+
ctx.role_obj(role)
|
|
520
|
+
for role, filler in tbox.role_domains:
|
|
521
|
+
ctx.role_obj(role).domain.append(_translate_concept(filler, ctx))
|
|
522
|
+
for role, filler in tbox.role_ranges:
|
|
523
|
+
ctx.role_obj(role).range.append(_translate_concept(filler, ctx))
|
|
524
|
+
for sub, sup in tbox.inclusions:
|
|
525
|
+
_add_gci(sub, sup, ctx)
|
|
526
|
+
for individual, concept in abox.concept_assertions:
|
|
527
|
+
ctx.ind(individual).is_a.append(_translate_concept(concept, ctx))
|
|
528
|
+
for a, b, role in abox.role_assertions:
|
|
529
|
+
role_obj = ctx.role_obj(role)
|
|
530
|
+
if issubclass(role_obj, ctx.ow.FunctionalProperty):
|
|
531
|
+
# owlready2 keeps the value of a FUNCTIONAL property as ONE attribute (None
|
|
532
|
+
# while unset), not as a list to append to, and a second assignment would
|
|
533
|
+
# REPLACE the first: two successors of one individual (which functionality
|
|
534
|
+
# then identifies, or which `distinct_assertions` makes inconsistent) would
|
|
535
|
+
# reach HermiT as one. The assertion r(a, b) is stated instead as the class
|
|
536
|
+
# assertion a : ∃r.{b} (ObjectHasValue), which OWL 2 defines to mean the same
|
|
537
|
+
# and which the negative assertions below already use. The test is the property
|
|
538
|
+
# CLASS, not the role names the TBox declared functional: a sub-property of a
|
|
539
|
+
# functional property is a Python subclass of it and holds one value as well.
|
|
540
|
+
ctx.ind(a).is_a.append(role_obj.value(ctx.ind(b)))
|
|
541
|
+
else:
|
|
542
|
+
getattr(ctx.ind(a), role_obj.name).append(ctx.ind(b))
|
|
543
|
+
for a, b in abox.distinct_assertions:
|
|
544
|
+
ctx.ow.AllDifferent([ctx.ind(a), ctx.ind(b)])
|
|
545
|
+
for a, b in abox.same_assertions:
|
|
546
|
+
# owlready2's own spelling of SameIndividual: the two individuals
|
|
547
|
+
# become one entity by equivalence, which is what HermiT then reads.
|
|
548
|
+
ctx.ind(a).equivalent_to.append(ctx.ind(b))
|
|
549
|
+
for a, b, role in abox.negative_role_assertions:
|
|
550
|
+
role_obj = ctx.role_obj(role)
|
|
551
|
+
ctx.ind(a).is_a.append(ctx.ow.Not(role_obj.value(ctx.ind(b))))
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _reject_data_layer(tbox: TBox, abox: ABox) -> None:
|
|
555
|
+
"""Refuse, by name, a knowledge base that carries a DATA axiom kind.
|
|
556
|
+
|
|
557
|
+
This module is one of the kit's two INDEPENDENT oracles, and an oracle that
|
|
558
|
+
silently dropped the axiom under test would agree with everything. owlready2
|
|
559
|
+
has the machinery for data properties and datatypes, but this module's
|
|
560
|
+
translation does not use it, so the honest answer is a refusal that names
|
|
561
|
+
the kinds and the routes that DO answer (the FOL image; for facet arithmetic
|
|
562
|
+
alone, ``atp.z3_arith``). Data CONCEPTS are refused at translation time, in
|
|
563
|
+
:func:`_translate_concept`.
|
|
564
|
+
"""
|
|
565
|
+
kinds = _data_layer_kinds(tbox, abox)
|
|
566
|
+
if kinds:
|
|
567
|
+
raise UnsupportedAxiomError(
|
|
568
|
+
f"dl.owl_reasoner: this knowledge base carries data-layer axiom "
|
|
569
|
+
f"kinds ({', '.join(kinds)}), which this external route does not "
|
|
570
|
+
f"translate — it refuses them by name rather than answer for a "
|
|
571
|
+
f"weaker knowledge base. The in-house tableau has no data domain "
|
|
572
|
+
f"either, so the route that answers is the FOL image: "
|
|
573
|
+
f"kb = dl.kb_to_fol(tbox, abox, query=[concept]), then "
|
|
574
|
+
f"api.prove(kb.unsatisfiability_goal(concept), kb.tbox_premises) — "
|
|
575
|
+
f"or kb.subsumption_goal / kb.instance_goal; 'proved' transfers to "
|
|
576
|
+
f"OWL 2, 'refuted' does not (kb.refutation_is_decisive). Facet "
|
|
577
|
+
f"arithmetic over the data ranges alone is decided by "
|
|
578
|
+
f"atp.z3_arith.is_valid_arith.")
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _guard_inputs(tbox: Optional[TBox], abox: Optional[ABox],
|
|
582
|
+
concepts=()) -> None:
|
|
583
|
+
"""The refusals every ``external_*`` function owes BEFORE it builds
|
|
584
|
+
anything — in ONE place, so an entry point that makes no HermiT call at all
|
|
585
|
+
(``external_realize`` on an empty vocabulary, ``external_realize_all`` and
|
|
586
|
+
``external_instance_retrieval`` on an empty ABox) still runs them: those
|
|
587
|
+
returned a quiet ``[]`` for a knowledge base the other seven refuse.
|
|
588
|
+
|
|
589
|
+
The stored role box and data box must be well-formed — the SAME validation
|
|
590
|
+
the in-house tableau and the FOL image run (:func:`_validate_role_box`) —;
|
|
591
|
+
an ABox assertion must not carry an OWL 2 built-in property name; the data
|
|
592
|
+
layer is refused by name; the role box must satisfy OWL 2's simple-role
|
|
593
|
+
restriction; and no class expression may use a built-in property name as a
|
|
594
|
+
role.
|
|
595
|
+
"""
|
|
596
|
+
tbox = tbox if tbox is not None else TBox()
|
|
597
|
+
_validate_role_box(tbox, where="dl.owl_reasoner")
|
|
598
|
+
_validate_data_box(tbox, where="dl.owl_reasoner")
|
|
599
|
+
_reject_abox_roles(abox, where="dl.owl_reasoner")
|
|
600
|
+
_reject_data_layer(tbox, abox if abox is not None else ABox())
|
|
601
|
+
_check_simple_role_box(tbox, _RBox.from_tbox(tbox))
|
|
602
|
+
for concept in concepts:
|
|
603
|
+
_reject_concept_roles_deep(concept, where="dl.owl_reasoner")
|
|
604
|
+
for concept in _tbox_class_expressions(tbox):
|
|
605
|
+
_reject_concept_roles_deep(concept, where="dl.owl_reasoner")
|
|
606
|
+
if abox is not None:
|
|
607
|
+
for _individual, concept in abox.concept_assertions:
|
|
608
|
+
_reject_concept_roles_deep(concept, where="dl.owl_reasoner")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _kb_consistent(tbox: Optional[TBox], abox: ABox) -> bool:
|
|
612
|
+
"""Build a fresh ``owlready2`` ``World``/``Ontology`` encoding ``(tbox,
|
|
613
|
+
abox)``, run HermiT via ``sync_reasoner_hermit``, and return True iff the
|
|
614
|
+
knowledge base is consistent. The ONE HermiT-invoking primitive every
|
|
615
|
+
other function in this module reduces to (see the module docstring's
|
|
616
|
+
"What each function decides" section).
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
OwlReasonerError: owlready2 is missing, or HermiT/the JVM failed for
|
|
620
|
+
a reason other than genuine inconsistency.
|
|
621
|
+
~unicode_logic_kit.dl.tableau.NonSimpleRoleError:
|
|
622
|
+
the role box violates OWL 2's own SIMPLE-role
|
|
623
|
+
restriction (Structural Specification §11). Checked with the KIT's
|
|
624
|
+
own check, so the same role box is refused with the same message on
|
|
625
|
+
EVERY route, rather than surfacing as a HermiT error here and as a
|
|
626
|
+
tableau error there.
|
|
627
|
+
"""
|
|
628
|
+
# The kit's own refusals come first, so that a knowledge base it refuses is refused with
|
|
629
|
+
# the same error whether or not owlready2 is installed.
|
|
630
|
+
tbox = tbox if tbox is not None else TBox()
|
|
631
|
+
_guard_inputs(tbox, abox)
|
|
632
|
+
ow = _require_available()
|
|
633
|
+
world = ow.World()
|
|
634
|
+
onto = world.get_ontology("http://unicode-logic-kit.invalid/kb#")
|
|
635
|
+
ctx = _Ctx(ow, world, onto, _characteristics(tbox))
|
|
636
|
+
with onto:
|
|
637
|
+
_build_kb(ctx, tbox, abox)
|
|
638
|
+
try:
|
|
639
|
+
ow.sync_reasoner_hermit(world, debug=0)
|
|
640
|
+
return True
|
|
641
|
+
except ow.OwlReadyInconsistentOntologyError:
|
|
642
|
+
return False
|
|
643
|
+
except ow.OwlReadyJavaError as exc:
|
|
644
|
+
raise OwlReasonerError(
|
|
645
|
+
f"dl.owl_reasoner: HermiT (invoked via owlready2, requires a JVM "
|
|
646
|
+
f"on PATH) failed: {exc}") from exc
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# --------------------------------------------------------------------------- #
|
|
650
|
+
# Public API — mirrors dl.tableau's, over the ALCHQ + I + O fragment.
|
|
651
|
+
# --------------------------------------------------------------------------- #
|
|
652
|
+
|
|
653
|
+
#: The name :func:`external_concept_satisfiable` gives its probe individual when
|
|
654
|
+
#: no individual of the question already has it.
|
|
655
|
+
_PROBE_INDIVIDUAL = "_probe"
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _fresh_individual(base: str, taken: Set[str]) -> str:
|
|
659
|
+
"""``base``, or ``base`` followed by the first number that makes it a name
|
|
660
|
+
outside ``taken`` — an individual name the question does not already use.
|
|
661
|
+
|
|
662
|
+
Individual names are compared exactly (they are case-sensitive strings), so
|
|
663
|
+
exact membership in ``taken`` is the whole test.
|
|
664
|
+
"""
|
|
665
|
+
name, number = base, 0
|
|
666
|
+
while name in taken:
|
|
667
|
+
number += 1
|
|
668
|
+
name = f"{base}{number}"
|
|
669
|
+
return name
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def external_concept_satisfiable(concept: Concept, tbox: Optional[TBox] = None) -> bool:
|
|
673
|
+
"""Return True iff ``concept`` is satisfiable with respect to ``tbox`` —
|
|
674
|
+
the external-reasoner twin of :func:`unicode_logic_kit.dl.tableau.concept_satisfiable`,
|
|
675
|
+
but over the FULL ALCHQ + I + O fragment (inverse roles, nominals). Reduced
|
|
676
|
+
to :func:`_kb_consistent` via a single probe individual, exactly the way
|
|
677
|
+
the in-house tableau reduces it to one fresh branch node. The probe is
|
|
678
|
+
called by a name that no nominal or value restriction of ``concept`` or of
|
|
679
|
+
``tbox`` mentions: a probe that shared the name of such an individual would
|
|
680
|
+
BE that individual, and the question would be asked about it and not about
|
|
681
|
+
some element.
|
|
682
|
+
"""
|
|
683
|
+
_guard_inputs(tbox, ABox().assert_concept(_PROBE_INDIVIDUAL, concept))
|
|
684
|
+
_require_available()
|
|
685
|
+
probe = _fresh_individual(_PROBE_INDIVIDUAL, _concept_individual_names(concept).union(
|
|
686
|
+
*(_concept_individual_names(expression)
|
|
687
|
+
for expression in _tbox_class_expressions(tbox))))
|
|
688
|
+
return _kb_consistent(tbox, ABox().assert_concept(probe, concept))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def external_concept_unsatisfiable(concept: Concept, tbox: Optional[TBox] = None) -> bool:
|
|
692
|
+
"""Return True iff ``concept`` is unsatisfiable with respect to ``tbox``."""
|
|
693
|
+
return not external_concept_satisfiable(concept, tbox)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def external_subsumes(sub: Concept, sup: Concept, tbox: Optional[TBox] = None) -> bool:
|
|
697
|
+
"""Return True iff ``tbox`` entails ``sub ⊑ sup`` — the same ``sub ⊓ ¬sup``
|
|
698
|
+
unsatisfiability reduction :func:`unicode_logic_kit.dl.tableau.subsumes` uses.
|
|
699
|
+
"""
|
|
700
|
+
return not external_concept_satisfiable(And(sub, Not(sup)), tbox)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def external_equivalent(c: Concept, d: Concept, tbox: Optional[TBox] = None) -> bool:
|
|
704
|
+
"""Return True iff ``tbox`` entails ``c ≡ d`` (mutual subsumption)."""
|
|
705
|
+
return external_subsumes(c, d, tbox) and external_subsumes(d, c, tbox)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def external_abox_consistent(abox: ABox, tbox: Optional[TBox] = None) -> bool:
|
|
709
|
+
"""Return True iff the knowledge base ``(tbox, abox)`` is consistent."""
|
|
710
|
+
return _kb_consistent(tbox, abox)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _abox_with(abox: ABox, individual: str, concept: Concept) -> ABox:
|
|
714
|
+
"""A copy of ``abox`` with one extra concept assertion ``individual : concept``
|
|
715
|
+
(used by :func:`external_instance_check`'s entailment reduction, mirroring
|
|
716
|
+
:func:`unicode_logic_kit.dl.tableau.instance_check`'s own copy-and-extend).
|
|
717
|
+
|
|
718
|
+
:meth:`~unicode_logic_kit.dl.tableau.ABox.copy`, not a field-by-field
|
|
719
|
+
reconstruction: this module is one of the kit's INDEPENDENT oracles, and
|
|
720
|
+
an oracle that silently drops an assertion kind the caller supplied
|
|
721
|
+
agrees with the in-house tableau for the wrong reason. (Field-by-field is
|
|
722
|
+
what this did until 0.30.0, and it dropped ``same_assertions`` /
|
|
723
|
+
``negative_role_assertions`` the moment they existed.)
|
|
724
|
+
"""
|
|
725
|
+
probe = abox.copy()
|
|
726
|
+
probe.assert_concept(individual, concept)
|
|
727
|
+
return probe
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def external_instance_check(abox: ABox, individual: str, concept: Concept,
|
|
731
|
+
tbox: Optional[TBox] = None) -> bool:
|
|
732
|
+
"""Return True iff ``(tbox, abox)`` entails ``individual : concept`` — the
|
|
733
|
+
same "does asserting the complement make it inconsistent" reduction
|
|
734
|
+
:func:`unicode_logic_kit.dl.tableau.instance_check` uses. Open-world, exactly
|
|
735
|
+
like that function: False means NOT entailed, not "entailed false".
|
|
736
|
+
"""
|
|
737
|
+
return not external_abox_consistent(_abox_with(abox, individual, Not(concept)), tbox)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _all_individuals(abox: ABox) -> Set[str]:
|
|
741
|
+
"""Every individual name mentioned in ``abox`` (ANY assertion list) — the
|
|
742
|
+
SAME scan the tableau's sweeps and ``kb_to_fol(...).individuals`` read
|
|
743
|
+
(:func:`~unicode_logic_kit.dl.tableau._abox_individual_names`, driven by the
|
|
744
|
+
axiom-kind table), and NO anonymous fallback individual: an ABox that names
|
|
745
|
+
nobody has no one to retrieve or realize. (This used to fall back to an
|
|
746
|
+
invented ``"a"`` and report it as a member — ``TBox().add(Top(), A)`` over
|
|
747
|
+
an empty ABox "retrieved" ``{"a"}`` — and its own hand-written scan missed
|
|
748
|
+
the data assertions.)
|
|
749
|
+
"""
|
|
750
|
+
return set(_abox_individual_names(abox))
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def external_instance_retrieval(abox: ABox, concept: Concept,
|
|
754
|
+
tbox: Optional[TBox] = None) -> Set[str]:
|
|
755
|
+
"""Return every individual of ``abox`` that ``(tbox, abox)`` entails is a
|
|
756
|
+
``concept`` — sweeps :func:`external_instance_check` exactly like
|
|
757
|
+
:func:`unicode_logic_kit.dl.tableau.instance_retrieval` does.
|
|
758
|
+
|
|
759
|
+
The shared refusals run FIRST (:func:`_guard_inputs`): an ABox that names
|
|
760
|
+
nobody makes the sweep below call :func:`external_instance_check` zero
|
|
761
|
+
times, and a knowledge base the other entry points refuse got ``set()``.
|
|
762
|
+
"""
|
|
763
|
+
_guard_inputs(tbox, abox, [concept])
|
|
764
|
+
return {ind for ind in sorted(_all_individuals(abox))
|
|
765
|
+
if external_instance_check(abox, ind, concept, tbox)}
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def external_realize(abox: ABox, individual: str, vocabulary: List[Concept],
|
|
769
|
+
tbox: Optional[TBox] = None) -> List[Concept]:
|
|
770
|
+
"""Return ``individual``'s most-specific concepts from ``vocabulary`` — the
|
|
771
|
+
same filter-then-drop-non-minimal reduction
|
|
772
|
+
:func:`unicode_logic_kit.dl.tableau.realize` uses. The shared refusals run
|
|
773
|
+
first (:func:`_guard_inputs`), since an empty ``vocabulary`` reaches no
|
|
774
|
+
HermiT call.
|
|
775
|
+
"""
|
|
776
|
+
_guard_inputs(tbox, abox, vocabulary)
|
|
777
|
+
candidates = [c for c in vocabulary if external_instance_check(abox, individual, c, tbox)]
|
|
778
|
+
return [c for c in candidates
|
|
779
|
+
if not any(external_subsumes(d, c, tbox) and not external_subsumes(c, d, tbox)
|
|
780
|
+
for d in candidates)]
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def external_realize_all(abox: ABox, vocabulary: List[Concept],
|
|
784
|
+
tbox: Optional[TBox] = None) -> Dict[str, List[Concept]]:
|
|
785
|
+
"""Return :func:`external_realize` for every individual named in ``abox``
|
|
786
|
+
(``{}`` for an ABox that names none; the shared refusals run first, see
|
|
787
|
+
:func:`_guard_inputs`)."""
|
|
788
|
+
_guard_inputs(tbox, abox, vocabulary)
|
|
789
|
+
return {ind: external_realize(abox, ind, vocabulary, tbox)
|
|
790
|
+
for ind in sorted(_all_individuals(abox))}
|