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,1055 @@
|
|
|
1
|
+
"""Solver-independent core for the finite-domain (ASP / CP) refutation backends.
|
|
2
|
+
|
|
3
|
+
Two backends (``ClingoBackend``, ``MinizincBackend`` — built alongside this
|
|
4
|
+
module, not in it) decide FOL questions by searching for a finite
|
|
5
|
+
countermodel of ``premises ∧ ¬φ``: they GROUND that goal into ASP or CP and
|
|
6
|
+
let clingo / MiniZinc search. Everything the two backends would otherwise
|
|
7
|
+
duplicate lives here instead, so ``unsupported`` and a re-verified
|
|
8
|
+
countermodel read identically whichever solver was asked:
|
|
9
|
+
|
|
10
|
+
* :func:`lower_msfol` — the many-sorted-to-classical FRONT DOOR both
|
|
11
|
+
backends call before :func:`fragment_check` ever runs: many-sorted input
|
|
12
|
+
(``SortedQuantifier``/``SortedConstant``/``SortedCount``/
|
|
13
|
+
``SortedCardinality``) is relativised to plain classical FOL with the
|
|
14
|
+
kit's own :func:`~unicode_logic_kit.fol.nodes.to_fol` — exactly the pattern
|
|
15
|
+
:mod:`~unicode_logic_kit.atp.z3_arith` already uses ahead of its own
|
|
16
|
+
(unrelated) arithmetic translation — so every OTHER piece of this module
|
|
17
|
+
sees ordinary unary predicates where a sort guard used to be, and needs no
|
|
18
|
+
per-sort concept of its own. See "Many-sorted input" below.
|
|
19
|
+
* :class:`FiniteDomainProblem` — the one problem shape both backends encode:
|
|
20
|
+
a batch of sentences to satisfy simultaneously, a domain size, a
|
|
21
|
+
:class:`~unicode_logic_kit.fol.signature.Signature`, and the
|
|
22
|
+
distinct-constants convention.
|
|
23
|
+
* :func:`fragment_check` — the single gate: is a sentence even IN the
|
|
24
|
+
fragment these backends can ground? Unsorted classical FOL plus the
|
|
25
|
+
counting fragment (``Count``, ``Cardinality``) AND function symbols
|
|
26
|
+
(``Function``) is encodable; everything without a reading over one
|
|
27
|
+
finite, bivalent, first-order structure is refused BY NAME (see "Fragment
|
|
28
|
+
boundary" below). A ``Function`` with a SORTED declaration (a
|
|
29
|
+
:class:`~unicode_logic_kit.fol.signature.FunctionDecl` carrying
|
|
30
|
+
``arg_sorts``/``result_sort``) is refused separately, by
|
|
31
|
+
:class:`FiniteDomainProblem` itself — see "Sorted function symbols" below.
|
|
32
|
+
* :func:`structure_from_solution` — turn a solver's true ground atoms back
|
|
33
|
+
into a :class:`~unicode_logic_kit.semantics.structures.FiniteStructure`,
|
|
34
|
+
including the shared "function = total relation + functionality
|
|
35
|
+
constraint" reconstruction so neither backend has to invent it twice.
|
|
36
|
+
* :func:`verify_model` — the safety net: re-check a reconstructed structure
|
|
37
|
+
against the ORIGINAL sentences with the kit's own independent evaluator
|
|
38
|
+
before a backend is allowed to call anything REFUTED. See "Known
|
|
39
|
+
verification gap" below — a narrow residual collision between this
|
|
40
|
+
module's own scope and an existing module it does not own, down to ONE
|
|
41
|
+
node type (``Contrast``) now that the counting fragment AND function
|
|
42
|
+
symbols are both independently verifiable.
|
|
43
|
+
|
|
44
|
+
This module never grounds anything itself. clingo and MiniZinc both ground
|
|
45
|
+
better than a hand-rolled loop here would (see the design note), so the job
|
|
46
|
+
here stops at emitting a solver-agnostic PROBLEM and re-checking a
|
|
47
|
+
solver-agnostic SOLUTION; the ASP/CP text itself is each backend's own.
|
|
48
|
+
|
|
49
|
+
THE ONE RULE THIS MODULE EXISTS TO PROTECT
|
|
50
|
+
-------------------------------------------
|
|
51
|
+
Both backends are refutation-only: a finite model of ``premises ∧ ¬φ``
|
|
52
|
+
proves ``φ`` REFUTED, but the absence of one up to a size bound proves
|
|
53
|
+
NOTHING (first-order logic has no finite model property). Neither backend
|
|
54
|
+
may ever report ``PROVED`` from this search. :func:`verify_model` is the
|
|
55
|
+
second half of that discipline: a model-finder that hands back a structure
|
|
56
|
+
that does not actually satisfy the goal is worse than one that finds
|
|
57
|
+
nothing, so every witness is re-checked here before a backend is allowed to
|
|
58
|
+
call it REFUTED.
|
|
59
|
+
|
|
60
|
+
Fragment boundary (what :func:`fragment_check` accepts and refuses)
|
|
61
|
+
---------------------------------------------------------------------
|
|
62
|
+
IN: ``Atom``, ``Not``, ``And``, ``Or``, ``Xor``, ``Implies``, ``Iff``,
|
|
63
|
+
``Contrast`` (truth-functionally ``And`` — see its own docstring),
|
|
64
|
+
``Quantifier`` (unsorted ``∀``/``∃`` only), ``Count`` (``∃≥n``/``∃≤n``/
|
|
65
|
+
``∃=n``), ``Cardinality`` (``|S|`` as a term), and the term vocabulary
|
|
66
|
+
``Variable``/``Constant``/``Number``/``Function``. This is "unsorted
|
|
67
|
+
classical FOL plus the counting fragment plus function symbols" — the
|
|
68
|
+
scope the design this module implements calls for, closed in two steps: the
|
|
69
|
+
counting fragment first, ``Function`` here (see "Known verification gap"
|
|
70
|
+
below for how each became independently verifiable, not merely
|
|
71
|
+
groundable). ``Function`` is admitted by NODE TYPE only, and two narrower refusals sit
|
|
72
|
+
downstream of this gate rather than in it: a *sorted* function DECLARATION
|
|
73
|
+
is refused at :class:`FiniteDomainProblem` itself (see "Sorted function
|
|
74
|
+
symbols" below), because ``fragment_check`` walks ``sentences`` and has no
|
|
75
|
+
``signature`` to consult; and the four arithmetic operator NAMES (``+``,
|
|
76
|
+
``-``, ``*``, ``/``) stay refused too, but by
|
|
77
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` never
|
|
78
|
+
declaring them as user functions in the first place (the ``_BUILTIN_FUNCS``
|
|
79
|
+
carve-out) — a ``Function`` node using one of these names passes THIS gate
|
|
80
|
+
(it is a ``Function`` node like any other) and is refused downstream instead,
|
|
81
|
+
by each encoder's own loud "symbol not declared in signature" error.
|
|
82
|
+
|
|
83
|
+
OUT, each refused by name with the reason that node type has no reading
|
|
84
|
+
over a SINGLE finite bivalent structure:
|
|
85
|
+
|
|
86
|
+
* ``Measure`` — a degree on an uninterpreted ORDERED codomain; nothing for
|
|
87
|
+
``#count``/``sum`` to range over.
|
|
88
|
+
* the sorted family (``SortedQuantifier``, ``SortedConstant``,
|
|
89
|
+
``SortedCount``, ``SortedCardinality``) — refused HERE because this gate
|
|
90
|
+
has no per-sort concept and :class:`FiniteDomainProblem` deliberately
|
|
91
|
+
stays sort-blind. Neither backend actually hands this gate a sorted node,
|
|
92
|
+
though: :func:`lower_msfol` relativises many-sorted input to plain
|
|
93
|
+
classical FOL FIRST (see "Many-sorted input" below), so this refusal only
|
|
94
|
+
fires for a caller that builds a :class:`FiniteDomainProblem` directly,
|
|
95
|
+
bypassing that front door — defense in depth, not the live path.
|
|
96
|
+
* the modal/temporal/epistemic/hybrid family (``Box``, ``Diamond``,
|
|
97
|
+
``Knows``, ``Believes``, ``Says``, ``Wants``, ``Always``, ``Eventually``,
|
|
98
|
+
``Next``, ``Until``, ``Historically``, ``Once``, ``Previous``, ``Since``,
|
|
99
|
+
``Obligatory``, ``Permitted``, ``Would``, ``Might``, ``Announce``,
|
|
100
|
+
``AnnounceDiamond``, ``Nominal``, ``At``) — these quantify over POSSIBLE
|
|
101
|
+
WORLDS, not domain individuals; the modal family already has its own
|
|
102
|
+
finite-model backend (``kripke-enum``).
|
|
103
|
+
* ``SecondOrderQuantifier`` — ranges over relations, not individuals.
|
|
104
|
+
* the substructural family (linear logic's ``Tensor``, ``With``, ``OPlus``,
|
|
105
|
+
``LinearImplies``, ``OfCourse``, ``One``, ``Top``, ``Zero``; Lambek's
|
|
106
|
+
``Product``, ``Under``, ``Over``) — these track RESOURCE USE, not truth in
|
|
107
|
+
one structure.
|
|
108
|
+
* the lambda family (``LambdaVar``, ``Lambda``, ``Application``) — functions
|
|
109
|
+
FROM formulas TO formulas; higher-order, not first-order individuals.
|
|
110
|
+
* the team-semantic family (``Dependence``, ``SlashedExists``) — evaluated
|
|
111
|
+
against a TEAM (a set of assignments), not a single structure; not named
|
|
112
|
+
in the originating design's exclusion table but excluded for the
|
|
113
|
+
identical reason, so it gets its own family here rather than being
|
|
114
|
+
silently swept into "modal" or left unclassified.
|
|
115
|
+
* the Łukasiewicz fuzzy family (``WeakConjunction``, ``WeakDisjunction``,
|
|
116
|
+
``StrongConjunction``, ``StrongDisjunction``, ``LukNegation``,
|
|
117
|
+
``LukImplication``, ``LukEquivalence``) — many-valued; a finite
|
|
118
|
+
countermodel search assumes classical, two-valued truth. Also not named
|
|
119
|
+
in the design's own table, excluded for the same reason as team
|
|
120
|
+
semantics.
|
|
121
|
+
|
|
122
|
+
A node type this module has never heard of (a future addition to the kit's
|
|
123
|
+
AST) is refused too, with a generic reason, rather than silently accepted —
|
|
124
|
+
default-deny, matching :mod:`~unicode_logic_kit.fol.signature`'s own
|
|
125
|
+
loud-refusal convention.
|
|
126
|
+
|
|
127
|
+
Sorted function symbols — admitted by node type, refused by DECLARATION
|
|
128
|
+
-------------------------------------------------------------------------
|
|
129
|
+
``Function`` is IN the fragment above, but only in its plain, UNSORTED
|
|
130
|
+
reading — a :class:`~unicode_logic_kit.fol.signature.FunctionDecl` whose
|
|
131
|
+
``arg_sorts`` or ``result_sort`` is not ``None`` is refused, by
|
|
132
|
+
``ValueError``, in :meth:`FiniteDomainProblem.__post_init__` itself. This is
|
|
133
|
+
an explicit decision, not an oversight: the two backends' function encoders
|
|
134
|
+
(``_AspEncoder._function_rule``'s total-relation choice rule;
|
|
135
|
+
:class:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend`'s
|
|
136
|
+
``array[DOM, …] of var DOM`` declaration) both ground a function's arguments
|
|
137
|
+
and result over the WHOLE domain regardless of any declared sort — neither
|
|
138
|
+
consults ``arg_sorts``/``result_sort`` at all — so admitting a sorted
|
|
139
|
+
:class:`~unicode_logic_kit.fol.signature.FunctionDecl` without refusing it
|
|
140
|
+
would silently IGNORE the caller's own stated constraint rather than enforce
|
|
141
|
+
or verify it: exactly the kind of silent semantic substitution this kit
|
|
142
|
+
refuses elsewhere (see e.g. the ``all_different`` footnote on
|
|
143
|
+
:class:`FiniteDomainProblem`). Even setting the encoders aside,
|
|
144
|
+
:class:`~unicode_logic_kit.semantics.structures.FiniteStructure` — what
|
|
145
|
+
:func:`verify_model` checks a countermodel back against — has NO ``sorts``
|
|
146
|
+
concept at all (see
|
|
147
|
+
:mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring, which weighs
|
|
148
|
+
and declines exactly this for ``SortedCount``), so a sort-respecting
|
|
149
|
+
function graph could not even be VERIFIED if the encoders somehow ground it
|
|
150
|
+
correctly. Refusing loudly, by name, at construction time is therefore the
|
|
151
|
+
same choice this module already made for the sorted QUANTIFIER family (see
|
|
152
|
+
the bullet above) — narrowed here to the one sorted DECLARATION shape that
|
|
153
|
+
family's own four node types do not cover, since there is no
|
|
154
|
+
``SortedFunction`` AST node.
|
|
155
|
+
|
|
156
|
+
This refusal can only fire for a caller that hand-builds ``signature`` and
|
|
157
|
+
passes it to :class:`FiniteDomainProblem` directly:
|
|
158
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` — what both
|
|
159
|
+
:meth:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend.decide` and
|
|
160
|
+
:meth:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend.decide` always
|
|
161
|
+
use to build their OWN signature — never sets ``arg_sorts``/``result_sort``
|
|
162
|
+
for a function (it infers only arity; see that method's own "scope" section),
|
|
163
|
+
and a many-sorted ``SortedQuantifier``/``SortedConstant`` has already been
|
|
164
|
+
relativised away by :func:`lower_msfol` before ``Signature.from_formulas``
|
|
165
|
+
ever runs (see "Many-sorted input" below) — so it is defense in depth, not a
|
|
166
|
+
live path through either backend today, exactly like the sorted-quantifier
|
|
167
|
+
refusal above.
|
|
168
|
+
|
|
169
|
+
Many-sorted input (:func:`lower_msfol`)
|
|
170
|
+
-----------------------------------------
|
|
171
|
+
Many-sorted FOL (MSFOL) — ``SortedQuantifier`` (``∀x:S φ`` / ``∃x:S φ``),
|
|
172
|
+
``SortedConstant`` (``alice:Human``), ``SortedCount`` (``∃≥n x:S φ``), and
|
|
173
|
+
``SortedCardinality`` (``|{x:S : φ}|``) — never reaches :func:`fragment_check`
|
|
174
|
+
directly when a caller goes through :func:`lower_msfol` first, which both
|
|
175
|
+
:class:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend` and
|
|
176
|
+
:class:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend` do, right
|
|
177
|
+
after building their refutation-goal sentences and before either
|
|
178
|
+
:func:`fragment_check` or ``Signature.from_formulas`` sees them. It is
|
|
179
|
+
RELATIVISED to plain classical FOL with the kit's existing
|
|
180
|
+
:func:`~unicode_logic_kit.fol.nodes.to_fol`: ``∀x:S φ`` becomes
|
|
181
|
+
``∀x (S(x) → φ)``, ``∃x:S φ`` becomes ``∃x (S(x) ∧ φ)``, ``alice:Human``
|
|
182
|
+
becomes the plain constant ``alice`` plus the fact ``Human(alice)``, and
|
|
183
|
+
``SortedCount``/``SortedCardinality`` guard their matrix with the sort atom
|
|
184
|
+
and fall back to the already-encodable ``Count``/``Cardinality``. Once
|
|
185
|
+
lowered, a sort name is nothing more than an ordinary unary predicate:
|
|
186
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas`,
|
|
187
|
+
:func:`fragment_check`, both backends' own encoders, and :func:`verify_model`
|
|
188
|
+
all need NO sort-specific handling at all.
|
|
189
|
+
|
|
190
|
+
One soundness subtlety ``to_fol``'s relativisation alone does not cover:
|
|
191
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_model` /
|
|
192
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_countermodel` — the
|
|
193
|
+
independent oracle this lowering is checked against — enumerate ONLY
|
|
194
|
+
NON-EMPTY subsets of the domain as a sort's universe
|
|
195
|
+
(``modelfinder._nonempty_subsets``), because a many-sorted-logic sort is, by
|
|
196
|
+
convention, never empty. A bare relativisation carries no such guarantee:
|
|
197
|
+
``∀x:S φ`` becomes vacuously TRUE the moment a countermodel search makes
|
|
198
|
+
``S`` empty, which would let this module's ASP/CP route accept a
|
|
199
|
+
"countermodel" :mod:`~unicode_logic_kit.semantics.modelfinder` would never
|
|
200
|
+
even consider a legal structure — the two routes would silently disagree.
|
|
201
|
+
:func:`lower_msfol` closes this by asserting one EXTRA sentence per distinct
|
|
202
|
+
sort name referenced anywhere in the input, ``∃x (S(x))``, alongside the
|
|
203
|
+
relativised originals.
|
|
204
|
+
|
|
205
|
+
:func:`lower_msfol` is a no-op — returns its input, ``tuple``-coerced,
|
|
206
|
+
completely untouched, never even passed through ``to_fol`` — when NO
|
|
207
|
+
sentence contains any of the four sorted node types (one
|
|
208
|
+
:meth:`~unicode_logic_kit.fol.nodes.Node.walk` pass over the batch to confirm
|
|
209
|
+
this). So the entire pre-existing unsorted-only test suite, in this module
|
|
210
|
+
and both backends', sees byte-identical sentences and pays zero overhead;
|
|
211
|
+
this many-sorted path is purely additive. This no-op check is PER SENTENCE,
|
|
212
|
+
not merely per batch: even once one sentence in a batch IS sorted, every
|
|
213
|
+
OTHER sentence that is not — an unrelated ``Count``/``Contrast`` premise
|
|
214
|
+
sharing a ``decide()`` call with a sorted one, say — is left completely
|
|
215
|
+
untouched too, rather than being routed through ``to_fol`` as collateral
|
|
216
|
+
damage. This matters because ``to_fol``'s own ``_reduce_nl_nodes`` phase
|
|
217
|
+
unconditionally expands ``Count`` via its bounded (``n<=500``) O(n²)
|
|
218
|
+
distinct-witnesses encoding; only a sentence that is ITSELF sorted may pay
|
|
219
|
+
that cost, and every other sentence keeps reaching its backend's own native
|
|
220
|
+
(unbounded, cheaper) counting encoding exactly as before.
|
|
221
|
+
|
|
222
|
+
Known verification gap (an objection, not a silent deviation)
|
|
223
|
+
-----------------------------------------------------------------
|
|
224
|
+
:func:`verify_model` is specified to re-check a reconstructed structure by
|
|
225
|
+
running the kit's OWN independent evaluator,
|
|
226
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate` (re-exported as
|
|
227
|
+
``evaluate_in_structure``), over every sentence — and that is exactly what
|
|
228
|
+
it does here. Three of the four node types that used to defeat that
|
|
229
|
+
evaluator no longer do — this is the SECOND time this exact narrative
|
|
230
|
+
pattern needed updating, and it follows the identical playbook the first
|
|
231
|
+
time (the counting fragment) did:
|
|
232
|
+
|
|
233
|
+
``Cardinality`` and ``Number`` (as a comparison operand) are read
|
|
234
|
+
ARITHMETICALLY by ``evaluate_in_structure`` itself: its comparison branch
|
|
235
|
+
(``_atom_value``, via ``_numeric_value``) counts ``|{v : φ}|`` and compares
|
|
236
|
+
it against another count or a bare numeral whenever the comparison has at
|
|
237
|
+
least one numeric operand (see that module's own docstring, "Two kinds of
|
|
238
|
+
term value, kept apart"). The counting fragment this whole design exists to
|
|
239
|
+
decide (see the design's §1) is therefore DECIDED AND VERIFIED end to
|
|
240
|
+
end: ``|{x : P(x)}| > |{y : Q(y)}|`` can be correctly REFUTED by
|
|
241
|
+
clingo/MiniZinc, reconstructed by :func:`structure_from_solution`, AND
|
|
242
|
+
independently confirmed here.
|
|
243
|
+
|
|
244
|
+
``Function`` is now read the SAME way ``structure_from_solution`` already
|
|
245
|
+
built it: ``evaluate_in_structure``'s term evaluator (``_term_value``, via
|
|
246
|
+
its ``Function`` case) resolves ``f(t1,...,tk)`` off the ``(name, arity+1)``
|
|
247
|
+
total-relation extension the reconstruction produces — the unique row whose
|
|
248
|
+
leading ``k`` components match the (recursively evaluated) arguments — and
|
|
249
|
+
is refused loudly, never silently mismatched, if that row is missing or not
|
|
250
|
+
unique (see :mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring for
|
|
251
|
+
the full account). A function-bearing countermodel is therefore also
|
|
252
|
+
DECIDED AND VERIFIED end to end now: ``∃x (op(x,x) ≠ x)`` (idempotence
|
|
253
|
+
refuted) can be correctly REFUTED by clingo/MiniZinc, reconstructed, AND
|
|
254
|
+
independently confirmed here — the identical closure the counting fragment
|
|
255
|
+
already got, now for functions.
|
|
256
|
+
|
|
257
|
+
That leaves ONE node type this evaluator still cannot touch:
|
|
258
|
+
|
|
259
|
+
* ``Contrast`` IS in the encodable fragment (:func:`fragment_check` admits
|
|
260
|
+
it — truth-functionally ``And``, see its own docstring) but is genuinely
|
|
261
|
+
NOT in ``evaluate_in_structure``'s supported-node list (that module's own
|
|
262
|
+
"Supported nodes" section stops at ``Atom``/``Not``/``And``/``Or``/
|
|
263
|
+
``Xor``/``Implies``/``Iff``/``Quantifier``/``Count``) — it raises
|
|
264
|
+
``UnsupportedNode`` there like any node type the evaluator has never
|
|
265
|
+
heard of. A sentence built with ``Contrast`` can therefore still be
|
|
266
|
+
correctly REFUTED by a backend and correctly reconstructed, and still
|
|
267
|
+
fail :func:`verify_model` — not because the countermodel is wrong, but
|
|
268
|
+
because the independent checker this module is told to call cannot
|
|
269
|
+
evaluate ``Contrast`` AT ALL. Per this module's OWN rule above (never
|
|
270
|
+
hand back an unverified countermodel), that failure is treated as "could
|
|
271
|
+
not verify" and a calling backend must downgrade to ``ERROR``/``"infra"``
|
|
272
|
+
rather than ever return ``REFUTED`` for such a sentence — SOUND, but,
|
|
273
|
+
unlike the counting fragment and ``Function`` above, this one IS still
|
|
274
|
+
live: closing it needs
|
|
275
|
+
either :mod:`~unicode_logic_kit.semantics.model_eval` to grow a
|
|
276
|
+
``Contrast`` case (out of this file's assignment — that module is owned
|
|
277
|
+
elsewhere) or :func:`verify_model` to be redesigned with its own
|
|
278
|
+
evaluator for this one connective (a decision this module deliberately
|
|
279
|
+
does not make unilaterally; see the instructions this file was written
|
|
280
|
+
under).
|
|
281
|
+
|
|
282
|
+
Separately, and NOT a gap: comparing a domain INDIVIDUAL with a numeral
|
|
283
|
+
remains refused, deliberately. ``∀x (x = 1)`` reaches the same comparison
|
|
284
|
+
branch as a genuine counting comparison, but ``_numeric_value`` is only
|
|
285
|
+
ever handed the operand syntactically marked numeric (``Cardinality``/
|
|
286
|
+
``Number``); called on the OTHER, individual-denoting operand it raises
|
|
287
|
+
``UnsupportedNode`` ("does not denote a number, so it cannot be compared
|
|
288
|
+
with one"). Admitting numeric terms into comparisons between two numeric
|
|
289
|
+
operands is the whole of what the counting fragment calls for; admitting
|
|
290
|
+
them into a comparison against a domain individual was never in scope, and
|
|
291
|
+
staying refused there is correct, not incomplete — a structure whose
|
|
292
|
+
individuals happen to be named ``"0"``/``"1"``/… must not tempt anyone into
|
|
293
|
+
reading those names as integers.
|
|
294
|
+
|
|
295
|
+
This is reported here, in the code, rather than silently worked around, and
|
|
296
|
+
again in :func:`verify_model`'s own docstring.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
import itertools
|
|
300
|
+
from dataclasses import dataclass
|
|
301
|
+
from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Type
|
|
302
|
+
|
|
303
|
+
from ..fol.nodes import (
|
|
304
|
+
Node,
|
|
305
|
+
Variable, Constant, Number, Function,
|
|
306
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
307
|
+
Count, Cardinality, Contrast, Measure,
|
|
308
|
+
SortedQuantifier, SortedConstant, SortedCount, SortedCardinality,
|
|
309
|
+
to_fol,
|
|
310
|
+
Box, Diamond, Knows, Believes, Says, Wants,
|
|
311
|
+
Always, Eventually, Next, Until,
|
|
312
|
+
Historically, Once, Previous, Since,
|
|
313
|
+
Obligatory, Permitted, Would, Might,
|
|
314
|
+
Announce, AnnounceDiamond,
|
|
315
|
+
SecondOrderQuantifier,
|
|
316
|
+
Nominal, At,
|
|
317
|
+
Dependence, SlashedExists,
|
|
318
|
+
Tensor, With, OPlus, LinearImplies, OfCourse, One, Top, Zero,
|
|
319
|
+
Product, Under, Over,
|
|
320
|
+
WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
|
|
321
|
+
LukNegation, LukImplication, LukEquivalence,
|
|
322
|
+
LambdaVar, Lambda, Application,
|
|
323
|
+
)
|
|
324
|
+
from ..fol._msfl_nodes import nonempty_sort_axioms
|
|
325
|
+
from ..fol._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
|
|
326
|
+
from ..fol.signature import Signature
|
|
327
|
+
from ..semantics.structures import FiniteStructure
|
|
328
|
+
from ..semantics.model_eval import (
|
|
329
|
+
evaluate as _evaluate_in_structure,
|
|
330
|
+
UninterpretedSymbol, UnsupportedNode,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
__all__ = [
|
|
334
|
+
"lower_msfol",
|
|
335
|
+
"FiniteDomainProblem", "fragment_check", "free_variable_reason", "structure_from_solution",
|
|
336
|
+
"verify_model",
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# =============================================================================
|
|
341
|
+
# lower_msfol — many-sorted FOL to classical FOL, ahead of fragment_check
|
|
342
|
+
# =============================================================================
|
|
343
|
+
|
|
344
|
+
# The four many-sorted node types lower_msfol looks for; every one of them
|
|
345
|
+
# carries a `.sort: str` field, which is all the sort-name collection below
|
|
346
|
+
# needs.
|
|
347
|
+
_SORTED_NODE_TYPES = (SortedQuantifier, SortedConstant, SortedCount, SortedCardinality)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def lower_msfol(sentences: Sequence[Node]) -> Tuple[Node, ...]:
|
|
351
|
+
"""Relativise many-sorted sentences to classical FOL, or pass them through.
|
|
352
|
+
|
|
353
|
+
Both :meth:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend.decide` and
|
|
354
|
+
:meth:`~unicode_logic_kit.atp.minizinc_backend.MinizincBackend.decide` call
|
|
355
|
+
this on their refutation-goal ``sentences`` BEFORE :func:`fragment_check`
|
|
356
|
+
(or, for the MiniZinc backend, ``Signature.from_formulas``) ever sees
|
|
357
|
+
them — see the module docstring's "Many-sorted input" section for the
|
|
358
|
+
full design: why :func:`~unicode_logic_kit.fol.nodes.to_fol` alone is not
|
|
359
|
+
enough for soundness, and why the fast path below guarantees zero
|
|
360
|
+
behaviour change for every unsorted-only caller (i.e. the entire
|
|
361
|
+
pre-existing test suite).
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
sentences: the sentences to lower — typically a backend's own
|
|
365
|
+
``premises + (goal,)``, already universally closed.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
``sentences``, coerced to a ``tuple`` and otherwise BYTE-IDENTICAL
|
|
369
|
+
(never even passed through ``to_fol``), when none of them contains a
|
|
370
|
+
``SortedQuantifier``/``SortedConstant``/``SortedCount``/
|
|
371
|
+
``SortedCardinality`` node anywhere. Otherwise: each sentence that
|
|
372
|
+
itself contains a sorted node is run through
|
|
373
|
+
``to_fol(s, include_sort_facts=True)`` (so a sorted constant's own
|
|
374
|
+
sort membership is asserted, not merely used as a guard elsewhere);
|
|
375
|
+
every OTHER sentence in the same batch — one with no sorted node of
|
|
376
|
+
its own, e.g. an unrelated ``Count``/``Contrast`` premise — is left
|
|
377
|
+
completely untouched, so it keeps reaching the calling backend's own
|
|
378
|
+
native encoding for that construct instead of being routed through
|
|
379
|
+
``to_fol``'s unrelated ``Count``-expansion phase (which is both
|
|
380
|
+
bounded at ``n<=500`` and, well under that bound, asymptotically
|
|
381
|
+
worse than a backend's native counting encoding). PLUS one extra
|
|
382
|
+
sentence ``∃x (S(x))`` per distinct sort name referenced anywhere in
|
|
383
|
+
the input — in first-occurrence order — asserting that ``S``'s
|
|
384
|
+
universe is non-empty, the soundness guarantee ``to_fol``'s
|
|
385
|
+
relativisation alone does not supply (see the module docstring).
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
TypeError: a member of ``sentences`` is not a
|
|
389
|
+
:class:`~unicode_logic_kit.fol.nodes.Node`.
|
|
390
|
+
"""
|
|
391
|
+
sentences = tuple(sentences)
|
|
392
|
+
for s in sentences:
|
|
393
|
+
if not isinstance(s, Node):
|
|
394
|
+
raise TypeError(
|
|
395
|
+
f"lower_msfol: every sentence must be a Node, got {type(s).__name__}."
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# One pass over every sentence: collect every distinct sort name in
|
|
399
|
+
# first-occurrence order, AND which individual sentences contain a
|
|
400
|
+
# sorted node at all. An empty sort_names IS the fast-path signal (no
|
|
401
|
+
# sorted node anywhere), so this single walk answers "is there anything
|
|
402
|
+
# to do", "which sorts", and "which sentences actually need lowering"
|
|
403
|
+
# all at once -- the last of these matters because to_fol's third phase
|
|
404
|
+
# (_reduce_nl_nodes) unconditionally expands a plain Count/SortedCount
|
|
405
|
+
# node via its O(n^2) distinct-witnesses encoding (bounded at n<=500),
|
|
406
|
+
# so an unrelated large-n Count sentence must never be routed through
|
|
407
|
+
# to_fol merely because SOME OTHER sentence in the same batch is sorted.
|
|
408
|
+
sort_names: List[str] = []
|
|
409
|
+
sentence_is_sorted: List[bool] = []
|
|
410
|
+
for s in sentences:
|
|
411
|
+
is_sorted = False
|
|
412
|
+
for node in s.walk():
|
|
413
|
+
if isinstance(node, _SORTED_NODE_TYPES):
|
|
414
|
+
is_sorted = True
|
|
415
|
+
if node.sort not in sort_names:
|
|
416
|
+
sort_names.append(node.sort)
|
|
417
|
+
sentence_is_sorted.append(is_sorted)
|
|
418
|
+
|
|
419
|
+
if not sort_names:
|
|
420
|
+
# Fast path: nothing sorted anywhere. Returned untouched -- not even
|
|
421
|
+
# round-tripped through to_fol -- so a plain-FOL caller's sentences
|
|
422
|
+
# stay byte-identical (the no-op guarantee the module docstring
|
|
423
|
+
# promises).
|
|
424
|
+
return sentences
|
|
425
|
+
|
|
426
|
+
# Slow path, but still per-sentence: only a sentence that itself
|
|
427
|
+
# contains a sorted node is run through to_fol. A sentence with nothing
|
|
428
|
+
# sorted in it (e.g. an unrelated Count/Contrast sentence sharing this
|
|
429
|
+
# batch with a sorted one) is left completely untouched, so it keeps
|
|
430
|
+
# reaching each backend's own native encoding for that construct
|
|
431
|
+
# (clingo's #count aggregate, MiniZinc's counting encoding) instead of
|
|
432
|
+
# being silently pre-expanded/collapsed by to_fol's unrelated third
|
|
433
|
+
# phase.
|
|
434
|
+
lowered = tuple(
|
|
435
|
+
to_fol(s, include_sort_facts=True) if is_sorted else s
|
|
436
|
+
for s, is_sorted in zip(sentences, sentence_is_sorted)
|
|
437
|
+
)
|
|
438
|
+
# The non-emptiness sentences themselves are built by the shared helper
|
|
439
|
+
# (unicode_logic_kit.fol._msfl_nodes.nonempty_sort_axioms) — every OTHER
|
|
440
|
+
# classical decision route with the same soundness obligation (Z3,
|
|
441
|
+
# cvc5, Prover9, the TPTP fof export, eval.equivalence's solver level)
|
|
442
|
+
# reuses that exact construction too, so this module and every one of
|
|
443
|
+
# them can never drift apart on what "S is non-empty" means as a
|
|
444
|
+
# sentence. This re-walks ``sentences`` once more (a second, cheap pass
|
|
445
|
+
# on top of the one above that decided ``sentence_is_sorted``) rather
|
|
446
|
+
# than threading ``sort_names`` through by hand, so the two collection
|
|
447
|
+
# sites cannot silently diverge either.
|
|
448
|
+
nonempty = nonempty_sort_axioms(*sentences)
|
|
449
|
+
return lowered + nonempty
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# =============================================================================
|
|
453
|
+
# FiniteDomainProblem
|
|
454
|
+
# =============================================================================
|
|
455
|
+
|
|
456
|
+
@dataclass(frozen=True)
|
|
457
|
+
class FiniteDomainProblem:
|
|
458
|
+
"""A finite-domain search problem: sentences to satisfy over a bounded domain.
|
|
459
|
+
|
|
460
|
+
Both backends encode the SAME shape into their own solver language, so a
|
|
461
|
+
problem built once is what ``ClingoBackend`` and ``MinizincBackend`` both
|
|
462
|
+
read — this is what makes the two backends' answers comparable at all.
|
|
463
|
+
|
|
464
|
+
``sentences`` are conjoined (every one must hold simultaneously); for a
|
|
465
|
+
refutation search a caller passes ``premises + (¬φ,)``, but this class
|
|
466
|
+
has no opinion about where its sentences came from — it is the shared
|
|
467
|
+
CONTAINER, not the refutation-goal-building logic (that lives in each
|
|
468
|
+
backend's own ``decide()``).
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
sentences: the formulas to satisfy together. Must be non-empty —
|
|
472
|
+
an empty problem has no goal to search for.
|
|
473
|
+
size: the domain size ``n``; individuals are ``0 … n-1``. Must be
|
|
474
|
+
``>= 1`` — an empty domain satisfies no ``∃`` and is never a
|
|
475
|
+
useful search.
|
|
476
|
+
signature: the declared vocabulary. When ``None`` (the default) it
|
|
477
|
+
is inferred from ``sentences`` via
|
|
478
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas`
|
|
479
|
+
— the canonical way to get one, so most callers never need to
|
|
480
|
+
build it by hand. Inference NEVER produces a sorted
|
|
481
|
+
:class:`~unicode_logic_kit.fol.signature.FunctionDecl` (see
|
|
482
|
+
``Raises`` below and the module docstring's "Sorted function
|
|
483
|
+
symbols" section), so this refusal is inert for every caller
|
|
484
|
+
that leaves ``signature=None`` — it only fires for a caller that
|
|
485
|
+
hand-builds a sorted one and passes it in directly.
|
|
486
|
+
all_different: whether every declared CONSTANT must denote a
|
|
487
|
+
pairwise-distinct individual (the unique-names convention some
|
|
488
|
+
encodings rely on). This is a property of the SOLUTION a
|
|
489
|
+
backend is allowed to return, not of ``sentences`` themselves —
|
|
490
|
+
it is deliberately a separate flag rather than baked into the
|
|
491
|
+
sentences as explicit ``≠`` atoms, and it is UNRELATED to
|
|
492
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate`'s own
|
|
493
|
+
``all_different`` parameter (that one governs whether separately
|
|
494
|
+
quantified EXISTENTIAL VARIABLES must denote distinct
|
|
495
|
+
individuals — a different symbol class, a different convention;
|
|
496
|
+
conflating the two would be exactly the kind of silent semantic
|
|
497
|
+
substitution this kit refuses elsewhere). Defaults to ``False``.
|
|
498
|
+
|
|
499
|
+
Raises:
|
|
500
|
+
TypeError: a sentence is not a
|
|
501
|
+
:class:`~unicode_logic_kit.fol.nodes.Node`, ``signature`` is
|
|
502
|
+
neither ``None`` nor a
|
|
503
|
+
:class:`~unicode_logic_kit.fol.signature.Signature`, or
|
|
504
|
+
``all_different`` is not a ``bool``.
|
|
505
|
+
ValueError: ``sentences`` is empty, ``size < 1``, or (this
|
|
506
|
+
module's explicit "sorted function symbols" decision — see the
|
|
507
|
+
module docstring) the resolved ``signature`` declares a
|
|
508
|
+
:class:`~unicode_logic_kit.fol.signature.FunctionDecl` with a
|
|
509
|
+
non-``None`` ``arg_sorts`` or ``result_sort``.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
sentences: Tuple[Node, ...]
|
|
513
|
+
size: int
|
|
514
|
+
signature: Optional[Signature] = None
|
|
515
|
+
all_different: bool = False
|
|
516
|
+
|
|
517
|
+
def __post_init__(self):
|
|
518
|
+
object.__setattr__(self, "sentences", tuple(self.sentences))
|
|
519
|
+
if not self.sentences:
|
|
520
|
+
raise ValueError(
|
|
521
|
+
"FiniteDomainProblem: sentences must be non-empty — there is "
|
|
522
|
+
"no goal to search for otherwise."
|
|
523
|
+
)
|
|
524
|
+
for s in self.sentences:
|
|
525
|
+
if not isinstance(s, Node):
|
|
526
|
+
raise TypeError(
|
|
527
|
+
f"FiniteDomainProblem: every sentence must be a Node, got "
|
|
528
|
+
f"{type(s).__name__}."
|
|
529
|
+
)
|
|
530
|
+
if isinstance(self.size, bool) or not isinstance(self.size, int) or self.size < 1:
|
|
531
|
+
raise ValueError(
|
|
532
|
+
f"FiniteDomainProblem: size must be an int >= 1, got {self.size!r}."
|
|
533
|
+
)
|
|
534
|
+
if self.signature is None:
|
|
535
|
+
object.__setattr__(self, "signature", Signature.from_formulas(self.sentences))
|
|
536
|
+
elif not isinstance(self.signature, Signature):
|
|
537
|
+
raise TypeError(
|
|
538
|
+
f"FiniteDomainProblem: signature must be a Signature or None, "
|
|
539
|
+
f"got {type(self.signature).__name__}."
|
|
540
|
+
)
|
|
541
|
+
# Sorted function symbols: refused HERE, by name, rather than
|
|
542
|
+
# admitted and silently mishandled — see the module docstring's
|
|
543
|
+
# "Sorted function symbols" section for the full argument. This can
|
|
544
|
+
# only fire for a caller that hand-builds `signature` (inference via
|
|
545
|
+
# Signature.from_formulas never sets arg_sorts/result_sort for a
|
|
546
|
+
# function), so it is defense in depth, not a live path through
|
|
547
|
+
# either backend's own decide().
|
|
548
|
+
sorted_functions = sorted(
|
|
549
|
+
name for name, decl in self.signature.functions.items()
|
|
550
|
+
if decl.arg_sorts is not None or decl.result_sort is not None
|
|
551
|
+
)
|
|
552
|
+
if sorted_functions:
|
|
553
|
+
raise ValueError(
|
|
554
|
+
f"FiniteDomainProblem: sorted function symbol(s) "
|
|
555
|
+
f"{sorted_functions} declare a non-None arg_sorts/"
|
|
556
|
+
"result_sort, but this backend's function encoding "
|
|
557
|
+
"(_AspEncoder._function_rule's total-relation choice rule; "
|
|
558
|
+
"MinizincBackend's `array[DOM, ...] of var DOM` declaration) "
|
|
559
|
+
"treats every function as fully UNSORTED — its arguments and "
|
|
560
|
+
"result range over the WHOLE domain — so a declared sort "
|
|
561
|
+
"constraint would be silently ignored rather than enforced "
|
|
562
|
+
"or verified. FiniteStructure (what verify_model checks a "
|
|
563
|
+
"countermodel back against) has no sorts concept either "
|
|
564
|
+
"(see semantics.model_eval's own docstring on SortedCount), "
|
|
565
|
+
"so a sort-respecting function graph could not even be "
|
|
566
|
+
"checked back if the encoders grounded it. Declare an "
|
|
567
|
+
"unsorted FunctionDecl (arg_sorts=None, result_sort=None) "
|
|
568
|
+
"instead, or express the sort constraint as an explicit "
|
|
569
|
+
"guard atom in the sentences themselves (the reading "
|
|
570
|
+
"lower_msfol already gives a SortedQuantifier)."
|
|
571
|
+
)
|
|
572
|
+
# Subsort edges: the encoders read only the vocabulary, so a declared
|
|
573
|
+
# S < T would be silently ignored and a countermodel could put an S
|
|
574
|
+
# outside T. Refused by name; the edges are plain unsorted sentences
|
|
575
|
+
# over the same guard predicates lower_msfol emits, so the caller can
|
|
576
|
+
# state them as sentences instead.
|
|
577
|
+
if getattr(self.signature, "subsorts", None):
|
|
578
|
+
raise ValueError(
|
|
579
|
+
f"FiniteDomainProblem: the signature declares subsort edges "
|
|
580
|
+
f"{sorted((c, p) for c, ps in self.signature.subsorts.items() for p in ps)}, "
|
|
581
|
+
"which this backend's encoders do not read — they would be "
|
|
582
|
+
"silently ignored. Add fol.subsort_axioms(signature) to the "
|
|
583
|
+
"sentences instead (plain ∀x (S(x) → T(x)) implications over "
|
|
584
|
+
"the same guard predicates lower_msfol relativises to), and "
|
|
585
|
+
"pass a signature without subsorts."
|
|
586
|
+
)
|
|
587
|
+
if not isinstance(self.all_different, bool):
|
|
588
|
+
raise TypeError(
|
|
589
|
+
f"FiniteDomainProblem: all_different must be a bool, got "
|
|
590
|
+
f"{type(self.all_different).__name__}."
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# =============================================================================
|
|
595
|
+
# fragment_check — the single gate
|
|
596
|
+
# =============================================================================
|
|
597
|
+
|
|
598
|
+
# The counting-fragment-plus-unsorted-classical-FOL node types both backends
|
|
599
|
+
# may encode. Deliberately an ALLOW-list, not a deny-list: a future AST
|
|
600
|
+
# addition this module has never seen is refused by default (see
|
|
601
|
+
# _GENERIC_REASON below), matching the kit's loud-refusal convention rather
|
|
602
|
+
# than silently letting an unrecognised node type through.
|
|
603
|
+
_ALLOWED_NODE_TYPES: frozenset = frozenset({
|
|
604
|
+
Variable, Constant, Number, Function,
|
|
605
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Contrast,
|
|
606
|
+
Quantifier, Count, Cardinality,
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _family(reason: str, *classes: Type[Node]) -> Dict[Type[Node], str]:
|
|
611
|
+
"""Map every class in ``classes`` to the SAME rejection ``reason``.
|
|
612
|
+
|
|
613
|
+
A small helper so each family below states its reason exactly once
|
|
614
|
+
rather than repeating the string per node type — a family that grows a
|
|
615
|
+
new node type later needs one new entry in a tuple, not a new copy of
|
|
616
|
+
the prose.
|
|
617
|
+
"""
|
|
618
|
+
return {cls: reason for cls in classes}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
_SORTED_REASON = (
|
|
622
|
+
"the sorted/many-sorted family needs per-sort subdomains that "
|
|
623
|
+
"FiniteDomainProblem does not carry — a real extension, not a detail, "
|
|
624
|
+
"and out of scope for this backend"
|
|
625
|
+
)
|
|
626
|
+
_MODAL_REASON = (
|
|
627
|
+
"modal/temporal/epistemic/hybrid operators quantify over POSSIBLE "
|
|
628
|
+
"WORLDS, not domain individuals — no finite-domain reading here; the "
|
|
629
|
+
"modal family already has its own finite-model backend (kripke-enum)"
|
|
630
|
+
)
|
|
631
|
+
_SECOND_ORDER_REASON = (
|
|
632
|
+
"second-order quantification ranges over relations, not individuals — "
|
|
633
|
+
"no finite-DOMAIN (single-structure) reading"
|
|
634
|
+
)
|
|
635
|
+
_SUBSTRUCTURAL_REASON = (
|
|
636
|
+
"linear/Lambek connectives track RESOURCE USE (how many times a "
|
|
637
|
+
"formula is consumed), not truth in one structure — no finite-domain "
|
|
638
|
+
"reading; see the dedicated linear/Lambek provers for this fragment"
|
|
639
|
+
)
|
|
640
|
+
_LAMBDA_REASON = (
|
|
641
|
+
"lambda terms are functions FROM formulas TO formulas — higher-order, "
|
|
642
|
+
"not first-order individuals — no finite-domain reading"
|
|
643
|
+
)
|
|
644
|
+
_TEAM_REASON = (
|
|
645
|
+
"team-semantic connectives are evaluated against a TEAM (a set of "
|
|
646
|
+
"assignments), not a single structure — no finite-domain reading"
|
|
647
|
+
)
|
|
648
|
+
_FUZZY_REASON = (
|
|
649
|
+
"Łukasiewicz connectives are many-valued — a finite-domain "
|
|
650
|
+
"countermodel search assumes classical, two-valued truth"
|
|
651
|
+
)
|
|
652
|
+
_MEASURE_REASON = (
|
|
653
|
+
"Measure denotes a degree on an uninterpreted ORDERED codomain, not a "
|
|
654
|
+
"set to count — nothing for #count/sum to range over"
|
|
655
|
+
)
|
|
656
|
+
_GENERIC_REASON = (
|
|
657
|
+
"not part of the unsorted-classical-FOL-plus-Count/Cardinality "
|
|
658
|
+
"fragment these backends encode"
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
_REJECTED_NODE_REASONS: Dict[Type[Node], str] = {}
|
|
662
|
+
_REJECTED_NODE_REASONS.update(_family(
|
|
663
|
+
_SORTED_REASON,
|
|
664
|
+
SortedQuantifier, SortedConstant, SortedCount, SortedCardinality,
|
|
665
|
+
))
|
|
666
|
+
_REJECTED_NODE_REASONS.update(_family(
|
|
667
|
+
_MODAL_REASON,
|
|
668
|
+
Box, Diamond, Knows, Believes, Says, Wants,
|
|
669
|
+
Always, Eventually, Next, Until,
|
|
670
|
+
Historically, Once, Previous, Since,
|
|
671
|
+
Obligatory, Permitted, Would, Might,
|
|
672
|
+
Announce, AnnounceDiamond, Nominal, At,
|
|
673
|
+
))
|
|
674
|
+
_REJECTED_NODE_REASONS.update(_family(_SECOND_ORDER_REASON, SecondOrderQuantifier))
|
|
675
|
+
_REJECTED_NODE_REASONS.update(_family(
|
|
676
|
+
_SUBSTRUCTURAL_REASON,
|
|
677
|
+
Tensor, With, OPlus, LinearImplies, OfCourse, One, Top, Zero,
|
|
678
|
+
Product, Under, Over,
|
|
679
|
+
))
|
|
680
|
+
_REJECTED_NODE_REASONS.update(_family(_LAMBDA_REASON, LambdaVar, Lambda, Application))
|
|
681
|
+
_REJECTED_NODE_REASONS.update(_family(_TEAM_REASON, Dependence, SlashedExists))
|
|
682
|
+
_REJECTED_NODE_REASONS.update(_family(
|
|
683
|
+
_FUZZY_REASON,
|
|
684
|
+
WeakConjunction, WeakDisjunction, StrongConjunction, StrongDisjunction,
|
|
685
|
+
LukNegation, LukImplication, LukEquivalence,
|
|
686
|
+
))
|
|
687
|
+
_REJECTED_NODE_REASONS.update(_family(_MEASURE_REASON, Measure))
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def free_variable_reason(sentences: Iterable[Node]) -> Optional[str]:
|
|
691
|
+
"""Return why ``sentences`` cannot be WRITTEN as a search problem as they stand, or ``None``.
|
|
692
|
+
|
|
693
|
+
The reason is a free variable. A solver's program has no place for a variable that
|
|
694
|
+
nothing binds, and the readings a writer could pick on its own (every element, one
|
|
695
|
+
sentence at a time; or some element of each sentence) both differ from what a free
|
|
696
|
+
variable means on the kit's routes: a parameter, ONE unknown element that all sentences
|
|
697
|
+
of the problem share, so ``P(x)`` together with ``¬P(x)`` has no model while ``P(x)``
|
|
698
|
+
together with ``¬P(alpha)`` has one. The two backends replace every free variable by
|
|
699
|
+
such a parameter before they write a problem
|
|
700
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`); the writers
|
|
701
|
+
(``to_asp``, ``to_minizinc``) are handed sentences and refuse an open one with this
|
|
702
|
+
reason, so that a program is never written under another reading.
|
|
703
|
+
"""
|
|
704
|
+
from ..fol._free_parameters import free_parameter_names
|
|
705
|
+
|
|
706
|
+
names = free_parameter_names(sentences)
|
|
707
|
+
if not names:
|
|
708
|
+
return None
|
|
709
|
+
return (
|
|
710
|
+
f"a sentence has the free variable{'s' if len(names) > 1 else ''} "
|
|
711
|
+
f"{', '.join(repr(name) for name in names)}. A free variable is a parameter of the "
|
|
712
|
+
"whole problem (one unknown element, the same in every sentence), which a program "
|
|
713
|
+
"writer cannot state for sentences it is handed one by one: replace it by a constant "
|
|
714
|
+
"in all sentences together (fol._free_parameters.parameterize) or bind it with a "
|
|
715
|
+
"quantifier."
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def fragment_check(sentences: Iterable[Node]) -> Optional[str]:
|
|
720
|
+
"""Return why ``sentences`` cannot be finite-domain-encoded, or ``None``.
|
|
721
|
+
|
|
722
|
+
The single gate both :class:`ClingoBackend
|
|
723
|
+
<unicode_logic_kit.atp.clingo_backend.ClingoBackend>` and
|
|
724
|
+
:class:`MinizincBackend
|
|
725
|
+
<unicode_logic_kit.atp.minizinc_backend.MinizincBackend>` consult before
|
|
726
|
+
attempting to ground anything, so an ``UNKNOWN``/``"unsupported"``
|
|
727
|
+
verdict (see :mod:`unicode_logic_kit.atp.protocol`) names the exact same
|
|
728
|
+
offending node type and reason regardless of which solver was asked.
|
|
729
|
+
Walks every sentence with
|
|
730
|
+
:meth:`~unicode_logic_kit.fol.nodes.Node.walk` (pre-order, every
|
|
731
|
+
descendant), so a disallowed node buried under an allowed one — e.g. a
|
|
732
|
+
``Box`` nested inside an otherwise-plain ``And`` — is still caught; see
|
|
733
|
+
the module docstring's "Fragment boundary" section for the exact
|
|
734
|
+
allow/refuse lists and the reason given for each refused family.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
sentences: the formulas to check, e.g. a
|
|
738
|
+
:class:`FiniteDomainProblem`'s ``sentences``.
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
``None`` if every sentence stays inside the encodable fragment;
|
|
742
|
+
otherwise a message of the form ``"<NodeType> is not encodable:
|
|
743
|
+
<reason>"`` naming the FIRST offending node type encountered (in
|
|
744
|
+
sentence order, then pre-order within a sentence).
|
|
745
|
+
|
|
746
|
+
Raises:
|
|
747
|
+
TypeError: a member of ``sentences`` is not a
|
|
748
|
+
:class:`~unicode_logic_kit.fol.nodes.Node`.
|
|
749
|
+
"""
|
|
750
|
+
for sentence in sentences:
|
|
751
|
+
if not isinstance(sentence, Node):
|
|
752
|
+
raise TypeError(
|
|
753
|
+
f"fragment_check: every sentence must be a Node, got "
|
|
754
|
+
f"{type(sentence).__name__}."
|
|
755
|
+
)
|
|
756
|
+
for node in sentence.walk():
|
|
757
|
+
node_type = type(node)
|
|
758
|
+
if node_type is Atom and _is_tptp_boolean_atom(node):
|
|
759
|
+
# TPTP's defined propositions are the truth constants on every
|
|
760
|
+
# route that reads them (to_z3, the model finder, the TPTP
|
|
761
|
+
# writers). The grounding here would treat them as a relation
|
|
762
|
+
# the solver may choose, and report a 'countermodel' of `$true`.
|
|
763
|
+
return (f"Atom is not encodable: {node.predicate} is TPTP's defined "
|
|
764
|
+
f"proposition, a truth constant, and this finite-domain "
|
|
765
|
+
f"encoding has no constant for it (it would become a "
|
|
766
|
+
f"relation the solver may choose). Decide the formula "
|
|
767
|
+
f"with a route that reads it: api.prove(..., "
|
|
768
|
+
f"backends=['z3']) or the finite model finder.")
|
|
769
|
+
if node_type in _ALLOWED_NODE_TYPES:
|
|
770
|
+
continue
|
|
771
|
+
reason = _REJECTED_NODE_REASONS.get(node_type, _GENERIC_REASON)
|
|
772
|
+
return f"{node_type.__name__} is not encodable: {reason}"
|
|
773
|
+
return None
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# =============================================================================
|
|
777
|
+
# structure_from_solution — the shared reconstruction
|
|
778
|
+
# =============================================================================
|
|
779
|
+
|
|
780
|
+
def structure_from_solution(
|
|
781
|
+
signature: Signature,
|
|
782
|
+
atoms: Iterable[Tuple[str, Sequence[int]]],
|
|
783
|
+
size: int,
|
|
784
|
+
*,
|
|
785
|
+
all_different: bool = False,
|
|
786
|
+
) -> FiniteStructure:
|
|
787
|
+
"""Rebuild a :class:`~unicode_logic_kit.semantics.structures.FiniteStructure`
|
|
788
|
+
from a solver's true ground atoms.
|
|
789
|
+
|
|
790
|
+
``atoms`` is the one solver-agnostic shape both backends must translate
|
|
791
|
+
their native output INTO before calling this function: pairs of
|
|
792
|
+
``(symbol_name, args)`` where ``args`` is a tuple of individual
|
|
793
|
+
indices in ``0 … size-1``. A backend's OWN auxiliary/helper atoms (a
|
|
794
|
+
``dom/1`` domain fact, an aggregate's internal bookkeeping atom, …) must
|
|
795
|
+
already be filtered out by the caller — every symbol name reaching this
|
|
796
|
+
function is checked against ``signature`` and an unrecognised one is
|
|
797
|
+
refused (see Raises), so a leaked helper atom surfaces as a loud error
|
|
798
|
+
here rather than silently becoming a phantom predicate.
|
|
799
|
+
|
|
800
|
+
Three namespaces, told apart by which section of ``signature`` the
|
|
801
|
+
symbol name is declared in:
|
|
802
|
+
|
|
803
|
+
* a PREDICATE ``p`` of arity ``k`` — each atom supplies one ``k``-tuple
|
|
804
|
+
of the relation's extension. A predicate with zero true atoms is a
|
|
805
|
+
perfectly valid (empty) extension, not an error — a countermodel is
|
|
806
|
+
free to make a relation entirely false.
|
|
807
|
+
* a FUNCTION ``f`` of arity ``k`` — the standard finite-model-finding
|
|
808
|
+
"total relation" reading: each atom is a ``(k+1)``-tuple, the first
|
|
809
|
+
``k`` entries the arguments and the last the result. This is where
|
|
810
|
+
the "functionality constraint (exactly one value per argument
|
|
811
|
+
tuple)" the design calls for is actually enforced — see Raises — so
|
|
812
|
+
neither backend has to re-derive that check from its own solver
|
|
813
|
+
output.
|
|
814
|
+
* a CONSTANT ``c`` — a 1-tuple naming the single individual it denotes;
|
|
815
|
+
the arity-0 special case of the same "exactly one value" reading.
|
|
816
|
+
|
|
817
|
+
Every declared predicate/function/constant gets an extension entry even
|
|
818
|
+
when no atom mentions it (an all-false relation still needs a ``set()``
|
|
819
|
+
in ``extensions``, or
|
|
820
|
+
:meth:`~unicode_logic_kit.semantics.structures.FiniteStructure.holds`
|
|
821
|
+
would read "no atoms" as "uninterpreted" and raise, rather than as the
|
|
822
|
+
correct answer "false everywhere").
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
signature: the problem's declared vocabulary (typically
|
|
826
|
+
``problem.signature`` for some :class:`FiniteDomainProblem`).
|
|
827
|
+
atoms: the solver's true ground atoms, translated into the shared
|
|
828
|
+
``(name, args)`` shape described above.
|
|
829
|
+
size: the domain size; individuals are named ``"0" … "<size-1>"``.
|
|
830
|
+
all_different: when ``True``, additionally check that every
|
|
831
|
+
declared constant denotes a DISTINCT individual (see
|
|
832
|
+
:class:`FiniteDomainProblem`'s ``all_different``) — a solver
|
|
833
|
+
whose encoding was supposed to enforce this but did not is a
|
|
834
|
+
solver/encoding bug, and this catches it here rather than
|
|
835
|
+
handing back a structure that quietly violates the convention
|
|
836
|
+
it was asked to honour.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
The reconstructed structure: ``domain`` is ``("0", …,
|
|
840
|
+
"<size-1>")``; every predicate AND every function (stored as an
|
|
841
|
+
arity-``(k+1)`` relation) has an extension; every constant is set.
|
|
842
|
+
|
|
843
|
+
Raises:
|
|
844
|
+
TypeError: ``signature`` is not a
|
|
845
|
+
:class:`~unicode_logic_kit.fol.signature.Signature`, or an
|
|
846
|
+
atom's individual index is not a plain ``int``.
|
|
847
|
+
ValueError: ``size < 1``; an atom names an individual index outside
|
|
848
|
+
``0 … size-1``; an atom's arity does not match its symbol's
|
|
849
|
+
declared arity; an atom names a symbol ``signature`` does not
|
|
850
|
+
declare as a predicate, function, or constant; a function or
|
|
851
|
+
constant is given two DIFFERENT results for the same inputs
|
|
852
|
+
(functionality violated); a function is missing a result for
|
|
853
|
+
some input tuple (totality violated); a constant is never
|
|
854
|
+
assigned an individual; or ``all_different=True`` and two
|
|
855
|
+
constants denote the same individual.
|
|
856
|
+
"""
|
|
857
|
+
if not isinstance(signature, Signature):
|
|
858
|
+
raise TypeError(
|
|
859
|
+
f"structure_from_solution: signature must be a Signature, got "
|
|
860
|
+
f"{type(signature).__name__}."
|
|
861
|
+
)
|
|
862
|
+
if isinstance(size, bool) or not isinstance(size, int) or size < 1:
|
|
863
|
+
raise ValueError(f"structure_from_solution: size must be an int >= 1, got {size!r}.")
|
|
864
|
+
|
|
865
|
+
domain_range = range(size)
|
|
866
|
+
pred_rows: Dict[Tuple[str, int], set] = {}
|
|
867
|
+
func_graph: Dict[str, Dict[Tuple[int, ...], int]] = {}
|
|
868
|
+
const_val: Dict[str, int] = {}
|
|
869
|
+
|
|
870
|
+
for name, raw_args in atoms:
|
|
871
|
+
args = tuple(raw_args)
|
|
872
|
+
for a in args:
|
|
873
|
+
if isinstance(a, bool) or not isinstance(a, int):
|
|
874
|
+
raise TypeError(
|
|
875
|
+
f"structure_from_solution: atom {name}{args} has a "
|
|
876
|
+
f"non-int individual index {a!r}."
|
|
877
|
+
)
|
|
878
|
+
bad = [a for a in args if a not in domain_range]
|
|
879
|
+
if bad:
|
|
880
|
+
raise ValueError(
|
|
881
|
+
f"structure_from_solution: atom {name}{args} names "
|
|
882
|
+
f"individual index(es) {bad} outside the domain "
|
|
883
|
+
f"0..{size - 1}."
|
|
884
|
+
)
|
|
885
|
+
if name in signature.predicates:
|
|
886
|
+
decl = signature.predicates[name]
|
|
887
|
+
if len(args) != decl.arity:
|
|
888
|
+
raise ValueError(
|
|
889
|
+
f"structure_from_solution: predicate {name!r} is "
|
|
890
|
+
f"declared arity {decl.arity}, but the solver returned "
|
|
891
|
+
f"a {len(args)}-tuple {args}."
|
|
892
|
+
)
|
|
893
|
+
pred_rows.setdefault((name, decl.arity), set()).add(args)
|
|
894
|
+
elif name in signature.functions:
|
|
895
|
+
decl = signature.functions[name]
|
|
896
|
+
expected = decl.arity + 1 # k inputs + 1 result
|
|
897
|
+
if len(args) != expected:
|
|
898
|
+
raise ValueError(
|
|
899
|
+
f"structure_from_solution: function {name!r} is "
|
|
900
|
+
f"declared arity {decl.arity}, so its total-relation "
|
|
901
|
+
f"encoding needs {expected} args (inputs + result), "
|
|
902
|
+
f"but the solver returned a {len(args)}-tuple {args}."
|
|
903
|
+
)
|
|
904
|
+
inputs, result = args[:-1], args[-1]
|
|
905
|
+
graph = func_graph.setdefault(name, {})
|
|
906
|
+
if inputs in graph and graph[inputs] != result:
|
|
907
|
+
raise ValueError(
|
|
908
|
+
f"structure_from_solution: functionality violated for "
|
|
909
|
+
f"{name}{inputs} — both {graph[inputs]} and {result} "
|
|
910
|
+
f"are claimed as the result (a solver/encoding bug: a "
|
|
911
|
+
f"function must have EXACTLY one value per argument "
|
|
912
|
+
f"tuple)."
|
|
913
|
+
)
|
|
914
|
+
graph[inputs] = result
|
|
915
|
+
elif name in signature.constants:
|
|
916
|
+
if len(args) != 1:
|
|
917
|
+
raise ValueError(
|
|
918
|
+
f"structure_from_solution: constant {name!r} names "
|
|
919
|
+
f"exactly one individual, but the solver returned a "
|
|
920
|
+
f"{len(args)}-tuple {args}."
|
|
921
|
+
)
|
|
922
|
+
(val,) = args
|
|
923
|
+
if name in const_val and const_val[name] != val:
|
|
924
|
+
raise ValueError(
|
|
925
|
+
f"structure_from_solution: functionality violated for "
|
|
926
|
+
f"constant {name!r} — both {const_val[name]} and {val} "
|
|
927
|
+
f"are claimed as its denotation."
|
|
928
|
+
)
|
|
929
|
+
const_val[name] = val
|
|
930
|
+
else:
|
|
931
|
+
raise ValueError(
|
|
932
|
+
f"structure_from_solution: the solver returned an atom for "
|
|
933
|
+
f"{name!r}, which the signature does not declare as a "
|
|
934
|
+
f"predicate, function, or constant."
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
# Totality: every function symbol needs a result for EVERY input tuple,
|
|
938
|
+
# not just the ones an atom happened to mention — functionality (checked
|
|
939
|
+
# above, one value at most) is only half of "total relation"; this is
|
|
940
|
+
# the other half. Bounded by size ** arity, the same finite domain the
|
|
941
|
+
# search already covers, so this is cheap validation of an
|
|
942
|
+
# already-finite solution, not the grounding this module otherwise
|
|
943
|
+
# deliberately avoids (see the module docstring).
|
|
944
|
+
for name, decl in signature.functions.items():
|
|
945
|
+
graph = func_graph.get(name, {})
|
|
946
|
+
for inputs in itertools.product(domain_range, repeat=decl.arity):
|
|
947
|
+
if inputs not in graph:
|
|
948
|
+
raise ValueError(
|
|
949
|
+
f"structure_from_solution: totality violated for "
|
|
950
|
+
f"{name}{inputs} — the total-relation encoding must "
|
|
951
|
+
f"give it SOME result, but the solver returned none "
|
|
952
|
+
f"(a solver/encoding bug)."
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
for name in signature.constants:
|
|
956
|
+
if name not in const_val:
|
|
957
|
+
raise ValueError(
|
|
958
|
+
f"structure_from_solution: constant {name!r} was never "
|
|
959
|
+
f"assigned an individual by the solver."
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
if all_different:
|
|
963
|
+
seen: Dict[int, str] = {}
|
|
964
|
+
for name, val in const_val.items():
|
|
965
|
+
if val in seen:
|
|
966
|
+
raise ValueError(
|
|
967
|
+
f"structure_from_solution: all_different=True requires "
|
|
968
|
+
f"every constant to denote a distinct individual, but "
|
|
969
|
+
f"{name!r} and {seen[val]!r} both denote {val}."
|
|
970
|
+
)
|
|
971
|
+
seen[val] = name
|
|
972
|
+
|
|
973
|
+
domain = tuple(str(i) for i in domain_range)
|
|
974
|
+
extensions: Dict[Tuple[str, int], set] = {
|
|
975
|
+
key: {tuple(str(i) for i in row) for row in rows}
|
|
976
|
+
for key, rows in pred_rows.items()
|
|
977
|
+
}
|
|
978
|
+
# Every declared predicate gets an entry even with zero true atoms — see
|
|
979
|
+
# the "empty extension, not uninterpreted" note in the docstring above.
|
|
980
|
+
for name, decl in signature.predicates.items():
|
|
981
|
+
extensions.setdefault((name, decl.arity), set())
|
|
982
|
+
for name, decl in signature.functions.items():
|
|
983
|
+
graph = func_graph.get(name, {})
|
|
984
|
+
extensions[(name, decl.arity + 1)] = {
|
|
985
|
+
tuple(str(i) for i in (*inputs, result))
|
|
986
|
+
for inputs, result in graph.items()
|
|
987
|
+
}
|
|
988
|
+
constants = {name: str(val) for name, val in const_val.items()}
|
|
989
|
+
|
|
990
|
+
return FiniteStructure(domain=domain, extensions=extensions, constants=constants)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
# =============================================================================
|
|
994
|
+
# verify_model — the safety net
|
|
995
|
+
# =============================================================================
|
|
996
|
+
|
|
997
|
+
def verify_model(structure: FiniteStructure, sentences: Iterable[Node]) -> Optional[str]:
|
|
998
|
+
"""Re-check ``structure`` against ``sentences`` with the kit's OWN evaluator.
|
|
999
|
+
|
|
1000
|
+
The §3 safety net: a backend must call this on every reconstructed
|
|
1001
|
+
structure before reporting ``REFUTED`` and, if it returns anything but
|
|
1002
|
+
``None``, report ``ERROR``/``"infra"`` instead — never hand back a
|
|
1003
|
+
countermodel this function could not confirm. Delegates every sentence,
|
|
1004
|
+
whole, to
|
|
1005
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate` (re-exported as
|
|
1006
|
+
``evaluate_in_structure``) — the kit's independent, hand-checked
|
|
1007
|
+
structural evaluator, so a mistake in a backend's OWN ASP/CP encoder
|
|
1008
|
+
cannot also be the thing that validates its output.
|
|
1009
|
+
|
|
1010
|
+
.. warning::
|
|
1011
|
+
``evaluate_in_structure`` evaluates ``Cardinality`` and ``Number``
|
|
1012
|
+
(as a comparison operand) ARITHMETICALLY, and ``Function`` off the
|
|
1013
|
+
``(name, arity+1)`` total-relation extension
|
|
1014
|
+
:func:`structure_from_solution` builds — see
|
|
1015
|
+
:mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring — so
|
|
1016
|
+
both the counting fragment AND function-bearing sentences are fully
|
|
1017
|
+
re-verified here, not merely decided. ONE gap remains: ``Contrast``
|
|
1018
|
+
genuinely still raises ``UnsupportedNode`` here even though
|
|
1019
|
+
:func:`fragment_check` admits it into the encodable fragment — a
|
|
1020
|
+
live collision, not a moot one. This function then reports "could
|
|
1021
|
+
not verify", and the caller still MUST treat that as a reason not to
|
|
1022
|
+
report ``REFUTED`` (per this module's one rule: never hand back an
|
|
1023
|
+
unverified countermodel). Comparing a domain individual with a bare
|
|
1024
|
+
numeral (``∀x (x = 1)``) is ALSO reported as "could not verify" —
|
|
1025
|
+
but that is the deliberate edge of the counting fragment, not a gap.
|
|
1026
|
+
See the module docstring's "Known verification gap" section for the
|
|
1027
|
+
full account.
|
|
1028
|
+
|
|
1029
|
+
Args:
|
|
1030
|
+
structure: the candidate countermodel, typically fresh out of
|
|
1031
|
+
:func:`structure_from_solution`.
|
|
1032
|
+
sentences: the sentences ``structure`` is claimed to satisfy —
|
|
1033
|
+
typically a :class:`FiniteDomainProblem`'s ``sentences``.
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
``None`` if every sentence evaluates ``True`` in ``structure``;
|
|
1037
|
+
otherwise a message naming the FIRST sentence (in order) that
|
|
1038
|
+
either evaluates ``False`` or could not be evaluated at all, and
|
|
1039
|
+
why.
|
|
1040
|
+
"""
|
|
1041
|
+
for sentence in sentences:
|
|
1042
|
+
try:
|
|
1043
|
+
holds = _evaluate_in_structure(sentence, structure)
|
|
1044
|
+
except (UninterpretedSymbol, UnsupportedNode, ValueError) as exc:
|
|
1045
|
+
return (
|
|
1046
|
+
f"could not verify {sentence.to_unicode_str()!r}: the "
|
|
1047
|
+
f"independent checker (evaluate_in_structure) could not "
|
|
1048
|
+
f"evaluate it — {exc}"
|
|
1049
|
+
)
|
|
1050
|
+
if not holds:
|
|
1051
|
+
return (
|
|
1052
|
+
f"{sentence.to_unicode_str()!r} does not hold in the "
|
|
1053
|
+
f"reconstructed structure."
|
|
1054
|
+
)
|
|
1055
|
+
return None
|