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,1180 @@
|
|
|
1
|
+
"""ASP-backed finite-domain refutation search via `clingo <https://potassco.org>`_.
|
|
2
|
+
|
|
3
|
+
:class:`ClingoBackend` decides ``premises ⊨ formula`` by searching for a
|
|
4
|
+
finite model of ``premises ∧ ¬formula`` at increasing domain sizes, exactly
|
|
5
|
+
the :class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem` contract
|
|
6
|
+
:mod:`~unicode_logic_kit.atp.finite_domain` was built for — see that module's
|
|
7
|
+
docstring for the shared problem shape, the fragment gate
|
|
8
|
+
(:func:`~unicode_logic_kit.atp.finite_domain.fragment_check`), the countermodel
|
|
9
|
+
reconstruction (:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`),
|
|
10
|
+
and the re-verification safety net
|
|
11
|
+
(:func:`~unicode_logic_kit.atp.finite_domain.verify_model`). This module is
|
|
12
|
+
solver-specific: :func:`to_asp` renders a
|
|
13
|
+
:class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem` into an ASP
|
|
14
|
+
program, and :class:`ClingoBackend` runs it through clingo's Python API.
|
|
15
|
+
|
|
16
|
+
Why compositional (Tseitin-style) translation, not a flat constraint
|
|
17
|
+
----------------------------------------------------------------------
|
|
18
|
+
An ASP rule body is a CONJUNCTION of literals; there is no direct way to
|
|
19
|
+
write "an arbitrary Boolean formula with ``∨``/``¬`` nested under other
|
|
20
|
+
connectives" as one rule. This module instead gives every subformula
|
|
21
|
+
occurrence its own DEFINED atom ``satK`` (``K`` a fresh, globally unique
|
|
22
|
+
counter — see ``_AspEncoder._fresh``), built bottom-up so a parent's rule
|
|
23
|
+
only ever references its children's already-defined atoms:
|
|
24
|
+
|
|
25
|
+
* ``And``/``Contrast`` — one rule ANDing both children's atoms.
|
|
26
|
+
* ``Or`` — TWO rules, one per disjunct (a predicate with two independent
|
|
27
|
+
defining rules is true if EITHER fires — this is exactly disjunction under
|
|
28
|
+
the stable-model semantics clingo implements).
|
|
29
|
+
* ``Not``/``Xor``/``Implies``/``Iff`` — combinations of the above plus
|
|
30
|
+
default negation (``not satK``). This is safe (not merely convenient):
|
|
31
|
+
every subformula tree here is FINITE and ACYCLIC, so negation only ever
|
|
32
|
+
refers to a strictly smaller child — the whole program is stratified, and
|
|
33
|
+
stratified negation is exactly classical two-valued evaluation, with no
|
|
34
|
+
extra argument needed for why the stable model coincides with the
|
|
35
|
+
intended truth table.
|
|
36
|
+
* ``Quantifier`` — ``∃`` is one rule with a fresh domain-ranging variable in
|
|
37
|
+
the body (a positive occurrence proves existence by construction); ``∀``
|
|
38
|
+
is the double-negation idiom (``violK`` holds if some individual violates
|
|
39
|
+
the body; the quantifier's own atom holds if NOT violK) rather than a
|
|
40
|
+
literal ASP ``∀`` (ASP has none — this is the standard encoding).
|
|
41
|
+
* ``Count``/``Cardinality`` — both map directly to a clingo ``#count``
|
|
42
|
+
aggregate assigned to a fresh integer variable, compared arithmetically.
|
|
43
|
+
This is a genuine improvement over
|
|
44
|
+
:meth:`~unicode_logic_kit.fol.nodes.Count._expand`'s distinct-witnesses
|
|
45
|
+
lowering (used by ``to_z3``/``to_prover9``/``to_tptp``): that expansion is
|
|
46
|
+
O(n²) constraints under n nested quantifiers and refuses n > 500; the
|
|
47
|
+
aggregate has no such bound, because clingo counts the guessed relation's
|
|
48
|
+
extension directly rather than enumerating n Skolem witnesses.
|
|
49
|
+
|
|
50
|
+
**Each subformula's OWN free variables (via
|
|
51
|
+
:func:`~unicode_logic_kit.fol.nodes.free_variables`, not a hand-rolled scope
|
|
52
|
+
stack) are exactly its ``satK`` atom's argument tuple.** This single choice
|
|
53
|
+
is what makes quantifier SHADOWING correct for free: ``∀x(P(x) ∧ ∃x Q(x))``
|
|
54
|
+
gives the inner ``∃x`` its own atom with ARITY 0 (``free_variables`` already
|
|
55
|
+
subtracts its own bound ``x``, so the inner atom never mentions the outer
|
|
56
|
+
``x`` at all), and ASP's own per-RULE variable scoping means the outer and
|
|
57
|
+
inner rules can both legally use the ASP variable name ``Vx`` without
|
|
58
|
+
colliding — no manual alpha-renaming or explicit scope-stack bookkeeping is
|
|
59
|
+
needed anywhere in this module. Reusing ``free_variables`` (already
|
|
60
|
+
exercised elsewhere in the kit, e.g. by ``Count._expand``) also means this
|
|
61
|
+
encoder inherits a binder-correctness argument it did not have to make from
|
|
62
|
+
scratch — the rejected alternative (tracking a binder-scope stack by hand
|
|
63
|
+
through the recursion) is exactly the kind of bookkeeping most likely to
|
|
64
|
+
hide an off-by-one shadowing bug.
|
|
65
|
+
|
|
66
|
+
Comparison predicates — matching ``model_eval`` exactly
|
|
67
|
+
------------------------------------------------------------
|
|
68
|
+
``=``/``≠`` at arity 2 are lowered to NATIVE ASP term (in)equality on the
|
|
69
|
+
two operands' resolved domain-index variables — never a stored relation —
|
|
70
|
+
mirroring :func:`~unicode_logic_kit.semantics.model_eval.evaluate`'s own
|
|
71
|
+
special-case (identity via Python ``==``, never routed through the
|
|
72
|
+
structure). Every OTHER atom, INCLUDING ``<``/``>``/``≤``/``≥`` over plain
|
|
73
|
+
individuals, is lowered the same way ``model_eval`` reads it: as an ORDINARY
|
|
74
|
+
predicate the countermodel is free to interpret however it likes (see
|
|
75
|
+
``model_eval``'s module docstring — it special-cases only ``=``/``≠``;
|
|
76
|
+
``<`` etc. fall through to ``_holds``, i.e. a stored/looked-up relation,
|
|
77
|
+
NOT built-in arithmetic ordering — the domain individuals are opaque
|
|
78
|
+
labels, not numbers, so "less than" has no intrinsic meaning here). Because
|
|
79
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` deliberately
|
|
80
|
+
EXCLUDES the six comparison symbols from user vocabulary (they are the
|
|
81
|
+
kit's built-in operators — see that module's docstring), this encoder
|
|
82
|
+
tracks such "comparison-symbol-as-plain-relation" usages ITSELF and folds
|
|
83
|
+
them into a LOCAL, extended reconstruction Signature
|
|
84
|
+
(:attr:`_AspEncoder.reconstruction_signature`) used only for
|
|
85
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution` — never
|
|
86
|
+
mutating or replacing ``problem.signature`` itself.
|
|
87
|
+
|
|
88
|
+
The one exception: when EITHER operand of a ``=``/``≠``/``<``/``>``/``≤``/
|
|
89
|
+
``≥`` comparison is a counting term (``Cardinality`` or ``Number``), the
|
|
90
|
+
comparison is arithmetic over NATURAL NUMBERS (a ``#count`` aggregate result,
|
|
91
|
+
or a literal), not over domain individuals — this is the counting fragment
|
|
92
|
+
this whole design exists to close (see
|
|
93
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s module docstring, §1 of the
|
|
94
|
+
originating design note). Mixing a counting term against a plain
|
|
95
|
+
individual-denoting term on the other side of the same comparison has no
|
|
96
|
+
coherent reading and is refused loudly (``_EncodingError``) rather than
|
|
97
|
+
guessed at.
|
|
98
|
+
|
|
99
|
+
**Numerals.** That arithmetic reading belongs to a COUNT: a ``Number`` is read as a number
|
|
100
|
+
only as the bound a ``Cardinality`` is compared with (``|{x : P(x)}| ≥ 2``; a float with a
|
|
101
|
+
whole value is that integer). Everywhere else a numeral is, as on every other route, a
|
|
102
|
+
constant identified by its value (``1`` and ``1.0`` one constant, ``1 ≠ 2`` not valid), and
|
|
103
|
+
this encoder has no symbol for such a constant, so a ``Number`` used as an individual, and a
|
|
104
|
+
comparison of numerals with no cardinality in it, are refused by name (``UNKNOWN`` /
|
|
105
|
+
``"unsupported"``) — never read as a domain index or as arithmetic, which would make
|
|
106
|
+
``(∀x ∀y x = y) → 1 = 2`` (valid: one element) refutable.
|
|
107
|
+
|
|
108
|
+
Counting AND function comparisons now verify: the closed gaps, and the one that remains
|
|
109
|
+
-------------------------------------------------------------------------------------------
|
|
110
|
+
This section used to list four node types this backend could ground and
|
|
111
|
+
solve but never verify. That list is down to one, closed in two steps.
|
|
112
|
+
|
|
113
|
+
``Cardinality`` — and a ``Number`` compared as a counting term — used to be
|
|
114
|
+
on it: :func:`~unicode_logic_kit.atp.finite_domain.verify_model`'s underlying
|
|
115
|
+
evaluator raised ``UnsupportedNode`` for them, so a genuinely correct
|
|
116
|
+
countermodel was reported ``ERROR``/``"infra"`` instead of ``REFUTED``.
|
|
117
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate` reads a comparison
|
|
118
|
+
with a counting-term operand arithmetically (counting ``|{v :
|
|
119
|
+
φ}|`` over the domain — see that module's ``_numeric_value``/``_atom_value``
|
|
120
|
+
split), so :func:`~unicode_logic_kit.atp.finite_domain.verify_model` can check
|
|
121
|
+
these sentences and :meth:`ClingoBackend.decide` reports a verified
|
|
122
|
+
``REFUTED`` for them like any other. Example: ``|{x : P(x)}| >
|
|
123
|
+
|{y : Q(y)}|`` at ``max_size=4`` comes back ``REFUTED`` with
|
|
124
|
+
``countermodel={"kind": "finite_structure", ...}`` and ``detail=None`` (a
|
|
125
|
+
non-``None`` detail would mean verification failed or was skipped) — run
|
|
126
|
+
against this module directly on 2026-08-14, clingo 5.8.1.
|
|
127
|
+
|
|
128
|
+
``Function`` was on this list too, for a DIFFERENT reason: the gate itself
|
|
129
|
+
used to refuse every ``Function`` node BY NAME before this encoder ever ran
|
|
130
|
+
(``fragment_check`` reported ``UNKNOWN``/``"unsupported"`` straight from the
|
|
131
|
+
gate — "we correctly declined to look", not merely "we found a countermodel
|
|
132
|
+
we cannot vouch for"), because :func:`~unicode_logic_kit.semantics.model_eval.evaluate`
|
|
133
|
+
had no case for a ``Function`` TERM. That upstream refusal is gone:
|
|
134
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check` now ADMITS
|
|
135
|
+
``Function`` generally (a *sorted* ``FunctionDecl`` stays refused — a
|
|
136
|
+
separate, narrower decision, see that module's "Sorted function symbols"
|
|
137
|
+
section), and ``evaluate_in_structure``'s new ``Function`` case reads
|
|
138
|
+
``f(t1,...,tk)`` off the SAME ``(name, arity+1)`` total-relation extension
|
|
139
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution` already
|
|
140
|
+
built (:mod:`~unicode_logic_kit.semantics.model_eval`'s own docstring has the
|
|
141
|
+
full account), so this encoder's dormant ``Function`` branch in
|
|
142
|
+
:meth:`_AspEncoder._term` (already documented above — the choice-rule
|
|
143
|
+
total-relation encoding) is REACHABLE and its output is genuinely VERIFIED,
|
|
144
|
+
not merely produced. A function-bearing countermodel now goes through the
|
|
145
|
+
identical closure the counting fragment already got: solved by clingo,
|
|
146
|
+
reconstructed by :func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`,
|
|
147
|
+
confirmed by :func:`~unicode_logic_kit.atp.finite_domain.verify_model`, and
|
|
148
|
+
reported ``REFUTED`` with ``detail=None``. Example: ``∀x (f(f(x)) = x)``
|
|
149
|
+
("f is an involution") at ``max_size=4`` comes back ``REFUTED`` at the
|
|
150
|
+
hand-derivable minimal size 2 (the only total function on a 1-element domain
|
|
151
|
+
is forced to be the identity, so no countermodel exists there) with
|
|
152
|
+
``detail=None`` — run against this module directly on 2026-09-17, clingo
|
|
153
|
+
5.8.1 (see ``tests/test_clingo_backend.py``'s
|
|
154
|
+
``test_involution_claim_refuted_with_verified_countermodel``). The four
|
|
155
|
+
arithmetic operator names (``+``/``-``/``*``/``/``) stay refused regardless
|
|
156
|
+
— they are
|
|
157
|
+
``Function`` nodes like any other and pass ``fragment_check``, but
|
|
158
|
+
``Signature.from_formulas`` never declares them in ``functions`` (the
|
|
159
|
+
``_BUILTIN_FUNCS`` carve-out), so :meth:`_AspEncoder._term`'s existing
|
|
160
|
+
"function not declared in signature" ``_EncodingError`` catches them, with
|
|
161
|
+
no new special-casing needed in this module.
|
|
162
|
+
|
|
163
|
+
``Contrast`` is the one node type left on the list, genuinely unchanged:
|
|
164
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check` still admits it
|
|
165
|
+
(truth-functionally ``And``, so this encoder's ``And``/``Contrast`` rule —
|
|
166
|
+
see above — grounds and solves it exactly like ``And``), but
|
|
167
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate` still raises
|
|
168
|
+
``UnsupportedNode`` for it (a scope decision made in that module, not this
|
|
169
|
+
one). :func:`~unicode_logic_kit.atp.finite_domain.verify_model` therefore
|
|
170
|
+
still reports "could not verify" for a ``Contrast``-bearing sentence, and
|
|
171
|
+
:meth:`ClingoBackend.decide` still reports ``ERROR``/``"infra"`` rather than
|
|
172
|
+
``REFUTED`` for it, EVEN THOUGH the countermodel this module found is
|
|
173
|
+
genuinely correct — the same trade this whole section used to describe for
|
|
174
|
+
all four node types, now narrowed to just this one. Per the ONE rule these
|
|
175
|
+
backends exist to protect (never hand back an unverified countermodel — see
|
|
176
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s "Known verification gap"), that
|
|
177
|
+
trade is accepted rather than bypassed. Fixing it means extending
|
|
178
|
+
``model_eval`` (owned elsewhere) or redesigning ``verify_model`` (a decision
|
|
179
|
+
this module does not make unilaterally) — not this module's bug to fix.
|
|
180
|
+
Plain classical-FOL-plus-``Count``/``Cardinality`` sentences, including
|
|
181
|
+
counting comparisons, now verify and report ``REFUTED`` normally.
|
|
182
|
+
|
|
183
|
+
A free variable is a parameter of the problem
|
|
184
|
+
-------------------------------------------------------------------
|
|
185
|
+
:meth:`ClingoBackend.decide` reads a free variable as ONE unknown element, the same in
|
|
186
|
+
every premise and in the goal (the assignment-wise consequence relation: ``Γ ⊨ φ`` iff every
|
|
187
|
+
structure AND assignment that satisfies ``Γ`` satisfies ``φ``). Before anything else it replaces
|
|
188
|
+
every free variable of ``premises`` and ``formula`` together by a constant of its own name
|
|
189
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`), and only then forms
|
|
190
|
+
``premises ∧ ¬formula``. A premise is never closed universally: ``P(x)`` does not entail
|
|
191
|
+
``P(alpha)`` (universe ``{0, 1}``, ``x`` ↦ 1, ``alpha`` ↦ 0, ``P`` = ``{1}``), and the countermodel
|
|
192
|
+
reports the element under the variable's name, ``constants['x']``. A free variable spelled like a
|
|
193
|
+
constant of the problem is refused (``UNKNOWN``/``"unsupported"``): a structure holds one entry
|
|
194
|
+
per name.
|
|
195
|
+
|
|
196
|
+
The parameters are replaced BEFORE the goal is negated: ``¬∀x φ(x)`` (some assignment falsifies
|
|
197
|
+
φ) is NOT the same formula as ``∀x ¬φ(x)``, which a closure applied after the negation would
|
|
198
|
+
produce. For a problem with no premise the reading coincides with the universal closure of the
|
|
199
|
+
goal (``φ`` holds under every assignment iff ``∀x φ`` holds); with a premise it does not, since
|
|
200
|
+
closing every premise universally would read ``P(x)`` as ``∀x P(x)`` and find no countermodel of
|
|
201
|
+
``P(x) ⊢ P(alpha)``. Both the encoder and the independent checker
|
|
202
|
+
(:func:`~unicode_logic_kit.atp.finite_domain.verify_model`) are handed the identical sentences, so
|
|
203
|
+
they cannot disagree about what a free variable means: neither of them ever sees one.
|
|
204
|
+
|
|
205
|
+
``all_different`` (every CONSTANT denotes a pairwise-distinct individual) concerns the constants
|
|
206
|
+
of the problem, never a parameter: a variable may denote the same element as a constant, so the
|
|
207
|
+
distinctness constraint, and its re-check on the reconstructed structure, leave the parameters out.
|
|
208
|
+
|
|
209
|
+
Many-sorted input
|
|
210
|
+
-------------------
|
|
211
|
+
:meth:`ClingoBackend.decide` builds ``sentences`` (the premises, and the goal negated,
|
|
212
|
+
every free variable already read as a parameter per the section above) and then, before anything else,
|
|
213
|
+
calls :func:`~unicode_logic_kit.atp.finite_domain.lower_msfol` on the whole
|
|
214
|
+
batch — a no-op for every plain-FOL caller, and for a many-sorted one a
|
|
215
|
+
relativisation to classical FOL (plus one non-emptiness sentence per
|
|
216
|
+
distinct sort name) that lets :func:`~unicode_logic_kit.atp.finite_domain.fragment_check`,
|
|
217
|
+
:func:`to_asp`, and :func:`~unicode_logic_kit.atp.finite_domain.verify_model`
|
|
218
|
+
all stay entirely sort-blind. See
|
|
219
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s own "Many-sorted input" section
|
|
220
|
+
for the full design, including why the non-emptiness sentence is required
|
|
221
|
+
for soundness against :mod:`~unicode_logic_kit.semantics.modelfinder`, the
|
|
222
|
+
oracle this is differentially tested against.
|
|
223
|
+
|
|
224
|
+
Availability and timeout
|
|
225
|
+
-------------------------------
|
|
226
|
+
``clingo`` (MIT, wheels bundle the solver) is a hard, non-external Python
|
|
227
|
+
dependency once installed — :func:`clingo_available` is pure discovery
|
|
228
|
+
(``importlib.util.find_spec``, mirroring
|
|
229
|
+
:meth:`~unicode_logic_kit.atp.cvc5_backend.Cvc5Backend.available`) and the
|
|
230
|
+
``clingo`` module itself is imported lazily inside the solve step so probing
|
|
231
|
+
availability never pays its load cost. A size's solve runs through clingo's
|
|
232
|
+
ASYNC handle (``Control.solve(async_=True)``) so the remaining millisecond
|
|
233
|
+
budget can be enforced with ``handle.wait(remaining)`` +
|
|
234
|
+
``handle.cancel()``; a cancellation that lands exactly as the solve
|
|
235
|
+
legitimately finished still yields the REAL (sat/unsat) answer rather than a
|
|
236
|
+
spurious timeout — ``handle.get()`` after ``cancel()`` returns whatever the
|
|
237
|
+
solver actually decided, and only a genuinely interrupted search comes back
|
|
238
|
+
with neither ``satisfiable`` nor ``unsatisfiable`` set (empirically verified
|
|
239
|
+
against clingo 5.8.1 before relying on it here).
|
|
240
|
+
|
|
241
|
+
This module never imports ``PROVED`` from
|
|
242
|
+
:mod:`~unicode_logic_kit.atp.protocol` — deliberately: a backend that cannot
|
|
243
|
+
even name the symbol cannot accidentally return it. First-order logic has
|
|
244
|
+
no finite model property, so "no countermodel up to ``max_size``" is
|
|
245
|
+
``UNKNOWN``/``"bound_hit"``, never a validity proof (the ONE rule these
|
|
246
|
+
finite-domain backends exist to protect — see
|
|
247
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s module docstring).
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
import importlib.util
|
|
251
|
+
import re
|
|
252
|
+
import time
|
|
253
|
+
from typing import Dict, FrozenSet, List, Optional, Sequence, Set, Tuple
|
|
254
|
+
|
|
255
|
+
from ..fol._free_parameters import parameterize
|
|
256
|
+
from ..fol.nodes import (
|
|
257
|
+
Node, Variable, Constant, Number, Function,
|
|
258
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Contrast, Quantifier,
|
|
259
|
+
Count, Cardinality,
|
|
260
|
+
free_variables,
|
|
261
|
+
)
|
|
262
|
+
from ..fol.signature import Signature, PredicateDecl
|
|
263
|
+
from .finite_domain import (
|
|
264
|
+
FiniteDomainProblem, fragment_check, free_variable_reason, lower_msfol,
|
|
265
|
+
structure_from_solution, verify_model,
|
|
266
|
+
)
|
|
267
|
+
from .protocol import ProverBackend, Verdict, REFUTED, UNKNOWN, ERROR
|
|
268
|
+
|
|
269
|
+
__all__ = ["ClingoBackend", "clingo_available", "to_asp"]
|
|
270
|
+
|
|
271
|
+
_FORALL = ("forall", "∀")
|
|
272
|
+
_EXISTS = ("exists", "∃")
|
|
273
|
+
_COMPARISON_SYMBOLS = frozenset({"=", "≠", "<", ">", "≤", "≥"})
|
|
274
|
+
_NUMERIC_ASP_OP = {"=": "=", "≠": "!=", "<": "<", ">": ">", "≤": "<=", "≥": ">="}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def clingo_available() -> bool:
|
|
278
|
+
"""Return whether the ``clingo`` package is importable (pure discovery).
|
|
279
|
+
|
|
280
|
+
Never imports ``clingo`` itself — only checks
|
|
281
|
+
``importlib.util.find_spec`` — so calling this to decide whether to try
|
|
282
|
+
:class:`ClingoBackend` never pays the solver binding's load cost, the
|
|
283
|
+
same discipline
|
|
284
|
+
:meth:`~unicode_logic_kit.atp.cvc5_backend.Cvc5Backend.available` follows
|
|
285
|
+
for its own optional dependency.
|
|
286
|
+
"""
|
|
287
|
+
return importlib.util.find_spec("clingo") is not None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# =============================================================================
|
|
291
|
+
# Refutation-goal construction
|
|
292
|
+
# =============================================================================
|
|
293
|
+
|
|
294
|
+
def _universal_closure(node: Node) -> Node:
|
|
295
|
+
"""Wrap ``node`` in a ∀ for each of its own free variables (deterministic order).
|
|
296
|
+
|
|
297
|
+
The universal closure of ONE formula: it says the formula holds under every
|
|
298
|
+
assignment, which is what a test of a countermodel needs when it re-evaluates a
|
|
299
|
+
single formula's refutation goal ``¬∀x φ(x)``. It is not how :meth:`ClingoBackend.decide`
|
|
300
|
+
reads a PROBLEM: a free variable there is a parameter shared by the premises and the
|
|
301
|
+
goal (see the module docstring's "A free variable is a parameter of the problem"),
|
|
302
|
+
because closing each premise on its own would read ``P(x)`` as ``∀x P(x)``.
|
|
303
|
+
Variable names sorted, innermost-first, as
|
|
304
|
+
:mod:`~unicode_logic_kit.semantics.modelfinder`'s module-private closure helper.
|
|
305
|
+
"""
|
|
306
|
+
result = node
|
|
307
|
+
for name in sorted({v.name for v in free_variables(node)}, reverse=True):
|
|
308
|
+
result = Quantifier("∀", Variable(name), result)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _repeated_constant(constants, parameters: FrozenSet[str]) -> Optional[str]:
|
|
313
|
+
"""A message naming two constants of ``constants`` that denote one individual, or ``None``.
|
|
314
|
+
|
|
315
|
+
The parameters that stand for free variables are left out: ``all_different`` is the
|
|
316
|
+
unique-names convention of the problem's constants, and a variable may denote the
|
|
317
|
+
element of a constant.
|
|
318
|
+
"""
|
|
319
|
+
seen: Dict[object, str] = {}
|
|
320
|
+
for name in sorted(constants):
|
|
321
|
+
if name in parameters:
|
|
322
|
+
continue
|
|
323
|
+
value = constants[name]
|
|
324
|
+
if value in seen:
|
|
325
|
+
return (f"structure_from_solution: all_different=True requires every constant to "
|
|
326
|
+
f"denote a distinct individual, but {name!r} and {seen[value]!r} both "
|
|
327
|
+
f"denote {value}.")
|
|
328
|
+
seen[value] = name
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
#: A variable name that is written ``"V" + name`` as it stands: ASCII letters,
|
|
333
|
+
#: digits and underscores, the first of them a letter or digit (see
|
|
334
|
+
#: :meth:`_AspEncoder._asp_var`).
|
|
335
|
+
_ASP_PLAIN_VARIABLE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_]*")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class _EncodingError(ValueError):
|
|
339
|
+
"""A sentence passed ``fragment_check`` but this encoder still cannot lower it.
|
|
340
|
+
|
|
341
|
+
``fragment_check`` (:mod:`~unicode_logic_kit.atp.finite_domain`) only
|
|
342
|
+
screens NODE TYPES — a shared, solver-agnostic gate the MiniZinc backend
|
|
343
|
+
also consults, so it deliberately does not know about ASP-specific
|
|
344
|
+
encodability. A few finer-grained shapes slip past it and are caught
|
|
345
|
+
here instead: a non-integer ``Number``, a comparison mixing a counting
|
|
346
|
+
term (``Cardinality``/``Number``) against a plain individual-denoting
|
|
347
|
+
term, or a symbol a sentence uses but the problem's Signature does not
|
|
348
|
+
declare (only reachable via a hand-built
|
|
349
|
+
:class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem` whose
|
|
350
|
+
``signature=`` was supplied explicitly and does not match its
|
|
351
|
+
``sentences``). :class:`ClingoBackend` catches this subclass of
|
|
352
|
+
``ValueError`` and reports ``UNKNOWN``/``"unsupported"`` — the same
|
|
353
|
+
honest verdict ``fragment_check`` itself would have given had its
|
|
354
|
+
coarser gate been able to see this deep.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _is_numeric_term(node: Node) -> bool:
|
|
359
|
+
"""Whether ``node`` denotes a NATURAL NUMBER (a counting term), not an individual."""
|
|
360
|
+
return isinstance(node, (Cardinality, Number))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _atom_mode(atom: Atom) -> str:
|
|
364
|
+
"""Classify how ``atom`` must be lowered to ASP; see the module docstring's
|
|
365
|
+
"Comparison predicates" section for the full rationale.
|
|
366
|
+
|
|
367
|
+
Returns one of:
|
|
368
|
+
|
|
369
|
+
* ``"identity"`` — ``=``/``≠`` at arity 2 over plain individual terms:
|
|
370
|
+
native ASP term (in)equality, mirroring
|
|
371
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate`'s own
|
|
372
|
+
``=``/``≠`` special-case exactly.
|
|
373
|
+
* ``"numeric"`` — any of the six comparison symbols at arity 2 with a
|
|
374
|
+
counting-term (``Cardinality``/``Number``) operand: arithmetic over
|
|
375
|
+
natural numbers via a ``#count`` aggregate and/or a literal integer.
|
|
376
|
+
* ``"relation"`` — any of the six comparison symbols used OUTSIDE the
|
|
377
|
+
two cases above (individual-mode ``<``/``>``/``≤``/``≥``, or any of
|
|
378
|
+
the six at an arity other than 2): an ORDINARY predicate the
|
|
379
|
+
countermodel is free to interpret arbitrarily, exactly how
|
|
380
|
+
``model_eval`` reads them (it special-cases only 2-ary ``=``/``≠``).
|
|
381
|
+
* ``"predicate"`` — every other symbol: must already be declared in the
|
|
382
|
+
problem's Signature.
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
_EncodingError: the comparison mixes a counting-term operand
|
|
386
|
+
against a plain individual-denoting one — no coherent reading —
|
|
387
|
+
or compares numerals with each other and with no cardinality:
|
|
388
|
+
the arithmetic reading belongs to a count compared with a
|
|
389
|
+
number, and a numeral that is no bound of a count is a
|
|
390
|
+
constant of the kit (see :meth:`_AspEncoder._term`).
|
|
391
|
+
"""
|
|
392
|
+
if atom.predicate in _COMPARISON_SYMBOLS and len(atom.args) == 2:
|
|
393
|
+
left_numeric = _is_numeric_term(atom.args[0])
|
|
394
|
+
right_numeric = _is_numeric_term(atom.args[1])
|
|
395
|
+
if left_numeric or right_numeric:
|
|
396
|
+
if not (left_numeric and right_numeric):
|
|
397
|
+
raise _EncodingError(
|
|
398
|
+
f"{atom.predicate!r} compares a counting term "
|
|
399
|
+
"(Cardinality/Number) against a plain individual-denoting "
|
|
400
|
+
"term — both sides of a counting comparison must "
|
|
401
|
+
"themselves be counting terms."
|
|
402
|
+
)
|
|
403
|
+
if not any(isinstance(a, Cardinality) for a in atom.args):
|
|
404
|
+
raise _EncodingError(
|
|
405
|
+
f"{atom.predicate!r} compares numerals without a cardinality. A numeral is a "
|
|
406
|
+
"constant identified by its value, and nothing else is known about it, so "
|
|
407
|
+
"such a comparison is a statement about constants ('1 ≠ 2' is not valid, "
|
|
408
|
+
"'(∀x ∀y x = y) → 1 = 2' is), which this encoder does not state: it reads a "
|
|
409
|
+
"number only as the bound a count |{v : φ}| is compared with, and a "
|
|
410
|
+
"refutation found under the arithmetic reading would be wrong for the "
|
|
411
|
+
"constants. Use a solver that reads numerals as constants (z3, the finite "
|
|
412
|
+
"model finder), or compare a cardinality.")
|
|
413
|
+
return "numeric"
|
|
414
|
+
if atom.predicate in ("=", "≠"):
|
|
415
|
+
return "identity"
|
|
416
|
+
return "relation"
|
|
417
|
+
return "relation" if atom.predicate in _COMPARISON_SYMBOLS else "predicate"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# =============================================================================
|
|
421
|
+
# The encoder — shared by to_asp() and ClingoBackend.decide()
|
|
422
|
+
# =============================================================================
|
|
423
|
+
|
|
424
|
+
class _AspEncoder:
|
|
425
|
+
"""Lowers one :class:`FiniteDomainProblem` to ASP text, and back again.
|
|
426
|
+
|
|
427
|
+
Built once per (sentences, size) pair. :meth:`render` produces the
|
|
428
|
+
program text (what :func:`to_asp` returns); :meth:`decode_model` and
|
|
429
|
+
:attr:`reconstruction_signature` are the matching REVERSE direction,
|
|
430
|
+
used only by :class:`ClingoBackend` (never by :func:`to_asp`, which has
|
|
431
|
+
no solver to hand a model back from) to turn clingo's shown atoms into
|
|
432
|
+
the ``(name, args)`` shape
|
|
433
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`
|
|
434
|
+
expects. Kept as ONE class, not two free functions sharing module
|
|
435
|
+
globals, so the symbol-name maps built while rendering are exactly what
|
|
436
|
+
decoding reads back — a mismatch between the two directions cannot occur
|
|
437
|
+
by construction, not by discipline.
|
|
438
|
+
|
|
439
|
+
Deliberately rebuilt from scratch for every domain size a caller tries
|
|
440
|
+
(see :meth:`ClingoBackend.decide`), even though the symbol-name
|
|
441
|
+
assignments themselves do not depend on ``size`` — ``size`` is coupled
|
|
442
|
+
into :class:`FiniteDomainProblem` itself, and re-deriving a few small
|
|
443
|
+
dicts per size is cheap next to actually grounding and solving; the
|
|
444
|
+
alternative (splitting a size-independent "skeleton" out for reuse
|
|
445
|
+
across sizes) is a real optimisation this module deliberately does not
|
|
446
|
+
attempt, in favour of one obviously-correct code path.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def __init__(self, problem: FiniteDomainProblem, parameters: FrozenSet[str] = frozenset()):
|
|
450
|
+
"""``parameters`` names the constants that stand for a free variable: the
|
|
451
|
+
problem's ``all_different`` (every CONSTANT denotes a pairwise-distinct
|
|
452
|
+
individual) leaves them out, since a variable may denote the element of a
|
|
453
|
+
constant."""
|
|
454
|
+
if not isinstance(problem, FiniteDomainProblem):
|
|
455
|
+
raise TypeError(
|
|
456
|
+
f"_AspEncoder: problem must be a FiniteDomainProblem, got "
|
|
457
|
+
f"{type(problem).__name__}."
|
|
458
|
+
)
|
|
459
|
+
self.problem = problem
|
|
460
|
+
self._parameters = frozenset(parameters)
|
|
461
|
+
self.sig: Signature = problem.signature
|
|
462
|
+
self._lines: List[str] = []
|
|
463
|
+
self._fresh_n = 0
|
|
464
|
+
self._pred_asp: Dict[Tuple[str, int], str] = {}
|
|
465
|
+
self._func_asp: Dict[str, str] = {}
|
|
466
|
+
self._const_asp: Dict[str, str] = {}
|
|
467
|
+
self._built = False
|
|
468
|
+
|
|
469
|
+
# -- naming -------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
def _fresh(self, prefix: str) -> str:
|
|
472
|
+
"""A globally unique name ``{prefix}{n}`` — see the class docstring's
|
|
473
|
+
naming-collision argument in the module docstring (no two calls,
|
|
474
|
+
regardless of prefix, ever share a counter value)."""
|
|
475
|
+
self._fresh_n += 1
|
|
476
|
+
return f"{prefix}{self._fresh_n}"
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _asp_var(kit_name: str) -> str:
|
|
480
|
+
"""Map a kit variable name to an ASP variable name; two names never share one.
|
|
481
|
+
|
|
482
|
+
An ASP variable is an upper-case letter followed by ASCII letters, digits
|
|
483
|
+
and underscores. A name of ASCII letters, digits and underscores that
|
|
484
|
+
starts with a letter or digit (``x``, ``x0``, ``x_0``: what the grammar
|
|
485
|
+
and the kit's own fresh names give) is written ``"V" + name``, as it
|
|
486
|
+
always was. Every other name is written ``"V_" + code``, where the code
|
|
487
|
+
keeps ASCII letters and digits and turns each other character into an
|
|
488
|
+
underscore, its code point in lower-case hexadecimal and an underscore
|
|
489
|
+
(``é`` is ``V__e9_``, ``x-1`` is ``V_x_2d_1``). The VARIABLE terminal
|
|
490
|
+
admits any Unicode letter, so ``∀é P(é)`` reaches this encoder from
|
|
491
|
+
text; ``Vé`` is no ASP variable, and clingo's parser, handed that text,
|
|
492
|
+
ended the call with an error of the decoder instead of a verdict.
|
|
493
|
+
|
|
494
|
+
The two forms never meet (the second character is a letter or digit in
|
|
495
|
+
the first, an underscore in the second), and the code reads back one way
|
|
496
|
+
only (a letter or digit stands for itself, every underscore opens one
|
|
497
|
+
``_<hex>_`` group), so two variable names always get two ASP variables.
|
|
498
|
+
The names this encoder mints itself start with ``C`` or ``F``.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
_EncodingError: the name is not a string.
|
|
502
|
+
"""
|
|
503
|
+
if not isinstance(kit_name, str):
|
|
504
|
+
raise _EncodingError(
|
|
505
|
+
f"_AspEncoder: the variable name {kit_name!r} is of type "
|
|
506
|
+
f"{type(kit_name).__name__}, not a string, so it has no ASP variable.")
|
|
507
|
+
if _ASP_PLAIN_VARIABLE.fullmatch(kit_name):
|
|
508
|
+
return f"V{kit_name}"
|
|
509
|
+
return "V_" + "".join(
|
|
510
|
+
ch if ch.isascii() and ch.isalnum() else f"_{ord(ch):x}_" for ch in kit_name)
|
|
511
|
+
|
|
512
|
+
@staticmethod
|
|
513
|
+
def _format_atom(name: str, args: Sequence[str]) -> str:
|
|
514
|
+
"""Render an applied atom, or a bare name for an empty arg list.
|
|
515
|
+
|
|
516
|
+
clingo does not accept ``name()`` for a nullary atom, so the empty
|
|
517
|
+
case omits the parentheses entirely rather than emitting them empty.
|
|
518
|
+
"""
|
|
519
|
+
return name if not args else f"{name}({','.join(args)})"
|
|
520
|
+
|
|
521
|
+
def _call(self, asp_name: str, kit_var_names: Sequence[str]) -> str:
|
|
522
|
+
return self._format_atom(asp_name, [self._asp_var(n) for n in kit_var_names])
|
|
523
|
+
|
|
524
|
+
def _fv_tuple(self, node: Node) -> List[str]:
|
|
525
|
+
"""The sorted kit variable NAMES free in ``node`` — see the module
|
|
526
|
+
docstring's shadowing argument for why sorted-by-name determinism
|
|
527
|
+
plus per-rule ASP scoping is sufficient, with no scope stack."""
|
|
528
|
+
return sorted({v.name for v in free_variables(node) if isinstance(v, Variable)})
|
|
529
|
+
|
|
530
|
+
def _dom_prefix(self, kit_var_names: Sequence[str]) -> List[str]:
|
|
531
|
+
return [f"dom({self._asp_var(n)})" for n in kit_var_names]
|
|
532
|
+
|
|
533
|
+
def _rule(self, head: str, body: List[str]) -> None:
|
|
534
|
+
if body:
|
|
535
|
+
self._lines.append(f"{head} :- {', '.join(body)}.")
|
|
536
|
+
else:
|
|
537
|
+
self._lines.append(f"{head}.")
|
|
538
|
+
|
|
539
|
+
# -- declarations ---------------------------------------------------
|
|
540
|
+
|
|
541
|
+
def _discover_comparison_relations(self) -> Set[Tuple[str, int]]:
|
|
542
|
+
"""Every ``(symbol, arity)`` among the six comparison predicates that
|
|
543
|
+
must become an ORDINARY guessed relation (see :func:`_atom_mode`'s
|
|
544
|
+
``"relation"`` case) — walked up front so its choice rule can be
|
|
545
|
+
emitted alongside every other predicate's, in one deterministic pass.
|
|
546
|
+
"""
|
|
547
|
+
found: Set[Tuple[str, int]] = set()
|
|
548
|
+
for sentence in self.problem.sentences:
|
|
549
|
+
for node in sentence.walk():
|
|
550
|
+
if isinstance(node, Atom) and node.predicate in _COMPARISON_SYMBOLS:
|
|
551
|
+
if _atom_mode(node) == "relation":
|
|
552
|
+
found.add((node.predicate, len(node.args)))
|
|
553
|
+
return found
|
|
554
|
+
|
|
555
|
+
def _assign_names(self) -> None:
|
|
556
|
+
sig_keys = {(name, decl.arity) for name, decl in self.sig.predicates.items()}
|
|
557
|
+
for i, key in enumerate(sorted(sig_keys | self._discover_comparison_relations())):
|
|
558
|
+
self._pred_asp[key] = f"pred{i}"
|
|
559
|
+
for name, _decl in sorted(self.sig.functions.items()):
|
|
560
|
+
self._func_asp[name] = f"func{len(self._func_asp)}"
|
|
561
|
+
for name in sorted(self.sig.constants):
|
|
562
|
+
self._const_asp[name] = f"const{len(self._const_asp)}"
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def _choice_rule(asp_name: str, arity: int) -> str:
|
|
566
|
+
"""A free choice over a relation's WHOLE extension.
|
|
567
|
+
|
|
568
|
+
The standard ASP idiom for "the countermodel may interpret this
|
|
569
|
+
predicate however it likes": ``{ p(X,Y) : dom(X), dom(Y) }``.
|
|
570
|
+
"""
|
|
571
|
+
if arity == 0:
|
|
572
|
+
return f"{{ {asp_name} }}."
|
|
573
|
+
xs = [f"X{i}" for i in range(arity)]
|
|
574
|
+
conds = ",".join(f"dom({x})" for x in xs)
|
|
575
|
+
return f"{{ {asp_name}({','.join(xs)}) : {conds} }}."
|
|
576
|
+
|
|
577
|
+
@staticmethod
|
|
578
|
+
def _function_rule(asp_name: str, arity: int) -> str:
|
|
579
|
+
"""The total-relation encoding: exactly one result per input tuple,
|
|
580
|
+
via a bounded cardinality choice ``1 { … } 1`` — enforces BOTH
|
|
581
|
+
functionality and totality at once (see
|
|
582
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`'s
|
|
583
|
+
own docstring for why both matter to the reconstruction step)."""
|
|
584
|
+
if arity == 0:
|
|
585
|
+
return f"1 {{ {asp_name}(Y) : dom(Y) }} 1."
|
|
586
|
+
xs = [f"X{i}" for i in range(arity)]
|
|
587
|
+
conds = ",".join(f"dom({x})" for x in xs)
|
|
588
|
+
return f"1 {{ {asp_name}({','.join(xs)},Y) : dom(Y) }} 1 :- {conds}."
|
|
589
|
+
|
|
590
|
+
@staticmethod
|
|
591
|
+
def _const_rule(asp_name: str) -> str:
|
|
592
|
+
"""A constant is the arity-0 case of the function idiom above: exactly
|
|
593
|
+
one denotation, chosen freely."""
|
|
594
|
+
return f"1 {{ {asp_name}(Y) : dom(Y) }} 1."
|
|
595
|
+
|
|
596
|
+
# -- term translation -------------------------------------------------
|
|
597
|
+
|
|
598
|
+
def _term(self, node: Node) -> Tuple[str, List[str]]:
|
|
599
|
+
"""Translate an INDIVIDUAL-denoting term to ``(asp_term, extra_body_literals)``.
|
|
600
|
+
|
|
601
|
+
``extra_body_literals`` must be conjoined into whichever rule body
|
|
602
|
+
is being built around this term (a fresh constant/function-result
|
|
603
|
+
variable is meaningless without the literal that binds it).
|
|
604
|
+
"""
|
|
605
|
+
if isinstance(node, Variable):
|
|
606
|
+
return self._asp_var(node.name), []
|
|
607
|
+
if isinstance(node, Constant):
|
|
608
|
+
if node.name not in self._const_asp:
|
|
609
|
+
raise _EncodingError(
|
|
610
|
+
f"_AspEncoder: constant {node.name!r} is used in a sentence "
|
|
611
|
+
"but is not declared in the problem's signature."
|
|
612
|
+
)
|
|
613
|
+
v = self._fresh("Cst")
|
|
614
|
+
return v, [f"{self._const_asp[node.name]}({v})"]
|
|
615
|
+
if isinstance(node, Function):
|
|
616
|
+
if node.name not in self._func_asp:
|
|
617
|
+
raise _EncodingError(
|
|
618
|
+
f"_AspEncoder: function {node.name!r} is used in a sentence "
|
|
619
|
+
"but is not declared in the problem's signature."
|
|
620
|
+
)
|
|
621
|
+
arg_terms: List[str] = []
|
|
622
|
+
extra: List[str] = []
|
|
623
|
+
for a in node.args:
|
|
624
|
+
t, ex = self._term(a)
|
|
625
|
+
arg_terms.append(t)
|
|
626
|
+
extra.extend(ex)
|
|
627
|
+
v = self._fresh("Fn")
|
|
628
|
+
extra.append(self._format_atom(self._func_asp[node.name], arg_terms + [v]))
|
|
629
|
+
return v, extra
|
|
630
|
+
if isinstance(node, Number):
|
|
631
|
+
raise _EncodingError(
|
|
632
|
+
f"_AspEncoder: the numeral {node.value!r} is used as an individual. A numeral "
|
|
633
|
+
"is a constant identified by its value (1 and 1.0 are one constant), and this "
|
|
634
|
+
"encoder has no symbol for it: it reads a number only as the bound a count "
|
|
635
|
+
"|{v : φ}| is compared with, never as a domain element, because the "
|
|
636
|
+
"index reading would make 1 and 2 two different elements that the kit's "
|
|
637
|
+
"numerals need not be. Use a solver that reads numerals as constants (z3, the "
|
|
638
|
+
"finite model finder), or name the individual with a constant.")
|
|
639
|
+
raise _EncodingError(
|
|
640
|
+
f"_AspEncoder: no individual-term translation for node type "
|
|
641
|
+
f"{type(node).__name__} (fragment_check should have rejected this "
|
|
642
|
+
"sentence before it reached the encoder)."
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def _numeric_term(self, node: Node) -> Tuple[str, List[str]]:
|
|
646
|
+
"""Translate a COUNTING term (``Cardinality`` or ``Number``) to
|
|
647
|
+
``(asp_expr, extra_body_literals)`` — a natural-number VALUE, not an
|
|
648
|
+
individual. See :func:`_atom_mode`'s ``"numeric"`` case."""
|
|
649
|
+
if isinstance(node, Number):
|
|
650
|
+
value = node.value
|
|
651
|
+
if isinstance(value, float) and value.is_integer():
|
|
652
|
+
value = int(value) # one numeral per value: 2.0 is the bound 2
|
|
653
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
654
|
+
raise _EncodingError(
|
|
655
|
+
f"_AspEncoder: Number({node.value!r}) is not a plain "
|
|
656
|
+
"non-negative integer — the counting fragment only "
|
|
657
|
+
"supports integer bounds."
|
|
658
|
+
)
|
|
659
|
+
return str(value), []
|
|
660
|
+
if isinstance(node, Cardinality):
|
|
661
|
+
inner_call = self._translate_formula(node.formula)
|
|
662
|
+
v = self._asp_var(node.variable.name)
|
|
663
|
+
c = self._fresh("Card")
|
|
664
|
+
agg = f"{c} = #count {{ {v} : dom({v}), {inner_call} }}"
|
|
665
|
+
return c, [agg]
|
|
666
|
+
raise _EncodingError(
|
|
667
|
+
f"_AspEncoder: {type(node).__name__} is not a counting term "
|
|
668
|
+
"(expected Cardinality or Number)."
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# -- atom / formula translation ----------------------------------------
|
|
672
|
+
|
|
673
|
+
def _emit_atom(self, atom: Atom, head: str, dom_lits: List[str]) -> None:
|
|
674
|
+
mode = _atom_mode(atom)
|
|
675
|
+
if mode == "identity":
|
|
676
|
+
t1, ex1 = self._term(atom.args[0])
|
|
677
|
+
t2, ex2 = self._term(atom.args[1])
|
|
678
|
+
op = "=" if atom.predicate == "=" else "!="
|
|
679
|
+
self._rule(head, dom_lits + ex1 + ex2 + [f"{t1} {op} {t2}"])
|
|
680
|
+
return
|
|
681
|
+
if mode == "numeric":
|
|
682
|
+
t1, ex1 = self._numeric_term(atom.args[0])
|
|
683
|
+
t2, ex2 = self._numeric_term(atom.args[1])
|
|
684
|
+
op = _NUMERIC_ASP_OP[atom.predicate]
|
|
685
|
+
self._rule(head, dom_lits + ex1 + ex2 + [f"{t1} {op} {t2}"])
|
|
686
|
+
return
|
|
687
|
+
# mode in ("relation", "predicate"): an ordinary looked-up relation.
|
|
688
|
+
key = (atom.predicate, len(atom.args))
|
|
689
|
+
if key not in self._pred_asp:
|
|
690
|
+
raise _EncodingError(
|
|
691
|
+
f"_AspEncoder: predicate {atom.predicate!r}/{len(atom.args)} is "
|
|
692
|
+
"used in a sentence but is not declared in the problem's "
|
|
693
|
+
"signature."
|
|
694
|
+
)
|
|
695
|
+
asp_name = self._pred_asp[key]
|
|
696
|
+
terms: List[str] = []
|
|
697
|
+
extra: List[str] = []
|
|
698
|
+
for a in atom.args:
|
|
699
|
+
t, ex = self._term(a)
|
|
700
|
+
terms.append(t)
|
|
701
|
+
extra.extend(ex)
|
|
702
|
+
self._rule(head, dom_lits + extra + [self._format_atom(asp_name, terms)])
|
|
703
|
+
|
|
704
|
+
def _emit_quantifier(self, node: Quantifier, head: str, fv: List[str],
|
|
705
|
+
dom_lits: List[str]) -> None:
|
|
706
|
+
inner_var = self._asp_var(node.variable.name)
|
|
707
|
+
inner_call = self._translate_formula(node.formula)
|
|
708
|
+
if node.type in _FORALL:
|
|
709
|
+
viol = self._fresh("viol")
|
|
710
|
+
viol_head = self._call(viol, fv)
|
|
711
|
+
self._rule(viol_head, dom_lits + [f"dom({inner_var})", f"not {inner_call}"])
|
|
712
|
+
self._rule(head, dom_lits + [f"not {viol_head}"])
|
|
713
|
+
elif node.type in _EXISTS:
|
|
714
|
+
self._rule(head, dom_lits + [f"dom({inner_var})", inner_call])
|
|
715
|
+
else:
|
|
716
|
+
raise _EncodingError(f"_AspEncoder: unknown quantifier type {node.type!r}.")
|
|
717
|
+
|
|
718
|
+
def _translate_formula(self, node: Node) -> str:
|
|
719
|
+
"""Translate a FORMULA node to the ASP call for its freshly minted ``satK`` atom.
|
|
720
|
+
|
|
721
|
+
Appends the atom's defining rule(s) to this encoder's line buffer as
|
|
722
|
+
a side effect. See the module docstring for the per-connective
|
|
723
|
+
encoding and the shadowing argument.
|
|
724
|
+
"""
|
|
725
|
+
fv = self._fv_tuple(node)
|
|
726
|
+
sat_name = self._fresh("sat")
|
|
727
|
+
head = self._call(sat_name, fv)
|
|
728
|
+
dom_lits = self._dom_prefix(fv)
|
|
729
|
+
|
|
730
|
+
if isinstance(node, Atom):
|
|
731
|
+
self._emit_atom(node, head, dom_lits)
|
|
732
|
+
elif isinstance(node, Not):
|
|
733
|
+
inner = self._translate_formula(node.formula)
|
|
734
|
+
self._rule(head, dom_lits + [f"not {inner}"])
|
|
735
|
+
elif isinstance(node, (And, Contrast)):
|
|
736
|
+
left = self._translate_formula(node.left)
|
|
737
|
+
right = self._translate_formula(node.right)
|
|
738
|
+
self._rule(head, dom_lits + [left, right])
|
|
739
|
+
elif isinstance(node, Or):
|
|
740
|
+
left = self._translate_formula(node.left)
|
|
741
|
+
right = self._translate_formula(node.right)
|
|
742
|
+
self._rule(head, dom_lits + [left])
|
|
743
|
+
self._rule(head, dom_lits + [right])
|
|
744
|
+
elif isinstance(node, Xor):
|
|
745
|
+
left = self._translate_formula(node.left)
|
|
746
|
+
right = self._translate_formula(node.right)
|
|
747
|
+
self._rule(head, dom_lits + [left, f"not {right}"])
|
|
748
|
+
self._rule(head, dom_lits + [right, f"not {left}"])
|
|
749
|
+
elif isinstance(node, Implies):
|
|
750
|
+
left = self._translate_formula(node.left)
|
|
751
|
+
right = self._translate_formula(node.right)
|
|
752
|
+
self._rule(head, dom_lits + [f"not {left}"])
|
|
753
|
+
self._rule(head, dom_lits + [right])
|
|
754
|
+
elif isinstance(node, Iff):
|
|
755
|
+
left = self._translate_formula(node.left)
|
|
756
|
+
right = self._translate_formula(node.right)
|
|
757
|
+
self._rule(head, dom_lits + [left, right])
|
|
758
|
+
self._rule(head, dom_lits + [f"not {left}", f"not {right}"])
|
|
759
|
+
elif isinstance(node, Quantifier):
|
|
760
|
+
self._emit_quantifier(node, head, fv, dom_lits)
|
|
761
|
+
elif isinstance(node, Count):
|
|
762
|
+
inner_call = self._translate_formula(node.formula)
|
|
763
|
+
v = self._asp_var(node.variable.name)
|
|
764
|
+
c = self._fresh("Card")
|
|
765
|
+
op = {"ge": ">=", "le": "<=", "eq": "="}[node.op]
|
|
766
|
+
agg = f"{c} = #count {{ {v} : dom({v}), {inner_call} }}"
|
|
767
|
+
self._rule(head, dom_lits + [agg, f"{c} {op} {node.n.value}"])
|
|
768
|
+
else:
|
|
769
|
+
raise _EncodingError(
|
|
770
|
+
f"_AspEncoder: no ASP translation rule for node type "
|
|
771
|
+
f"{type(node).__name__} used as a formula (fragment_check "
|
|
772
|
+
"should have rejected this sentence before it reached the "
|
|
773
|
+
"encoder, or this node type is a TERM used where a formula "
|
|
774
|
+
"was expected)."
|
|
775
|
+
)
|
|
776
|
+
return head
|
|
777
|
+
|
|
778
|
+
def _assert_sentence(self, sentence: Node) -> None:
|
|
779
|
+
"""Emit the top-level integrity constraint asserting ``sentence`` holds.
|
|
780
|
+
|
|
781
|
+
Shape: ``:- dom(X)…, not satRoot(X)….`` — this ALSO auto-closes any
|
|
782
|
+
of ``sentence``'s own free variables universally (a free ASP
|
|
783
|
+
variable ranging over ``dom`` inside a constraint body IS a
|
|
784
|
+
∀-reading) — see the module docstring's "Refutation goal" section.
|
|
785
|
+
"""
|
|
786
|
+
fv = self._fv_tuple(sentence)
|
|
787
|
+
dom_lits = self._dom_prefix(fv)
|
|
788
|
+
call = self._translate_formula(sentence)
|
|
789
|
+
self._lines.append(f":- {', '.join(dom_lits + [f'not {call}'])}.")
|
|
790
|
+
|
|
791
|
+
def _emit_show(self) -> None:
|
|
792
|
+
self._lines.append("")
|
|
793
|
+
# A bare "#show." disables clingo's DEFAULT "no #show directives present
|
|
794
|
+
# -> show every atom" behaviour, which would otherwise leak dom/1 facts
|
|
795
|
+
# and every satK/violK/CardK auxiliary atom into the model whenever a
|
|
796
|
+
# problem happens to declare NO predicate/function/constant at all
|
|
797
|
+
# (e.g. a sentence built only from "="/"≠" and Count/Cardinality, which
|
|
798
|
+
# never register a guessed relation) — reproduced and confirmed against
|
|
799
|
+
# clingo 5.8.1 before adding this line. Harmless when other #show
|
|
800
|
+
# directives follow (verified: it only suppresses the default, never
|
|
801
|
+
# an explicit #show that comes after it).
|
|
802
|
+
self._lines.append("#show.")
|
|
803
|
+
for key in sorted(self._pred_asp):
|
|
804
|
+
name, arity = key
|
|
805
|
+
self._lines.append(f"#show {self._pred_asp[key]}/{arity}.")
|
|
806
|
+
for name, decl in sorted(self.sig.functions.items()):
|
|
807
|
+
self._lines.append(f"#show {self._func_asp[name]}/{decl.arity + 1}.")
|
|
808
|
+
for name in sorted(self._const_asp):
|
|
809
|
+
self._lines.append(f"#show {self._const_asp[name]}/1.")
|
|
810
|
+
|
|
811
|
+
# -- public surface -----------------------------------------------------
|
|
812
|
+
|
|
813
|
+
def render(self) -> str:
|
|
814
|
+
"""The full ASP program text for this problem (memoised)."""
|
|
815
|
+
if not self._built:
|
|
816
|
+
n = self.problem.size
|
|
817
|
+
self._lines.append(
|
|
818
|
+
f"% finite-domain refutation search: |D| = {n}, "
|
|
819
|
+
f"{len(self.problem.sentences)} sentence(s)"
|
|
820
|
+
)
|
|
821
|
+
self._lines.append(f"dom(0..{n - 1}).")
|
|
822
|
+
self._lines.append("")
|
|
823
|
+
|
|
824
|
+
self._assign_names()
|
|
825
|
+
for key in sorted(self._pred_asp):
|
|
826
|
+
self._lines.append(self._choice_rule(self._pred_asp[key], key[1]))
|
|
827
|
+
for name, decl in sorted(self.sig.functions.items()):
|
|
828
|
+
self._lines.append(self._function_rule(self._func_asp[name], decl.arity))
|
|
829
|
+
for name in sorted(self._const_asp):
|
|
830
|
+
self._lines.append(self._const_rule(self._const_asp[name]))
|
|
831
|
+
|
|
832
|
+
distinct = [n2 for n2 in sorted(self._const_asp) if n2 not in self._parameters]
|
|
833
|
+
if self.problem.all_different and len(distinct) >= 2:
|
|
834
|
+
names = [self._const_asp[n2] for n2 in distinct]
|
|
835
|
+
for i in range(len(names)):
|
|
836
|
+
for j in range(i + 1, len(names)):
|
|
837
|
+
self._lines.append(f":- {names[i]}(Y), {names[j]}(Y).")
|
|
838
|
+
|
|
839
|
+
self._lines.append("")
|
|
840
|
+
for sentence in self.problem.sentences:
|
|
841
|
+
self._assert_sentence(sentence)
|
|
842
|
+
|
|
843
|
+
self._emit_show()
|
|
844
|
+
self._built = True
|
|
845
|
+
return "\n".join(self._lines)
|
|
846
|
+
|
|
847
|
+
@property
|
|
848
|
+
def reconstruction_signature(self) -> Signature:
|
|
849
|
+
"""The problem's own Signature, extended with every comparison symbol
|
|
850
|
+
this encoder registered as an ordinary guessed relation.
|
|
851
|
+
|
|
852
|
+
See :func:`_atom_mode`'s ``"relation"`` case — this extension exists
|
|
853
|
+
because
|
|
854
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas`
|
|
855
|
+
deliberately never declares ``=``/``≠``/``<``/``>``/``≤``/``≥`` as
|
|
856
|
+
user vocabulary (they are the kit's built-in operators), yet
|
|
857
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`
|
|
858
|
+
needs a declaration for every symbol name it is handed. Built fresh
|
|
859
|
+
on every access rather than cached — it is cheap (a handful of dict
|
|
860
|
+
entries) and this keeps the encoder's mutable build state in one
|
|
861
|
+
place (:meth:`render`) rather than two.
|
|
862
|
+
"""
|
|
863
|
+
if not self._built:
|
|
864
|
+
self.render()
|
|
865
|
+
preds = dict(self.sig.predicates)
|
|
866
|
+
seen_arity: Dict[str, int] = {name: decl.arity for name, decl in self.sig.predicates.items()}
|
|
867
|
+
for name, arity in self._pred_asp:
|
|
868
|
+
if name in seen_arity:
|
|
869
|
+
if seen_arity[name] != arity:
|
|
870
|
+
raise _EncodingError(
|
|
871
|
+
f"_AspEncoder: comparison predicate {name!r} is used at "
|
|
872
|
+
f"both arity {seen_arity[name]} and arity {arity} — a "
|
|
873
|
+
"Signature can only declare one arity per symbol."
|
|
874
|
+
)
|
|
875
|
+
continue
|
|
876
|
+
seen_arity[name] = arity
|
|
877
|
+
preds[name] = PredicateDecl(name, arity)
|
|
878
|
+
return Signature(predicates=preds, functions=self.sig.functions,
|
|
879
|
+
constants=self.sig.constants, sorts=self.sig.sorts)
|
|
880
|
+
|
|
881
|
+
def decode_model(self, symbols) -> List[Tuple[str, Tuple[int, ...]]]:
|
|
882
|
+
"""Turn clingo's shown :class:`clingo.Symbol` objects into the
|
|
883
|
+
``(kit_name, args)`` pairs
|
|
884
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`
|
|
885
|
+
expects. Only ``pred*``/``func*``/``const*`` names are ever shown
|
|
886
|
+
(see the ``#show`` directives :meth:`render` emits), so no
|
|
887
|
+
``dom``/``sat*``/``viol*``/``Card*`` auxiliary atom can reach this
|
|
888
|
+
method at all — the filtering
|
|
889
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`'s
|
|
890
|
+
own docstring asks callers to do is performed by clingo's ``#show``
|
|
891
|
+
machinery itself, not by a second pass here.
|
|
892
|
+
"""
|
|
893
|
+
reverse_pred = {asp: name for (name, _arity), asp in self._pred_asp.items()}
|
|
894
|
+
reverse_func = {asp: name for name, asp in self._func_asp.items()}
|
|
895
|
+
reverse_const = {asp: name for name, asp in self._const_asp.items()}
|
|
896
|
+
atoms: List[Tuple[str, Tuple[int, ...]]] = []
|
|
897
|
+
for sym in symbols:
|
|
898
|
+
asp_name = sym.name
|
|
899
|
+
args = tuple(a.number for a in sym.arguments)
|
|
900
|
+
if asp_name in reverse_pred:
|
|
901
|
+
atoms.append((reverse_pred[asp_name], args))
|
|
902
|
+
elif asp_name in reverse_func:
|
|
903
|
+
atoms.append((reverse_func[asp_name], args))
|
|
904
|
+
elif asp_name in reverse_const:
|
|
905
|
+
atoms.append((reverse_const[asp_name], args))
|
|
906
|
+
else:
|
|
907
|
+
raise _EncodingError(
|
|
908
|
+
f"_AspEncoder: clingo returned a shown atom {asp_name!r} "
|
|
909
|
+
"this encoder does not recognise (internal encoder / "
|
|
910
|
+
"#show mismatch — this indicates a bug in this module, "
|
|
911
|
+
"not in the input formula)."
|
|
912
|
+
)
|
|
913
|
+
return atoms
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def to_asp(problem: FiniteDomainProblem) -> str:
|
|
917
|
+
"""Render ``problem`` to ASP program text (clingo / gringo syntax).
|
|
918
|
+
|
|
919
|
+
Pure text generation — never imports or touches ``clingo`` itself, so
|
|
920
|
+
this can be tested, read, and pinned as a fixture with no solver
|
|
921
|
+
installed. See the module docstring for the encoding this produces.
|
|
922
|
+
|
|
923
|
+
Args:
|
|
924
|
+
problem: the finite-domain problem to encode.
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
The full program text: a ``dom/1`` domain declaration, one free-choice
|
|
928
|
+
or total-relation rule per declared predicate/function/constant, the
|
|
929
|
+
compositional ``satK``/``violK`` rules for every sentence, one
|
|
930
|
+
top-level integrity constraint per sentence, and ``#show`` directives
|
|
931
|
+
naming exactly the predicate/function/constant atoms.
|
|
932
|
+
|
|
933
|
+
Raises:
|
|
934
|
+
TypeError: ``problem`` is not a
|
|
935
|
+
:class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem`.
|
|
936
|
+
ValueError: a sentence, though accepted by
|
|
937
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check`, still
|
|
938
|
+
cannot be lowered to ASP by this encoder (see
|
|
939
|
+
:class:`_EncodingError`'s docstring for the exact cases); or a
|
|
940
|
+
sentence has a free variable
|
|
941
|
+
(:func:`~unicode_logic_kit.atp.finite_domain.free_variable_reason`:
|
|
942
|
+
the program would read it as "every element", one sentence at a
|
|
943
|
+
time, which is not what a free variable means on any route of
|
|
944
|
+
this kit; :meth:`ClingoBackend.decide` replaces it by a parameter
|
|
945
|
+
before it writes).
|
|
946
|
+
"""
|
|
947
|
+
encoder = _AspEncoder(problem)
|
|
948
|
+
reason = free_variable_reason(problem.sentences)
|
|
949
|
+
if reason is not None:
|
|
950
|
+
raise ValueError(f"to_asp: {reason}")
|
|
951
|
+
return encoder.render()
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
# =============================================================================
|
|
955
|
+
# Solving
|
|
956
|
+
# =============================================================================
|
|
957
|
+
|
|
958
|
+
def _solve(program_text: str, remaining_seconds: Optional[float]):
|
|
959
|
+
"""Ground and solve ``program_text``, stopping at the first model.
|
|
960
|
+
|
|
961
|
+
Returns ``("sat", shown_symbols)``, ``("unsat", None)``, or
|
|
962
|
+
``("timeout", None)``. ``remaining_seconds=None`` waits indefinitely
|
|
963
|
+
(``clingo.SolveHandle.wait(None)`` blocks until the solve genuinely
|
|
964
|
+
finishes — verified directly against clingo 5.8.1 rather than assumed).
|
|
965
|
+
A timeout cancels the async handle; ``handle.get()`` afterwards still
|
|
966
|
+
returns a genuine ``satisfiable``/``unsatisfiable`` result if the solve
|
|
967
|
+
happened to finish right around the cancellation (also verified
|
|
968
|
+
empirically), so only a truly interrupted search (``result.unknown``)
|
|
969
|
+
reports ``"timeout"``.
|
|
970
|
+
"""
|
|
971
|
+
import clingo
|
|
972
|
+
|
|
973
|
+
ctl = clingo.Control(logger=lambda code, msg: None)
|
|
974
|
+
ctl.configuration.solve.models = 1
|
|
975
|
+
ctl.add("base", [], program_text)
|
|
976
|
+
ctl.ground([("base", [])])
|
|
977
|
+
|
|
978
|
+
captured: List[list] = []
|
|
979
|
+
|
|
980
|
+
def on_model(model) -> None:
|
|
981
|
+
captured.append(list(model.symbols(shown=True)))
|
|
982
|
+
|
|
983
|
+
with ctl.solve(on_model=on_model, async_=True) as handle:
|
|
984
|
+
finished = handle.wait(remaining_seconds)
|
|
985
|
+
if not finished:
|
|
986
|
+
handle.cancel()
|
|
987
|
+
result = handle.get()
|
|
988
|
+
|
|
989
|
+
if result.satisfiable:
|
|
990
|
+
return "sat", (captured[0] if captured else [])
|
|
991
|
+
if result.unsatisfiable:
|
|
992
|
+
return "unsat", None
|
|
993
|
+
return "timeout", None
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
# =============================================================================
|
|
997
|
+
# ClingoBackend
|
|
998
|
+
# =============================================================================
|
|
999
|
+
|
|
1000
|
+
class ClingoBackend(ProverBackend):
|
|
1001
|
+
"""Bounded finite-model search over classical FOL plus counting, via
|
|
1002
|
+
clingo — refutation-only (see the module docstring's ONE-rule note).
|
|
1003
|
+
Many-sorted input is accepted too: :func:`~unicode_logic_kit.atp.finite_domain.lower_msfol`
|
|
1004
|
+
relativises it to plain classical FOL before anything else in
|
|
1005
|
+
:meth:`decide` runs (see the module docstring's "Many-sorted input"
|
|
1006
|
+
section).
|
|
1007
|
+
|
|
1008
|
+
Registered automatically by ``atp/protocol.py`` (this module does not
|
|
1009
|
+
touch the registry itself — see that module's bottom-of-registration
|
|
1010
|
+
import block).
|
|
1011
|
+
"""
|
|
1012
|
+
|
|
1013
|
+
name = "clingo"
|
|
1014
|
+
logics = frozenset({"fol"})
|
|
1015
|
+
external = False # pip package (optional extra), not a spawned binary
|
|
1016
|
+
|
|
1017
|
+
def available(self) -> bool:
|
|
1018
|
+
"""Pure discovery: is the ``clingo`` package importable? (No import.)"""
|
|
1019
|
+
return clingo_available()
|
|
1020
|
+
|
|
1021
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
1022
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
1023
|
+
"""Search for a finite countermodel of ``premises ∧ ¬formula``.
|
|
1024
|
+
|
|
1025
|
+
Args:
|
|
1026
|
+
formula: the goal.
|
|
1027
|
+
premises: entailment premises (``⊨ formula`` when empty). A free
|
|
1028
|
+
variable of the premises and the goal is a PARAMETER, one unknown
|
|
1029
|
+
element shared by all of them (see the module docstring), reported
|
|
1030
|
+
by a countermodel as the constant of the variable's own name; a free
|
|
1031
|
+
variable spelled like a constant of the problem is ``UNKNOWN`` /
|
|
1032
|
+
``"unsupported"``.
|
|
1033
|
+
timeout: milliseconds for the WHOLE search (across every domain
|
|
1034
|
+
size tried); ``0``/negative disables the limit, matching
|
|
1035
|
+
:meth:`~unicode_logic_kit.atp.cvc5_backend.Cvc5Backend.decide`'s
|
|
1036
|
+
own convention for the same parameter.
|
|
1037
|
+
**options: ``max_size`` (default ``6``) — the largest domain
|
|
1038
|
+
size tried, sizes ``1 … max_size`` in turn (matches
|
|
1039
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_countermodel`'s
|
|
1040
|
+
own parameter name; a larger default than that function's
|
|
1041
|
+
``4`` is reasonable here since ASP grounding/solving is far
|
|
1042
|
+
faster per size than plain Python enumeration). ``all_different``
|
|
1043
|
+
(default ``False``) — forwarded to
|
|
1044
|
+
:class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem`
|
|
1045
|
+
(every constant denotes a pairwise-distinct individual).
|
|
1046
|
+
``verify`` (default ``True``) — whether to run every
|
|
1047
|
+
candidate countermodel through
|
|
1048
|
+
:func:`~unicode_logic_kit.atp.finite_domain.verify_model`
|
|
1049
|
+
before reporting ``REFUTED``; ``False`` exists ONLY for
|
|
1050
|
+
measuring verification overhead (see
|
|
1051
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s module
|
|
1052
|
+
docstring) — an unverified countermodel is still marked as
|
|
1053
|
+
such in the returned ``Verdict.detail``, never silently.
|
|
1054
|
+
|
|
1055
|
+
Returns:
|
|
1056
|
+
A :class:`~unicode_logic_kit.atp.protocol.Verdict` with ``status``
|
|
1057
|
+
in ``{"refuted", "unknown", "error"}`` — NEVER ``"proved"`` (see
|
|
1058
|
+
the module docstring). ``REFUTED`` carries
|
|
1059
|
+
``countermodel={"kind": "finite_structure", "data": structure.to_dict()}``.
|
|
1060
|
+
``UNKNOWN`` reasons: ``"unsupported"`` (fragment gate or a
|
|
1061
|
+
deeper encoding-time rejection), ``"bound_hit"`` (no
|
|
1062
|
+
countermodel up to ``max_size``), ``"timeout"`` (the millisecond
|
|
1063
|
+
budget expired first). ``ERROR``/``"infra"`` covers a clingo/
|
|
1064
|
+
grounding failure, a solver output ``structure_from_solution``
|
|
1065
|
+
rejected, or (per the ONE rule) a candidate countermodel that
|
|
1066
|
+
failed re-verification.
|
|
1067
|
+
"""
|
|
1068
|
+
max_size = options.pop("max_size", 6)
|
|
1069
|
+
if isinstance(max_size, bool) or not isinstance(max_size, int) or max_size < 1:
|
|
1070
|
+
raise ValueError(f"clingo: max_size must be an int >= 1, got {max_size!r}.")
|
|
1071
|
+
all_different = bool(options.pop("all_different", False))
|
|
1072
|
+
verify = bool(options.pop("verify", True))
|
|
1073
|
+
|
|
1074
|
+
# A free variable is a PARAMETER of the problem (see the module docstring): every
|
|
1075
|
+
# one is replaced HERE, once, in the premises and the goal together, by a
|
|
1076
|
+
# constant of its own name, and only then is the goal negated (¬∀x φ(x) is not
|
|
1077
|
+
# ∀x ¬φ(x)). The ASP encoding reads a free variable in a constraint as
|
|
1078
|
+
# implicitly ∀-bound but `evaluate_in_structure` raises for a variable with no
|
|
1079
|
+
# assignment, so encoder and checker must see the same parameter-free sentences;
|
|
1080
|
+
# this is where that is guaranteed. No premise is closed universally.
|
|
1081
|
+
try:
|
|
1082
|
+
read, parameters = parameterize(list(premises) + [formula], after_variables=True)
|
|
1083
|
+
except NotImplementedError as exc:
|
|
1084
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported", detail=str(exc))
|
|
1085
|
+
goal = Not(read[-1])
|
|
1086
|
+
sentences = tuple(read[:-1]) + (goal,)
|
|
1087
|
+
parameter_names = frozenset(parameters)
|
|
1088
|
+
# Many-sorted input (SortedQuantifier/SortedConstant/SortedCount/
|
|
1089
|
+
# SortedCardinality) is relativised to plain classical FOL HERE, once,
|
|
1090
|
+
# before fragment_check ever sees it -- see
|
|
1091
|
+
# finite_domain.lower_msfol's own docstring and finite_domain's module
|
|
1092
|
+
# docstring "Many-sorted input" section. A no-op for every
|
|
1093
|
+
# unsorted-only caller (the pre-existing test suite): returns
|
|
1094
|
+
# `sentences` untouched when nothing sorted is present.
|
|
1095
|
+
sentences = lower_msfol(sentences)
|
|
1096
|
+
|
|
1097
|
+
problem_msg = fragment_check(sentences)
|
|
1098
|
+
if problem_msg is not None:
|
|
1099
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported", detail=problem_msg)
|
|
1100
|
+
|
|
1101
|
+
try:
|
|
1102
|
+
signature = Signature.from_formulas(sentences)
|
|
1103
|
+
except (TypeError, ValueError) as exc:
|
|
1104
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported",
|
|
1105
|
+
detail=f"signature inference failed: {exc}")
|
|
1106
|
+
|
|
1107
|
+
start = time.perf_counter()
|
|
1108
|
+
deadline = None if timeout is None or timeout <= 0 else start + timeout / 1000.0
|
|
1109
|
+
|
|
1110
|
+
for size in range(1, max_size + 1):
|
|
1111
|
+
if deadline is not None:
|
|
1112
|
+
remaining = deadline - time.perf_counter()
|
|
1113
|
+
if remaining <= 0:
|
|
1114
|
+
return Verdict(UNKNOWN, self.name, reason="timeout",
|
|
1115
|
+
wall_time=time.perf_counter() - start,
|
|
1116
|
+
detail=f"no countermodel found up to size {size - 1} "
|
|
1117
|
+
f"before the {timeout}ms budget expired")
|
|
1118
|
+
else:
|
|
1119
|
+
remaining = None
|
|
1120
|
+
|
|
1121
|
+
try:
|
|
1122
|
+
problem = FiniteDomainProblem(sentences, size, signature=signature,
|
|
1123
|
+
all_different=all_different)
|
|
1124
|
+
encoder = _AspEncoder(problem, parameter_names)
|
|
1125
|
+
program_text = encoder.render()
|
|
1126
|
+
except _EncodingError as exc:
|
|
1127
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported",
|
|
1128
|
+
wall_time=time.perf_counter() - start, detail=str(exc))
|
|
1129
|
+
|
|
1130
|
+
try:
|
|
1131
|
+
outcome, symbols = _solve(program_text, remaining)
|
|
1132
|
+
except Exception as exc: # noqa: BLE001 - clingo raises plain RuntimeError/etc.
|
|
1133
|
+
return Verdict(ERROR, self.name, reason="infra",
|
|
1134
|
+
wall_time=time.perf_counter() - start,
|
|
1135
|
+
detail=f"{type(exc).__name__}: {exc}")
|
|
1136
|
+
|
|
1137
|
+
if outcome == "timeout":
|
|
1138
|
+
return Verdict(UNKNOWN, self.name, reason="timeout",
|
|
1139
|
+
wall_time=time.perf_counter() - start,
|
|
1140
|
+
detail=f"clingo did not finish size {size} within the "
|
|
1141
|
+
f"{timeout}ms budget")
|
|
1142
|
+
if outcome == "unsat":
|
|
1143
|
+
continue
|
|
1144
|
+
|
|
1145
|
+
# outcome == "sat"
|
|
1146
|
+
try:
|
|
1147
|
+
atoms = encoder.decode_model(symbols)
|
|
1148
|
+
# the distinctness of the CONSTANTS is checked here when a parameter is
|
|
1149
|
+
# among them: a variable may denote the element of a constant
|
|
1150
|
+
structure = structure_from_solution(
|
|
1151
|
+
encoder.reconstruction_signature, atoms, size,
|
|
1152
|
+
all_different=all_different and not parameter_names)
|
|
1153
|
+
if all_different and parameter_names:
|
|
1154
|
+
clash = _repeated_constant(structure.constants, parameter_names)
|
|
1155
|
+
if clash is not None:
|
|
1156
|
+
raise ValueError(clash)
|
|
1157
|
+
except (TypeError, ValueError) as exc:
|
|
1158
|
+
return Verdict(ERROR, self.name, reason="infra",
|
|
1159
|
+
wall_time=time.perf_counter() - start,
|
|
1160
|
+
detail=f"could not reconstruct clingo's own model at "
|
|
1161
|
+
f"size {size}: {exc}")
|
|
1162
|
+
|
|
1163
|
+
if verify:
|
|
1164
|
+
unverified = verify_model(structure, sentences)
|
|
1165
|
+
if unverified is not None:
|
|
1166
|
+
return Verdict(ERROR, self.name, reason="infra",
|
|
1167
|
+
wall_time=time.perf_counter() - start,
|
|
1168
|
+
detail=f"unverified countermodel at size {size}: "
|
|
1169
|
+
f"{unverified}")
|
|
1170
|
+
detail = None
|
|
1171
|
+
else:
|
|
1172
|
+
detail = "model not independently verified (verify=False)"
|
|
1173
|
+
|
|
1174
|
+
return Verdict(REFUTED, self.name, wall_time=time.perf_counter() - start,
|
|
1175
|
+
countermodel={"kind": "finite_structure", "data": structure.to_dict()},
|
|
1176
|
+
detail=detail)
|
|
1177
|
+
|
|
1178
|
+
return Verdict(UNKNOWN, self.name, reason="bound_hit",
|
|
1179
|
+
wall_time=time.perf_counter() - start,
|
|
1180
|
+
detail=f"no countermodel found up to size {max_size}")
|